Compare commits

..

5 Commits

Author SHA1 Message Date
mertalev 57933af9b0 add env to docs 2025-04-19 17:21:42 -04:00
mertalev 1c4a8c3968 fix tests 2025-04-19 17:06:53 -04:00
mertalev e2d80755c6 use arena by default in native installation 2025-04-19 16:56:53 -04:00
mertalev 5a3b11d603 add test 2025-04-19 16:31:00 -04:00
mertalev 543bc72ae3 coreml 2025-04-19 16:31:00 -04:00
983 changed files with 35145 additions and 48262 deletions
+1 -1
View File
@@ -1 +1 @@
22.15.1 22.14.0
-118
View File
@@ -1,118 +0,0 @@
name: 'Single arch image build'
description: 'Build single-arch image on platform appropriate runner'
inputs:
image:
description: 'Name of the image to build'
required: true
ghcr-token:
description: 'GitHub Container Registry token'
required: true
platform:
description: 'Platform to build for'
required: true
artifact-key-base:
description: 'Base key for artifact name'
required: true
context:
description: 'Path to build context'
required: true
dockerfile:
description: 'Path to Dockerfile'
required: true
build-args:
description: 'Docker build arguments'
required: false
runs:
using: 'composite'
steps:
- name: Prepare
id: prepare
shell: bash
env:
PLATFORM: ${{ inputs.platform }}
run: |
echo "platform-pair=${PLATFORM//\//-}" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ inputs.ghcr-token }}
- name: Generate cache key suffix
id: cache-key-suffix
shell: bash
env:
REF: ${{ github.ref_name }}
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "cache-key-suffix=pr-${{ github.event.number }}" >> $GITHUB_OUTPUT
else
SUFFIX=$(echo "${REF}" | sed 's/[^a-zA-Z0-9]/-/g')
echo "suffix=${SUFFIX}" >> $GITHUB_OUTPUT
fi
- name: Generate cache target
id: cache-target
shell: bash
env:
BUILD_ARGS: ${{ inputs.build-args }}
IMAGE: ${{ inputs.image }}
SUFFIX: ${{ steps.cache-key-suffix.outputs.suffix }}
PLATFORM_PAIR: ${{ steps.prepare.outputs.platform-pair }}
run: |
HASH=$(sha256sum <<< "${BUILD_ARGS}" | cut -d' ' -f1)
CACHE_KEY="${PLATFORM_PAIR}-${HASH}"
echo "cache-key-base=${CACHE_KEY}" >> $GITHUB_OUTPUT
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
# Essentially just ignore the cache output (forks can't write to registry cache)
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
else
echo "cache-to=type=registry,ref=${IMAGE}-build-cache:${CACHE_KEY}-${SUFFIX},mode=max,compression=zstd" >> $GITHUB_OUTPUT
fi
- name: Generate docker image tags
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
env:
DOCKER_METADATA_PR_HEAD_SHA: 'true'
- name: Build and push image
id: build
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
with:
context: ${{ inputs.context }}
file: ${{ inputs.dockerfile }}
platforms: ${{ inputs.platform }}
labels: ${{ steps.meta.outputs.labels }}
cache-to: ${{ steps.cache-target.outputs.cache-to }}
cache-from: |
type=registry,ref=${{ inputs.image }}-build-cache:${{ steps.cache-target.outputs.cache-key-base }}-${{ steps.cache-key-suffix.outputs.suffix }}
type=registry,ref=${{ inputs.image }}-build-cache:${{ steps.cache-target.outputs.cache-key-base }}-main
outputs: type=image,"name=${{ inputs.image }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }}
build-args: |
BUILD_ID=${{ github.run_id }}
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.meta.outputs.tags }}
BUILD_SOURCE_REF=${{ github.ref_name }}
BUILD_SOURCE_COMMIT=${{ github.sha }}
${{ inputs.build-args }}
- name: Export digest
shell: bash
run: | # zizmor: ignore[template-injection]
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ${{ inputs.artifact-key-base }}-${{ steps.cache-target.outputs.cache-key-base }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
+6 -6
View File
@@ -35,12 +35,12 @@ jobs:
should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
- id: found_paths - id: found_paths
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
with: with:
filters: | filters: |
mobile: mobile:
@@ -61,19 +61,19 @@ jobs:
runs-on: macos-14 runs-on: macos-14
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
ref: ${{ inputs.ref || github.sha }} ref: ${{ inputs.ref || github.sha }}
persist-credentials: false persist-credentials: false
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4
with: with:
distribution: 'zulu' distribution: 'zulu'
java-version: '17' java-version: '17'
cache: 'gradle' cache: 'gradle'
- name: Setup Flutter SDK - name: Setup Flutter SDK
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0 uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2
with: with:
channel: 'stable' channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml flutter-version-file: ./mobile/pubspec.yaml
@@ -104,7 +104,7 @@ jobs:
flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64 flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
- name: Publish Android Artifact - name: Publish Android Artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with: with:
name: release-apk-signed name: release-apk-signed
path: mobile/build/app/outputs/flutter-apk/*.apk path: mobile/build/app/outputs/flutter-apk/*.apk
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
actions: write actions: write
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
+6 -6
View File
@@ -29,12 +29,12 @@ jobs:
working-directory: ./cli working-directory: ./cli
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
# Setup .npmrc file to publish to npm # Setup .npmrc file to publish to npm
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version-file: './cli/.nvmrc' node-version-file: './cli/.nvmrc'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
@@ -59,7 +59,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
@@ -70,7 +70,7 @@ jobs:
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
if: ${{ !github.event.pull_request.head.repo.fork }} if: ${{ !github.event.pull_request.head.repo.fork }}
with: with:
registry: ghcr.io registry: ghcr.io
@@ -85,7 +85,7 @@ jobs:
- name: Generate docker image tags - name: Generate docker image tags
id: metadata id: metadata
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
with: with:
flavor: | flavor: |
latest=false latest=false
@@ -96,7 +96,7 @@ jobs:
type=raw,value=latest,enable=${{ github.event_name == 'release' }} type=raw,value=latest,enable=${{ github.event_name == 'release' }}
- name: Build and push image - name: Build and push image
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0 uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
with: with:
file: cli/Dockerfile file: cli/Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
+4 -4
View File
@@ -44,13 +44,13 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 uses: github/codeql-action/init@45775bd8235c68ba998cffa5171334d58593da47 # v3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -63,7 +63,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 uses: github/codeql-action/autobuild@45775bd8235c68ba998cffa5171334d58593da47 # v3
# ️ Command-line programs to run using the OS shell. # ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -76,6 +76,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh # ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 uses: github/codeql-action/analyze@45775bd8235c68ba998cffa5171334d58593da47 # v3
with: with:
category: '/language:${{matrix.language}}' category: '/language:${{matrix.language}}'
+413 -62
View File
@@ -24,11 +24,11 @@ jobs:
should_run_ml: ${{ steps.found_paths.outputs.machine-learning == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run_ml: ${{ steps.found_paths.outputs.machine-learning == 'true' || steps.should_force.outputs.should_force == 'true' }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
- id: found_paths - id: found_paths
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
with: with:
filters: | filters: |
server: server:
@@ -40,8 +40,6 @@ jobs:
- 'machine-learning/**' - 'machine-learning/**'
workflow: workflow:
- '.github/workflows/docker.yml' - '.github/workflows/docker.yml'
- '.github/workflows/multi-runner-build.yml'
- '.github/actions/image-build'
- name: Check if we should force jobs to run - name: Check if we should force jobs to run
id: should_force id: should_force
@@ -60,7 +58,7 @@ jobs:
suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn'] suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn']
steps: steps:
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -89,7 +87,7 @@ jobs:
suffix: [''] suffix: ['']
steps: steps:
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -105,74 +103,429 @@ jobs:
docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}" docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}" docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
machine-learning: build_and_push_ml:
name: Build and Push ML name: Build and Push ML
needs: pre-job needs: pre-job
permissions:
contents: read
packages: write
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }} if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }}
runs-on: ${{ matrix.runner }}
env:
image: immich-machine-learning
context: machine-learning
file: machine-learning/Dockerfile
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-machine-learning
strategy:
# Prevent a failure in one image from stopping the other builds
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
device: cpu
- platform: linux/arm64
runner: ubuntu-24.04-arm
device: cpu
- platform: linux/amd64
runner: ubuntu-latest
device: cuda
suffix: -cuda
- platform: linux/amd64
runner: mich
device: rocm
suffix: -rocm
- platform: linux/amd64
runner: ubuntu-latest
device: openvino
suffix: -openvino
- platform: linux/arm64
runner: ubuntu-24.04-arm
device: armnn
suffix: -armnn
- platform: linux/arm64
runner: ubuntu-24.04-arm
device: rknn
suffix: -rknn
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate cache key suffix
env:
REF: ${{ github.ref_name }}
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV
else
SUFFIX=$(echo "${REF}" | sed 's/[^a-zA-Z0-9]/-/g')
echo "CACHE_KEY_SUFFIX=${SUFFIX}" >> $GITHUB_ENV
fi
- name: Generate cache target
id: cache-target
run: |
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
# Essentially just ignore the cache output (forks can't write to registry cache)
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
else
echo "cache-to=type=registry,ref=${GHCR_REPO}-build-cache:${PLATFORM_PAIR}-${{ matrix.device }}-${CACHE_KEY_SUFFIX},mode=max,compression=zstd" >> $GITHUB_OUTPUT
fi
- name: Generate docker image tags
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
env:
DOCKER_METADATA_PR_HEAD_SHA: 'true'
- name: Build and push image
id: build
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
with:
context: ${{ env.context }}
file: ${{ env.file }}
platforms: ${{ matrix.platforms }}
labels: ${{ steps.meta.outputs.labels }}
cache-to: ${{ steps.cache-target.outputs.cache-to }}
cache-from: |
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }}
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-main
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }}
build-args: |
DEVICE=${{ matrix.device }}
BUILD_ID=${{ github.run_id }}
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
BUILD_SOURCE_REF=${{ github.ref_name }}
BUILD_SOURCE_COMMIT=${{ github.sha }}
- name: Export digest
run: |
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ml-digests-${{ matrix.device }}-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
merge_ml:
name: Merge & Push ML
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
packages: write
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' && !github.event.pull_request.head.repo.fork }}
env:
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-machine-learning
DOCKER_REPO: altran1502/immich-machine-learning
strategy:
matrix:
include:
- device: cpu
- device: cuda
suffix: -cuda
- device: rocm
suffix: -rocm
- device: openvino
suffix: -openvino
- device: armnn
suffix: -armnn
- device: rknn
suffix: -rknn
needs:
- build_and_push_ml
steps:
- name: Download digests
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with:
path: ${{ runner.temp }}/digests
pattern: ml-digests-${{ matrix.device }}-*
merge-multiple: true
- name: Login to Docker Hub
if: ${{ github.event_name == 'release' }}
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- name: Generate docker image tags
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
env:
DOCKER_METADATA_PR_HEAD_SHA: 'true'
with:
flavor: |
# Disable latest tag
latest=false
suffix=${{ matrix.suffix }}
images: |
name=${{ env.GHCR_REPO }}
name=${{ env.DOCKER_REPO }},enable=${{ github.event_name == 'release' }}
tags: |
# Tag with branch name
type=ref,event=branch
# Tag with pr-number
type=ref,event=pr
# Tag with long commit sha hash
type=sha,format=long,prefix=commit-
# Tag with git tag on release
type=ref,event=tag
type=raw,value=release,enable=${{ github.event_name == 'release' }}
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
run: |
# Process annotations
declare -a ANNOTATIONS=()
if [[ -n "$DOCKER_METADATA_OUTPUT_JSON" ]]; then
while IFS= read -r annotation; do
# Extract key and value by removing the manifest: prefix
if [[ "$annotation" =~ ^manifest:(.+)=(.+)$ ]]; then
key="${BASH_REMATCH[1]}"
value="${BASH_REMATCH[2]}"
# Use array to properly handle arguments with spaces
ANNOTATIONS+=(--annotation "index:$key=$value")
fi
done < <(jq -r '.annotations[]' <<< "$DOCKER_METADATA_OUTPUT_JSON")
fi
TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
SOURCE_ARGS=$(printf "${GHCR_REPO}@sha256:%s " *)
docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS
build_and_push_server:
name: Build and Push Server
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
env:
image: immich-server
context: .
file: server/Dockerfile
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- device: cpu - platform: linux/amd64
tag-suffix: '' runner: ubuntu-latest
- device: cuda - platform: linux/arm64
tag-suffix: '-cuda' runner: ubuntu-24.04-arm
platforms: linux/amd64 steps:
- device: openvino - name: Prepare
tag-suffix: '-openvino' run: |
platforms: linux/amd64 platform=${{ matrix.platform }}
- device: armnn echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
tag-suffix: '-armnn'
platforms: linux/arm64
- device: rknn
tag-suffix: '-rknn'
platforms: linux/arm64
- device: rocm
tag-suffix: '-rocm'
platforms: linux/amd64
runner-mapping: '{"linux/amd64": "mich"}'
uses: ./.github/workflows/multi-runner-build.yml
permissions:
contents: read
actions: read
packages: write
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
with:
image: immich-machine-learning
context: machine-learning
dockerfile: machine-learning/Dockerfile
platforms: ${{ matrix.platforms }}
runner-mapping: ${{ matrix.runner-mapping }}
tag-suffix: ${{ matrix.tag-suffix }}
dockerhub-push: ${{ github.event_name == 'release' }}
build-args: |
DEVICE=${{ matrix.device }}
server: - name: Checkout
name: Build and Push Server uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
needs: pre-job with:
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }} persist-credentials: false
uses: ./.github/workflows/multi-runner-build.yml
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate cache key suffix
env:
REF: ${{ github.ref_name }}
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV
else
SUFFIX=$(echo "${REF}" | sed 's/[^a-zA-Z0-9]/-/g')
echo "CACHE_KEY_SUFFIX=${SUFFIX}" >> $GITHUB_ENV
fi
- name: Generate cache target
id: cache-target
run: |
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
# Essentially just ignore the cache output (forks can't write to registry cache)
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
else
echo "cache-to=type=registry,ref=${GHCR_REPO}-build-cache:${PLATFORM_PAIR}-${CACHE_KEY_SUFFIX},mode=max,compression=zstd" >> $GITHUB_OUTPUT
fi
- name: Generate docker image tags
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
env:
DOCKER_METADATA_PR_HEAD_SHA: 'true'
- name: Build and push image
id: build
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
with:
context: ${{ env.context }}
file: ${{ env.file }}
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
cache-to: ${{ steps.cache-target.outputs.cache-to }}
cache-from: |
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ env.CACHE_KEY_SUFFIX }}
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-main
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }}
build-args: |
DEVICE=cpu
BUILD_ID=${{ github.run_id }}
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
BUILD_SOURCE_REF=${{ github.ref_name }}
BUILD_SOURCE_COMMIT=${{ github.sha }}
- name: Export digest
run: |
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: server-digests-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
merge_server:
name: Merge & Push Server
runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
actions: read actions: read
packages: write packages: write
secrets: if: ${{ needs.pre-job.outputs.should_run_server == 'true' && !github.event.pull_request.head.repo.fork }}
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} env:
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server
with: DOCKER_REPO: altran1502/immich-server
image: immich-server needs:
context: . - build_and_push_server
dockerfile: server/Dockerfile steps:
dockerhub-push: ${{ github.event_name == 'release' }} - name: Download digests
build-args: | uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
DEVICE=cpu with:
path: ${{ runner.temp }}/digests
pattern: server-digests-*
merge-multiple: true
- name: Login to Docker Hub
if: ${{ github.event_name == 'release' }}
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- name: Generate docker image tags
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
env:
DOCKER_METADATA_PR_HEAD_SHA: 'true'
with:
flavor: |
# Disable latest tag
latest=false
suffix=${{ matrix.suffix }}
images: |
name=${{ env.GHCR_REPO }}
name=${{ env.DOCKER_REPO }},enable=${{ github.event_name == 'release' }}
tags: |
# Tag with branch name
type=ref,event=branch
# Tag with pr-number
type=ref,event=pr
# Tag with long commit sha hash
type=sha,format=long,prefix=commit-
# Tag with git tag on release
type=ref,event=tag
type=raw,value=release,enable=${{ github.event_name == 'release' }}
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
run: |
# Process annotations
declare -a ANNOTATIONS=()
if [[ -n "$DOCKER_METADATA_OUTPUT_JSON" ]]; then
while IFS= read -r annotation; do
# Extract key and value by removing the manifest: prefix
if [[ "$annotation" =~ ^manifest:(.+)=(.+)$ ]]; then
key="${BASH_REMATCH[1]}"
value="${BASH_REMATCH[2]}"
# Use array to properly handle arguments with spaces
ANNOTATIONS+=(--annotation "index:$key=$value")
fi
done < <(jq -r '.annotations[]' <<< "$DOCKER_METADATA_OUTPUT_JSON")
fi
TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
SOURCE_ARGS=$(printf "${GHCR_REPO}@sha256:%s " *)
docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS
success-check-server: success-check-server:
name: Docker Build & Push Server Success name: Docker Build & Push Server Success
needs: [server, retag_server] needs: [merge_server, retag_server]
permissions: {} permissions: {}
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: always() if: always()
@@ -182,12 +535,11 @@ jobs:
run: exit 1 run: exit 1
- name: All jobs passed or skipped - name: All jobs passed or skipped
if: ${{ !(contains(needs.*.result, 'failure')) }} if: ${{ !(contains(needs.*.result, 'failure')) }}
# zizmor: ignore[template-injection]
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}" run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
success-check-ml: success-check-ml:
name: Docker Build & Push ML Success name: Docker Build & Push ML Success
needs: [machine-learning, retag_ml] needs: [merge_ml, retag_ml]
permissions: {} permissions: {}
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: always() if: always()
@@ -197,5 +549,4 @@ jobs:
run: exit 1 run: exit 1
- name: All jobs passed or skipped - name: All jobs passed or skipped
if: ${{ !(contains(needs.*.result, 'failure')) }} if: ${{ !(contains(needs.*.result, 'failure')) }}
# zizmor: ignore[template-injection]
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}" run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
+5 -6
View File
@@ -21,11 +21,11 @@ jobs:
should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.should_force.outputs.should_force == 'true' }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
- id: found_paths - id: found_paths
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
with: with:
filters: | filters: |
docs: docs:
@@ -49,12 +49,12 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version-file: './docs/.nvmrc' node-version-file: './docs/.nvmrc'
@@ -68,9 +68,8 @@ jobs:
run: npm run build run: npm run build
- name: Upload build output - name: Upload build output
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with: with:
name: docs-build-output name: docs-build-output
path: docs/build/ path: docs/build/
include-hidden-files: true
retention-days: 1 retention-days: 1
+17 -18
View File
@@ -1,6 +1,6 @@
name: Docs deploy name: Docs deploy
on: on:
workflow_run: # zizmor: ignore[dangerous-triggers] no attacker inputs are used here workflow_run:
workflows: ['Docs build'] workflows: ['Docs build']
types: types:
- completed - completed
@@ -20,7 +20,7 @@ jobs:
run: echo 'The triggering workflow did not succeed' && exit 1 run: echo 'The triggering workflow did not succeed' && exit 1
- name: Get artifact - name: Get artifact
id: get-artifact id: get-artifact
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
with: with:
script: | script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
@@ -38,7 +38,7 @@ jobs:
return { found: true, id: matchArtifact.id }; return { found: true, id: matchArtifact.id };
- name: Determine deploy parameters - name: Determine deploy parameters
id: parameters id: parameters
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
env: env:
HEAD_SHA: ${{ github.event.workflow_run.head_sha }} HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
with: with:
@@ -108,29 +108,29 @@ jobs:
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }} if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
- name: Load parameters - name: Load parameters
id: parameters id: parameters
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
env:
PARAM_JSON: ${{ needs.checks.outputs.parameters }}
with: with:
script: | script: |
const parameters = JSON.parse(process.env.PARAM_JSON); const json = `${{ needs.checks.outputs.parameters }}`;
const parameters = JSON.parse(json);
core.setOutput("event", parameters.event); core.setOutput("event", parameters.event);
core.setOutput("name", parameters.name); core.setOutput("name", parameters.name);
core.setOutput("shouldDeploy", parameters.shouldDeploy); core.setOutput("shouldDeploy", parameters.shouldDeploy);
- run: |
echo "Starting docs deployment for ${{ steps.parameters.outputs.event }} ${{ steps.parameters.outputs.name }}"
- name: Download artifact - name: Download artifact
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
env:
ARTIFACT_JSON: ${{ needs.checks.outputs.artifact }}
with: with:
script: | script: |
let artifact = JSON.parse(process.env.ARTIFACT_JSON); let artifact = ${{ needs.checks.outputs.artifact }};
let download = await github.rest.actions.downloadArtifact({ let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
@@ -150,7 +150,7 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }} TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
uses: gruntwork-io/terragrunt-action@aee21a7df999be8b471c2a8564c6cd853cb674e1 # v2.1.8 uses: gruntwork-io/terragrunt-action@9559e51d05873b0ea467c42bbabcb5c067642ccc # v2
with: with:
tg_version: '0.58.12' tg_version: '0.58.12'
tofu_version: '1.7.1' tofu_version: '1.7.1'
@@ -165,7 +165,7 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }} TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
uses: gruntwork-io/terragrunt-action@aee21a7df999be8b471c2a8564c6cd853cb674e1 # v2.1.8 uses: gruntwork-io/terragrunt-action@9559e51d05873b0ea467c42bbabcb5c067642ccc # v2
with: with:
tg_version: '0.58.12' tg_version: '0.58.12'
tofu_version: '1.7.1' tofu_version: '1.7.1'
@@ -181,8 +181,7 @@ jobs:
echo "output=$CLEANED" >> $GITHUB_OUTPUT echo "output=$CLEANED" >> $GITHUB_OUTPUT
- name: Publish to Cloudflare Pages - name: Publish to Cloudflare Pages
# TODO: Action is deprecated uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca # v1
uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca # v1.5.0
with: with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN_PAGES_UPLOAD }} apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN_PAGES_UPLOAD }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
@@ -199,7 +198,7 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }} TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
uses: gruntwork-io/terragrunt-action@aee21a7df999be8b471c2a8564c6cd853cb674e1 # v2.1.8 uses: gruntwork-io/terragrunt-action@9559e51d05873b0ea467c42bbabcb5c067642ccc # v2
with: with:
tg_version: '0.58.12' tg_version: '0.58.12'
tofu_version: '1.7.1' tofu_version: '1.7.1'
@@ -207,7 +206,7 @@ jobs:
tg_command: 'apply' tg_command: 'apply'
- name: Comment - name: Comment
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0 uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3
if: ${{ steps.parameters.outputs.event == 'pr' }} if: ${{ steps.parameters.outputs.event == 'pr' }}
with: with:
number: ${{ fromJson(needs.checks.outputs.parameters).pr_number }} number: ${{ fromJson(needs.checks.outputs.parameters).pr_number }}
+4 -4
View File
@@ -1,6 +1,6 @@
name: Docs destroy name: Docs destroy
on: on:
pull_request_target: # zizmor: ignore[dangerous-triggers] no attacker inputs are used here pull_request_target:
types: [closed] types: [closed]
permissions: {} permissions: {}
@@ -14,7 +14,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
@@ -25,7 +25,7 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }} TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
uses: gruntwork-io/terragrunt-action@aee21a7df999be8b471c2a8564c6cd853cb674e1 # v2.1.8 uses: gruntwork-io/terragrunt-action@9559e51d05873b0ea467c42bbabcb5c067642ccc # v2
with: with:
tg_version: '0.58.12' tg_version: '0.58.12'
tofu_version: '1.7.1' tofu_version: '1.7.1'
@@ -33,7 +33,7 @@ jobs:
tg_command: 'destroy -refresh=false' tg_command: 'destroy -refresh=false'
- name: Comment - name: Comment
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0 uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3
with: with:
number: ${{ github.event.number }} number: ${{ github.event.number }}
delete: true delete: true
+5 -5
View File
@@ -16,20 +16,20 @@ jobs:
steps: steps:
- name: Generate a token - name: Generate a token
id: generate-token id: generate-token
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: 'Checkout' - name: 'Checkout'
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
ref: ${{ github.event.pull_request.head.ref }} ref: ${{ github.event.pull_request.head.ref }}
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}
persist-credentials: true persist-credentials: true
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
@@ -37,13 +37,13 @@ jobs:
run: make install-all && make format-all run: make install-all && make format-all
- name: Commit and push - name: Commit and push
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4 uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9
with: with:
default_author: github_actions default_author: github_actions
message: 'chore: fix formatting' message: 'chore: fix formatting'
- name: Remove label - name: Remove label
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
if: always() if: always()
with: with:
script: | script: |
-185
View File
@@ -1,185 +0,0 @@
name: 'Multi-runner container image build'
on:
workflow_call:
inputs:
image:
description: 'Name of the image'
type: string
required: true
context:
description: 'Path to build context'
type: string
required: true
dockerfile:
description: 'Path to Dockerfile'
type: string
required: true
tag-suffix:
description: 'Suffix to append to the image tag'
type: string
default: ''
dockerhub-push:
description: 'Push to Docker Hub'
type: boolean
default: false
build-args:
description: 'Docker build arguments'
type: string
required: false
platforms:
description: 'Platforms to build for'
type: string
runner-mapping:
description: 'Mapping from platforms to runners'
type: string
secrets:
DOCKERHUB_USERNAME:
required: false
DOCKERHUB_TOKEN:
required: false
env:
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ inputs.image }}
DOCKERHUB_IMAGE: altran1502/${{ inputs.image }}
jobs:
matrix:
name: 'Generate matrix'
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.matrix.outputs.matrix }}
key: ${{ steps.artifact-key.outputs.base }}
steps:
- name: Generate build matrix
id: matrix
shell: bash
env:
PLATFORMS: ${{ inputs.platforms || 'linux/amd64,linux/arm64' }}
RUNNER_MAPPING: ${{ inputs.runner-mapping || '{"linux/amd64":"ubuntu-latest","linux/arm64":"ubuntu-24.04-arm"}' }}
run: |
matrix=$(jq -R -c \
--argjson runner_mapping "${RUNNER_MAPPING}" \
'split(",") | map({platform: ., runner: $runner_mapping[.]})' \
<<< "${PLATFORMS}")
echo "${matrix}"
echo "matrix=${matrix}" >> $GITHUB_OUTPUT
- name: Determine artifact key
id: artifact-key
shell: bash
env:
IMAGE: ${{ inputs.image }}
SUFFIX: ${{ inputs.tag-suffix }}
run: |
if [[ -n "${SUFFIX}" ]]; then
base="${IMAGE}${SUFFIX}-digests"
else
base="${IMAGE}-digests"
fi
echo "${base}"
echo "base=${base}" >> $GITHUB_OUTPUT
build:
needs: matrix
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.matrix.outputs.matrix) }}
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- uses: ./.github/actions/image-build
with:
context: ${{ inputs.context }}
dockerfile: ${{ inputs.dockerfile }}
image: ${{ env.GHCR_IMAGE }}
ghcr-token: ${{ secrets.GITHUB_TOKEN }}
platform: ${{ matrix.platform }}
artifact-key-base: ${{ needs.matrix.outputs.key }}
build-args: ${{ inputs.build-args }}
merge:
needs: [matrix, build]
runs-on: ubuntu-latest
if: ${{ !github.event.pull_request.head.repo.fork }}
permissions:
contents: read
actions: read
packages: write
steps:
- name: Download digests
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
path: ${{ runner.temp }}/digests
pattern: ${{ needs.matrix.outputs.key }}-*
merge-multiple: true
- name: Login to Docker Hub
if: ${{ inputs.dockerhub-push }}
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- name: Generate docker image tags
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
env:
DOCKER_METADATA_PR_HEAD_SHA: 'true'
with:
flavor: |
# Disable latest tag
latest=false
suffix=${{ inputs.tag-suffix }}
images: |
name=${{ env.GHCR_IMAGE }}
name=${{ env.DOCKERHUB_IMAGE }},enable=${{ inputs.dockerhub-push }}
tags: |
# Tag with branch name
type=ref,event=branch
# Tag with pr-number
type=ref,event=pr
# Tag with long commit sha hash
type=sha,format=long,prefix=commit-
# Tag with git tag on release
type=ref,event=tag
type=raw,value=release,enable=${{ github.event_name == 'release' }}
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
run: |
# Process annotations
declare -a ANNOTATIONS=()
if [[ -n "$DOCKER_METADATA_OUTPUT_JSON" ]]; then
while IFS= read -r annotation; do
# Extract key and value by removing the manifest: prefix
if [[ "$annotation" =~ ^manifest:(.+)=(.+)$ ]]; then
key="${BASH_REMATCH[1]}"
value="${BASH_REMATCH[2]}"
# Use array to properly handle arguments with spaces
ANNOTATIONS+=(--annotation "index:$key=$value")
fi
done < <(jq -r '.annotations[]' <<< "$DOCKER_METADATA_OUTPUT_JSON")
fi
TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
SOURCE_ARGS=$(printf "${GHCR_IMAGE}@sha256:%s " *)
docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS
+2 -2
View File
@@ -1,7 +1,7 @@
name: PR Label Validation name: PR Label Validation
on: on:
pull_request_target: # zizmor: ignore[dangerous-triggers] no attacker inputs are used here pull_request_target:
types: [opened, labeled, unlabeled, synchronize] types: [opened, labeled, unlabeled, synchronize]
permissions: {} permissions: {}
@@ -14,7 +14,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Require PR to have a changelog label - name: Require PR to have a changelog label
uses: mheap/github-action-required-labels@388fd6af37b34cdfe5a23b37060e763217e58b03 # v5.5.0 uses: mheap/github-action-required-labels@388fd6af37b34cdfe5a23b37060e763217e58b03 # v5
with: with:
mode: exactly mode: exactly
count: 1 count: 1
+2 -2
View File
@@ -1,6 +1,6 @@
name: 'Pull Request Labeler' name: 'Pull Request Labeler'
on: on:
- pull_request_target # zizmor: ignore[dangerous-triggers] no attacker inputs are used here - pull_request_target
permissions: {} permissions: {}
@@ -11,4 +11,4 @@ jobs:
pull-requests: write pull-requests: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0 - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5
+9 -14
View File
@@ -32,29 +32,26 @@ jobs:
steps: steps:
- name: Generate a token - name: Generate a token
id: generate-token id: generate-token
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout - name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}
persist-credentials: true persist-credentials: true
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
- name: Bump version - name: Bump version
env: run: misc/release/pump-version.sh -s "${{ inputs.serverBump }}" -m "${{ inputs.mobileBump }}"
SERVER_BUMP: ${{ inputs.serverBump }}
MOBILE_BUMP: ${{ inputs.mobileBump }}
run: misc/release/pump-version.sh -s "${SERVER_BUMP}" -m "${MOBILE_BUMP}"
- name: Commit and tag - name: Commit and tag
id: push-tag id: push-tag
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4 uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9
with: with:
default_author: github_actions default_author: github_actions
message: 'chore: version ${{ env.IMMICH_VERSION }}' message: 'chore: version ${{ env.IMMICH_VERSION }}'
@@ -64,8 +61,6 @@ jobs:
build_mobile: build_mobile:
uses: ./.github/workflows/build-mobile.yml uses: ./.github/workflows/build-mobile.yml
needs: bump_version needs: bump_version
permissions:
contents: read
secrets: secrets:
KEY_JKS: ${{ secrets.KEY_JKS }} KEY_JKS: ${{ secrets.KEY_JKS }}
ALIAS: ${{ secrets.ALIAS }} ALIAS: ${{ secrets.ALIAS }}
@@ -83,24 +78,24 @@ jobs:
steps: steps:
- name: Generate a token - name: Generate a token
id: generate-token id: generate-token
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout - name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}
persist-credentials: false persist-credentials: false
- name: Download APK - name: Download APK
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with: with:
name: release-apk-signed name: release-apk-signed
- name: Create draft release - name: Create draft release
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2 uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2
with: with:
draft: true draft: true
tag_name: ${{ env.IMMICH_VERSION }} tag_name: ${{ env.IMMICH_VERSION }}
+2 -2
View File
@@ -13,7 +13,7 @@ jobs:
permissions: permissions:
pull-requests: write pull-requests: write
steps: steps:
- uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2 - uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2
with: with:
message-id: 'preview-status' message-id: 'preview-status'
message: 'Deploying preview environment to https://pr-${{ github.event.pull_request.number }}.preview.internal.immich.cloud/' message: 'Deploying preview environment to https://pr-${{ github.event.pull_request.number }}.preview.internal.immich.cloud/'
@@ -24,7 +24,7 @@ jobs:
permissions: permissions:
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
with: with:
script: | script: |
github.rest.issues.removeLabel({ github.rest.issues.removeLabel({
+2 -2
View File
@@ -16,12 +16,12 @@ jobs:
run: run:
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
# Setup .npmrc file to publish to npm # Setup .npmrc file to publish to npm
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version-file: './open-api/typescript-sdk/.nvmrc' node-version-file: './open-api/typescript-sdk/.nvmrc'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
+5 -32
View File
@@ -20,11 +20,11 @@ jobs:
should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
- id: found_paths - id: found_paths
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
with: with:
filters: | filters: |
mobile: mobile:
@@ -44,12 +44,12 @@ jobs:
contents: read contents: read
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Flutter SDK - name: Setup Flutter SDK
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0 uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2
with: with:
channel: 'stable' channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml flutter-version-file: ./mobile/pubspec.yaml
@@ -67,7 +67,7 @@ jobs:
working-directory: ./mobile working-directory: ./mobile
- name: Find file changes - name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20
id: verify-changed-files id: verify-changed-files
with: with:
files: | files: |
@@ -95,30 +95,3 @@ jobs:
- name: Run dart custom_lint - name: Run dart custom_lint
run: dart run custom_lint run: dart run custom_lint
working-directory: ./mobile working-directory: ./mobile
zizmor:
name: zizmor
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- name: Run zizmor 🌈
run: uvx zizmor --format=sarif . > results.sarif
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
with:
sarif_file: results.sarif
category: zizmor
+43 -141
View File
@@ -17,7 +17,6 @@ jobs:
permissions: permissions:
contents: read contents: read
outputs: outputs:
should_run_i18n: ${{ steps.found_paths.outputs.i18n == 'true' || steps.should_force.outputs.should_force == 'true' }}
should_run_web: ${{ steps.found_paths.outputs.web == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run_web: ${{ steps.found_paths.outputs.web == 'true' || steps.should_force.outputs.should_force == 'true' }}
should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }}
should_run_cli: ${{ steps.found_paths.outputs.cli == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run_cli: ${{ steps.found_paths.outputs.cli == 'true' || steps.should_force.outputs.should_force == 'true' }}
@@ -29,16 +28,14 @@ jobs:
should_run_.github: ${{ steps.found_paths.outputs['.github'] == 'true' || steps.should_force.outputs.should_force == 'true' }} # redundant to have should_force but if someone changes the trigger then this won't have to be changed should_run_.github: ${{ steps.found_paths.outputs['.github'] == 'true' || steps.should_force.outputs.should_force == 'true' }} # redundant to have should_force but if someone changes the trigger then this won't have to be changed
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
- id: found_paths - id: found_paths
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
with: with:
filters: | filters: |
i18n:
- 'i18n/**'
web: web:
- 'web/**' - 'web/**'
- 'i18n/**' - 'i18n/**'
@@ -76,12 +73,12 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
@@ -117,12 +114,12 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version-file: './cli/.nvmrc' node-version-file: './cli/.nvmrc'
@@ -162,12 +159,12 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version-file: './cli/.nvmrc' node-version-file: './cli/.nvmrc'
@@ -187,11 +184,11 @@ jobs:
run: npm run test:cov run: npm run test:cov
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
web-lint: web-unit-tests:
name: Lint Web name: Test & Lint Web
needs: pre-job needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_web == 'true' }} if: ${{ needs.pre-job.outputs.should_run_web == 'true' }}
runs-on: mich runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
defaults: defaults:
@@ -200,12 +197,12 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version-file: './web/.nvmrc' node-version-file: './web/.nvmrc'
@@ -217,7 +214,7 @@ jobs:
run: npm ci run: npm ci
- name: Run linter - name: Run linter
run: npm run lint:p run: npm run lint
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run formatter - name: Run formatter
@@ -228,35 +225,6 @@ jobs:
run: npm run check:svelte run: npm run check:svelte
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
web-unit-tests:
name: Test Web
needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_web == 'true' }}
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: ./web
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './web/.nvmrc'
- name: Run setup typescript-sdk
run: npm ci && npm run build
working-directory: ./open-api/typescript-sdk
- name: Run npm install
run: npm ci
- name: Run tsc - name: Run tsc
run: npm run check:typescript run: npm run check:typescript
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
@@ -265,46 +233,6 @@ jobs:
run: npm run test:cov run: npm run test:cov
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
i18n-tests:
name: Test i18n
needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_i18n == 'true' }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './web/.nvmrc'
- name: Install dependencies
run: npm --prefix=web ci
- name: Format
run: npm --prefix=web run format:i18n
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-files
with:
files: |
i18n/**
- name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true'
env:
CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }}
run: |
echo "ERROR: i18n files not up to date!"
echo "Changed files: ${CHANGED_FILES}"
exit 1
e2e-tests-lint: e2e-tests-lint:
name: End-to-End Lint name: End-to-End Lint
needs: pre-job needs: pre-job
@@ -318,12 +246,12 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version-file: './e2e/.nvmrc' node-version-file: './e2e/.nvmrc'
@@ -361,12 +289,12 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
@@ -381,25 +309,22 @@ jobs:
name: End-to-End Tests (Server & CLI) name: End-to-End Tests (Server & CLI)
needs: pre-job needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_e2e_server_cli == 'true' }} if: ${{ needs.pre-job.outputs.should_run_e2e_server_cli == 'true' }}
runs-on: ${{ matrix.runner }} runs-on: mich
permissions: permissions:
contents: read contents: read
defaults: defaults:
run: run:
working-directory: ./e2e working-directory: ./e2e
strategy:
matrix:
runner: [ubuntu-latest, ubuntu-24.04-arm]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
submodules: 'recursive' submodules: 'recursive'
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version-file: './e2e/.nvmrc' node-version-file: './e2e/.nvmrc'
@@ -429,25 +354,22 @@ jobs:
name: End-to-End Tests (Web) name: End-to-End Tests (Web)
needs: pre-job needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_e2e_web == 'true' }} if: ${{ needs.pre-job.outputs.should_run_e2e_web == 'true' }}
runs-on: ${{ matrix.runner }} runs-on: mich
permissions: permissions:
contents: read contents: read
defaults: defaults:
run: run:
working-directory: ./e2e working-directory: ./e2e
strategy:
matrix:
runner: [ubuntu-latest, ubuntu-24.04-arm]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
submodules: 'recursive' submodules: 'recursive'
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version-file: './e2e/.nvmrc' node-version-file: './e2e/.nvmrc'
@@ -472,21 +394,6 @@ jobs:
run: npx playwright test run: npx playwright test
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
success-check-e2e:
name: End-to-End Tests Success
needs: [e2e-tests-server-cli, e2e-tests-web]
permissions: {}
runs-on: ubuntu-latest
if: always()
steps:
- name: Any jobs failed?
if: ${{ contains(needs.*.result, 'failure') }}
run: exit 1
- name: All jobs passed or skipped
if: ${{ !(contains(needs.*.result, 'failure')) }}
# zizmor: ignore[template-injection]
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
mobile-unit-tests: mobile-unit-tests:
name: Unit Test Mobile name: Unit Test Mobile
needs: pre-job needs: pre-job
@@ -495,20 +402,15 @@ jobs:
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Flutter SDK - name: Setup Flutter SDK
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0 uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2
with: with:
channel: 'stable' channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml flutter-version-file: ./mobile/pubspec.yaml
- name: Generate translation file
run: make translation
working-directory: ./mobile
- name: Run tests - name: Run tests
working-directory: ./mobile working-directory: ./mobile
run: flutter test -j 1 run: flutter test -j 1
@@ -524,13 +426,13 @@ jobs:
run: run:
working-directory: ./machine-learning working-directory: ./machine-learning
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
# TODO: add caching when supported (https://github.com/actions/setup-python/pull/818) # TODO: add caching when supported (https://github.com/actions/setup-python/pull/818)
# with: # with:
# python-version: 3.11 # python-version: 3.11
@@ -564,12 +466,12 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version-file: './.github/.nvmrc' node-version-file: './.github/.nvmrc'
@@ -586,7 +488,7 @@ jobs:
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
@@ -605,12 +507,12 @@ jobs:
contents: read contents: read
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
@@ -624,7 +526,7 @@ jobs:
run: make open-api run: make open-api
- name: Find file changes - name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20
id: verify-changed-files id: verify-changed-files
with: with:
files: | files: |
@@ -641,14 +543,14 @@ jobs:
echo "Changed files: ${CHANGED_FILES}" echo "Changed files: ${CHANGED_FILES}"
exit 1 exit 1
sql-schema-up-to-date: generated-typeorm-migrations-up-to-date:
name: SQL Schema Checks name: TypeORM Checks
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
services: services:
postgres: postgres:
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:14bec5d02e8704081eafd566029204a4eb6bb75f3056cfb34e81c5ab1657a490 image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
env: env:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres POSTGRES_USER: postgres
@@ -666,12 +568,12 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
@@ -689,10 +591,10 @@ jobs:
- name: Generate new migrations - name: Generate new migrations
continue-on-error: true continue-on-error: true
run: npm run migrations:generate src/TestMigration run: npm run migrations:generate TestMigration
- name: Find file changes - name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20
id: verify-changed-files id: verify-changed-files
with: with:
files: | files: |
@@ -713,7 +615,7 @@ jobs:
DB_URL: postgres://postgres:postgres@localhost:5432/immich DB_URL: postgres://postgres:postgres@localhost:5432/immich
- name: Find file changes - name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20
id: verify-changed-sql-files id: verify-changed-sql-files
with: with:
files: | files: |
+3 -4
View File
@@ -15,11 +15,11 @@ jobs:
should_run: ${{ steps.found_paths.outputs.i18n == 'true' && github.head_ref != 'chore/translations'}} should_run: ${{ steps.found_paths.outputs.i18n == 'true' && github.head_ref != 'chore/translations'}}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
- id: found_paths - id: found_paths
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
with: with:
filters: | filters: |
i18n: i18n:
@@ -38,7 +38,7 @@ jobs:
exit 1 exit 1
fi fi
- name: Find Pull Request - name: Find Pull Request
uses: juliangruber/find-pull-request-action@48b6133aa6c826f267ebd33aa2d29470f9d9e7d0 # v1.9.0 uses: juliangruber/find-pull-request-action@48b6133aa6c826f267ebd33aa2d29470f9d9e7d0 # v1
id: find-pr id: find-pr
with: with:
branch: chore/translations branch: chore/translations
@@ -57,5 +57,4 @@ jobs:
run: exit 1 run: exit 1
- name: All jobs passed or skipped - name: All jobs passed or skipped
if: ${{ !(contains(needs.*.result, 'failure')) }} if: ${{ !(contains(needs.*.result, 'failure')) }}
# zizmor: ignore[template-injection]
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}" run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
+31 -49
View File
@@ -1,63 +1,45 @@
{ {
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2,
"editor.formatOnSave": true
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2,
"editor.formatOnSave": true
},
"[css]": { "[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true, "editor.tabSize": 2,
"editor.formatOnSave": true
},
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode",
"editor.tabSize": 2 "editor.tabSize": 2
}, },
"svelte.enable-ts-plugin": true,
"eslint.validate": [
"javascript",
"svelte"
],
"typescript.preferences.importModuleSpecifier": "non-relative",
"[dart]": { "[dart]": {
"editor.defaultFormatter": "Dart-Code.dart-code",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.selectionHighlight": false, "editor.selectionHighlight": false,
"editor.suggest.snippetsPreventQuickSuggestions": false, "editor.suggest.snippetsPreventQuickSuggestions": false,
"editor.suggestSelection": "first", "editor.suggestSelection": "first",
"editor.tabCompletion": "onlySnippets", "editor.tabCompletion": "onlySnippets",
"editor.wordBasedSuggestions": "off" "editor.wordBasedSuggestions": "off",
"editor.defaultFormatter": "Dart-Code.dart-code"
}, },
"[javascript]": { "cSpell.words": [
"editor.codeActionsOnSave": { "immich"
"source.organizeImports": "explicit", ],
"source.removeUnusedImports": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.tabSize": 2
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.tabSize": 2
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.tabSize": 2
},
"[svelte]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.removeUnusedImports": "explicit"
},
"editor.defaultFormatter": "svelte.svelte-vscode",
"editor.formatOnSave": true,
"editor.tabSize": 2
},
"[typescript]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.removeUnusedImports": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.tabSize": 2
},
"cSpell.words": ["immich"],
"editor.formatOnSave": true,
"eslint.validate": ["javascript", "svelte"],
"explorer.fileNesting.enabled": true, "explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": { "explorer.fileNesting.patterns": {
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart", "*.ts": "${capture}.spec.ts,${capture}.mock.ts",
"*.ts": "${capture}.spec.ts,${capture}.mock.ts" "*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart"
}, }
"svelte.enable-ts-plugin": true, }
"typescript.preferences.importModuleSpecifier": "non-relative"
}
-3
View File
@@ -17,9 +17,6 @@ e2e:
prod: prod:
docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
prod-down:
docker compose -f ./docker/docker-compose.prod.yml down --remove-orphans
prod-scale: prod-scale:
docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
+1 -1
View File
@@ -1 +1 @@
22.15.1 22.14.0
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:22.15.0-alpine3.20@sha256:686b8892b69879ef5bfd6047589666933508f9a5451c67320df3070ba0e9807b AS core FROM node:22.14.0-alpine3.20@sha256:40be979442621049f40b1d51a26b55e281246b5de4e5f51a18da7beb6e17e3f9 AS core
WORKDIR /usr/src/open-api/typescript-sdk WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
+163 -1198
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.67", "version": "2.2.61",
"description": "Command Line Interface (CLI) for Immich", "description": "Command Line Interface (CLI) for Immich",
"type": "module", "type": "module",
"exports": "./dist/index.js", "exports": "./dist/index.js",
@@ -21,7 +21,7 @@
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/micromatch": "^4.0.9", "@types/micromatch": "^4.0.9",
"@types/mock-fs": "^4.13.1", "@types/mock-fs": "^4.13.1",
"@types/node": "^22.15.18", "@types/node": "^22.14.0",
"@vitest/coverage-v8": "^3.0.0", "@vitest/coverage-v8": "^3.0.0",
"byte-size": "^9.0.0", "byte-size": "^9.0.0",
"cli-progress": "^3.12.0", "cli-progress": "^3.12.0",
@@ -69,6 +69,6 @@
"micromatch": "^4.0.8" "micromatch": "^4.0.8"
}, },
"volta": { "volta": {
"node": "22.15.1" "node": "22.14.0"
} }
} }
+20 -2
View File
@@ -116,13 +116,13 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:ff21bc0f8194dc9c105b769aeabf9585fea6a8ed649c0781caeac5cb3c247884 image: docker.io/valkey/valkey:8-bookworm@sha256:42cba146593a5ea9a622002c1b7cba5da7be248650cbb64ecb9c6c33d29794b1
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
database: database:
container_name: immich_postgres container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0-pgvectors0.2.0 image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
env_file: env_file:
- .env - .env
environment: environment:
@@ -134,6 +134,24 @@ services:
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data - ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
ports: ports:
- 5432:5432 - 5432:5432
healthcheck:
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
# set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics # set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics
# immich-prometheus: # immich-prometheus:
+12 -4
View File
@@ -56,14 +56,14 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:ff21bc0f8194dc9c105b769aeabf9585fea6a8ed649c0781caeac5cb3c247884 image: docker.io/valkey/valkey:8-bookworm@sha256:42cba146593a5ea9a622002c1b7cba5da7be248650cbb64ecb9c6c33d29794b1
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
restart: always restart: always
database: database:
container_name: immich_postgres container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0-pgvectors0.2.0 image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
env_file: env_file:
- .env - .env
environment: environment:
@@ -75,6 +75,14 @@ services:
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data - ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
ports: ports:
- 5432:5432 - 5432:5432
healthcheck:
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
restart: always restart: always
# set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics # set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics
@@ -82,7 +90,7 @@ services:
container_name: immich_prometheus container_name: immich_prometheus
ports: ports:
- 9090:9090 - 9090:9090
image: prom/prometheus@sha256:78ed1f9050eb9eaf766af6e580230b1c4965728650e332cd1ee918c0c4699775 image: prom/prometheus@sha256:502ad90314c7485892ce696cb14a99fceab9fc27af29f4b427f41bd39701a199
volumes: volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml - ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus - prometheus-data:/prometheus
@@ -94,7 +102,7 @@ services:
command: [ './run.sh', '-disable-reporting' ] command: [ './run.sh', '-disable-reporting' ]
ports: ports:
- 3000:3000 - 3000:3000
image: grafana/grafana:11.6.1-ubuntu@sha256:6fc273288470ef499dd3c6b36aeade093170d4f608f864c5dd3a7fabeae77b50 image: grafana/grafana:11.6.0-ubuntu@sha256:fd8fa48213c624e1a95122f1d93abbf1cf1cbe85fc73212c1e599dbd76c63ff8
volumes: volumes:
- grafana-data:/var/lib/grafana - grafana-data:/var/lib/grafana
+10 -4
View File
@@ -49,24 +49,30 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:ff21bc0f8194dc9c105b769aeabf9585fea6a8ed649c0781caeac5cb3c247884 image: docker.io/valkey/valkey:8-bookworm@sha256:42cba146593a5ea9a622002c1b7cba5da7be248650cbb64ecb9c6c33d29794b1
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
restart: always restart: always
database: database:
container_name: immich_postgres container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0-pgvectors0.2.0 image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
environment: environment:
POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME} POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME} POSTGRES_DB: ${DB_DATABASE_NAME}
POSTGRES_INITDB_ARGS: '--data-checksums' POSTGRES_INITDB_ARGS: '--data-checksums'
# Uncomment the DB_STORAGE_TYPE: 'HDD' var if your database isn't stored on SSDs
# DB_STORAGE_TYPE: 'HDD'
volumes: volumes:
# 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 # 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 - ${DB_DATA_LOCATION}:/var/lib/postgresql/data
healthcheck:
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
restart: always restart: always
volumes: volumes:
+1 -1
View File
@@ -1 +1 @@
22.15.1 22.14.0
+11 -20
View File
@@ -23,32 +23,23 @@ Refer to the official [postgres documentation](https://www.postgresql.org/docs/c
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. 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 Dumps ### Automatic Database Backups
:::warning For convenience, Immich will automatically create database backups by default. The backups are stored in `UPLOAD_LOCATION/backups`.
The automatic database dumps can be used to restore the database in the event of damage to the Postgres database files. As mentioned above, you should make your own backup of these together with the asset folders as noted below.
There is no monitoring for these dumps and you will not be notified if they are unsuccessful. 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.
:::caution #### Trigger Backup
The database dumps do **NOT** contain any pictures or videos, only metadata. They are only usable with a copy of the other files in `UPLOAD_LOCATION` as outlined below.
:::
For disaster-recovery purposes, Immich will automatically create database dumps. The dumps are stored in `UPLOAD_LOCATION/backups`. You are able to trigger a backup in the [admin job status page](http://my.immich.app/admin/jobs-status).
Please be sure to make your own, independent backup of the database together with the asset folders as noted below. Visit the page, open the "Create job" modal from the top right, select "Backup Database" and click "Confirm".
You can adjust the schedule and amount of kept database dumps in the [admin settings](http://my.immich.app/admin/system-settings?isOpen=backup). A job will run and trigger a backup, you can verify this worked correctly by checking the logs or the backup folder.
By default, Immich will keep the last 14 database dumps and create a new dump every day at 2:00 AM. This backup will count towards the last X backups that will be kept based on your settings.
#### Trigger Dump
You are able to trigger a database dump in the [admin job status page](http://my.immich.app/admin/jobs-status).
Visit the page, open the "Create job" modal from the top right, select "Create Database Dump" and click "Confirm".
A job will run and trigger a dump, you can verify this worked correctly by checking the logs or the `backups/` folder.
This dumps will count towards the last `X` dumps that will be kept based on your settings.
#### Restoring #### Restoring
We hope to make restoring simpler in future versions, for now you can find the database dumps in the `UPLOAD_LOCATION/backups` folder on your host. 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. Then please follow the steps in the following section for restoring the database.
### Manual Backup and Restore ### Manual Backup and Restore
+12 -76
View File
@@ -10,16 +10,12 @@ Running with a pre-existing Postgres server can unlock powerful administrative f
## Prerequisites ## Prerequisites
You must install `pgvector` (`>= 0.7.0, < 1.0.0`), as it is a prerequisite for `vchord`. 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'`.
The easiest way to do this on Debian/Ubuntu is by adding the [PostgreSQL Apt repository][pg-apt] and then
running `apt install postgresql-NN-pgvector`, where `NN` is your Postgres version (e.g., `16`).
You must install VectorChord into your instance of Postgres using their [instructions][vchord-install]. After installation, add `shared_preload_libraries = 'vchord.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, vchord.so'`.
:::note :::note
Immich is known to work with Postgres versions `>= 14, < 18`. 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 VectorChord is compatible with your version of Immich. The current accepted range for VectorChord is `>= 0.3.0, < 0.4.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 ## Specifying the connection URL
@@ -57,81 +53,21 @@ CREATE DATABASE <immichdatabasename>;
\c <immichdatabasename> \c <immichdatabasename>
BEGIN; BEGIN;
ALTER DATABASE <immichdatabasename> OWNER TO <immichdbusername>; ALTER DATABASE <immichdatabasename> OWNER TO <immichdbusername>;
CREATE EXTENSION vchord CASCADE; CREATE EXTENSION vectors;
CREATE EXTENSION earthdistance CASCADE; CREATE EXTENSION earthdistance CASCADE;
ALTER DATABASE <immichdatabasename> SET search_path TO "$user", public, vectors;
ALTER SCHEMA vectors OWNER TO <immichdbusername>;
COMMIT; COMMIT;
``` ```
### Updating VectorChord ### Updating pgvecto.rs
When installing a new version of VectorChord, you will need to manually update the extension by connecting to the Immich database and running `ALTER EXTENSION vchord UPDATE;`. When installing a new version of pgvecto.rs, you will need to manually update the extension by connecting to the Immich database and running `ALTER EXTENSION vectors UPDATE;`.
## Migrating to VectorChord ### Common errors
VectorChord is the successor extension to pgvecto.rs, allowing for higher performance, lower memory usage and higher quality results for smart search and facial recognition. #### Permission denied for view
### Migrating from pgvecto.rs If you get the error `driverError: error: permission denied for view pg_vector_index_stat`, you can fix this by connecting to the Immich database and running `GRANT SELECT ON TABLE pg_vector_index_stat TO <immichdbusername>;`.
Support for pgvecto.rs will be dropped in a later release, hence we recommend all users currently using pgvecto.rs to migrate to VectorChord at their convenience. There are two primary approaches to do so. [vectors-install]: https://docs.vectorchord.ai/getting-started/installation.html
The easiest option is to have both extensions installed during the migration:
1. Ensure you still have pgvecto.rs installed
2. Install `pgvector` (`>= 0.7.0, < 1.0.0`). The easiest way to do this is on Debian/Ubuntu by adding the [PostgreSQL Apt repository][pg-apt] and then running `apt install postgresql-NN-pgvector`, where `NN` is your Postgres version (e.g., `16`)
3. [Install VectorChord][vchord-install]
4. Add `shared_preload_libraries= 'vchord.so, vectors.so'` to your `postgresql.conf`, making sure to include _both_ `vchord.so` and `vectors.so`. You may include other libraries here as well if needed
5. Restart the Postgres database
6. If Immich does not have superuser permissions, run the SQL command `CREATE EXTENSION vchord CASCADE;` using psql or your choice of database client
7. Start Immich and wait for the logs `Reindexed face_index` and `Reindexed clip_index` to be output
8. If Immich does not have superuser permissions, run the SQL command `DROP EXTENSION vectors;`
9. Drop the old schema by running `DROP SCHEMA vectors;`
10. Remove the `vectors.so` entry from the `shared_preload_libraries` setting
11. Restart the Postgres database
12. Uninstall pgvecto.rs (e.g. `apt-get purge vectors-pg14` on Debian-based environments, replacing `pg14` as appropriate). `pgvector` must remain installed as it provides the data types used by `vchord`
If it is not possible to have both VectorChord and pgvecto.rs installed at the same time, you can perform the migration with more manual steps:
1. While pgvecto.rs is still installed, run the following SQL command using psql or your choice of database client. Take note of the number outputted by this command as you will need it later
```sql
SELECT atttypmod as dimsize
FROM pg_attribute f
JOIN pg_class c ON c.oid = f.attrelid
WHERE c.relkind = 'r'::char
AND f.attnum > 0
AND c.relname = 'smart_search'::text
AND f.attname = 'embedding'::text;
```
2. Remove references to pgvecto.rs using the below SQL commands
```sql
DROP INDEX IF EXISTS clip_index;
DROP INDEX IF EXISTS face_index;
ALTER TABLE smart_search ALTER COLUMN embedding SET DATA TYPE real[];
ALTER TABLE face_search ALTER COLUMN embedding SET DATA TYPE real[];
```
3. [Install VectorChord][vchord-install]
4. Change the columns back to the appropriate vector types, replacing `<number>` with the number from step 1
```sql
CREATE EXTENSION IF NOT EXISTS vchord CASCADE;
ALTER TABLE smart_search ALTER COLUMN embedding SET DATA TYPE vector(<number>);
ALTER TABLE face_search ALTER COLUMN embedding SET DATA TYPE vector(512);
```
5. Start Immich and let it create new indices using VectorChord
### Migrating from pgvector
1. Ensure you have at least 0.7.0 of pgvector installed. If it is below that, please upgrade it and run the SQL command `ALTER EXTENSION vector UPDATE;` using psql or your choice of database client
2. Follow the Prerequisites to install VectorChord
3. If Immich does not have superuser permissions, run the SQL command `CREATE EXTENSION vchord CASCADE;`
4. Remove the `DB_VECTOR_EXTENSION=pgvector` environmental variable as it will make Immich still use pgvector if set
5. Start Immich and let it create new indices using VectorChord
Note that VectorChord itself uses pgvector types, so you should not uninstall pgvector after following these steps.
[vchord-install]: https://docs.vectorchord.ai/vectorchord/getting-started/installation.html
[pg-apt]: https://www.postgresql.org/download/linux/#generic
+1 -1
View File
@@ -22,7 +22,7 @@ server {
client_max_body_size 50000M; client_max_body_size 50000M;
# Set headers # Set headers
proxy_set_header Host $host; proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
+5 -6
View File
@@ -75,12 +75,11 @@ npm run dev
To see local changes to `@immich/ui` in Immich, do the following: To see local changes to `@immich/ui` in Immich, do the following:
1. Install `@immich/ui` as a sibling to `immich/`, for example `/home/user/immich` and `/home/user/ui` 1. Install `@immich/ui` as a sibling to `immich/`, for example `/home/user/immich` and `/home/user/ui`
2. Build the `@immich/ui` project via `npm run build` 1. Build the `@immich/ui` project via `npm run build`
3. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yaml` file (`../../ui:/usr/ui`) 1. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yaml` file (`../../ui:/usr/ui`)
4. Uncomment the corresponding alias in the `web/vite.config.js` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui')`) 1. Uncomment the corresponding alias in the `web/vite.config.js` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui')`)
5. Uncomment the import statement in `web/src/app.css` file `@import '/usr/ui/dist/theme/default.css';` and comment out `@import '@immich/ui/theme/default.css';` 1. Start up the stack via `make dev`
6. Start up the stack via `make dev` 1. After making changes in `@immich/ui`, rebuild it (`npm run build`)
7. After making changes in `@immich/ui`, rebuild it (`npm run build`)
### Mobile app ### Mobile app
-11
View File
@@ -1,11 +0,0 @@
# Chromecast support
Immich supports the Google's Cast protocol so that photos and videos can be cast to devices such as a Chromecast and a Nest Hub. This feature is considered experimental and has several important limitations listed below. Currently, this feature is only supported by the web client, support on Android and iOS is planned for the future.
## Limitations
To use casting with Immich, there are a few prerequisites:
1. Your instance must be accessed via an HTTPS connection in order for the casting menu to show.
2. Your instance must be publicly accessible via HTTPS and a DNS record for the server must be accessible via Google's DNS servers (`8.8.8.8` and `8.8.4.4`)
3. Videos must be in a format that is compatible with Google Cast. For more info, check out [Google's documentation](https://developers.google.com/cast/docs/media)
+2 -2
View File
@@ -121,6 +121,6 @@ Once this is done, you can continue to step 3 of "Basic Setup".
[hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.transcoding.yml [hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.transcoding.yml
[nvct]: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html [nvct]: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html
[jellyfin-lp]: https://jellyfin.org/docs/general/post-install/transcoding/hardware-acceleration/intel#low-power-encoding [jellyfin-lp]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#configure-and-verify-lp-mode-on-linux
[jellyfin-kernel-bug]: https://jellyfin.org/docs/general/post-install/transcoding/hardware-acceleration/intel#known-issues-and-limitations-on-linux [jellyfin-kernel-bug]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#known-issues-and-limitations
[libmali-rockchip]: https://github.com/tsukumijima/libmali-rockchip/releases [libmali-rockchip]: https://github.com/tsukumijima/libmali-rockchip/releases
+1 -1
View File
@@ -72,7 +72,7 @@ In rare cases, the library watcher can hang, preventing Immich from starting up.
### Nightly job ### Nightly job
There is an automatic scan job that is scheduled to run once a day. This job also cleans up any libraries stuck in deletion. It is possible to trigger the cleanup by clicking "Scan all libraries" in the library management page. There is an automatic scan job that is scheduled to run once a day. This job also cleans up any libraries stuck in deletion. It is possible to trigger the cleanup by clicking "Scan all libraries" in the library managment page.
## Usage ## Usage
@@ -42,7 +42,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
- The GPU must have compute capability 5.2 or greater. - The GPU must have compute capability 5.2 or greater.
- The server must have the official NVIDIA driver installed. - The server must have the official NVIDIA driver installed.
- The installed driver must be >= 545 (it must support CUDA 12.3). - The installed driver must be >= 535 (it must support CUDA 12.2).
- On Linux (except for WSL2), you also need to have [NVIDIA Container Toolkit][nvct] installed. - On Linux (except for WSL2), you also need to have [NVIDIA Container Toolkit][nvct] installed.
#### ROCm #### ROCm
+2 -2
View File
@@ -5,7 +5,7 @@ import TabItem from '@theme/TabItem';
Immich uses Postgres as its search database for both metadata and contextual CLIP search. Immich uses Postgres as its search database for both metadata and contextual CLIP search.
Contextual CLIP search is powered by the [VectorChord](https://github.com/tensorchord/VectorChord) extension, utilizing machine learning models like [CLIP](https://openai.com/research/clip) to provide relevant search results. This allows for freeform searches without requiring specific keywords in the image or video metadata. Contextual CLIP search is powered by the [pgvecto.rs](https://github.com/tensorchord/pgvecto.rs) extension, utilizing machine learning models like [CLIP](https://openai.com/research/clip) to provide relevant search results. This allows for freeform searches without requiring specific keywords in the image or video metadata.
## Advanced Search Filters ## Advanced Search Filters
@@ -92,7 +92,7 @@ Memory and execution time estimates were obtained without acceleration on a 7800
**Execution Time (ms)**: After warming up the model with one pass, the mean execution time of 100 passes with the same input. **Execution Time (ms)**: After warming up the model with one pass, the mean execution time of 100 passes with the same input.
**Memory (MiB)**: The peak RSS usage of the process after performing the above timing benchmark. Does not include image decoding, concurrent processing, the web server, etc., which are relatively constant factors. **Memory (MiB)**: The peak RSS usage of the process afer performing the above timing benchmark. Does not include image decoding, concurrent processing, the web server, etc., which are relatively constant factors.
**Recall (%)**: Evaluated on Crossmodal-3600, the average of the recall@1, recall@5 and recall@10 results for zeroshot image retrieval. Chinese (Simplified), English, French, German, Italian, Japanese, Korean, Polish, Russian, Spanish and Turkish are additionally tested on XTD-10. Chinese (Simplified) and English are additionally tested on Flickr30k. The recall metrics are the average across all tested datasets. **Recall (%)**: Evaluated on Crossmodal-3600, the average of the recall@1, recall@5 and recall@10 results for zeroshot image retrieval. Chinese (Simplified), English, French, German, Italian, Japanese, Korean, Polish, Russian, Spanish and Turkish are additionally tested on XTD-10. Chinese (Simplified) and English are additionally tested on Flickr30k. The recall metrics are the average across all tested datasets.
+5 -5
View File
@@ -14,14 +14,14 @@ online generators you can use.
2. Paste the link to your JSON style in either the **Light Style** or **Dark Style**. (You can add different styles which will help make the map style more appropriate depending on whether you set **Immich** to Light or Dark mode.) 2. Paste the link to your JSON style in either the **Light Style** or **Dark Style**. (You can add different styles which will help make the map style more appropriate depending on whether you set **Immich** to Light or Dark mode.)
3. Save your selections. Reload the map, and enjoy your custom map style! 3. Save your selections. Reload the map, and enjoy your custom map style!
## Use MapTiler to build a custom style ## Use Maptiler to build a custom style
Customizing the map style can be done easily using MapTiler, if you do not want to write an entire JSON document by hand. Customizing the map style can be done easily using Maptiler, if you do not want to write an entire JSON document by hand.
1. Create a free account at https://cloud.maptiler.com 1. Create a free account at https://cloud.maptiler.com
2. Once logged in, you can either create a brand new map by clicking on **New Map**, selecting a starter map, and then clicking **Customize**, OR by selecting a **Standard Map** and customizing it from there. 2. Once logged in, you can either create a brand new map by clicking on **New Map**, selecting a starter map, and then clicking **Customize**, OR by selecting a **Standard Map** and customizing it from there.
3. The **editor** interface is self-explanatory. You can change colors, remove visible layers, or add optional layers (e.g., administrative, topo, hydro, etc.) in the composer. 3. The **editor** interface is self-explanatory. You can change colors, remove visible layers, or add optional layers (e.g., administrative, topo, hydro, etc.) in the composer.
4. Once you have your map composed, click on **Save** at the top right. Give it a unique name to save it to your account. 4. Once you have your map composed, click on **Save** at the top right. Give it a unique name to save it to your account.
5. Next, **Publish** your style using the **Publish** button at the top right. This will deploy it to production, which means it is able to be exposed over the Internet. MapTiler will present an interactive side-by-side map with the original and your changes prior to publication.<br/>![MapTiler Publication Settings](img/immich_map_styles_publish.webp) 5. Next, **Publish** your style using the **Publish** button at the top right. This will deploy it to production, which means it is able to be exposed over the Internet. Maptiler will present an interactive side-by-side map with the original and your changes prior to publication.<br/>![Maptiler Publication Settings](img/immich_map_styles_publish.webp)
6. MapTiler will warn you that changing the map will change it across all apps using the map. Since no apps are using the map yet, this is okay. 6. Maptiler will warn you that changing the map will change it across all apps using the map. Since no apps are using the map yet, this is okay.
7. Clicking on the name of your new map at the top left will bring you to the item's **details** page. From here, copy the link to the JSON style under **Use vector style**. This link will automatically contain your personal API key to MapTiler. 7. Clicking on the name of your new map at the top left will bring you to the item's **details** page. From here, copy the link to the JSON style under **Use vector style**. This link will automatically contain your personal API key to Maptiler.
+1 -1
View File
@@ -1,7 +1,7 @@
# Database Queries # Database Queries
:::danger :::danger
Keep in mind that mucking around in the database might set the Moon on fire. Avoid modifying the database directly when possible, and always have current backups. Keep in mind that mucking around in the database might set the moon on fire. Avoid modifying the database directly when possible, and always have current backups.
::: :::
:::tip :::tip
+45 -2
View File
@@ -2,13 +2,53 @@
sidebar_position: 30 sidebar_position: 30
--- ---
import CodeBlock from '@theme/CodeBlock';
import ExampleEnv from '!!raw-loader!../../../docker/example.env';
# Docker Compose [Recommended] # Docker Compose [Recommended]
Docker Compose is the recommended method to run Immich in production. Below are the steps to deploy Immich with Docker Compose. Docker Compose is the recommended method to run Immich in production. Below are the steps to deploy Immich with Docker Compose.
import DockerComposeSteps from '/docs/partials/_docker-compose-install-steps.mdx'; ## Step 1 - Download the required files
<DockerComposeSteps /> Create a directory of your choice (e.g. `./immich-app`) to hold the `docker-compose.yml` and `.env` files.
```bash title="Move to the directory you created"
mkdir ./immich-app
cd ./immich-app
```
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
```
```bash title="Get .env file"
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`.
## Step 2 - Populate the .env file with custom values
<CodeBlock language="bash" title="Default environmental variable content">
{ExampleEnv}
</CodeBlock>
- 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 publicly 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`. `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
From the directory you created in Step 1 (which should now contain your customized `docker-compose.yml` and `.env` files), run the following command to start Immich as a background service:
```bash title="Start the containers"
docker compose up -d
```
:::info Docker version :::info Docker version
If you get an error such as `unknown shorthand flag: 'd' in -d` or `open <location of your .env file>: permission denied`, 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. If you get an error such as `unknown shorthand flag: 'd' in -d` or `open <location of your .env file>: permission denied`, 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.
@@ -30,3 +70,6 @@ If you get an error `can't set healthcheck.start_interval as feature require Doc
## Next Steps ## Next Steps
Read the [Post Installation](/docs/install/post-install.mdx) steps and [upgrade instructions](/docs/install/upgrading.md). Read the [Post Installation](/docs/install/post-install.mdx) steps and [upgrade instructions](/docs/install/upgrading.md).
[compose-file]: https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
[env-file]: https://github.com/immich-app/immich/releases/latest/download/example.env
+36 -36
View File
@@ -72,21 +72,20 @@ Information on the current workers can be found [here](/docs/administration/jobs
## Database ## Database
| Variable | Description | Default | Containers | | Variable | Description | Default | Containers |
| :---------------------------------- | :--------------------------------------------------------------------------- | :--------: | :----------------------------- | | :---------------------------------- | :----------------------------------------------------------------------- | :----------: | :----------------------------- |
| `DB_URL` | Database URL | | server | | `DB_URL` | Database URL | | server |
| `DB_HOSTNAME` | Database host | `database` | server | | `DB_HOSTNAME` | Database host | `database` | server |
| `DB_PORT` | Database port | `5432` | server | | `DB_PORT` | Database port | `5432` | server |
| `DB_USERNAME` | Database user | `postgres` | server, database<sup>\*1</sup> | | `DB_USERNAME` | Database user | `postgres` | server, database<sup>\*1</sup> |
| `DB_PASSWORD` | Database password | `postgres` | server, database<sup>\*1</sup> | | `DB_PASSWORD` | Database password | `postgres` | server, database<sup>\*1</sup> |
| `DB_DATABASE_NAME` | Database name | `immich` | server, database<sup>\*1</sup> | | `DB_DATABASE_NAME` | Database name | `immich` | server, database<sup>\*1</sup> |
| `DB_SSL_MODE` | Database SSL mode | | server | | `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`pgvector`, `pgvecto.rs`]) | `pgvecto.rs` | server |
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`vectorchord`, `pgvector`, `pgvecto.rs`]) | | server | | `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
\*1: The values of `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE_NAME` are passed to the Postgres container as the variables `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` in `docker-compose.yml`. \*1: The values of `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE_NAME` are passed to the Postgres container as the variables `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` in `docker-compose.yml`.
\*2: If not provided, the appropriate extension to use is auto-detected at startup by introspecting the database. When multiple extensions are installed, the order of preference is VectorChord, pgvecto.rs, pgvector. \*2: This setting cannot be changed after the server has successfully started up.
:::info :::info
@@ -149,30 +148,31 @@ Redis (Sentinel) URL example JSON before encoding:
## Machine Learning ## Machine Learning
| Variable | Description | Default | Containers | | 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` | 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_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_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_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_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_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_MODEL_ARENA` | Pre-allocates CPU memory to avoid memory fragmentation | true | machine learning |
| `MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S`<sup>\*3</sup> | HTTP Keep-alive time in seconds | `2` | machine learning | | `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | 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_HTTP_KEEPALIVE_TIMEOUT_S`<sup>\*3</sup> | HTTP Keep-alive time in seconds | `2` | machine learning |
| `MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL` | Comma-separated list of (textual) CLIP model(s) to preload and cache | | machine learning | | `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `300` | machine learning |
| `MACHINE_LEARNING_PRELOAD__CLIP__VISUAL` | Comma-separated list of (visual) CLIP model(s) to preload and cache | | machine learning | | `MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL` | Comma-separated list of (textual) CLIP model(s) to preload and cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION` | Comma-separated list of (recognition) facial recognition model(s) to preload and cache | | machine learning | | `MACHINE_LEARNING_PRELOAD__CLIP__VISUAL` | Comma-separated list of (visual) CLIP model(s) to preload and cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION` | Comma-separated list of (detection) facial recognition model(s) to preload and cache | | machine learning | | `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION` | Comma-separated list of (recognition) facial recognition model(s) to preload and cache | | machine learning |
| `MACHINE_LEARNING_ANN` | Enable ARM-NN hardware acceleration if supported | `True` | machine learning | | `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION` | Comma-separated list of (detection) facial recognition model(s) to preload and cache | | 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` | Enable ARM-NN hardware acceleration if supported | `True` | machine learning |
| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | 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_DEVICE_IDS`<sup>\*4</sup> | Device IDs to use in multi-GPU environments | `0` | machine learning | | `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | 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 | | `MACHINE_LEARNING_DEVICE_IDS`<sup>\*4</sup> | Device IDs to use in multi-GPU environments | `0` | machine learning |
| `MACHINE_LEARNING_PING_TIMEOUT` | How long (ms) to wait for a PING response when checking if an ML server is available | `2000` | server | | `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 |
| `MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME` | How long to ignore ML servers that are offline before trying again | `30000` | server | | `MACHINE_LEARNING_PING_TIMEOUT` | How long (ms) to wait for a PING response when checking if an ML server is available | `2000` | server |
| `MACHINE_LEARNING_RKNN` | Enable RKNN hardware acceleration if supported | `True` | machine learning | | `MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME` | How long to ignore ML servers that are offline before trying again | `30000` | server |
| `MACHINE_LEARNING_RKNN_THREADS` | How many threads of RKNN runtime should be spinned up while inferencing. | `1` | machine learning | | `MACHINE_LEARNING_RKNN` | Enable RKNN hardware acceleration if supported | `True` | machine learning |
| `MACHINE_LEARNING_RKNN_THREADS` | How many threads of RKNN runtime should be spinned up while inferencing. | `1` | 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. \*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.
+1 -1
View File
@@ -29,7 +29,7 @@ Download [`docker-compose.yml`](https://github.com/immich-app/immich/releases/la
## Step 2 - Populate the .env file with custom values ## Step 2 - Populate the .env file with custom values
Follow [Step 2 in Docker Compose](/docs/install/docker-compose#step-2---populate-the-env-file-with-custom-values) for instructions on customizing the `.env` file, and then return back to this guide to continue. Follow [Step 2 in Docker Compose](./docker-compose#step-2---populate-the-env-file-with-custom-values) for instructions on customizing the `.env` file, and then return back to this guide to continue.
## Step 3 - Create a new project in Container Manager ## Step 3 - Create a new project in Container Manager
+16 -16
View File
@@ -2,7 +2,7 @@
sidebar_position: 80 sidebar_position: 80
--- ---
# TrueNAS [Community] # TrueNAS SCALE [Community]
:::note :::note
This is a community contribution and not officially supported by the Immich team, but included here for convenience. This is a community contribution and not officially supported by the Immich team, but included here for convenience.
@@ -12,17 +12,17 @@ Community support can be found in the dedicated channel on the [Discord Server](
**Please report app issues to the corresponding [Github Repository](https://github.com/truenas/charts/tree/master/community/immich).** **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 Community Edition via the **Community** train application. Immich can easily be installed on TrueNAS SCALE via the **Community** train application.
Consider reviewing the TrueNAS [Apps resources](https://apps.truenas.com/getting-started/) if you have not previously configured applications on your system. Consider reviewing the TrueNAS [Apps tutorial](https://www.truenas.com/docs/scale/scaletutorials/apps/) if you have not previously configured applications on your system.
TrueNAS Community Edition makes installing and updating Immich easy, but you must use the Immich web portal and mobile app to configure accounts and access libraries. TrueNAS SCALE makes installing and updating Immich easy, but you must use the Immich web portal and mobile app to configure accounts and access libraries.
## First Steps ## First Steps
The Immich app in TrueNAS Community Edition installs, completes the initial configuration, then starts the Immich web portal. The Immich app in TrueNAS SCALE installs, completes the initial configuration, then starts the Immich web portal.
When updates become available, TrueNAS alerts and provides easy updates. When updates become available, SCALE alerts and provides easy updates.
Before installing the Immich app in TrueNAS, review the [Environment Variables](#environment-variables) documentation to see if you want to configure any during installation. 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 may also configure environment variables at any time after deploying the application.
### Setting up Storage Datasets ### Setting up Storage Datasets
@@ -126,9 +126,9 @@ className="border rounded-xl"
Accept the default port `30041` in **WebUI Port** or enter a custom port number. Accept the default port `30041` in **WebUI Port** or enter a custom port number.
:::info Allowed Port Numbers :::info Allowed Port Numbers
Only numbers within the range 9000-65535 may be used on TrueNAS versions below TrueNAS Community Edition 24.10 Electric Eel. 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/solutions/optimizations/security/#truenas-default-ports). Regardless of version, to avoid port conflicts, don't use [ports on this list](https://www.truenas.com/docs/references/defaultports/).
::: :::
### Storage Configuration ### Storage Configuration
@@ -173,7 +173,7 @@ className="border rounded-xl"
You may configure [External Libraries](/docs/features/libraries) by mounting them using **Additional Storage**. You may configure [External Libraries](/docs/features/libraries) by mounting them using **Additional Storage**.
The **Mount Path** is the location you will need to copy and paste into the External Library settings within Immich. The **Mount Path** is the location you will need to copy and paste into the External Library settings within Immich.
The **Host Path** is the location on the TrueNAS Community Edition server where your external library is located. 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. --> <!-- A section for Labels would go here but I don't know what they do. -->
@@ -188,17 +188,17 @@ 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 **CPU** limit of `2` threads or specify the number of threads (CPUs with Multi-/Hyper-threading have 2 threads per core).
Specify the **Memory** limit in MB of RAM. Immich recommends at least 6000 MB (6GB). If you selected **Enable Machine Learning** in **Immich Configuration**, you should probably set this above 8000 MB. 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 TrueNAS Versions :::info Older SCALE Versions
Before TrueNAS Community Edition version 24.10 Electric Eel: 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 **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` 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 Passthrough Docs for TrueNAS Apps](https://apps.truenas.com/managing-apps/installing-apps/#gpu-passthrough) 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 Passthrough Docs for TrueNAS Apps](https://www.truenas.com/docs/truenasapps/#gpu-passthrough)
### Install ### Install
@@ -240,7 +240,7 @@ className="border rounded-xl"
/> />
:::info :::info
Some Environment Variables are not available for the TrueNAS Community Edition app. This is mainly because they can be configured through GUI options in the [Edit Immich screen](#edit-app-settings). 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`. Some examples are: `IMMICH_VERSION`, `UPLOAD_LOCATION`, `DB_DATA_LOCATION`, `TZ`, `IMMICH_LOG_LEVEL`, `DB_PASSWORD`, `REDIS_PASSWORD`.
::: :::
@@ -251,7 +251,7 @@ Some examples are: `IMMICH_VERSION`, `UPLOAD_LOCATION`, `DB_DATA_LOCATION`, `TZ`
Make sure to read the general [upgrade instructions](/docs/install/upgrading.md). Make sure to read the general [upgrade instructions](/docs/install/upgrading.md).
::: :::
When updates become available, TrueNAS alerts and provides easy updates. When updates become available, SCALE alerts and provides easy updates.
To update the app to the latest version: To update the app to the latest version:
- Go to the **Installed Applications** screen and select Immich from the list of installed applications. - Go to the **Installed Applications** screen and select Immich from the list of installed applications.
+1 -1
View File
@@ -1,5 +1,5 @@
--- ---
sidebar_position: 3 sidebar_position: 2
--- ---
# Comparison # Comparison
Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

@@ -2,13 +2,9 @@
sidebar_position: 1 sidebar_position: 1
--- ---
# Welcome to Immich # Introduction
<img <img src={require('./img/feature-panel.webp').default} alt="Immich - Self-hosted photos and videos backup tool" />
src={require('./img/social-preview-light.webp').default}
alt="Immich - Self-hosted photos and videos backup tool"
data-theme="light"
/>
## Welcome! ## Welcome!
+3 -18
View File
@@ -1,5 +1,5 @@
--- ---
sidebar_position: 2 sidebar_position: 3
--- ---
# Quick start # Quick start
@@ -10,20 +10,11 @@ to install and use it.
## Requirements ## Requirements
- A system with at least 4GB of RAM and 2 CPU cores. Check the [requirements page](/docs/install/requirements) to get started.
- [Docker](https://docs.docker.com/engine/install/)
> For a more detailed list of requirements, see the [requirements page](/docs/install/requirements).
---
## Set up the server ## Set up the server
import DockerComposeSteps from '/docs/partials/_docker-compose-install-steps.mdx'; Follow the [Docker Compose (Recommended)](/docs/install/docker-compose) instructions to install the server.
<DockerComposeSteps />
---
## Try the web app ## Try the web app
@@ -35,8 +26,6 @@ Try uploading a picture from your browser.
<img src={require('./img/upload-button.webp').default} title="Upload button" /> <img src={require('./img/upload-button.webp').default} title="Upload button" />
---
## Try the mobile app ## Try the mobile app
### Download the Mobile App ### Download the Mobile App
@@ -67,8 +56,6 @@ You can select the **Jobs** tab to see Immich processing your photos.
<img src={require('/docs/guides/img/jobs-tab.webp').default} title="Jobs tab" width={300} /> <img src={require('/docs/guides/img/jobs-tab.webp').default} title="Jobs tab" width={300} />
---
## Review the database backup and restore process ## Review the database backup and restore process
Immich has built-in database backups. You can refer to the Immich has built-in database backups. You can refer to the
@@ -78,8 +65,6 @@ Immich has built-in database backups. You can refer to the
The database only contains metadata and user information. You must setup manual backups of the images and videos stored in `UPLOAD_LOCATION`. The database only contains metadata and user information. You must setup manual backups of the images and videos stored in `UPLOAD_LOCATION`.
::: :::
---
## Where to go from here? ## Where to go from here?
You may decide you'd like to install the server a different way; the Install category on the left menu provides many options. You may decide you'd like to install the server a different way; the Install category on the left menu provides many options.
@@ -1,43 +0,0 @@
import CodeBlock from '@theme/CodeBlock';
import ExampleEnv from '!!raw-loader!../../../docker/example.env';
### 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.
```bash title="Move to the directory you created"
mkdir ./immich-app
cd ./immich-app
```
Download [`docker-compose.yml`](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml) and [`example.env`](https://github.com/immich-app/immich/releases/latest/download/example.env) 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
```
```bash title="Get .env file"
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`.
### Step 2 - Populate the .env file with custom values
<CodeBlock language="bash" title="Default environmental variable content">
{ExampleEnv}
</CodeBlock>
- 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 publicly 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`. `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
From the directory you created in Step 1 (which should now contain your customized `docker-compose.yml` and `.env` files), run the following command to start Immich as a background service:
```bash title="Start the containers"
docker compose up -d
```
+2 -8
View File
@@ -95,7 +95,7 @@ const config = {
position: 'right', position: 'right',
}, },
{ {
to: '/docs/overview/welcome', to: '/docs/overview/introduction',
position: 'right', position: 'right',
label: 'Docs', label: 'Docs',
}, },
@@ -124,12 +124,6 @@ const config = {
label: 'Discord', label: 'Discord',
position: 'right', position: 'right',
}, },
{
type: 'html',
position: 'right',
value:
'<a href="https://buy.immich.app" target="_blank" class="no-underline hover:no-underline"><button class="buy-button bg-immich-primary dark:bg-immich-dark-primary text-white dark:text-black rounded-xl">Buy Immich</button></a>',
},
], ],
}, },
footer: { footer: {
@@ -140,7 +134,7 @@ const config = {
items: [ items: [
{ {
label: 'Welcome', label: 'Welcome',
to: '/docs/overview/welcome', to: '/docs/overview/introduction',
}, },
{ {
label: 'Installation', label: 'Installation',
+1 -1
View File
@@ -57,6 +57,6 @@
"node": ">=20" "node": ">=20"
}, },
"volta": { "volta": {
"node": "22.15.1" "node": "22.14.0"
} }
} }
+7 -3
View File
@@ -40,9 +40,13 @@ const projects: CommunityProjectProps[] = [
}, },
{ {
title: 'Lightroom Immich Plugin: lrc-immich-plugin', title: 'Lightroom Immich Plugin: lrc-immich-plugin',
description: description: 'Another Lightroom plugin to publish or export photos from Lightroom to Immich.',
'Lightroom plugin to publish, export photos from Lightroom to Immich. Import from Immich to Lightroom is also supported.', url: 'https://github.com/bmachek/lrc-immich-plugin',
url: 'https://blog.fokuspunk.de/lrc-immich-plugin/', },
{
title: 'Immich Duplicate Finder',
description: 'Webapp that uses machine learning to identify near-duplicate images.',
url: 'https://github.com/vale46n1/immich_duplicate_finder',
}, },
{ {
title: 'Immich-Tiktok-Remover', title: 'Immich-Tiktok-Remover',
+9 -25
View File
@@ -7,22 +7,14 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@font-face { @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');
font-family: 'Overpass';
src: url('/fonts/overpass/Overpass.ttf') format('truetype-variations');
font-weight: 1 999;
font-style: normal;
ascent-override: 106.25%;
size-adjust: 106.25%;
}
@font-face { body {
font-family: 'Overpass Mono'; font-family: 'Be Vietnam Pro', serif;
src: url('/fonts/overpass/OverpassMono.ttf') format('truetype-variations'); font-optical-sizing: auto;
font-weight: 1 999; /* font-size: 1.125rem;
font-style: normal;
ascent-override: 106.25%; ascent-override: 106.25%;
size-adjust: 106.25%; size-adjust: 106.25%; */
} }
.breadcrumbs__link { .breadcrumbs__link {
@@ -37,7 +29,6 @@ img {
/* You can override the default Infima variables here. */ /* You can override the default Infima variables here. */
:root { :root {
font-family: 'Overpass', sans-serif;
--ifm-color-primary: #4250af; --ifm-color-primary: #4250af;
--ifm-color-primary-dark: #4250af; --ifm-color-primary-dark: #4250af;
--ifm-color-primary-darker: #4250af; --ifm-color-primary-darker: #4250af;
@@ -68,12 +59,14 @@ div[class^='announcementBar_'] {
} }
.menu__link { .menu__link {
padding: 10px 10px 10px 16px; padding: 10px;
padding-left: 16px;
border-radius: 24px; border-radius: 24px;
margin-right: 16px; margin-right: 16px;
} }
.menu__list-item-collapsible { .menu__list-item-collapsible {
border-radius: 10px;
margin-right: 16px; margin-right: 16px;
border-radius: 24px; border-radius: 24px;
} }
@@ -90,12 +83,3 @@ div[class*='navbar__items'] > li:has(a[class*='version-switcher-34ab39']) {
code { code {
font-weight: 600; font-weight: 600;
} }
.buy-button {
padding: 8px 14px;
border: 1px solid transparent;
font-family: 'Overpass', sans-serif;
font-weight: 500;
cursor: pointer;
box-shadow: 0 0 5px 2px rgba(181, 206, 254, 0.4);
}
-13
View File
@@ -2,7 +2,6 @@ import {
mdiBug, mdiBug,
mdiCalendarToday, mdiCalendarToday,
mdiCrosshairsOff, mdiCrosshairsOff,
mdiCrop,
mdiDatabase, mdiDatabase,
mdiLeadPencil, mdiLeadPencil,
mdiLockOff, mdiLockOff,
@@ -23,18 +22,6 @@ const withLanguage = (date: Date) => (language: string) => date.toLocaleDateStri
type Item = Omit<TimelineItem, 'done' | 'getDateLabel'> & { date: Date }; type Item = Omit<TimelineItem, 'done' | 'getDateLabel'> & { date: Date };
const items: Item[] = [ const items: Item[] = [
{
icon: mdiCrop,
iconColor: 'tomato',
title: 'Image dimensions in EXIF metadata are cursed',
description:
'The dimensions in EXIF metadata can be different from the actual dimensions of the image, causing issues with cropping and resizing.',
link: {
url: 'https://github.com/immich-app/immich/pull/17974',
text: '#17974',
},
date: new Date(2025, 5, 5),
},
{ {
icon: mdiMicrosoftWindows, icon: mdiMicrosoftWindows,
iconColor: '#357EC7', iconColor: '#357EC7',
+20 -25
View File
@@ -4,7 +4,6 @@ import Layout from '@theme/Layout';
import { discordPath, discordViewBox } from '@site/src/components/svg-paths'; import { discordPath, discordViewBox } from '@site/src/components/svg-paths';
import ThemedImage from '@theme/ThemedImage'; import ThemedImage from '@theme/ThemedImage';
import Icon from '@mdi/react'; import Icon from '@mdi/react';
function HomepageHeader() { function HomepageHeader() {
return ( return (
<header> <header>
@@ -13,14 +12,11 @@ function HomepageHeader() {
<div className="w-full h-[120vh] absolute left-0 top-0 backdrop-blur-3xl bg-immich-bg/40 dark:bg-transparent"></div> <div className="w-full h-[120vh] absolute left-0 top-0 backdrop-blur-3xl bg-immich-bg/40 dark:bg-transparent"></div>
</div> </div>
<section className="text-center pt-12 sm:pt-24 bg-immich-bg/50 dark:bg-immich-dark-bg/80"> <section className="text-center pt-12 sm:pt-24 bg-immich-bg/50 dark:bg-immich-dark-bg/80">
<a href="https://futo.org" target="_blank" rel="noopener noreferrer"> <ThemedImage
<ThemedImage sources={{ dark: 'img/logomark-dark.svg', light: 'img/logomark-light.svg' }}
sources={{ dark: 'img/logomark-dark-with-futo.svg', light: 'img/logomark-light-with-futo.svg' }} className="h-[115px] w-[115px] mb-2 antialiased rounded-none"
className="h-[125px] w-[125px] mb-2 antialiased rounded-none" alt="Immich logo"
alt="Immich logo" />
/>
</a>
<div className="mt-8"> <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"> <p className="text-3xl md:text-5xl sm:leading-tight mb-1 font-extrabold text-black/90 dark:text-white px-4">
Self-hosted{' '} Self-hosted{' '}
@@ -31,7 +27,7 @@ function HomepageHeader() {
solution<span className="block"></span> solution<span className="block"></span>
</p> </p>
<p className="max-w-1/4 m-auto mt-4 px-4 text-lg text-gray-700 dark:text-gray-100"> <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 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 <span className="sm:block"></span> browse, search and organize your photos and videos with ease, without
sacrificing your privacy. sacrificing your privacy.
@@ -39,21 +35,27 @@ function HomepageHeader() {
</div> </div>
<div className="flex flex-col sm:flex-row place-items-center place-content-center mt-9 gap-4 "> <div className="flex flex-col sm:flex-row place-items-center place-content-center mt-9 gap-4 ">
<Link <Link
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" 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/quick-start" to="docs/overview/introduction"
> >
Get Started Get started
</Link> </Link>
<Link <Link
className="flex place-items-center place-content-center py-3 px-8 border bg-white/90 dark:bg-gray-300 rounded-xl hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold" 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/" to="https://demo.immich.app/"
> >
Open Demo Demo
</Link>
<Link
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://immich.store"
>
Buy Merch
</Link> </Link>
</div> </div>
<div className="my-12 flex gap-1 font-medium place-items-center place-content-center text-immich-primary dark:text-immich-dark-primary">
<div className="my-8 flex gap-1 font-medium place-items-center place-content-center text-immich-primary dark:text-immich-dark-primary">
<Icon <Icon
path={discordPath} path={discordPath}
viewBox={discordViewBox} /* viewBox may show an error in your IDE but it is normal. */ viewBox={discordViewBox} /* viewBox may show an error in your IDE but it is normal. */
@@ -86,18 +88,11 @@ function HomepageHeader() {
<img className="h-24" alt="Get it on Google Play" src="/img/google-play-badge.png" /> <img className="h-24" alt="Get it on Google Play" src="/img/google-play-badge.png" />
</a> </a>
</div> </div>
<div className="h-24"> <div className="h-24">
<a href="https://apps.apple.com/sg/app/immich/id1613945652"> <a href="https://apps.apple.com/sg/app/immich/id1613945652">
<img className="h-24 sm:p-3.5 p-3" alt="Download on the App Store" src="/img/ios-app-store-badge.svg" /> <img className="h-24 sm:p-3.5 p-3" alt="Download on the App Store" src="/img/ios-app-store-badge.svg" />
</a> </a>
</div> </div>
<div className="h-24">
<a href="https://github.com/immich-app/immich/releases/latest">
<img className="h-24 sm:p-3.5 p-3" alt="Download APK" src="/img/download-apk-github.svg" />
</a>
</div>
</div> </div>
<ThemedImage <ThemedImage
sources={{ dark: '/img/app-qr-code-dark.svg', light: '/img/app-qr-code-light.svg' }} sources={{ dark: '/img/app-qr-code-dark.svg', light: '/img/app-qr-code-light.svg' }}
@@ -116,7 +111,7 @@ export default function Home(): JSX.Element {
<HomepageHeader /> <HomepageHeader />
<div className="flex flex-col place-items-center text-center place-content-center dark:bg-immich-dark-bg py-8"> <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>This project is available under GNU AGPL v3 license.</p>
<p className="text-sm">Privacy should not be a luxury</p> <p className="text-xs">Privacy should not be a luxury</p>
</div> </div>
</Layout> </Layout>
); );
+1
View File
@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import Link from '@docusaurus/Link';
import Layout from '@theme/Layout'; import Layout from '@theme/Layout';
function HomepageHeader() { function HomepageHeader() {
return ( return (
+11 -37
View File
@@ -76,18 +76,13 @@ import {
mdiWeb, mdiWeb,
mdiDatabaseOutline, mdiDatabaseOutline,
mdiLinkEdit, mdiLinkEdit,
mdiTagFaces,
mdiMovieOpenPlayOutline, mdiMovieOpenPlayOutline,
mdiCast,
} from '@mdi/js'; } from '@mdi/js';
import Layout from '@theme/Layout'; import Layout from '@theme/Layout';
import React from 'react'; import React from 'react';
import { Item, Timeline } from '../components/timeline'; import { Item, Timeline } from '../components/timeline';
const releases = { const releases = {
'v1.133.0': new Date(2025, 4, 21),
'v1.130.0': new Date(2025, 2, 25),
'v1.127.0': new Date(2025, 1, 26),
'v1.122.0': new Date(2024, 11, 5), 'v1.122.0': new Date(2024, 11, 5),
'v1.120.0': new Date(2024, 10, 6), 'v1.120.0': new Date(2024, 10, 6),
'v1.114.0': new Date(2024, 8, 6), 'v1.114.0': new Date(2024, 8, 6),
@@ -218,6 +213,14 @@ const roadmap: Item[] = [
iconColor: 'indianred', iconColor: 'indianred',
title: 'Stable release', title: 'Stable release',
description: 'Immich goes stable', description: 'Immich goes stable',
getDateLabel: () => 'Planned for early 2025',
},
{
done: false,
icon: mdiLockOutline,
iconColor: 'sandybrown',
title: 'Private/locked photos',
description: 'Private assets with extra protections',
getDateLabel: () => 'Planned for 2025', getDateLabel: () => 'Planned for 2025',
}, },
{ {
@@ -239,27 +242,6 @@ const roadmap: Item[] = [
]; ];
const milestones: Item[] = [ const milestones: Item[] = [
withRelease({
icon: mdiCast,
iconColor: 'aqua',
title: 'Google Cast (web)',
description: 'Cast assets to Google Cast/Chromecast compatible devices',
release: 'v1.133.0',
}),
withRelease({
icon: mdiLockOutline,
iconColor: 'sandybrown',
title: 'Private/locked photos',
description: 'Private assets with extra protections',
release: 'v1.133.0',
}),
withRelease({
icon: mdiFolderMultiple,
iconColor: 'brown',
title: 'Folders view in the mobile app',
description: 'Browse your photos and videos in their folder structure inside the mobile app',
release: 'v1.130.0',
}),
{ {
icon: mdiStar, icon: mdiStar,
iconColor: 'gold', iconColor: 'gold',
@@ -267,14 +249,6 @@ const milestones: Item[] = [
description: 'Reached 60K Stars on GitHub!', description: 'Reached 60K Stars on GitHub!',
getDateLabel: withLanguage(new Date(2025, 2, 4)), getDateLabel: withLanguage(new Date(2025, 2, 4)),
}, },
withRelease({
icon: mdiTagFaces,
iconColor: 'teal',
title: 'Manual face tagging',
description:
'Manually tag or remove faces in photos and videos, even when automatic detection misses or misidentifies them.',
release: 'v1.127.0',
}),
withRelease({ withRelease({
icon: mdiLinkEdit, icon: mdiLinkEdit,
iconColor: 'crimson', iconColor: 'crimson',
@@ -292,8 +266,8 @@ const milestones: Item[] = [
withRelease({ withRelease({
icon: mdiDatabaseOutline, icon: mdiDatabaseOutline,
iconColor: 'brown', iconColor: 'brown',
title: 'Automatic database dumps', title: 'Automatic database backups',
description: 'Database dumps are now integrated into the Immich server', description: 'Database backups are now integrated into the Immich server',
release: 'v1.120.0', release: 'v1.120.0',
}), }),
{ {
@@ -326,7 +300,7 @@ const milestones: Item[] = [
withRelease({ withRelease({
icon: mdiFolderMultiple, icon: mdiFolderMultiple,
iconColor: 'brown', iconColor: 'brown',
title: 'Folders view', title: 'Folders',
description: 'Browse your photos and videos in their folder structure', description: 'Browse your photos and videos in their folder structure',
release: 'v1.113.0', release: 'v1.113.0',
}), }),
-5
View File
@@ -1,5 +0,0 @@
Policy: https://github.com/immich-app/immich/blob/main/SECURITY.md
Contact: mailto:security@immich.app
Preferred-Languages: en
Expires: 2026-05-01T23:59:00.000Z
Canonical: https://immich.app/.well-known/security.txt
-1
View File
@@ -30,4 +30,3 @@
/docs/guides/api-album-sync /docs/community-projects 307 /docs/guides/api-album-sync /docs/community-projects 307
/docs/guides/remove-offline-files /docs/community-projects 307 /docs/guides/remove-offline-files /docs/community-projects 307
/milestones /roadmap 307 /milestones /roadmap 307
/docs/overview/introduction /docs/overview/welcome 307
+152 -12
View File
@@ -1,24 +1,36 @@
[ [
{
"label": "v1.133.1",
"url": "https://v1.133.1.archive.immich.app"
},
{
"label": "v1.133.0",
"url": "https://v1.133.0.archive.immich.app"
},
{
"label": "v1.132.3",
"url": "https://v1.132.3.archive.immich.app"
},
{ {
"label": "v1.131.3", "label": "v1.131.3",
"url": "https://v1.131.3.archive.immich.app" "url": "https://v1.131.3.archive.immich.app"
}, },
{
"label": "v1.131.2",
"url": "https://v1.131.2.archive.immich.app"
},
{
"label": "v1.131.1",
"url": "https://v1.131.1.archive.immich.app"
},
{
"label": "v1.131.0",
"url": "https://v1.131.0.archive.immich.app"
},
{ {
"label": "v1.130.3", "label": "v1.130.3",
"url": "https://v1.130.3.archive.immich.app" "url": "https://v1.130.3.archive.immich.app"
}, },
{
"label": "v1.130.2",
"url": "https://v1.130.2.archive.immich.app"
},
{
"label": "v1.130.1",
"url": "https://v1.130.1.archive.immich.app"
},
{
"label": "v1.130.0",
"url": "https://v1.130.0.archive.immich.app"
},
{ {
"label": "v1.129.0", "label": "v1.129.0",
"url": "https://v1.129.0.archive.immich.app" "url": "https://v1.129.0.archive.immich.app"
@@ -35,14 +47,46 @@
"label": "v1.126.1", "label": "v1.126.1",
"url": "https://v1.126.1.archive.immich.app" "url": "https://v1.126.1.archive.immich.app"
}, },
{
"label": "v1.126.0",
"url": "https://v1.126.0.archive.immich.app"
},
{ {
"label": "v1.125.7", "label": "v1.125.7",
"url": "https://v1.125.7.archive.immich.app" "url": "https://v1.125.7.archive.immich.app"
}, },
{
"label": "v1.125.6",
"url": "https://v1.125.6.archive.immich.app"
},
{
"label": "v1.125.5",
"url": "https://v1.125.5.archive.immich.app"
},
{
"label": "v1.125.3",
"url": "https://v1.125.3.archive.immich.app"
},
{
"label": "v1.125.2",
"url": "https://v1.125.2.archive.immich.app"
},
{
"label": "v1.125.1",
"url": "https://v1.125.1.archive.immich.app"
},
{ {
"label": "v1.124.2", "label": "v1.124.2",
"url": "https://v1.124.2.archive.immich.app" "url": "https://v1.124.2.archive.immich.app"
}, },
{
"label": "v1.124.1",
"url": "https://v1.124.1.archive.immich.app"
},
{
"label": "v1.124.0",
"url": "https://v1.124.0.archive.immich.app"
},
{ {
"label": "v1.123.0", "label": "v1.123.0",
"url": "https://v1.123.0.archive.immich.app" "url": "https://v1.123.0.archive.immich.app"
@@ -51,6 +95,18 @@
"label": "v1.122.3", "label": "v1.122.3",
"url": "https://v1.122.3.archive.immich.app" "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", "label": "v1.121.0",
"url": "https://v1.121.0.archive.immich.app" "url": "https://v1.121.0.archive.immich.app"
@@ -59,14 +115,34 @@
"label": "v1.120.2", "label": "v1.120.2",
"url": "https://v1.120.2.archive.immich.app" "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", "label": "v1.119.1",
"url": "https://v1.119.1.archive.immich.app" "url": "https://v1.119.1.archive.immich.app"
}, },
{
"label": "v1.119.0",
"url": "https://v1.119.0.archive.immich.app"
},
{ {
"label": "v1.118.2", "label": "v1.118.2",
"url": "https://v1.118.2.archive.immich.app" "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", "label": "v1.117.0",
"url": "https://v1.117.0.archive.immich.app" "url": "https://v1.117.0.archive.immich.app"
@@ -75,6 +151,14 @@
"label": "v1.116.2", "label": "v1.116.2",
"url": "https://v1.116.2.archive.immich.app" "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", "label": "v1.115.0",
"url": "https://v1.115.0.archive.immich.app" "url": "https://v1.115.0.archive.immich.app"
@@ -87,10 +171,18 @@
"label": "v1.113.1", "label": "v1.113.1",
"url": "https://v1.113.1.archive.immich.app" "url": "https://v1.113.1.archive.immich.app"
}, },
{
"label": "v1.113.0",
"url": "https://v1.113.0.archive.immich.app"
},
{ {
"label": "v1.112.1", "label": "v1.112.1",
"url": "https://v1.112.1.archive.immich.app" "url": "https://v1.112.1.archive.immich.app"
}, },
{
"label": "v1.112.0",
"url": "https://v1.112.0.archive.immich.app"
},
{ {
"label": "v1.111.0", "label": "v1.111.0",
"url": "https://v1.111.0.archive.immich.app" "url": "https://v1.111.0.archive.immich.app"
@@ -103,6 +195,14 @@
"label": "v1.109.2", "label": "v1.109.2",
"url": "https://v1.109.2.archive.immich.app" "url": "https://v1.109.2.archive.immich.app"
}, },
{
"label": "v1.109.1",
"url": "https://v1.109.1.archive.immich.app"
},
{
"label": "v1.109.0",
"url": "https://v1.109.0.archive.immich.app"
},
{ {
"label": "v1.108.0", "label": "v1.108.0",
"url": "https://v1.108.0.archive.immich.app" "url": "https://v1.108.0.archive.immich.app"
@@ -111,14 +211,38 @@
"label": "v1.107.2", "label": "v1.107.2",
"url": "https://v1.107.2.archive.immich.app" "url": "https://v1.107.2.archive.immich.app"
}, },
{
"label": "v1.107.1",
"url": "https://v1.107.1.archive.immich.app"
},
{
"label": "v1.107.0",
"url": "https://v1.107.0.archive.immich.app"
},
{ {
"label": "v1.106.4", "label": "v1.106.4",
"url": "https://v1.106.4.archive.immich.app" "url": "https://v1.106.4.archive.immich.app"
}, },
{
"label": "v1.106.3",
"url": "https://v1.106.3.archive.immich.app"
},
{
"label": "v1.106.2",
"url": "https://v1.106.2.archive.immich.app"
},
{
"label": "v1.106.1",
"url": "https://v1.106.1.archive.immich.app"
},
{ {
"label": "v1.105.1", "label": "v1.105.1",
"url": "https://v1.105.1.archive.immich.app" "url": "https://v1.105.1.archive.immich.app"
}, },
{
"label": "v1.105.0",
"url": "https://v1.105.0.archive.immich.app"
},
{ {
"label": "v1.104.0", "label": "v1.104.0",
"url": "https://v1.104.0.archive.immich.app" "url": "https://v1.104.0.archive.immich.app"
@@ -127,10 +251,26 @@
"label": "v1.103.1", "label": "v1.103.1",
"url": "https://v1.103.1.archive.immich.app" "url": "https://v1.103.1.archive.immich.app"
}, },
{
"label": "v1.103.0",
"url": "https://v1.103.0.archive.immich.app"
},
{ {
"label": "v1.102.3", "label": "v1.102.3",
"url": "https://v1.102.3.archive.immich.app" "url": "https://v1.102.3.archive.immich.app"
}, },
{
"label": "v1.102.2",
"url": "https://v1.102.2.archive.immich.app"
},
{
"label": "v1.102.1",
"url": "https://v1.102.1.archive.immich.app"
},
{
"label": "v1.102.0",
"url": "https://v1.102.0.archive.immich.app"
},
{ {
"label": "v1.101.0", "label": "v1.101.0",
"url": "https://v1.101.0.archive.immich.app" "url": "https://v1.101.0.archive.immich.app"
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 75 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 75 KiB

+1 -1
View File
@@ -1 +1 @@
22.15.1 22.14.0
+3 -3
View File
@@ -34,11 +34,11 @@ services:
- 2285:2285 - 2285:2285
redis: redis:
image: redis:6.2-alpine@sha256:3211c33a618c457e5d241922c975dbc4f446d0bdb2dc75694f5573ef8e2d01fa image: redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
database: database:
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0 image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
command: -c fsync=off -c shared_preload_libraries=vchord.so -c config_file=/var/lib/postgresql/data/postgresql.conf command: -c fsync=off -c shared_preload_libraries=vectors.so
environment: environment:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres POSTGRES_USER: postgres
+405 -1063
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.133.1", "version": "1.131.3",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
@@ -25,9 +25,9 @@
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1", "@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/node": "^22.15.18", "@types/node": "^22.14.0",
"@types/oidc-provider": "^8.5.1", "@types/oidc-provider": "^8.5.1",
"@types/pg": "^8.15.1", "@types/pg": "^8.11.0",
"@types/pngjs": "^6.0.4", "@types/pngjs": "^6.0.4",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"@vitest/coverage-v8": "^3.0.0", "@vitest/coverage-v8": "^3.0.0",
@@ -52,6 +52,6 @@
"vitest": "^3.0.0" "vitest": "^3.0.0"
}, },
"volta": { "volta": {
"node": "22.15.1" "node": "22.14.0"
} }
} }
+70
View File
@@ -46,6 +46,38 @@ describe('/activities', () => {
}); });
describe('GET /activities', () => { describe('GET /activities', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/activities');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require an albumId', async () => {
const { status, body } = await request(app)
.get('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
});
it('should reject an invalid albumId', async () => {
const { status, body } = await request(app)
.get('/activities')
.query({ albumId: uuidDto.invalid })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
});
it('should reject an invalid assetId', async () => {
const { status, body } = await request(app)
.get('/activities')
.query({ albumId: uuidDto.notFound, assetId: uuidDto.invalid })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID'])));
});
it('should start off empty', async () => { it('should start off empty', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/activities') .get('/activities')
@@ -160,6 +192,30 @@ describe('/activities', () => {
}); });
describe('POST /activities', () => { describe('POST /activities', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/activities');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require an albumId', async () => {
const { status, body } = await request(app)
.post('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: uuidDto.invalid });
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
});
it('should require a comment when type is comment', async () => {
const { status, body } = await request(app)
.post('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: uuidDto.notFound, type: 'comment', comment: null });
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(['comment must be a string', 'comment should not be empty']));
});
it('should add a comment to an album', async () => { it('should add a comment to an album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/activities') .post('/activities')
@@ -274,6 +330,20 @@ describe('/activities', () => {
}); });
describe('DELETE /activities/:id', () => { describe('DELETE /activities/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/activities/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid uuid', async () => {
const { status, body } = await request(app)
.delete(`/activities/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
});
it('should remove a comment from an album', async () => { it('should remove a comment from an album', async () => {
const reaction = await createActivity({ const reaction = await createActivity({
albumId: album.id, albumId: album.id,
+71 -1
View File
@@ -9,7 +9,7 @@ import {
LoginResponseDto, LoginResponseDto,
SharedLinkType, SharedLinkType,
} from '@immich/sdk'; } from '@immich/sdk';
import { createUserDto } from 'src/fixtures'; import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils'; import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
@@ -128,6 +128,28 @@ describe('/albums', () => {
}); });
describe('GET /albums', () => { describe('GET /albums', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/albums');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should reject an invalid shared param', async () => {
const { status, body } = await request(app)
.get('/albums?shared=invalid')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(['shared must be a boolean value']));
});
it('should reject an invalid assetId param', async () => {
const { status, body } = await request(app)
.get('/albums?assetId=invalid')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(['assetId must be a UUID']));
});
it("should not show other users' favorites", async () => { it("should not show other users' favorites", async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/albums/${user1Albums[0].id}?withoutAssets=false`) .get(`/albums/${user1Albums[0].id}?withoutAssets=false`)
@@ -301,6 +323,12 @@ describe('/albums', () => {
}); });
describe('GET /albums/:id', () => { describe('GET /albums/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/albums/${user1Albums[0].id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return album info for own album', async () => { it('should return album info for own album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/albums/${user1Albums[0].id}?withoutAssets=false`) .get(`/albums/${user1Albums[0].id}?withoutAssets=false`)
@@ -393,6 +421,12 @@ describe('/albums', () => {
}); });
describe('GET /albums/statistics', () => { describe('GET /albums/statistics', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/albums/statistics');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return total count of albums the user has access to', async () => { it('should return total count of albums the user has access to', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/albums/statistics') .get('/albums/statistics')
@@ -404,6 +438,12 @@ describe('/albums', () => {
}); });
describe('POST /albums', () => { describe('POST /albums', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/albums').send({ albumName: 'New album' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should create an album', async () => { it('should create an album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/albums') .post('/albums')
@@ -431,6 +471,12 @@ describe('/albums', () => {
}); });
describe('PUT /albums/:id/assets', () => { describe('PUT /albums/:id/assets', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/albums/${user1Albums[0].id}/assets`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should be able to add own asset to own album', async () => { it('should be able to add own asset to own album', async () => {
const asset = await utils.createAsset(user1.accessToken); const asset = await utils.createAsset(user1.accessToken);
const { status, body } = await request(app) const { status, body } = await request(app)
@@ -480,6 +526,14 @@ describe('/albums', () => {
}); });
describe('PATCH /albums/:id', () => { describe('PATCH /albums/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.patch(`/albums/${uuidDto.notFound}`)
.send({ albumName: 'New album name' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should update an album', async () => { it('should update an album', async () => {
const album = await utils.createAlbum(user1.accessToken, { const album = await utils.createAlbum(user1.accessToken, {
albumName: 'New album', albumName: 'New album',
@@ -522,6 +576,15 @@ describe('/albums', () => {
}); });
describe('DELETE /albums/:id/assets', () => { describe('DELETE /albums/:id/assets', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.delete(`/albums/${user1Albums[0].id}/assets`)
.send({ ids: [user1Asset1.id] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => { it('should require authorization', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/albums/${user1Albums[1].id}/assets`) .delete(`/albums/${user1Albums[1].id}/assets`)
@@ -616,6 +679,13 @@ describe('/albums', () => {
}); });
}); });
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/albums/${user1Albums[0].id}/users`).send({ sharedUserIds: [] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should be able to add user to own album', async () => { it('should be able to add user to own album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/albums/${album.id}/users`) .put(`/albums/${album.id}/users`)
+56 -1
View File
@@ -1,5 +1,5 @@
import { LoginResponseDto, Permission, createApiKey } from '@immich/sdk'; import { LoginResponseDto, Permission, createApiKey } from '@immich/sdk';
import { createUserDto } from 'src/fixtures'; import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils'; import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
@@ -24,6 +24,12 @@ describe('/api-keys', () => {
}); });
describe('POST /api-keys', () => { describe('POST /api-keys', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/api-keys').send({ name: 'API Key' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should not work without permission', async () => { it('should not work without permission', async () => {
const { secret } = await create(user.accessToken, [Permission.ApiKeyRead]); const { secret } = await create(user.accessToken, [Permission.ApiKeyRead]);
const { status, body } = await request(app).post('/api-keys').set('x-api-key', secret).send({ name: 'API Key' }); const { status, body } = await request(app).post('/api-keys').set('x-api-key', secret).send({ name: 'API Key' });
@@ -93,6 +99,12 @@ describe('/api-keys', () => {
}); });
describe('GET /api-keys', () => { describe('GET /api-keys', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/api-keys');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should start off empty', async () => { it('should start off empty', async () => {
const { status, body } = await request(app).get('/api-keys').set('Authorization', `Bearer ${admin.accessToken}`); const { status, body } = await request(app).get('/api-keys').set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual([]); expect(body).toEqual([]);
@@ -113,6 +125,12 @@ describe('/api-keys', () => {
}); });
describe('GET /api-keys/:id', () => { describe('GET /api-keys/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/api-keys/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => { it('should require authorization', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]); const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app) const { status, body } = await request(app)
@@ -122,6 +140,14 @@ describe('/api-keys', () => {
expect(body).toEqual(errorDto.badRequest('API Key not found')); expect(body).toEqual(errorDto.badRequest('API Key not found'));
}); });
it('should require a valid uuid', async () => {
const { status, body } = await request(app)
.get(`/api-keys/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
});
it('should get api key details', async () => { it('should get api key details', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]); const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app) const { status, body } = await request(app)
@@ -139,6 +165,12 @@ describe('/api-keys', () => {
}); });
describe('PUT /api-keys/:id', () => { describe('PUT /api-keys/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/api-keys/${uuidDto.notFound}`).send({ name: 'new name' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => { it('should require authorization', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]); const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app) const { status, body } = await request(app)
@@ -149,6 +181,15 @@ describe('/api-keys', () => {
expect(body).toEqual(errorDto.badRequest('API Key not found')); expect(body).toEqual(errorDto.badRequest('API Key not found'));
}); });
it('should require a valid uuid', async () => {
const { status, body } = await request(app)
.put(`/api-keys/${uuidDto.invalid}`)
.send({ name: 'new name' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
});
it('should update api key details', async () => { it('should update api key details', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]); const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app) const { status, body } = await request(app)
@@ -167,6 +208,12 @@ describe('/api-keys', () => {
}); });
describe('DELETE /api-keys/:id', () => { describe('DELETE /api-keys/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/api-keys/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => { it('should require authorization', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]); const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app) const { status, body } = await request(app)
@@ -176,6 +223,14 @@ describe('/api-keys', () => {
expect(body).toEqual(errorDto.badRequest('API Key not found')); expect(body).toEqual(errorDto.badRequest('API Key not found'));
}); });
it('should require a valid uuid', async () => {
const { status, body } = await request(app)
.delete(`/api-keys/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
});
it('should delete an api key', async () => { it('should delete an api key', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]); const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status } = await request(app) const { status } = await request(app)
+228 -57
View File
@@ -3,7 +3,6 @@ import {
AssetMediaStatus, AssetMediaStatus,
AssetResponseDto, AssetResponseDto,
AssetTypeEnum, AssetTypeEnum,
AssetVisibility,
getAssetInfo, getAssetInfo,
getMyUser, getMyUser,
LoginResponseDto, LoginResponseDto,
@@ -23,9 +22,27 @@ import { app, asBearerAuth, tempDir, TEN_TIMES, testAssetDir, utils } from 'src/
import request from 'supertest'; import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const makeUploadDto = (options?: { omit: string }): Record<string, any> => {
const dto: Record<string, any> = {
deviceAssetId: 'example-image',
deviceId: 'TEST',
fileCreatedAt: new Date().toISOString(),
fileModifiedAt: new Date().toISOString(),
isFavorite: 'testing',
duration: '0:00:00.000000',
};
const omit = options?.omit;
if (omit) {
delete dto[omit];
}
return dto;
};
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`; const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`; const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`;
const facesAssetDir = `${testAssetDir}/metadata/faces`; const facesAssetFilepath = `${testAssetDir}/metadata/faces/portrait.jpg`;
const readTags = async (bytes: Buffer, filename: string) => { const readTags = async (bytes: Buffer, filename: string) => {
const filepath = join(tempDir, filename); const filepath = join(tempDir, filename);
@@ -120,9 +137,9 @@ describe('/asset', () => {
// stats // stats
utils.createAsset(statsUser.accessToken), utils.createAsset(statsUser.accessToken),
utils.createAsset(statsUser.accessToken, { isFavorite: true }), utils.createAsset(statsUser.accessToken, { isFavorite: true }),
utils.createAsset(statsUser.accessToken, { visibility: AssetVisibility.Archive }), utils.createAsset(statsUser.accessToken, { isArchived: true }),
utils.createAsset(statsUser.accessToken, { utils.createAsset(statsUser.accessToken, {
visibility: AssetVisibility.Archive, isArchived: true,
isFavorite: true, isFavorite: true,
assetData: { filename: 'example.mp4' }, assetData: { filename: 'example.mp4' },
}), }),
@@ -143,6 +160,13 @@ describe('/asset', () => {
}); });
describe('GET /assets/:id/original', () => { describe('GET /assets/:id/original', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/assets/${uuidDto.notFound}/original`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should download the file', async () => { it('should download the file', async () => {
const response = await request(app) const response = await request(app)
.get(`/assets/${user1Assets[0].id}/original`) .get(`/assets/${user1Assets[0].id}/original`)
@@ -154,6 +178,20 @@ describe('/asset', () => {
}); });
describe('GET /assets/:id', () => { describe('GET /assets/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/assets/${uuidDto.notFound}`);
expect(body).toEqual(errorDto.unauthorized);
expect(status).toBe(401);
});
it('should require a valid id', async () => {
const { status, body } = await request(app)
.get(`/assets/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
});
it('should require access', async () => { it('should require access', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/assets/${user2Assets[0].id}`) .get(`/assets/${user2Assets[0].id}`)
@@ -186,22 +224,31 @@ describe('/asset', () => {
}); });
}); });
describe('faces', () => { it('should get the asset faces', async () => {
const metadataFaceTests = [ const config = await utils.getSystemConfig(admin.accessToken);
{ config.metadata.faces.import = true;
description: 'without orientation', await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) });
// asset faces
const facesAsset = await utils.createAsset(admin.accessToken, {
assetData: {
filename: 'portrait.jpg', filename: 'portrait.jpg',
bytes: await readFile(facesAssetFilepath),
}, },
{ });
description: 'adjusting face regions to orientation',
filename: 'portrait-orientation-6.jpg', await utils.waitForWebsocketEvent({ event: 'assetUpload', id: facesAsset.id });
},
]; const { status, body } = await request(app)
// should produce same resulting face region coordinates for any orientation .get(`/assets/${facesAsset.id}`)
const expectedFaces = [ .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body.id).toEqual(facesAsset.id);
expect(body.people).toMatchObject([
{ {
name: 'Marie Curie', name: 'Marie Curie',
birthDate: null, birthDate: null,
thumbnailPath: '',
isHidden: false, isHidden: false,
faces: [ faces: [
{ {
@@ -218,6 +265,7 @@ describe('/asset', () => {
{ {
name: 'Pierre Curie', name: 'Pierre Curie',
birthDate: null, birthDate: null,
thumbnailPath: '',
isHidden: false, isHidden: false,
faces: [ faces: [
{ {
@@ -231,30 +279,7 @@ describe('/asset', () => {
}, },
], ],
}, },
]; ]);
it.each(metadataFaceTests)('should get the asset faces from $filename $description', async ({ filename }) => {
const config = await utils.getSystemConfig(admin.accessToken);
config.metadata.faces.import = true;
await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) });
const facesAsset = await utils.createAsset(admin.accessToken, {
assetData: {
filename,
bytes: await readFile(`${facesAssetDir}/${filename}`),
},
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: facesAsset.id });
const { status, body } = await request(app)
.get(`/assets/${facesAsset.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body.id).toEqual(facesAsset.id);
expect(body.people).toMatchObject(expectedFaces);
});
}); });
it('should work with a shared link', async () => { it('should work with a shared link', async () => {
@@ -308,7 +333,7 @@ describe('/asset', () => {
}); });
it('disallows viewing archived assets', async () => { it('disallows viewing archived assets', async () => {
const asset = await utils.createAsset(user1.accessToken, { visibility: AssetVisibility.Archive }); const asset = await utils.createAsset(user1.accessToken, { isArchived: true });
const { status } = await request(app) const { status } = await request(app)
.get(`/assets/${asset.id}`) .get(`/assets/${asset.id}`)
@@ -329,6 +354,13 @@ describe('/asset', () => {
}); });
describe('GET /assets/statistics', () => { describe('GET /assets/statistics', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/assets/statistics');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return stats of all assets', async () => { it('should return stats of all assets', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/assets/statistics') .get('/assets/statistics')
@@ -352,7 +384,7 @@ describe('/asset', () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/assets/statistics') .get('/assets/statistics')
.set('Authorization', `Bearer ${statsUser.accessToken}`) .set('Authorization', `Bearer ${statsUser.accessToken}`)
.query({ visibility: AssetVisibility.Archive }); .query({ isArchived: true });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual({ images: 1, videos: 1, total: 2 }); expect(body).toEqual({ images: 1, videos: 1, total: 2 });
@@ -362,7 +394,7 @@ describe('/asset', () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/assets/statistics') .get('/assets/statistics')
.set('Authorization', `Bearer ${statsUser.accessToken}`) .set('Authorization', `Bearer ${statsUser.accessToken}`)
.query({ isFavorite: true, visibility: AssetVisibility.Archive }); .query({ isFavorite: true, isArchived: true });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual({ images: 0, videos: 1, total: 1 }); expect(body).toEqual({ images: 0, videos: 1, total: 1 });
@@ -372,7 +404,7 @@ describe('/asset', () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/assets/statistics') .get('/assets/statistics')
.set('Authorization', `Bearer ${statsUser.accessToken}`) .set('Authorization', `Bearer ${statsUser.accessToken}`)
.query({ isFavorite: false, visibility: AssetVisibility.Timeline }); .query({ isFavorite: false, isArchived: false });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual({ images: 1, videos: 0, total: 1 }); expect(body).toEqual({ images: 1, videos: 0, total: 1 });
@@ -393,6 +425,13 @@ describe('/asset', () => {
await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration'); await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration');
}); });
it('should require authentication', async () => {
const { status, body } = await request(app).get('/assets/random');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it.each(TEN_TIMES)('should return 1 random assets', async () => { it.each(TEN_TIMES)('should return 1 random assets', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/assets/random') .get('/assets/random')
@@ -428,9 +467,31 @@ describe('/asset', () => {
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: user2Assets[0].id })]); expect(body).toEqual([expect.objectContaining({ id: user2Assets[0].id })]);
}); });
it('should return error', async () => {
const { status } = await request(app)
.get('/assets/random?count=ABC')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
});
}); });
describe('PUT /assets/:id', () => { describe('PUT /assets/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/assets/:${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid id', async () => {
const { status, body } = await request(app)
.put(`/assets/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
});
it('should require access', async () => { it('should require access', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/assets/${user2Assets[0].id}`) .put(`/assets/${user2Assets[0].id}`)
@@ -458,7 +519,7 @@ describe('/asset', () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`) .put(`/assets/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ visibility: AssetVisibility.Archive }); .send({ isArchived: true });
expect(body).toMatchObject({ id: user1Assets[0].id, isArchived: true }); expect(body).toMatchObject({ id: user1Assets[0].id, isArchived: true });
expect(status).toEqual(200); expect(status).toEqual(200);
}); });
@@ -558,6 +619,28 @@ describe('/asset', () => {
expect(status).toEqual(200); expect(status).toEqual(200);
}); });
it('should reject invalid gps coordinates', async () => {
for (const test of [
{ latitude: 12 },
{ longitude: 12 },
{ latitude: 12, longitude: 'abc' },
{ latitude: 'abc', longitude: 12 },
{ latitude: null, longitude: 12 },
{ latitude: 12, longitude: null },
{ latitude: 91, longitude: 12 },
{ latitude: -91, longitude: 12 },
{ latitude: 12, longitude: -181 },
{ latitude: 12, longitude: 181 },
]) {
const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`)
.send(test)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
}
});
it('should update gps data', async () => { it('should update gps data', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`) .put(`/assets/${user1Assets[0].id}`)
@@ -629,6 +712,17 @@ describe('/asset', () => {
expect(status).toEqual(200); expect(status).toEqual(200);
}); });
it('should reject invalid rating', async () => {
for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: null }]) {
const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`)
.send(test)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
}
});
it('should return tagged people', async () => { it('should return tagged people', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`) .put(`/assets/${user1Assets[0].id}`)
@@ -652,6 +746,25 @@ describe('/asset', () => {
}); });
describe('DELETE /assets', () => { describe('DELETE /assets', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.delete(`/assets`)
.send({ ids: [uuidDto.notFound] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid uuid', async () => {
const { status, body } = await request(app)
.delete(`/assets`)
.send({ ids: [uuidDto.invalid] })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID']));
});
it('should throw an error when the id is not found', async () => { it('should throw an error when the id is not found', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/assets`) .delete(`/assets`)
@@ -764,6 +877,13 @@ describe('/asset', () => {
}); });
describe('GET /assets/:id/thumbnail', () => { describe('GET /assets/:id/thumbnail', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/assets/${locationAsset.id}/thumbnail`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should not include gps data for webp thumbnails', async () => { it('should not include gps data for webp thumbnails', async () => {
await utils.waitForWebsocketEvent({ await utils.waitForWebsocketEvent({
event: 'assetUpload', event: 'assetUpload',
@@ -799,6 +919,13 @@ describe('/asset', () => {
}); });
describe('GET /assets/:id/original', () => { describe('GET /assets/:id/original', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/assets/${locationAsset.id}/original`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should download the original', async () => { it('should download the original', async () => {
const { status, body, type } = await request(app) const { status, body, type } = await request(app)
.get(`/assets/${locationAsset.id}/original`) .get(`/assets/${locationAsset.id}/original`)
@@ -819,9 +946,43 @@ describe('/asset', () => {
}); });
}); });
describe('PUT /assets', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put('/assets');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
});
describe('POST /assets', () => { describe('POST /assets', () => {
beforeAll(setupTests, 30_000); beforeAll(setupTests, 30_000);
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/assets`);
expect(body).toEqual(errorDto.unauthorized);
expect(status).toBe(401);
});
it.each([
{ should: 'require `deviceAssetId`', dto: { ...makeUploadDto({ omit: 'deviceAssetId' }) } },
{ should: 'require `deviceId`', dto: { ...makeUploadDto({ omit: 'deviceId' }) } },
{ should: 'require `fileCreatedAt`', dto: { ...makeUploadDto({ omit: 'fileCreatedAt' }) } },
{ should: 'require `fileModifiedAt`', dto: { ...makeUploadDto({ omit: 'fileModifiedAt' }) } },
{ should: 'require `duration`', dto: { ...makeUploadDto({ omit: 'duration' }) } },
{ should: 'throw if `isFavorite` is not a boolean', dto: { ...makeUploadDto(), isFavorite: 'not-a-boolean' } },
{ should: 'throw if `isVisible` is not a boolean', dto: { ...makeUploadDto(), isVisible: 'not-a-boolean' } },
{ should: 'throw if `isArchived` is not a boolean', dto: { ...makeUploadDto(), isArchived: 'not-a-boolean' } },
])('should $should', async ({ dto }) => {
const { status, body } = await request(app)
.post('/assets')
.set('Authorization', `Bearer ${user1.accessToken}`)
.attach('assetData', makeRandomImage(), 'example.png')
.field(dto);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
const tests = [ const tests = [
{ {
input: 'formats/avif/8bit-sRGB.avif', input: 'formats/avif/8bit-sRGB.avif',
@@ -1083,21 +1244,31 @@ describe('/asset', () => {
}, },
]; ];
it.each(tests)(`should upload and generate a thumbnail for different file types`, async ({ input, expected }) => { it(`should upload and generate a thumbnail for different file types`, async () => {
const filepath = join(testAssetDir, input); // upload in parallel
const response = await utils.createAsset(admin.accessToken, { const assets = await Promise.all(
assetData: { bytes: await readFile(filepath), filename: basename(filepath) }, tests.map(async ({ input }) => {
}); const filepath = join(testAssetDir, input);
return utils.createAsset(admin.accessToken, {
assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
});
}),
);
expect(response.status).toBe(AssetMediaStatus.Created); for (const { id, status } of assets) {
const id = response.id; expect(status).toBe(AssetMediaStatus.Created);
// longer timeout as the thumbnail generation from full-size raw files can take a while // longer timeout as the thumbnail generation from full-size raw files can take a while
await utils.waitForWebsocketEvent({ event: 'assetUpload', id }); await utils.waitForWebsocketEvent({ event: 'assetUpload', id });
}
const asset = await utils.getAssetInfo(admin.accessToken, id); for (const [i, { id }] of assets.entries()) {
expect(asset.exifInfo).toBeDefined(); const { expected } = tests[i];
expect(asset.exifInfo).toMatchObject(expected.exifInfo); const asset = await utils.getAssetInfo(admin.accessToken, id);
expect(asset).toMatchObject(expected);
expect(asset.exifInfo).toBeDefined();
expect(asset.exifInfo).toMatchObject(expected.exifInfo);
expect(asset).toMatchObject(expected);
}
}); });
it('should handle a duplicate', async () => { it('should handle a duplicate', async () => {
+43
View File
@@ -0,0 +1,43 @@
import { deleteAssets, getAuditFiles, updateAsset, type LoginResponseDto } from '@immich/sdk';
import { asBearerAuth, utils } from 'src/utils';
import { beforeAll, describe, expect, it } from 'vitest';
describe('/audits', () => {
let admin: LoginResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
await utils.resetFilesystem();
admin = await utils.adminSetup();
});
// TODO: Enable these tests again once #7436 is resolved as these were flaky
describe.skip('GET :/file-report', () => {
it('excludes assets without issues from report', async () => {
const [trashedAsset, archivedAsset] = await Promise.all([
utils.createAsset(admin.accessToken),
utils.createAsset(admin.accessToken),
utils.createAsset(admin.accessToken),
]);
await Promise.all([
deleteAssets({ assetBulkDeleteDto: { ids: [trashedAsset.id] } }, { headers: asBearerAuth(admin.accessToken) }),
updateAsset(
{
id: archivedAsset.id,
updateAssetDto: { isArchived: true },
},
{ headers: asBearerAuth(admin.accessToken) },
),
]);
const body = await getAuditFiles({
headers: asBearerAuth(admin.accessToken),
});
expect(body.orphans).toHaveLength(0);
expect(body.extras).toHaveLength(0);
});
});
});
+35
View File
@@ -19,6 +19,17 @@ describe(`/auth/admin-sign-up`, () => {
expect(body).toEqual(signupResponseDto.admin); expect(body).toEqual(signupResponseDto.admin);
}); });
it('should sign up the admin with a local domain', async () => {
const { status, body } = await request(app)
.post('/auth/admin-sign-up')
.send({ ...signupDto.admin, email: 'admin@local' });
expect(status).toEqual(201);
expect(body).toEqual({
...signupResponseDto.admin,
email: 'admin@local',
});
});
it('should not allow a second admin to sign up', async () => { it('should not allow a second admin to sign up', async () => {
await signUpAdmin({ signUpDto: signupDto.admin }); await signUpAdmin({ signUpDto: signupDto.admin });
@@ -46,6 +57,22 @@ describe('/auth/*', () => {
expect(body).toEqual(errorDto.incorrectLogin); expect(body).toEqual(errorDto.incorrectLogin);
}); });
for (const key of Object.keys(loginDto.admin)) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
.post('/auth/login')
.send({ ...loginDto.admin, [key]: null });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
it('should reject an invalid email', async () => {
const { status, body } = await request(app).post('/auth/login').send({ email: [], password });
expect(status).toBe(400);
expect(body).toEqual(errorDto.invalidEmail);
});
}
it('should accept a correct password', async () => { it('should accept a correct password', async () => {
const { status, body, headers } = await request(app).post('/auth/login').send({ email, password }); const { status, body, headers } = await request(app).post('/auth/login').send({ email, password });
expect(status).toBe(201); expect(status).toBe(201);
@@ -100,6 +127,14 @@ describe('/auth/*', () => {
}); });
describe('POST /auth/change-password', () => { describe('POST /auth/change-password', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.post(`/auth/change-password`)
.send({ password, newPassword: 'Password1234' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require the current password', async () => { it('should require the current password', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post(`/auth/change-password`) .post(`/auth/change-password`)
+19
View File
@@ -1,5 +1,6 @@
import { AssetMediaResponseDto, LoginResponseDto } from '@immich/sdk'; import { AssetMediaResponseDto, LoginResponseDto } from '@immich/sdk';
import { readFile, writeFile } from 'node:fs/promises'; import { readFile, writeFile } from 'node:fs/promises';
import { errorDto } from 'src/responses';
import { app, tempDir, utils } from 'src/utils'; import { app, tempDir, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest'; import { beforeAll, describe, expect, it } from 'vitest';
@@ -16,6 +17,15 @@ describe('/download', () => {
}); });
describe('POST /download/info', () => { describe('POST /download/info', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.post(`/download/info`)
.send({ assetIds: [asset1.id] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should download info', async () => { it('should download info', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/download/info') .post('/download/info')
@@ -32,6 +42,15 @@ describe('/download', () => {
}); });
describe('POST /download/archive', () => { describe('POST /download/archive', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.post(`/download/archive`)
.send({ assetIds: [asset1.id, asset2.id] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should download an archive', async () => { it('should download an archive', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/download/archive') .post('/download/archive')
+2 -2
View File
@@ -1,4 +1,4 @@
import { AssetVisibility, LoginResponseDto } from '@immich/sdk'; import { LoginResponseDto } from '@immich/sdk';
import { readFile } from 'node:fs/promises'; import { readFile } from 'node:fs/promises';
import { basename, join } from 'node:path'; import { basename, join } from 'node:path';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
@@ -44,7 +44,7 @@ describe('/map', () => {
it('should get map markers for all non-archived assets', async () => { it('should get map markers for all non-archived assets', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/map/markers') .get('/map/markers')
.query({ visibility: AssetVisibility.Timeline }) .query({ isArchived: false })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
+28 -90
View File
@@ -6,7 +6,6 @@ import {
startOAuth, startOAuth,
updateConfig, updateConfig,
} from '@immich/sdk'; } from '@immich/sdk';
import { createHash, randomBytes } from 'node:crypto';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
import { OAuthClient, OAuthUser } from 'src/setup/auth-server'; import { OAuthClient, OAuthUser } from 'src/setup/auth-server';
import { app, asBearerAuth, baseUrl, utils } from 'src/utils'; import { app, asBearerAuth, baseUrl, utils } from 'src/utils';
@@ -22,30 +21,18 @@ const mobileOverrideRedirectUri = 'https://photos.immich.app/oauth/mobile-redire
const redirect = async (url: string, cookies?: string[]) => { const redirect = async (url: string, cookies?: string[]) => {
const { headers } = await request(url) const { headers } = await request(url)
.get('') .get('/')
.set('Cookie', cookies || []); .set('Cookie', cookies || []);
return { cookies: (headers['set-cookie'] as unknown as string[]) || [], location: headers.location }; return { cookies: (headers['set-cookie'] as unknown as string[]) || [], location: headers.location };
}; };
// Function to generate a code challenge from the verifier
const generateCodeChallenge = async (codeVerifier: string): Promise<string> => {
const hashed = createHash('sha256').update(codeVerifier).digest();
return hashed.toString('base64url');
};
const loginWithOAuth = async (sub: OAuthUser | string, redirectUri?: string) => { const loginWithOAuth = async (sub: OAuthUser | string, redirectUri?: string) => {
const state = randomBytes(16).toString('base64url'); const { url } = await startOAuth({ oAuthConfigDto: { redirectUri: redirectUri ?? `${baseUrl}/auth/login` } });
const codeVerifier = randomBytes(64).toString('base64url');
const codeChallenge = await generateCodeChallenge(codeVerifier);
const { url } = await startOAuth({
oAuthConfigDto: { redirectUri: redirectUri ?? `${baseUrl}/auth/login`, state, codeChallenge },
});
// login // login
const response1 = await redirect(url.replace(authServer.internal, authServer.external)); const response1 = await redirect(url.replace(authServer.internal, authServer.external));
const response2 = await request(authServer.external + response1.location) const response2 = await request(authServer.external + response1.location)
.post('') .post('/')
.set('Cookie', response1.cookies) .set('Cookie', response1.cookies)
.type('form') .type('form')
.send({ prompt: 'login', login: sub, password: 'password' }); .send({ prompt: 'login', login: sub, password: 'password' });
@@ -53,7 +40,7 @@ const loginWithOAuth = async (sub: OAuthUser | string, redirectUri?: string) =>
// approve // approve
const response3 = await redirect(response2.header.location, response1.cookies); const response3 = await redirect(response2.header.location, response1.cookies);
const response4 = await request(authServer.external + response3.location) const response4 = await request(authServer.external + response3.location)
.post('') .post('/')
.type('form') .type('form')
.set('Cookie', response3.cookies) .set('Cookie', response3.cookies)
.send({ prompt: 'consent' }); .send({ prompt: 'consent' });
@@ -64,9 +51,9 @@ const loginWithOAuth = async (sub: OAuthUser | string, redirectUri?: string) =>
expect(redirectUrl).toBeDefined(); expect(redirectUrl).toBeDefined();
const params = new URL(redirectUrl).searchParams; const params = new URL(redirectUrl).searchParams;
expect(params.get('code')).toBeDefined(); expect(params.get('code')).toBeDefined();
expect(params.get('state')).toBe(state); expect(params.get('state')).toBeDefined();
return { url: redirectUrl, state, codeVerifier }; return redirectUrl;
}; };
const setupOAuth = async (token: string, dto: Partial<SystemConfigOAuthDto>) => { const setupOAuth = async (token: string, dto: Partial<SystemConfigOAuthDto>) => {
@@ -132,42 +119,9 @@ describe(`/oauth`, () => {
expect(body).toEqual(errorDto.badRequest(['url should not be empty'])); expect(body).toEqual(errorDto.badRequest(['url should not be empty']));
}); });
it(`should throw an error if the state is not provided`, async () => {
const { url } = await loginWithOAuth('oauth-auto-register');
const { status, body } = await request(app).post('/oauth/callback').send({ url });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('OAuth state is missing'));
});
it(`should throw an error if the state mismatches`, async () => {
const callbackParams = await loginWithOAuth('oauth-auto-register');
const { state } = await loginWithOAuth('oauth-auto-register');
const { status } = await request(app)
.post('/oauth/callback')
.send({ ...callbackParams, state });
expect(status).toBeGreaterThanOrEqual(400);
});
it(`should throw an error if the codeVerifier is not provided`, async () => {
const { url, state } = await loginWithOAuth('oauth-auto-register');
const { status, body } = await request(app).post('/oauth/callback').send({ url, state });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('OAuth code verifier is missing'));
});
it(`should throw an error if the codeVerifier doesn't match the challenge`, async () => {
const callbackParams = await loginWithOAuth('oauth-auto-register');
const { codeVerifier } = await loginWithOAuth('oauth-auto-register');
const { status, body } = await request(app)
.post('/oauth/callback')
.send({ ...callbackParams, codeVerifier });
console.log(body);
expect(status).toBeGreaterThanOrEqual(400);
});
it('should auto register the user by default', async () => { it('should auto register the user by default', async () => {
const callbackParams = await loginWithOAuth('oauth-auto-register'); const url = await loginWithOAuth('oauth-auto-register');
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams); const { status, body } = await request(app).post('/oauth/callback').send({ url });
expect(status).toBe(201); expect(status).toBe(201);
expect(body).toMatchObject({ expect(body).toMatchObject({
accessToken: expect.any(String), accessToken: expect.any(String),
@@ -178,30 +132,16 @@ describe(`/oauth`, () => {
}); });
}); });
it('should allow passing state and codeVerifier via cookies', async () => {
const { url, state, codeVerifier } = await loginWithOAuth('oauth-auto-register');
const { status, body } = await request(app)
.post('/oauth/callback')
.set('Cookie', [`immich_oauth_state=${state}`, `immich_oauth_code_verifier=${codeVerifier}`])
.send({ url });
expect(status).toBe(201);
expect(body).toMatchObject({
accessToken: expect.any(String),
userId: expect.any(String),
userEmail: 'oauth-auto-register@immich.app',
});
});
it('should handle a user without an email', async () => { it('should handle a user without an email', async () => {
const callbackParams = await loginWithOAuth(OAuthUser.NO_EMAIL); const url = await loginWithOAuth(OAuthUser.NO_EMAIL);
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams); const { status, body } = await request(app).post('/oauth/callback').send({ url });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('OAuth profile does not have an email address')); expect(body).toEqual(errorDto.badRequest('OAuth profile does not have an email address'));
}); });
it('should set the quota from a claim', async () => { it('should set the quota from a claim', async () => {
const callbackParams = await loginWithOAuth(OAuthUser.WITH_QUOTA); const url = await loginWithOAuth(OAuthUser.WITH_QUOTA);
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams); const { status, body } = await request(app).post('/oauth/callback').send({ url });
expect(status).toBe(201); expect(status).toBe(201);
expect(body).toMatchObject({ expect(body).toMatchObject({
accessToken: expect.any(String), accessToken: expect.any(String),
@@ -214,8 +154,8 @@ describe(`/oauth`, () => {
}); });
it('should set the storage label from a claim', async () => { it('should set the storage label from a claim', async () => {
const callbackParams = await loginWithOAuth(OAuthUser.WITH_USERNAME); const url = await loginWithOAuth(OAuthUser.WITH_USERNAME);
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams); const { status, body } = await request(app).post('/oauth/callback').send({ url });
expect(status).toBe(201); expect(status).toBe(201);
expect(body).toMatchObject({ expect(body).toMatchObject({
accessToken: expect.any(String), accessToken: expect.any(String),
@@ -236,8 +176,8 @@ describe(`/oauth`, () => {
buttonText: 'Login with Immich', buttonText: 'Login with Immich',
signingAlgorithm: 'RS256', signingAlgorithm: 'RS256',
}); });
const callbackParams = await loginWithOAuth('oauth-RS256-token'); const url = await loginWithOAuth('oauth-RS256-token');
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams); const { status, body } = await request(app).post('/oauth/callback').send({ url });
expect(status).toBe(201); expect(status).toBe(201);
expect(body).toMatchObject({ expect(body).toMatchObject({
accessToken: expect.any(String), accessToken: expect.any(String),
@@ -256,8 +196,8 @@ describe(`/oauth`, () => {
buttonText: 'Login with Immich', buttonText: 'Login with Immich',
profileSigningAlgorithm: 'RS256', profileSigningAlgorithm: 'RS256',
}); });
const callbackParams = await loginWithOAuth('oauth-signed-profile'); const url = await loginWithOAuth('oauth-signed-profile');
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams); const { status, body } = await request(app).post('/oauth/callback').send({ url });
expect(status).toBe(201); expect(status).toBe(201);
expect(body).toMatchObject({ expect(body).toMatchObject({
userId: expect.any(String), userId: expect.any(String),
@@ -273,8 +213,8 @@ describe(`/oauth`, () => {
buttonText: 'Login with Immich', buttonText: 'Login with Immich',
signingAlgorithm: 'something-that-does-not-work', signingAlgorithm: 'something-that-does-not-work',
}); });
const callbackParams = await loginWithOAuth('oauth-signed-bad'); const url = await loginWithOAuth('oauth-signed-bad');
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams); const { status, body } = await request(app).post('/oauth/callback').send({ url });
expect(status).toBe(500); expect(status).toBe(500);
expect(body).toMatchObject({ expect(body).toMatchObject({
error: 'Internal Server Error', error: 'Internal Server Error',
@@ -295,8 +235,8 @@ describe(`/oauth`, () => {
}); });
it('should not auto register the user', async () => { it('should not auto register the user', async () => {
const callbackParams = await loginWithOAuth('oauth-no-auto-register'); const url = await loginWithOAuth('oauth-no-auto-register');
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams); const { status, body } = await request(app).post('/oauth/callback').send({ url });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('User does not exist and auto registering is disabled.')); expect(body).toEqual(errorDto.badRequest('User does not exist and auto registering is disabled.'));
}); });
@@ -307,8 +247,8 @@ describe(`/oauth`, () => {
email: 'oauth-user3@immich.app', email: 'oauth-user3@immich.app',
password: 'password', password: 'password',
}); });
const callbackParams = await loginWithOAuth('oauth-user3'); const url = await loginWithOAuth('oauth-user3');
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams); const { status, body } = await request(app).post('/oauth/callback').send({ url });
expect(status).toBe(201); expect(status).toBe(201);
expect(body).toMatchObject({ expect(body).toMatchObject({
userId, userId,
@@ -346,15 +286,13 @@ describe(`/oauth`, () => {
}); });
it('should auto register the user by default', async () => { it('should auto register the user by default', async () => {
const callbackParams = await loginWithOAuth('oauth-mobile-override', 'app.immich:///oauth-callback'); const url = await loginWithOAuth('oauth-mobile-override', 'app.immich:///oauth-callback');
expect(callbackParams.url).toEqual(expect.stringContaining(mobileOverrideRedirectUri)); expect(url).toEqual(expect.stringContaining(mobileOverrideRedirectUri));
// simulate redirecting back to mobile app // simulate redirecting back to mobile app
const url = callbackParams.url.replace(mobileOverrideRedirectUri, 'app.immich:///oauth-callback'); const redirectUri = url.replace(mobileOverrideRedirectUri, 'app.immich:///oauth-callback');
const { status, body } = await request(app) const { status, body } = await request(app).post('/oauth/callback').send({ url: redirectUri });
.post('/oauth/callback')
.send({ ...callbackParams, url });
expect(status).toBe(201); expect(status).toBe(201);
expect(body).toMatchObject({ expect(body).toMatchObject({
accessToken: expect.any(String), accessToken: expect.any(String),
+94
View File
@@ -5,6 +5,22 @@ import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
const invalidBirthday = [
{
birthDate: 'false',
response: ['birthDate must be a string in the format yyyy-MM-dd', 'Birth date cannot be in the future'],
},
{
birthDate: '123567',
response: ['birthDate must be a string in the format yyyy-MM-dd', 'Birth date cannot be in the future'],
},
{
birthDate: 123_567,
response: ['birthDate must be a string in the format yyyy-MM-dd', 'Birth date cannot be in the future'],
},
{ birthDate: '9999-01-01', response: ['Birth date cannot be in the future'] },
];
describe('/people', () => { describe('/people', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let visiblePerson: PersonResponseDto; let visiblePerson: PersonResponseDto;
@@ -42,6 +58,14 @@ describe('/people', () => {
describe('GET /people', () => { describe('GET /people', () => {
beforeEach(async () => {}); beforeEach(async () => {});
it('should require authentication', async () => {
const { status, body } = await request(app).get('/people');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return all people (including hidden)', async () => { it('should return all people (including hidden)', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/people') .get('/people')
@@ -93,6 +117,13 @@ describe('/people', () => {
}); });
describe('GET /people/:id', () => { describe('GET /people/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/people/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should throw error if person with id does not exist', async () => { it('should throw error if person with id does not exist', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/people/${uuidDto.notFound}`) .get(`/people/${uuidDto.notFound}`)
@@ -113,6 +144,13 @@ describe('/people', () => {
}); });
describe('GET /people/:id/statistics', () => { describe('GET /people/:id/statistics', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/people/${multipleAssetsPerson.id}/statistics`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should throw error if person with id does not exist', async () => { it('should throw error if person with id does not exist', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/people/${uuidDto.notFound}/statistics`) .get(`/people/${uuidDto.notFound}/statistics`)
@@ -133,6 +171,23 @@ describe('/people', () => {
}); });
describe('POST /people', () => { describe('POST /people', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/people`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
for (const { birthDate, response } of invalidBirthday) {
it(`should not accept an invalid birth date [${birthDate}]`, async () => {
const { status, body } = await request(app)
.post(`/people`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(response));
});
}
it('should create a person', async () => { it('should create a person', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post(`/people`) .post(`/people`)
@@ -168,6 +223,39 @@ describe('/people', () => {
}); });
describe('PUT /people/:id', () => { describe('PUT /people/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/people/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
for (const { key, type } of [
{ key: 'name', type: 'string' },
{ key: 'featureFaceAssetId', type: 'string' },
{ key: 'isHidden', type: 'boolean value' },
{ key: 'isFavorite', type: 'boolean value' },
]) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
.put(`/people/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ [key]: null });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest([`${key} must be a ${type}`]));
});
}
for (const { birthDate, response } of invalidBirthday) {
it(`should not accept an invalid birth date [${birthDate}]`, async () => {
const { status, body } = await request(app)
.put(`/people/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(response));
});
}
it('should update a date of birth', async () => { it('should update a date of birth', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/people/${visiblePerson.id}`) .put(`/people/${visiblePerson.id}`)
@@ -224,6 +312,12 @@ describe('/people', () => {
}); });
describe('POST /people/:id/merge', () => { describe('POST /people/:id/merge', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/people/${uuidDto.notFound}/merge`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should not supporting merging a person into themselves', async () => { it('should not supporting merging a person into themselves', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post(`/people/${visiblePerson.id}/merge`) .post(`/people/${visiblePerson.id}/merge`)
+119 -20
View File
@@ -1,15 +1,9 @@
import { import { AssetMediaResponseDto, AssetResponseDto, deleteAssets, LoginResponseDto, updateAsset } from '@immich/sdk';
AssetMediaResponseDto,
AssetResponseDto,
AssetVisibility,
deleteAssets,
LoginResponseDto,
updateAsset,
} from '@immich/sdk';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { readFile } from 'node:fs/promises'; import { readFile } from 'node:fs/promises';
import { join } from 'node:path'; import { join } from 'node:path';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, TEN_TIMES, testAssetDir, utils } from 'src/utils'; import { app, asBearerAuth, TEN_TIMES, testAssetDir, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest';
@@ -56,7 +50,7 @@ describe('/search', () => {
{ filename: '/formats/motionphoto/samsung-one-ui-6.heic' }, { filename: '/formats/motionphoto/samsung-one-ui-6.heic' },
{ filename: '/formats/motionphoto/samsung-one-ui-5.jpg' }, { filename: '/formats/motionphoto/samsung-one-ui-5.jpg' },
{ filename: '/metadata/gps-position/thompson-springs.jpg', dto: { visibility: AssetVisibility.Archive } }, { filename: '/metadata/gps-position/thompson-springs.jpg', dto: { isArchived: true } },
// used for search suggestions // used for search suggestions
{ filename: '/formats/png/density_plot.png' }, { filename: '/formats/png/density_plot.png' },
@@ -147,6 +141,65 @@ describe('/search', () => {
}); });
describe('POST /search/metadata', () => { describe('POST /search/metadata', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/search/metadata');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
const badTests = [
{
should: 'should reject page as a string',
dto: { page: 'abc' },
expected: ['page must not be less than 1', 'page must be an integer number'],
},
{
should: 'should reject page as a decimal',
dto: { page: 1.5 },
expected: ['page must be an integer number'],
},
{
should: 'should reject page as a negative number',
dto: { page: -10 },
expected: ['page must not be less than 1'],
},
{
should: 'should reject page as 0',
dto: { page: 0 },
expected: ['page must not be less than 1'],
},
{
should: 'should reject size as a string',
dto: { size: 'abc' },
expected: [
'size must not be greater than 1000',
'size must not be less than 1',
'size must be an integer number',
],
},
{
should: 'should reject an invalid size',
dto: { size: -1.5 },
expected: ['size must not be less than 1', 'size must be an integer number'],
},
...['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`],
})),
];
for (const { should, dto, expected } of badTests) {
it(should, async () => {
const { status, body } = await request(app)
.post('/search/metadata')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send(dto);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(expected));
});
}
const searchTests = [ const searchTests = [
{ {
should: 'should get my assets', should: 'should get my assets',
@@ -178,12 +231,12 @@ describe('/search', () => {
deferred: () => ({ dto: { size: 1, isFavorite: false }, assets: [assetLast] }), deferred: () => ({ dto: { size: 1, isFavorite: false }, assets: [assetLast] }),
}, },
{ {
should: 'should search by visibility (AssetVisibility.Archive)', should: 'should search by isArchived (true)',
deferred: () => ({ dto: { visibility: AssetVisibility.Archive }, assets: [assetSprings] }), deferred: () => ({ dto: { isArchived: true }, assets: [assetSprings] }),
}, },
{ {
should: 'should search by visibility (AssetVisibility.Timeline)', should: 'should search by isArchived (false)',
deferred: () => ({ dto: { size: 1, visibility: AssetVisibility.Timeline }, assets: [assetLast] }), deferred: () => ({ dto: { size: 1, isArchived: false }, assets: [assetLast] }),
}, },
{ {
should: 'should search by type (image)', should: 'should search by type (image)',
@@ -192,7 +245,7 @@ describe('/search', () => {
{ {
should: 'should search by type (video)', should: 'should search by type (video)',
deferred: () => ({ deferred: () => ({
dto: { type: 'VIDEO', visibility: AssetVisibility.Hidden }, dto: { type: 'VIDEO' },
assets: [ assets: [
// the three live motion photos // the three live motion photos
{ id: expect.any(String) }, { id: expect.any(String) },
@@ -236,6 +289,13 @@ describe('/search', () => {
should: 'should search by takenAfter (no results)', should: 'should search by takenAfter (no results)',
deferred: () => ({ dto: { takenAfter: today.plus({ hour: 1 }).toJSDate() }, assets: [] }), deferred: () => ({ dto: { takenAfter: today.plus({ hour: 1 }).toJSDate() }, assets: [] }),
}, },
// {
// should: 'should search by originalPath',
// deferred: () => ({
// dto: { originalPath: asset1.originalPath },
// assets: [asset1],
// }),
// },
{ {
should: 'should search by originalFilename', should: 'should search by originalFilename',
deferred: () => ({ deferred: () => ({
@@ -265,7 +325,7 @@ describe('/search', () => {
deferred: () => ({ deferred: () => ({
dto: { dto: {
city: '', city: '',
visibility: AssetVisibility.Timeline, isVisible: true,
includeNull: true, includeNull: true,
}, },
assets: [assetLast], assets: [assetLast],
@@ -276,7 +336,7 @@ describe('/search', () => {
deferred: () => ({ deferred: () => ({
dto: { dto: {
city: null, city: null,
visibility: AssetVisibility.Timeline, isVisible: true,
includeNull: true, includeNull: true,
}, },
assets: [assetLast], assets: [assetLast],
@@ -297,7 +357,7 @@ describe('/search', () => {
deferred: () => ({ deferred: () => ({
dto: { dto: {
state: '', state: '',
visibility: AssetVisibility.Timeline, isVisible: true,
withExif: true, withExif: true,
includeNull: true, includeNull: true,
}, },
@@ -309,7 +369,7 @@ describe('/search', () => {
deferred: () => ({ deferred: () => ({
dto: { dto: {
state: null, state: null,
visibility: AssetVisibility.Timeline, isVisible: true,
includeNull: true, includeNull: true,
}, },
assets: [assetLast, assetNotocactus], assets: [assetLast, assetNotocactus],
@@ -330,7 +390,7 @@ describe('/search', () => {
deferred: () => ({ deferred: () => ({
dto: { dto: {
country: '', country: '',
visibility: AssetVisibility.Timeline, isVisible: true,
includeNull: true, includeNull: true,
}, },
assets: [assetLast], assets: [assetLast],
@@ -341,7 +401,7 @@ describe('/search', () => {
deferred: () => ({ deferred: () => ({
dto: { dto: {
country: null, country: null,
visibility: AssetVisibility.Timeline, isVisible: true,
includeNull: true, includeNull: true,
}, },
assets: [assetLast], assets: [assetLast],
@@ -394,6 +454,14 @@ describe('/search', () => {
} }
}); });
describe('POST /search/smart', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/search/smart');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
});
describe('POST /search/random', () => { describe('POST /search/random', () => {
beforeAll(async () => { beforeAll(async () => {
await Promise.all([ await Promise.all([
@@ -408,6 +476,13 @@ describe('/search', () => {
await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration'); await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration');
}); });
it('should require authentication', async () => {
const { status, body } = await request(app).post('/search/random').send({ size: 1 });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it.each(TEN_TIMES)('should return 1 random assets', async () => { it.each(TEN_TIMES)('should return 1 random assets', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/search/random') .post('/search/random')
@@ -437,6 +512,12 @@ describe('/search', () => {
}); });
describe('GET /search/explore', () => { describe('GET /search/explore', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/search/explore');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get explore data', async () => { it('should get explore data', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/search/explore') .get('/search/explore')
@@ -447,6 +528,12 @@ describe('/search', () => {
}); });
describe('GET /search/places', () => { describe('GET /search/places', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/search/places');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get relevant places', async () => { it('should get relevant places', async () => {
const name = 'Paris'; const name = 'Paris';
@@ -465,6 +552,12 @@ describe('/search', () => {
}); });
describe('GET /search/cities', () => { describe('GET /search/cities', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/search/cities');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get all cities', async () => { it('should get all cities', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/search/cities') .get('/search/cities')
@@ -483,6 +576,12 @@ describe('/search', () => {
}); });
describe('GET /search/suggestions', () => { describe('GET /search/suggestions', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/search/suggestions');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get suggestions for country (including null)', async () => { it('should get suggestions for country (including null)', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/search/suggestions?type=country&includeNull=true') .get('/search/suggestions?type=country&includeNull=true')
+32 -46
View File
@@ -1,4 +1,4 @@
import { AssetMediaResponseDto, AssetVisibility, LoginResponseDto, SharedLinkType } from '@immich/sdk'; import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType, TimeBucketSize } from '@immich/sdk';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { createUserDto } from 'src/fixtures'; import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
@@ -52,7 +52,7 @@ describe('/timeline', () => {
describe('GET /timeline/buckets', () => { describe('GET /timeline/buckets', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get('/timeline/buckets'); const { status, body } = await request(app).get('/timeline/buckets').query({ size: TimeBucketSize.Month });
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
@@ -60,7 +60,8 @@ describe('/timeline', () => {
it('should get time buckets by month', async () => { it('should get time buckets by month', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/timeline/buckets') .get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`); .set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ size: TimeBucketSize.Month });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual( expect(body).toEqual(
@@ -77,17 +78,33 @@ describe('/timeline', () => {
assetIds: userAssets.map(({ id }) => id), assetIds: userAssets.map(({ id }) => id),
}); });
const { status, body } = await request(app).get('/timeline/buckets').query({ key: sharedLink.key }); const { status, body } = await request(app)
.get('/timeline/buckets')
.query({ key: sharedLink.key, size: TimeBucketSize.Month });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission); expect(body).toEqual(errorDto.noPermission);
}); });
it('should get time buckets by day', async () => {
const { status, body } = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ size: TimeBucketSize.Day });
expect(status).toBe(200);
expect(body).toEqual([
{ count: 2, timeBucket: '1970-02-11T00:00:00.000Z' },
{ count: 1, timeBucket: '1970-02-10T00:00:00.000Z' },
{ count: 1, timeBucket: '1970-01-01T00:00:00.000Z' },
]);
});
it('should return error if time bucket is requested with partners asset and archived', async () => { it('should return error if time bucket is requested with partners asset and archived', async () => {
const req1 = await request(app) const req1 = await request(app)
.get('/timeline/buckets') .get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`) .set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ withPartners: true, visibility: AssetVisibility.Archive }); .query({ size: TimeBucketSize.Month, withPartners: true, isArchived: true });
expect(req1.status).toBe(400); expect(req1.status).toBe(400);
expect(req1.body).toEqual(errorDto.badRequest()); expect(req1.body).toEqual(errorDto.badRequest());
@@ -95,7 +112,7 @@ describe('/timeline', () => {
const req2 = await request(app) const req2 = await request(app)
.get('/timeline/buckets') .get('/timeline/buckets')
.set('Authorization', `Bearer ${user.accessToken}`) .set('Authorization', `Bearer ${user.accessToken}`)
.query({ withPartners: true, visibility: undefined }); .query({ size: TimeBucketSize.Month, withPartners: true, isArchived: undefined });
expect(req2.status).toBe(400); expect(req2.status).toBe(400);
expect(req2.body).toEqual(errorDto.badRequest()); expect(req2.body).toEqual(errorDto.badRequest());
@@ -105,7 +122,7 @@ describe('/timeline', () => {
const req1 = await request(app) const req1 = await request(app)
.get('/timeline/buckets') .get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`) .set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ withPartners: true, isFavorite: true }); .query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: true });
expect(req1.status).toBe(400); expect(req1.status).toBe(400);
expect(req1.body).toEqual(errorDto.badRequest()); expect(req1.body).toEqual(errorDto.badRequest());
@@ -113,7 +130,7 @@ describe('/timeline', () => {
const req2 = await request(app) const req2 = await request(app)
.get('/timeline/buckets') .get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`) .set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ withPartners: true, isFavorite: false }); .query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: false });
expect(req2.status).toBe(400); expect(req2.status).toBe(400);
expect(req2.body).toEqual(errorDto.badRequest()); expect(req2.body).toEqual(errorDto.badRequest());
@@ -123,7 +140,7 @@ describe('/timeline', () => {
const req = await request(app) const req = await request(app)
.get('/timeline/buckets') .get('/timeline/buckets')
.set('Authorization', `Bearer ${user.accessToken}`) .set('Authorization', `Bearer ${user.accessToken}`)
.query({ withPartners: true, isTrashed: true }); .query({ size: TimeBucketSize.Month, withPartners: true, isTrashed: true });
expect(req.status).toBe(400); expect(req.status).toBe(400);
expect(req.body).toEqual(errorDto.badRequest()); expect(req.body).toEqual(errorDto.badRequest());
@@ -133,6 +150,7 @@ describe('/timeline', () => {
describe('GET /timeline/bucket', () => { describe('GET /timeline/bucket', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get('/timeline/bucket').query({ const { status, body } = await request(app).get('/timeline/bucket').query({
size: TimeBucketSize.Month,
timeBucket: '1900-01-01', timeBucket: '1900-01-01',
}); });
@@ -143,27 +161,11 @@ describe('/timeline', () => {
it('should handle 5 digit years', async () => { it('should handle 5 digit years', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/timeline/bucket') .get('/timeline/bucket')
.query({ timeBucket: '012345-01-01' }) .query({ size: TimeBucketSize.Month, timeBucket: '012345-01-01' })
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`); .set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual({ expect(body).toEqual([]);
city: [],
country: [],
duration: [],
id: [],
visibility: [],
isFavorite: [],
isImage: [],
isTrashed: [],
livePhotoVideoId: [],
localDateTime: [],
ownerId: [],
projectionType: [],
ratio: [],
status: [],
thumbhash: [],
});
}); });
// TODO enable date string validation while still accepting 5 digit years // TODO enable date string validation while still accepting 5 digit years
@@ -171,7 +173,7 @@ describe('/timeline', () => {
// const { status, body } = await request(app) // const { status, body } = await request(app)
// .get('/timeline/bucket') // .get('/timeline/bucket')
// .set('Authorization', `Bearer ${user.accessToken}`) // .set('Authorization', `Bearer ${user.accessToken}`)
// .query({ timeBucket: 'foo' }); // .query({ size: TimeBucketSize.Month, timeBucket: 'foo' });
// expect(status).toBe(400); // expect(status).toBe(400);
// expect(body).toEqual(errorDto.badRequest); // expect(body).toEqual(errorDto.badRequest);
@@ -181,26 +183,10 @@ describe('/timeline', () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/timeline/bucket') .get('/timeline/bucket')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`) .set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ timeBucket: '1970-02-10' }); .query({ size: TimeBucketSize.Month, timeBucket: '1970-02-10' });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual({ expect(body).toEqual([]);
city: [],
country: [],
duration: [],
id: [],
visibility: [],
isFavorite: [],
isImage: [],
isTrashed: [],
livePhotoVideoId: [],
localDateTime: [],
ownerId: [],
projectionType: [],
ratio: [],
status: [],
thumbhash: [],
});
}); });
}); });
}); });
+13 -13
View File
@@ -215,19 +215,6 @@ describe('/admin/users', () => {
const user = await getMyUser({ headers: asBearerAuth(token.accessToken) }); const user = await getMyUser({ headers: asBearerAuth(token.accessToken) });
expect(user).toMatchObject({ email: nonAdmin.userEmail }); expect(user).toMatchObject({ email: nonAdmin.userEmail });
}); });
it('should update the avatar color', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}`)
.send({ avatarColor: 'orange' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ avatarColor: 'orange' });
const after = await getUserAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ avatarColor: 'orange' });
});
}); });
describe('PUT /admin/users/:id/preferences', () => { describe('PUT /admin/users/:id/preferences', () => {
@@ -253,6 +240,19 @@ describe('/admin/users', () => {
expect(after).toMatchObject({ memories: { enabled: false } }); expect(after).toMatchObject({ memories: { enabled: false } });
}); });
it('should update the avatar color', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}/preferences`)
.send({ avatar: { color: 'orange' } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ avatar: { color: 'orange' } });
const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ avatar: { color: 'orange' } });
});
it('should update download archive size', async () => { it('should update download archive size', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}/preferences`) .put(`/admin/users/${admin.userId}/preferences`)
+13 -13
View File
@@ -139,19 +139,6 @@ describe('/users', () => {
profileChangedAt: expect.anything(), profileChangedAt: expect.anything(),
}); });
}); });
it('should update avatar color', async () => {
const { status, body } = await request(app)
.put(`/users/me`)
.send({ avatarColor: 'blue' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ avatarColor: 'blue' });
const after = await getMyUser({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ avatarColor: 'blue' });
});
}); });
describe('PUT /users/me/preferences', () => { describe('PUT /users/me/preferences', () => {
@@ -171,6 +158,19 @@ describe('/users', () => {
expect(after).toMatchObject({ memories: { enabled: false } }); expect(after).toMatchObject({ memories: { enabled: false } });
}); });
it('should update avatar color', async () => {
const { status, body } = await request(app)
.put(`/users/me/preferences`)
.send({ avatar: { color: 'blue' } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ avatar: { color: 'blue' } });
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ avatar: { color: 'blue' } });
});
it('should require an integer for download archive size', async () => { it('should require an integer for download archive size', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/users/me/preferences`) .put(`/users/me/preferences`)
+1 -5
View File
@@ -3,7 +3,6 @@ import {
AssetMediaCreateDto, AssetMediaCreateDto,
AssetMediaResponseDto, AssetMediaResponseDto,
AssetResponseDto, AssetResponseDto,
AssetVisibility,
CheckExistingAssetsDto, CheckExistingAssetsDto,
CreateAlbumDto, CreateAlbumDto,
CreateLibraryDto, CreateLibraryDto,
@@ -430,10 +429,7 @@ export const utils = {
}, },
archiveAssets: (accessToken: string, ids: string[]) => archiveAssets: (accessToken: string, ids: string[]) =>
updateAssets( updateAssets({ assetBulkUpdateDto: { ids, isArchived: true } }, { headers: asBearerAuth(accessToken) }),
{ assetBulkUpdateDto: { ids, visibility: AssetVisibility.Archive } },
{ headers: asBearerAuth(accessToken) },
),
deleteAssets: (accessToken: string, ids: string[]) => deleteAssets: (accessToken: string, ids: string[]) =>
deleteAssets({ assetBulkDeleteDto: { ids } }, { headers: asBearerAuth(accessToken) }), deleteAssets({ assetBulkDeleteDto: { ids } }, { headers: asBearerAuth(accessToken) }),
+3 -3
View File
@@ -25,7 +25,7 @@ test.describe('Registration', () => {
// login // login
await expect(page).toHaveTitle(/Login/); await expect(page).toHaveTitle(/Login/);
await page.goto('/auth/login?autoLaunch=0'); await page.goto('/auth/login');
await page.getByLabel('Email').fill('admin@immich.app'); await page.getByLabel('Email').fill('admin@immich.app');
await page.getByLabel('Password').fill('password'); await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Login' }).click(); await page.getByRole('button', { name: 'Login' }).click();
@@ -59,7 +59,7 @@ test.describe('Registration', () => {
await context.clearCookies(); await context.clearCookies();
// login // login
await page.goto('/auth/login?autoLaunch=0'); await page.goto('/auth/login');
await page.getByLabel('Email').fill('user@immich.cloud'); await page.getByLabel('Email').fill('user@immich.cloud');
await page.getByLabel('Password').fill('password'); await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Login' }).click(); await page.getByRole('button', { name: 'Login' }).click();
@@ -72,7 +72,7 @@ test.describe('Registration', () => {
await page.getByRole('button', { name: 'Change password' }).click(); await page.getByRole('button', { name: 'Change password' }).click();
// login with new password // login with new password
await expect(page).toHaveURL('/auth/login?autoLaunch=0'); await expect(page).toHaveURL('/auth/login');
await page.getByLabel('Email').fill('user@immich.cloud'); await page.getByLabel('Email').fill('user@immich.cloud');
await page.getByLabel('Password').fill('new-password'); await page.getByLabel('Password').fill('new-password');
await page.getByRole('button', { name: 'Login' }).click(); await page.getByRole('button', { name: 'Login' }).click();
@@ -21,9 +21,23 @@ test.describe('Photo Viewer', () => {
test.beforeEach(async ({ context, page }) => { test.beforeEach(async ({ context, page }) => {
// before each test, login as user // before each test, login as user
await utils.setAuthCookies(context, admin.accessToken); await utils.setAuthCookies(context, admin.accessToken);
await page.goto('/photos');
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
}); });
test('initially shows a loading spinner', async ({ page }) => {
await page.route(`/api/assets/${asset.id}/thumbnail**`, async (route) => {
// slow down the request for thumbnail, so spinner has chance to show up
await new Promise((f) => setTimeout(f, 2000));
await route.continue();
});
await page.goto(`/photos/${asset.id}`);
await page.waitForLoadState('load');
// this is the spinner
await page.waitForSelector('svg[role=status]');
await expect(page.getByTestId('loading-spinner')).toBeVisible();
});
test('loads original photo when zoomed', async ({ page }) => { test('loads original photo when zoomed', async ({ page }) => {
await page.goto(`/photos/${asset.id}`); await page.goto(`/photos/${asset.id}`);
await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('thumbnail'); await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('thumbnail');
+5 -2
View File
@@ -47,13 +47,16 @@ test.describe('Shared Links', () => {
await page.locator(`[data-asset-id="${asset.id}"]`).hover(); await page.locator(`[data-asset-id="${asset.id}"]`).hover();
await page.waitForSelector('[data-group] svg'); await page.waitForSelector('[data-group] svg');
await page.getByRole('checkbox').click(); await page.getByRole('checkbox').click();
await Promise.all([page.waitForEvent('download'), page.getByRole('button', { name: 'Download' }).click()]); await page.getByRole('button', { name: 'Download' }).click();
await page.waitForEvent('download');
}); });
test('download all from shared link', async ({ page }) => { test('download all from shared link', async ({ page }) => {
await page.goto(`/share/${sharedLink.key}`); await page.goto(`/share/${sharedLink.key}`);
await page.getByRole('heading', { name: 'Test Album' }).waitFor(); await page.getByRole('heading', { name: 'Test Album' }).waitFor();
await Promise.all([page.waitForEvent('download'), page.getByRole('button', { name: 'Download' }).click()]); await page.getByRole('button', { name: 'Download' }).click();
await page.getByText('DOWNLOADING', { exact: true }).waitFor();
await page.waitForEvent('download');
}); });
test('enter password for a shared link', async ({ page }) => { test('enter password for a shared link', async ({ page }) => {
+183 -13
View File
@@ -14,6 +14,7 @@
"add_a_location": "إضافة موقع", "add_a_location": "إضافة موقع",
"add_a_name": "إضافة إسم", "add_a_name": "إضافة إسم",
"add_a_title": "إضافة عنوان", "add_a_title": "إضافة عنوان",
"add_endpoint": "Add endpoint",
"add_exclusion_pattern": "إضافة نمط إستثناء", "add_exclusion_pattern": "إضافة نمط إستثناء",
"add_import_path": "إضافة مسار الإستيراد", "add_import_path": "إضافة مسار الإستيراد",
"add_location": "إضافة موقع", "add_location": "إضافة موقع",
@@ -186,13 +187,20 @@
"oauth_auto_register": "التسجيل التلقائي", "oauth_auto_register": "التسجيل التلقائي",
"oauth_auto_register_description": "التسجيل التلقائي للمستخدمين الجدد بعد تسجيل الدخول باستخدام OAuth", "oauth_auto_register_description": "التسجيل التلقائي للمستخدمين الجدد بعد تسجيل الدخول باستخدام OAuth",
"oauth_button_text": "نص الزر", "oauth_button_text": "نص الزر",
"oauth_client_id": "معرف العميل",
"oauth_client_secret": "الرمز السري للعميل",
"oauth_enable_description": "تسجيل الدخول باستخدام OAuth", "oauth_enable_description": "تسجيل الدخول باستخدام OAuth",
"oauth_issuer_url": "عنوان URL الخاص بجهة الإصدار",
"oauth_mobile_redirect_uri": "عنوان URI لإعادة التوجيه على الهاتف", "oauth_mobile_redirect_uri": "عنوان URI لإعادة التوجيه على الهاتف",
"oauth_mobile_redirect_uri_override": "تجاوز عنوان URI لإعادة التوجيه على الهاتف", "oauth_mobile_redirect_uri_override": "تجاوز عنوان URI لإعادة التوجيه على الهاتف",
"oauth_mobile_redirect_uri_override_description": "قم بتفعيله عندما لا يسمح موفر OAuth بمعرف URI للجوال، مثل '{callback}'", "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": "OAuth",
"oauth_settings_description": "إدارة إعدادات تسجيل الدخول 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": "المطالبة بتصنيف التخزين",
"oauth_storage_label_claim_description": "قم تلقائيًا بتعيين تصنيف التخزين الخاص بالمستخدم على قيمة هذه المطالبة.", "oauth_storage_label_claim_description": "قم تلقائيًا بتعيين تصنيف التخزين الخاص بالمستخدم على قيمة هذه المطالبة.",
"oauth_storage_quota_claim": "المطالبة بحصة التخزين", "oauth_storage_quota_claim": "المطالبة بحصة التخزين",
@@ -358,8 +366,12 @@
"admin_password": "كلمة سر المشرف", "admin_password": "كلمة سر المشرف",
"administration": "الإدارة", "administration": "الإدارة",
"advanced": "متقدم", "advanced": "متقدم",
"advanced_settings_log_level_title": "Log level: {}",
"advanced_settings_prefer_remote_subtitle": "تكون بعض الأجهزة بطيئة للغاية في تحميل الصور المصغرة من الأصول الموجودة على الجهاز. قم بتنشيط هذا الإعداد لتحميل الصور البعيدة بدلاً من ذلك.", "advanced_settings_prefer_remote_subtitle": "تكون بعض الأجهزة بطيئة للغاية في تحميل الصور المصغرة من الأصول الموجودة على الجهاز. قم بتنشيط هذا الإعداد لتحميل الصور البعيدة بدلاً من ذلك.",
"advanced_settings_prefer_remote_title": "تفضل الصور البعيدة", "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_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
"advanced_settings_self_signed_ssl_title": "السماح بشهادات SSL الموقعة ذاتيًا", "advanced_settings_self_signed_ssl_title": "السماح بشهادات SSL الموقعة ذاتيًا",
"advanced_settings_tile_subtitle": "إعدادات المستخدم المتقدمة", "advanced_settings_tile_subtitle": "إعدادات المستخدم المتقدمة",
"advanced_settings_troubleshooting_subtitle": "تمكين الميزات الإضافية لاستكشاف الأخطاء وإصلاحها", "advanced_settings_troubleshooting_subtitle": "تمكين الميزات الإضافية لاستكشاف الأخطاء وإصلاحها",
@@ -383,7 +395,9 @@
"album_remove_user_confirmation": "هل أنت متأكد أنك تريد إزالة {user}؟", "album_remove_user_confirmation": "هل أنت متأكد أنك تريد إزالة {user}؟",
"album_share_no_users": "يبدو أنك قمت بمشاركة هذا الألبوم مع جميع المستخدمين أو ليس لديك أي مستخدم للمشاركة معه.", "album_share_no_users": "يبدو أنك قمت بمشاركة هذا الألبوم مع جميع المستخدمين أو ليس لديك أي مستخدم للمشاركة معه.",
"album_thumbnail_card_item": "عنصر واحد", "album_thumbnail_card_item": "عنصر واحد",
"album_thumbnail_card_items": "{} items",
"album_thumbnail_card_shared": " · . مشترك", "album_thumbnail_card_shared": " · . مشترك",
"album_thumbnail_shared_by": "Shared by {}",
"album_updated": "تم تحديث الألبوم", "album_updated": "تم تحديث الألبوم",
"album_updated_setting_description": "تلقي إشعارًا عبر البريد الإلكتروني عندما يحتوي الألبوم المشترك على محتويات جديدة", "album_updated_setting_description": "تلقي إشعارًا عبر البريد الإلكتروني عندما يحتوي الألبوم المشترك على محتويات جديدة",
"album_user_left": "تم ترك {album}", "album_user_left": "تم ترك {album}",
@@ -421,8 +435,10 @@
"archive": "الأرشيف", "archive": "الأرشيف",
"archive_or_unarchive_photo": "أرشفة الصورة أو إلغاء أرشفتها", "archive_or_unarchive_photo": "أرشفة الصورة أو إلغاء أرشفتها",
"archive_page_no_archived_assets": "لم يتم العثور على الأصول المؤرشفة", "archive_page_no_archived_assets": "لم يتم العثور على الأصول المؤرشفة",
"archive_page_title": "Archive ({})",
"archive_size": "حجم الأرشيف", "archive_size": "حجم الأرشيف",
"archive_size_description": "تكوين حجم الأرشيف للتنزيلات (بالجيجابايت)", "archive_size_description": "تكوين حجم الأرشيف للتنزيلات (بالجيجابايت)",
"archived": "Archived",
"archived_count": "{count, plural, other {الأرشيف #}}", "archived_count": "{count, plural, other {الأرشيف #}}",
"are_these_the_same_person": "هل هؤلاء هم نفس الشخص؟", "are_these_the_same_person": "هل هؤلاء هم نفس الشخص؟",
"are_you_sure_to_do_this": "هل انت متأكد من أنك تريد أن تفعل هذا؟", "are_you_sure_to_do_this": "هل انت متأكد من أنك تريد أن تفعل هذا؟",
@@ -444,26 +460,39 @@
"asset_list_settings_title": "شبكة الصور", "asset_list_settings_title": "شبكة الصور",
"asset_offline": "المحتوى غير اتصال", "asset_offline": "المحتوى غير اتصال",
"asset_offline_description": "لم يعد هذا الأصل الخارجي موجودًا على القرص. يرجى الاتصال بمسؤول Immich للحصول على المساعدة.", "asset_offline_description": "لم يعد هذا الأصل الخارجي موجودًا على القرص. يرجى الاتصال بمسؤول Immich للحصول على المساعدة.",
"asset_restored_successfully": "Asset restored successfully",
"asset_skipped": "تم تخطيه", "asset_skipped": "تم تخطيه",
"asset_skipped_in_trash": "في سلة المهملات", "asset_skipped_in_trash": "في سلة المهملات",
"asset_uploaded": "تم الرفع", "asset_uploaded": "تم الرفع",
"asset_uploading": "جارٍ الرفع…", "asset_uploading": "جارٍ الرفع…",
"asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
"asset_viewer_settings_title": "عارض الأصول", "asset_viewer_settings_title": "عارض الأصول",
"assets": "المحتويات", "assets": "المحتويات",
"assets_added_count": "تمت إضافة {count, plural, one {# محتوى} other {# محتويات}}", "assets_added_count": "تمت إضافة {count, plural, one {# محتوى} other {# محتويات}}",
"assets_added_to_album_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_added_to_name_count": "تم إضافة {count, plural, one {# محتوى} other {# محتويات }} إلى {hasName, select, true {<b>{name}</b>} other {ألبوم جديد}}",
"assets_count": "{count, plural, one {# محتوى} other {# محتويات}}", "assets_count": "{count, plural, one {# محتوى} other {# محتويات}}",
"assets_deleted_permanently": "{} asset(s) deleted permanently",
"assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server",
"assets_moved_to_trash_count": "تم نقل {count, plural, one {# محتوى} other {# محتويات}} إلى سلة المهملات", "assets_moved_to_trash_count": "تم نقل {count, plural, one {# محتوى} other {# محتويات}} إلى سلة المهملات",
"assets_permanently_deleted_count": "تم حذف {count, plural, one {# هذا المحتوى} other {# هذه المحتويات}} بشكل دائم", "assets_permanently_deleted_count": "تم حذف {count, plural, one {# هذا المحتوى} other {# هذه المحتويات}} بشكل دائم",
"assets_removed_count": "تمت إزالة {count, plural, one {# محتوى} other {# محتويات}}", "assets_removed_count": "تمت إزالة {count, plural, one {# محتوى} other {# محتويات}}",
"assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device",
"assets_restore_confirmation": "هل أنت متأكد من أنك تريد استعادة جميع الأصول المحذوفة؟ لا يمكنك التراجع عن هذا الإجراء! لاحظ أنه لا يمكن استعادة أي أصول غير متصلة بهذه الطريقة.", "assets_restore_confirmation": "هل أنت متأكد من أنك تريد استعادة جميع الأصول المحذوفة؟ لا يمكنك التراجع عن هذا الإجراء! لاحظ أنه لا يمكن استعادة أي أصول غير متصلة بهذه الطريقة.",
"assets_restored_count": "تمت استعادة {count, plural, one {# محتوى} other {# محتويات}}", "assets_restored_count": "تمت استعادة {count, plural, one {# محتوى} other {# محتويات}}",
"assets_restored_successfully": "{} asset(s) restored successfully",
"assets_trashed": "{} asset(s) trashed",
"assets_trashed_count": "تم إرسال {count, plural, one {# محتوى} other {# محتويات}} إلى سلة المهملات", "assets_trashed_count": "تم إرسال {count, plural, one {# محتوى} other {# محتويات}} إلى سلة المهملات",
"assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
"assets_were_part_of_album_count": "{count, plural, one {هذا المحتوى} other {هذه المحتويات}} في الألبوم بالفعل", "assets_were_part_of_album_count": "{count, plural, one {هذا المحتوى} other {هذه المحتويات}} في الألبوم بالفعل",
"authorized_devices": "الأجهزه المخولة", "authorized_devices": "الأجهزه المخولة",
"automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
"automatic_endpoint_switching_title": "Automatic URL switching",
"back": "خلف", "back": "خلف",
"back_close_deselect": "الرجوع أو الإغلاق أو إلغاء التحديد", "back_close_deselect": "الرجوع أو الإغلاق أو إلغاء التحديد",
"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_albums_tap": "انقر للتضمين، وانقر نقرًا مزدوجًا للاستثناء",
"backup_album_selection_page_assets_scatter": "يمكن أن تنتشر الأصول عبر ألبومات متعددة. وبالتالي، يمكن تضمين الألبومات أو استبعادها أثناء عملية النسخ الاحتياطي.", "backup_album_selection_page_assets_scatter": "يمكن أن تنتشر الأصول عبر ألبومات متعددة. وبالتالي، يمكن تضمين الألبومات أو استبعادها أثناء عملية النسخ الاحتياطي.",
"backup_album_selection_page_select_albums": "حدد الألبومات", "backup_album_selection_page_select_albums": "حدد الألبومات",
@@ -472,9 +501,11 @@
"backup_all": "الجميع", "backup_all": "الجميع",
"backup_background_service_backup_failed_message": "فشل في النسخ الاحتياطي للأصول. جارٍ إعادة المحاولة...", "backup_background_service_backup_failed_message": "فشل في النسخ الاحتياطي للأصول. جارٍ إعادة المحاولة...",
"backup_background_service_connection_failed_message": "فشل في الاتصال بالخادم. جارٍ إعادة المحاولة...", "backup_background_service_connection_failed_message": "فشل في الاتصال بالخادم. جارٍ إعادة المحاولة...",
"backup_background_service_current_upload_notification": "Uploading {}",
"backup_background_service_default_notification": "التحقق من الأصول الجديدة ...", "backup_background_service_default_notification": "التحقق من الأصول الجديدة ...",
"backup_background_service_error_title": "خطأ في النسخ الاحتياطي", "backup_background_service_error_title": "خطأ في النسخ الاحتياطي",
"backup_background_service_in_progress_notification": "النسخ الاحتياطي للأصول الخاصة بك...", "backup_background_service_in_progress_notification": "النسخ الاحتياطي للأصول الخاصة بك...",
"backup_background_service_upload_failure_notification": "Failed to upload {}",
"backup_controller_page_albums": "ألبومات احتياطية", "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_disabled_title": "تم تعطيل تحديث التطبيق في الخلفية",
@@ -485,6 +516,7 @@
"backup_controller_page_background_battery_info_title": "تحسين البطارية", "backup_controller_page_background_battery_info_title": "تحسين البطارية",
"backup_controller_page_background_charging": "فقط أثناء الشحن", "backup_controller_page_background_charging": "فقط أثناء الشحن",
"backup_controller_page_background_configure_error": "فشل في تكوين خدمة الخلفية", "backup_controller_page_background_configure_error": "فشل في تكوين خدمة الخلفية",
"backup_controller_page_background_delay": "Delay new assets backup: {}",
"backup_controller_page_background_description": "قم بتشغيل خدمة الخلفية لإجراء نسخ احتياطي لأي أصول جديدة تلقائيًا دون الحاجة إلى فتح التطبيق", "backup_controller_page_background_description": "قم بتشغيل خدمة الخلفية لإجراء نسخ احتياطي لأي أصول جديدة تلقائيًا دون الحاجة إلى فتح التطبيق",
"backup_controller_page_background_is_off": "تم إيقاف النسخ الاحتياطي التلقائي للخلفية", "backup_controller_page_background_is_off": "تم إيقاف النسخ الاحتياطي التلقائي للخلفية",
"backup_controller_page_background_is_on": "النسخ الاحتياطي التلقائي للخلفية قيد التشغيل", "backup_controller_page_background_is_on": "النسخ الاحتياطي التلقائي للخلفية قيد التشغيل",
@@ -494,8 +526,12 @@
"backup_controller_page_backup": "دعم", "backup_controller_page_backup": "دعم",
"backup_controller_page_backup_selected": "المحدد: ", "backup_controller_page_backup_selected": "المحدد: ",
"backup_controller_page_backup_sub": "النسخ الاحتياطي للصور ومقاطع الفيديو", "backup_controller_page_backup_sub": "النسخ الاحتياطي للصور ومقاطع الفيديو",
"backup_controller_page_created": "Created on: {}",
"backup_controller_page_desc_backup": "قم بتشغيل النسخ الاحتياطي الأمامي لتحميل الأصول الجديدة تلقائيًا إلى الخادم عند فتح التطبيق.", "backup_controller_page_desc_backup": "قم بتشغيل النسخ الاحتياطي الأمامي لتحميل الأصول الجديدة تلقائيًا إلى الخادم عند فتح التطبيق.",
"backup_controller_page_excluded": "مستبعد: ", "backup_controller_page_excluded": "مستبعد: ",
"backup_controller_page_failed": "Failed ({})",
"backup_controller_page_filename": "File name: {} [{}]",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "معلومات النسخ الاحتياطي", "backup_controller_page_info": "معلومات النسخ الاحتياطي",
"backup_controller_page_none_selected": "لم يتم التحديد", "backup_controller_page_none_selected": "لم يتم التحديد",
"backup_controller_page_remainder": "بقية", "backup_controller_page_remainder": "بقية",
@@ -504,6 +540,7 @@
"backup_controller_page_start_backup": "بدء النسخ الاحتياطي", "backup_controller_page_start_backup": "بدء النسخ الاحتياطي",
"backup_controller_page_status_off": "النسخة الاحتياطية التلقائية غير فعالة", "backup_controller_page_status_off": "النسخة الاحتياطية التلقائية غير فعالة",
"backup_controller_page_status_on": "النسخة الاحتياطية التلقائية فعالة", "backup_controller_page_status_on": "النسخة الاحتياطية التلقائية فعالة",
"backup_controller_page_storage_format": "{} of {} used",
"backup_controller_page_to_backup": "الألبومات الاحتياطية", "backup_controller_page_to_backup": "الألبومات الاحتياطية",
"backup_controller_page_total_sub": "جميع الصور ومقاطع الفيديو الفريدة من ألبومات مختارة", "backup_controller_page_total_sub": "جميع الصور ومقاطع الفيديو الفريدة من ألبومات مختارة",
"backup_controller_page_turn_off": "قم بإيقاف تشغيل النسخ الاحتياطي المقدمة", "backup_controller_page_turn_off": "قم بإيقاف تشغيل النسخ الاحتياطي المقدمة",
@@ -516,6 +553,7 @@
"backup_manual_success": "نجاح", "backup_manual_success": "نجاح",
"backup_manual_title": "حالة التحميل", "backup_manual_title": "حالة التحميل",
"backup_options_page_title": "خيارات النسخ الاحتياطي", "backup_options_page_title": "خيارات النسخ الاحتياطي",
"backup_setting_subtitle": "Manage background and foreground upload settings",
"backward": "الى الوراء", "backward": "الى الوراء",
"birthdate_saved": "تم حفظ تاريخ الميلاد بنجاح", "birthdate_saved": "تم حفظ تاريخ الميلاد بنجاح",
"birthdate_set_description": "يتم استخدام تاريخ الميلاد لحساب عمر هذا الشخص وقت التقاط الصورة.", "birthdate_set_description": "يتم استخدام تاريخ الميلاد لحساب عمر هذا الشخص وقت التقاط الصورة.",
@@ -527,16 +565,21 @@
"bulk_keep_duplicates_confirmation": "هل أنت متأكد من أنك تريد الاحتفاظ بـ {count, plural, one {# محتوى مكرر} other {# محتويات مكررة}}؟ سيؤدي هذا إلى حل جميع مجموعات النسخ المكررة دون حذف أي شيء.", "bulk_keep_duplicates_confirmation": "هل أنت متأكد من أنك تريد الاحتفاظ بـ {count, plural, one {# محتوى مكرر} other {# محتويات مكررة}}؟ سيؤدي هذا إلى حل جميع مجموعات النسخ المكررة دون حذف أي شيء.",
"bulk_trash_duplicates_confirmation": "هل أنت متأكد من أنك تريد إرسال {count, plural, one {# محتوى مكرر} other {# محتويات مكررة}} إلى سلة المهملات ؟ سيحتفظ هذا بأكبر محتوى من كل مجموعة ويرسل جميع النسخ المكررة الأخرى إلى سلة المهملات.", "bulk_trash_duplicates_confirmation": "هل أنت متأكد من أنك تريد إرسال {count, plural, one {# محتوى مكرر} other {# محتويات مكررة}} إلى سلة المهملات ؟ سيحتفظ هذا بأكبر محتوى من كل مجموعة ويرسل جميع النسخ المكررة الأخرى إلى سلة المهملات.",
"buy": "شراء immich", "buy": "شراء immich",
"cache_settings_album_thumbnails": "Library page thumbnails ({} assets)",
"cache_settings_clear_cache_button": "مسح ذاكرة التخزين المؤقت", "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_clear_button": "واضح",
"cache_settings_duplicated_assets_subtitle": "الصور ومقاطع الفيديو اللتي تم تجاهلها المدرجة في التطبيق", "cache_settings_duplicated_assets_subtitle": "الصور ومقاطع الفيديو اللتي تم تجاهلها المدرجة في التطبيق",
"cache_settings_duplicated_assets_title": "Duplicated Assets ({})",
"cache_settings_image_cache_size": "Image cache size ({} assets)",
"cache_settings_statistics_album": "مكتبه الصور المصغره", "cache_settings_statistics_album": "مكتبه الصور المصغره",
"cache_settings_statistics_assets": "{} assets ({})",
"cache_settings_statistics_full": "صور كاملة", "cache_settings_statistics_full": "صور كاملة",
"cache_settings_statistics_shared": "صورة ألبوم مشتركة", "cache_settings_statistics_shared": "صورة ألبوم مشتركة",
"cache_settings_statistics_thumbnail": "الصورة المصغرة", "cache_settings_statistics_thumbnail": "الصورة المصغرة",
"cache_settings_statistics_title": "استخدام ذاكرة التخزين المؤقت", "cache_settings_statistics_title": "استخدام ذاكرة التخزين المؤقت",
"cache_settings_subtitle": "تحكم في سلوك التخزين المؤقت لتطبيق الجوال.", "cache_settings_subtitle": "تحكم في سلوك التخزين المؤقت لتطبيق الجوال.",
"cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)",
"cache_settings_tile_subtitle": "التحكم في سلوك التخزين المحلي", "cache_settings_tile_subtitle": "التحكم في سلوك التخزين المحلي",
"cache_settings_tile_title": "التخزين المحلي", "cache_settings_tile_title": "التخزين المحلي",
"cache_settings_title": "إعدادات التخزين المؤقت", "cache_settings_title": "إعدادات التخزين المؤقت",
@@ -545,10 +588,12 @@
"camera_model": "طراز الكاميرا", "camera_model": "طراز الكاميرا",
"cancel": "إلغاء", "cancel": "إلغاء",
"cancel_search": "الغي البحث", "cancel_search": "الغي البحث",
"canceled": "Canceled",
"cannot_merge_people": "لا يمكن دمج الأشخاص", "cannot_merge_people": "لا يمكن دمج الأشخاص",
"cannot_undo_this_action": "لا يمكنك التراجع عن هذا الإجراء!", "cannot_undo_this_action": "لا يمكنك التراجع عن هذا الإجراء!",
"cannot_update_the_description": "لا يمكن تحديث الوصف", "cannot_update_the_description": "لا يمكن تحديث الوصف",
"change_date": "غيّر التاريخ", "change_date": "غيّر التاريخ",
"change_display_order": "Change display order",
"change_expiration_time": "تغيير وقت انتهاء الصلاحية", "change_expiration_time": "تغيير وقت انتهاء الصلاحية",
"change_location": "غيّر الموقع", "change_location": "غيّر الموقع",
"change_name": "تغيير الإسم", "change_name": "تغيير الإسم",
@@ -560,10 +605,12 @@
"change_password_form_new_password": "كلمة المرور الجديدة", "change_password_form_new_password": "كلمة المرور الجديدة",
"change_password_form_password_mismatch": "كلمة المرور غير مطابقة", "change_password_form_password_mismatch": "كلمة المرور غير مطابقة",
"change_password_form_reenter_new_password": "أعد إدخال كلمة مرور جديدة", "change_password_form_reenter_new_password": "أعد إدخال كلمة مرور جديدة",
"change_pin_code": "تغيير الرقم السري",
"change_your_password": "غير كلمة المرور الخاصة بك", "change_your_password": "غير كلمة المرور الخاصة بك",
"changed_visibility_successfully": "تم تغيير الرؤية بنجاح", "changed_visibility_successfully": "تم تغيير الرؤية بنجاح",
"check_all": "تحقق من الكل", "check_all": "تحقق من الكل",
"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.",
"check_logs": "تحقق من السجلات", "check_logs": "تحقق من السجلات",
"choose_matching_people_to_merge": "اختر الأشخاص المتطابقين لدمجهم", "choose_matching_people_to_merge": "اختر الأشخاص المتطابقين لدمجهم",
"city": "المدينة", "city": "المدينة",
@@ -572,6 +619,14 @@
"clear_all_recent_searches": "مسح جميع عمليات البحث الأخيرة", "clear_all_recent_searches": "مسح جميع عمليات البحث الأخيرة",
"clear_message": "إخلاء الرسالة", "clear_message": "إخلاء الرسالة",
"clear_value": "إخلاء القيمة", "clear_value": "إخلاء القيمة",
"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_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",
"clockwise": "باتجاه عقارب الساعة", "clockwise": "باتجاه عقارب الساعة",
"close": "إغلاق", "close": "إغلاق",
"collapse": "طي", "collapse": "طي",
@@ -584,21 +639,23 @@
"comments_are_disabled": "التعليقات معطلة", "comments_are_disabled": "التعليقات معطلة",
"common_create_new_album": "إنشاء ألبوم جديد", "common_create_new_album": "إنشاء ألبوم جديد",
"common_server_error": "يرجى التحقق من اتصال الشبكة الخاص بك ، والتأكد من أن الجهاز قابل للوصول وإصدارات التطبيق/الجهاز متوافقة.", "common_server_error": "يرجى التحقق من اتصال الشبكة الخاص بك ، والتأكد من أن الجهاز قابل للوصول وإصدارات التطبيق/الجهاز متوافقة.",
"completed": "Completed",
"confirm": "تأكيد", "confirm": "تأكيد",
"confirm_admin_password": "تأكيد كلمة مرور المسؤول", "confirm_admin_password": "تأكيد كلمة مرور المسؤول",
"confirm_delete_face": "هل أنت متأكد من حذف وجه {name} من الأصول؟", "confirm_delete_face": "هل أنت متأكد من حذف وجه {name} من الأصول؟",
"confirm_delete_shared_link": "هل أنت متأكد أنك تريد حذف هذا الرابط المشترك؟", "confirm_delete_shared_link": "هل أنت متأكد أنك تريد حذف هذا الرابط المشترك؟",
"confirm_keep_this_delete_others": "سيتم حذف جميع الأصول الأخرى في المجموعة باستثناء هذا الأصل. هل أنت متأكد من أنك تريد المتابعة؟", "confirm_keep_this_delete_others": "سيتم حذف جميع الأصول الأخرى في المجموعة باستثناء هذا الأصل. هل أنت متأكد من أنك تريد المتابعة؟",
"confirm_new_pin_code": "ثبت الرقم السري الجديد",
"confirm_password": "تأكيد كلمة المرور", "confirm_password": "تأكيد كلمة المرور",
"contain": "محتواة", "contain": "محتواة",
"context": "السياق", "context": "السياق",
"continue": "متابعة", "continue": "متابعة",
"control_bottom_app_bar_album_info_shared": "{} items · Shared",
"control_bottom_app_bar_create_new_album": "إنشاء ألبوم جديد", "control_bottom_app_bar_create_new_album": "إنشاء ألبوم جديد",
"control_bottom_app_bar_delete_from_immich": " حذف منال تطبيق", "control_bottom_app_bar_delete_from_immich": " حذف منال تطبيق",
"control_bottom_app_bar_delete_from_local": "حذف من الجهاز", "control_bottom_app_bar_delete_from_local": "حذف من الجهاز",
"control_bottom_app_bar_edit_location": "تحديد الوجهة", "control_bottom_app_bar_edit_location": "تحديد الوجهة",
"control_bottom_app_bar_edit_time": "تحرير التاريخ والوقت", "control_bottom_app_bar_edit_time": "تحرير التاريخ والوقت",
"control_bottom_app_bar_share_link": "Share Link",
"control_bottom_app_bar_share_to": "مشاركة إلى", "control_bottom_app_bar_share_to": "مشاركة إلى",
"control_bottom_app_bar_trash_from_immich": "حذفه ونقله في سله المهملات", "control_bottom_app_bar_trash_from_immich": "حذفه ونقله في سله المهملات",
"copied_image_to_clipboard": "تم نسخ الصورة إلى الحافظة.", "copied_image_to_clipboard": "تم نسخ الصورة إلى الحافظة.",
@@ -620,6 +677,7 @@
"create_link": "إنشاء رابط", "create_link": "إنشاء رابط",
"create_link_to_share": "إنشاء رابط للمشاركة", "create_link_to_share": "إنشاء رابط للمشاركة",
"create_link_to_share_description": "السماح لأي شخص لديه الرابط بمشاهدة الصورة (الصور) المحددة", "create_link_to_share_description": "السماح لأي شخص لديه الرابط بمشاهدة الصورة (الصور) المحددة",
"create_new": "CREATE NEW",
"create_new_person": "إنشاء شخص جديد", "create_new_person": "إنشاء شخص جديد",
"create_new_person_hint": "تعيين المحتويات المحددة لشخص جديد", "create_new_person_hint": "تعيين المحتويات المحددة لشخص جديد",
"create_new_user": "إنشاء مستخدم جديد", "create_new_user": "إنشاء مستخدم جديد",
@@ -629,9 +687,10 @@
"create_tag_description": "أنشئ علامة جديدة. بالنسبة للعلامات المتداخلة، يرجى إدخال المسار الكامل للعلامة بما في ذلك الخطوط المائلة للأمام.", "create_tag_description": "أنشئ علامة جديدة. بالنسبة للعلامات المتداخلة، يرجى إدخال المسار الكامل للعلامة بما في ذلك الخطوط المائلة للأمام.",
"create_user": "إنشاء مستخدم", "create_user": "إنشاء مستخدم",
"created": "تم الإنشاء", "created": "تم الإنشاء",
"crop": "Crop",
"curated_object_page_title": "أشياء", "curated_object_page_title": "أشياء",
"current_device": "الجهاز الحالي", "current_device": "الجهاز الحالي",
"current_pin_code": "الرقم السري الحالي", "current_server_address": "Current server address",
"custom_locale": "لغة مخصصة", "custom_locale": "لغة مخصصة",
"custom_locale_description": "تنسيق التواريخ والأرقام بناءً على اللغة والمنطقة", "custom_locale_description": "تنسيق التواريخ والأرقام بناءً على اللغة والمنطقة",
"daily_title_text_date": "E ، MMM DD", "daily_title_text_date": "E ، MMM DD",
@@ -682,6 +741,7 @@
"direction": "الإتجاه", "direction": "الإتجاه",
"disabled": "معطل", "disabled": "معطل",
"disallow_edits": "منع التعديلات", "disallow_edits": "منع التعديلات",
"discord": "Discord",
"discover": "اكتشف", "discover": "اكتشف",
"dismiss_all_errors": "تجاهل كافة الأخطاء", "dismiss_all_errors": "تجاهل كافة الأخطاء",
"dismiss_error": "تجاهل الخطأ", "dismiss_error": "تجاهل الخطأ",
@@ -693,12 +753,26 @@
"documentation": "الوثائق", "documentation": "الوثائق",
"done": "تم", "done": "تم",
"download": "تنزيل", "download": "تنزيل",
"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",
"download_include_embedded_motion_videos": "مقاطع الفيديو المدمجة", "download_include_embedded_motion_videos": "مقاطع الفيديو المدمجة",
"download_include_embedded_motion_videos_description": "تضمين مقاطع الفيديو المضمنة في الصور المتحركة كملف منفصل", "download_include_embedded_motion_videos_description": "تضمين مقاطع الفيديو المضمنة في الصور المتحركة كملف منفصل",
"download_notfound": "Download not found",
"download_paused": "Download paused",
"download_settings": "التنزيلات", "download_settings": "التنزيلات",
"download_settings_description": "إدارة الإعدادات المتعلقة بتنزيل المحتويات", "download_settings_description": "إدارة الإعدادات المتعلقة بتنزيل المحتويات",
"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",
"downloading": "جارٍ التنزيل", "downloading": "جارٍ التنزيل",
"downloading_asset_filename": "{filename} قيد التنزيل", "downloading_asset_filename": "{filename} قيد التنزيل",
"downloading_media": "Downloading media",
"drop_files_to_upload": "قم بإسقاط الملفات في أي مكان لرفعها", "drop_files_to_upload": "قم بإسقاط الملفات في أي مكان لرفعها",
"duplicates": "التكرارات", "duplicates": "التكرارات",
"duplicates_description": "قم بحل كل مجموعة من خلال الإشارة إلى التكرارات، إن وجدت", "duplicates_description": "قم بحل كل مجموعة من خلال الإشارة إلى التكرارات، إن وجدت",
@@ -728,15 +802,19 @@
"editor_crop_tool_h2_aspect_ratios": "نسب العرض إلى الارتفاع", "editor_crop_tool_h2_aspect_ratios": "نسب العرض إلى الارتفاع",
"editor_crop_tool_h2_rotation": "التدوير", "editor_crop_tool_h2_rotation": "التدوير",
"email": "البريد الإلكتروني", "email": "البريد الإلكتروني",
"empty_folder": "This folder is empty",
"empty_trash": "أفرغ سلة المهملات", "empty_trash": "أفرغ سلة المهملات",
"empty_trash_confirmation": "هل أنت متأكد أنك تريد إفراغ سلة المهملات؟ سيؤدي هذا إلى إزالة جميع المحتويات الموجودة في سلة المهملات بشكل نهائي من Immich.\nلا يمكنك التراجع عن هذا الإجراء!", "empty_trash_confirmation": "هل أنت متأكد أنك تريد إفراغ سلة المهملات؟ سيؤدي هذا إلى إزالة جميع المحتويات الموجودة في سلة المهملات بشكل نهائي من Immich.\nلا يمكنك التراجع عن هذا الإجراء!",
"enable": "تفعيل", "enable": "تفعيل",
"enabled": "مفعل", "enabled": "مفعل",
"end_date": "تاريخ الإنتهاء", "end_date": "تاريخ الإنتهاء",
"enqueued": "Enqueued",
"enter_wifi_name": "Enter WiFi name", "enter_wifi_name": "Enter WiFi name",
"error": "خطأ", "error": "خطأ",
"error_change_sort_album": "Failed to change album sort order",
"error_delete_face": "حدث خطأ في حذف الوجه من الأصول", "error_delete_face": "حدث خطأ في حذف الوجه من الأصول",
"error_loading_image": "حدث خطأ أثناء تحميل الصورة", "error_loading_image": "حدث خطأ أثناء تحميل الصورة",
"error_saving_image": "Error: {}",
"error_title": "خطأ - حدث خللٌ ما", "error_title": "خطأ - حدث خللٌ ما",
"errors": { "errors": {
"cannot_navigate_next_asset": "لا يمكن الانتقال إلى المحتوى التالي", "cannot_navigate_next_asset": "لا يمكن الانتقال إلى المحتوى التالي",
@@ -870,6 +948,10 @@
"exif_bottom_sheet_location": "موقع", "exif_bottom_sheet_location": "موقع",
"exif_bottom_sheet_people": "الناس", "exif_bottom_sheet_people": "الناس",
"exif_bottom_sheet_person_add_person": "اضف اسما", "exif_bottom_sheet_person_add_person": "اضف اسما",
"exif_bottom_sheet_person_age": "Age {}",
"exif_bottom_sheet_person_age_months": "Age {} months",
"exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months",
"exif_bottom_sheet_person_age_years": "Age {}",
"exit_slideshow": "خروج من العرض التقديمي", "exit_slideshow": "خروج من العرض التقديمي",
"expand_all": "توسيع الكل", "expand_all": "توسيع الكل",
"experimental_settings_new_asset_list_subtitle": "أعمال جارية", "experimental_settings_new_asset_list_subtitle": "أعمال جارية",
@@ -886,9 +968,12 @@
"extension": "الإمتداد", "extension": "الإمتداد",
"external": "خارجي", "external": "خارجي",
"external_libraries": "المكتبات الخارجية", "external_libraries": "المكتبات الخارجية",
"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", "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",
"face_unassigned": "غير معين", "face_unassigned": "غير معين",
"failed": "Failed",
"failed_to_load_assets": "فشل تحميل الأصول", "failed_to_load_assets": "فشل تحميل الأصول",
"failed_to_load_folder": "Failed to load folder",
"favorite": "مفضل", "favorite": "مفضل",
"favorite_or_unfavorite_photo": "تفضيل أو إلغاء تفضيل الصورة", "favorite_or_unfavorite_photo": "تفضيل أو إلغاء تفضيل الصورة",
"favorites": "المفضلة", "favorites": "المفضلة",
@@ -900,18 +985,23 @@
"file_name_or_extension": "اسم الملف أو امتداده", "file_name_or_extension": "اسم الملف أو امتداده",
"filename": "اسم الملف", "filename": "اسم الملف",
"filetype": "نوع الملف", "filetype": "نوع الملف",
"filter": "Filter",
"filter_people": "تصفية الاشخاص", "filter_people": "تصفية الاشخاص",
"find_them_fast": "يمكنك العثور عليها بسرعة بالاسم من خلال البحث", "find_them_fast": "يمكنك العثور عليها بسرعة بالاسم من خلال البحث",
"fix_incorrect_match": "إصلاح المطابقة غير الصحيحة", "fix_incorrect_match": "إصلاح المطابقة غير الصحيحة",
"folder": "Folder",
"folder_not_found": "Folder not found",
"folders": "المجلدات", "folders": "المجلدات",
"folders_feature_description": "تصفح عرض المجلد للصور ومقاطع الفيديو الموجودة على نظام الملفات", "folders_feature_description": "تصفح عرض المجلد للصور ومقاطع الفيديو الموجودة على نظام الملفات",
"forward": "إلى الأمام", "forward": "إلى الأمام",
"general": "عام", "general": "عام",
"get_help": "الحصول على المساعدة", "get_help": "الحصول على المساعدة",
"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",
"getting_started": "البدء", "getting_started": "البدء",
"go_back": "الرجوع للخلف", "go_back": "الرجوع للخلف",
"go_to_folder": "اذهب إلى المجلد", "go_to_folder": "اذهب إلى المجلد",
"go_to_search": "اذهب إلى البحث", "go_to_search": "اذهب إلى البحث",
"grant_permission": "Grant permission",
"group_albums_by": "تجميع الألبومات حسب...", "group_albums_by": "تجميع الألبومات حسب...",
"group_country": "مجموعة البلد", "group_country": "مجموعة البلد",
"group_no": "بدون تجميع", "group_no": "بدون تجميع",
@@ -921,6 +1011,12 @@
"haptic_feedback_switch": "تمكين ردود الفعل اللمسية", "haptic_feedback_switch": "تمكين ردود الفعل اللمسية",
"haptic_feedback_title": "ردود فعل لمسية", "haptic_feedback_title": "ردود فعل لمسية",
"has_quota": "محدد بحصة", "has_quota": "محدد بحصة",
"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",
"headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request",
"headers_settings_tile_title": "Custom proxy headers",
"hi_user": "مرحبا {name} ({email})", "hi_user": "مرحبا {name} ({email})",
"hide_all_people": "إخفاء جميع الأشخاص", "hide_all_people": "إخفاء جميع الأشخاص",
"hide_gallery": "اخفاء المعرض", "hide_gallery": "اخفاء المعرض",
@@ -944,6 +1040,8 @@
"home_page_upload_err_limit": "لا يمكن إلا تحميل 30 أحد الأصول في وقت واحد ، سوف يتخطى", "home_page_upload_err_limit": "لا يمكن إلا تحميل 30 أحد الأصول في وقت واحد ، سوف يتخطى",
"host": "المضيف", "host": "المضيف",
"hour": "ساعة", "hour": "ساعة",
"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": "صورة", "image": "صورة",
"image_alt_text_date": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {date}", "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {date}",
"image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} تم التقاطها مع {person1} في {date}", "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} تم التقاطها مع {person1} في {date}",
@@ -955,6 +1053,7 @@
"image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}، {country} مع {person1} و{person2} في {date}", "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_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_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}, {country} with {person1}, {person2}, مع {additionalCount, number} آخرين في {date}",
"image_saved_successfully": "Image saved",
"image_viewer_page_state_provider_download_started": "بدأ التنزيل", "image_viewer_page_state_provider_download_started": "بدأ التنزيل",
"image_viewer_page_state_provider_download_success": "تم التنزيل بنجاح", "image_viewer_page_state_provider_download_success": "تم التنزيل بنجاح",
"image_viewer_page_state_provider_share_error": "خطأ في المشاركة", "image_viewer_page_state_provider_share_error": "خطأ في المشاركة",
@@ -976,6 +1075,8 @@
"night_at_midnight": "كل ليلة عند منتصف الليل", "night_at_midnight": "كل ليلة عند منتصف الليل",
"night_at_twoam": "كل ليلة الساعة 2 صباحا" "night_at_twoam": "كل ليلة الساعة 2 صباحا"
}, },
"invalid_date": "Invalid date",
"invalid_date_format": "Invalid date format",
"invite_people": "دعوة الأشخاص", "invite_people": "دعوة الأشخاص",
"invite_to_album": "دعوة إلى الألبوم", "invite_to_album": "دعوة إلى الألبوم",
"items_count": "{count, plural, one {# عنصر} other {# عناصر}}", "items_count": "{count, plural, one {# عنصر} other {# عناصر}}",
@@ -1011,6 +1112,9 @@
"list": "قائمة", "list": "قائمة",
"loading": "تحميل", "loading": "تحميل",
"loading_search_results_failed": "فشل تحميل نتائج البحث", "loading_search_results_failed": "فشل تحميل نتائج البحث",
"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_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_choose_on_map": "اختر على الخريطة",
"location_picker_latitude_error": "أدخل خط عرض صالح", "location_picker_latitude_error": "أدخل خط عرض صالح",
@@ -1026,6 +1130,7 @@
"login_form_api_exception": " استثناء برمجة التطبيقات. يرجى التحقق من عنوان الخادم والمحاولة مرة أخرى ", "login_form_api_exception": " استثناء برمجة التطبيقات. يرجى التحقق من عنوان الخادم والمحاولة مرة أخرى ",
"login_form_back_button_text": "الرجوع للخلف", "login_form_back_button_text": "الرجوع للخلف",
"login_form_email_hint": "yoursemail@email.com", "login_form_email_hint": "yoursemail@email.com",
"login_form_endpoint_hint": "http://your-server-ip:port",
"login_form_endpoint_url": "url نقطة نهاية الخادم", "login_form_endpoint_url": "url نقطة نهاية الخادم",
"login_form_err_http": "يرجى تحديد http:// أو https://", "login_form_err_http": "يرجى تحديد http:// أو https://",
"login_form_err_invalid_email": "بريد إلكتروني خاطئ", "login_form_err_invalid_email": "بريد إلكتروني خاطئ",
@@ -1059,6 +1164,8 @@
"manage_your_devices": "إدارة الأجهزة التي تم تسجيل الدخول إليها", "manage_your_devices": "إدارة الأجهزة التي تم تسجيل الدخول إليها",
"manage_your_oauth_connection": "إدارة اتصال OAuth الخاص بك", "manage_your_oauth_connection": "إدارة اتصال OAuth الخاص بك",
"map": "الخريطة", "map": "الخريطة",
"map_assets_in_bound": "{} photo",
"map_assets_in_bounds": "{} photos",
"map_cannot_get_user_location": "لا يمكن الحصول على موقع المستخدم", "map_cannot_get_user_location": "لا يمكن الحصول على موقع المستخدم",
"map_location_dialog_yes": "نعم", "map_location_dialog_yes": "نعم",
"map_location_picker_page_use_location": "استخدم هذا الموقع", "map_location_picker_page_use_location": "استخدم هذا الموقع",
@@ -1072,7 +1179,9 @@
"map_settings": "إعدادات الخريطة", "map_settings": "إعدادات الخريطة",
"map_settings_dark_mode": "الوضع المظلم", "map_settings_dark_mode": "الوضع المظلم",
"map_settings_date_range_option_day": "24 ساعة الماضية", "map_settings_date_range_option_day": "24 ساعة الماضية",
"map_settings_date_range_option_days": "Past {} days",
"map_settings_date_range_option_year": "السنة الفائتة", "map_settings_date_range_option_year": "السنة الفائتة",
"map_settings_date_range_option_years": "Past {} years",
"map_settings_dialog_title": "إعدادات الخريطة", "map_settings_dialog_title": "إعدادات الخريطة",
"map_settings_include_show_archived": "تشمل الأرشفة", "map_settings_include_show_archived": "تشمل الأرشفة",
"map_settings_include_show_partners": "تضمين الشركاء", "map_settings_include_show_partners": "تضمين الشركاء",
@@ -1087,6 +1196,8 @@
"memories_setting_description": "إدارة ما تراه في ذكرياتك", "memories_setting_description": "إدارة ما تراه في ذكرياتك",
"memories_start_over": "ابدأ من جديد", "memories_start_over": "ابدأ من جديد",
"memories_swipe_to_close": "اسحب لأعلى للإغلاق", "memories_swipe_to_close": "اسحب لأعلى للإغلاق",
"memories_year_ago": "A year ago",
"memories_years_ago": "{} years ago",
"memory": "ذكرى", "memory": "ذكرى",
"memory_lane_title": "ذكرياتٌ من {title}", "memory_lane_title": "ذكرياتٌ من {title}",
"menu": "القائمة", "menu": "القائمة",
@@ -1110,12 +1221,13 @@
"my_albums": "ألبوماتي", "my_albums": "ألبوماتي",
"name": "الاسم", "name": "الاسم",
"name_or_nickname": "الاسم أو اللقب", "name_or_nickname": "الاسم أو اللقب",
"networking_settings": "Networking",
"networking_subtitle": "Manage the server endpoint settings",
"never": "أبداً", "never": "أبداً",
"new_album": "البوم جديد", "new_album": "البوم جديد",
"new_api_key": "مفتاح API جديد", "new_api_key": "مفتاح API جديد",
"new_password": "كلمة المرور الجديدة", "new_password": "كلمة المرور الجديدة",
"new_person": "شخص جديد", "new_person": "شخص جديد",
"new_pin_code": "الرقم السري الجديد",
"new_user_created": "تم إنشاء مستخدم جديد", "new_user_created": "تم إنشاء مستخدم جديد",
"new_version_available": "إصدار جديد متاح", "new_version_available": "إصدار جديد متاح",
"newest_first": "الأحدث أولاً", "newest_first": "الأحدث أولاً",
@@ -1139,6 +1251,7 @@
"no_results_description": "جرب كلمة رئيسية مرادفة أو أكثر عمومية", "no_results_description": "جرب كلمة رئيسية مرادفة أو أكثر عمومية",
"no_shared_albums_message": "قم بإنشاء ألبوم لمشاركة الصور ومقاطع الفيديو مع الأشخاص في شبكتك", "no_shared_albums_message": "قم بإنشاء ألبوم لمشاركة الصور ومقاطع الفيديو مع الأشخاص في شبكتك",
"not_in_any_album": "ليست في أي ألبوم", "not_in_any_album": "ليست في أي ألبوم",
"not_selected": "Not selected",
"note_apply_storage_label_to_previously_uploaded assets": "ملاحظة: لتطبيق تسمية التخزين على المحتويات التي تم رفعها مسبقًا، قم بتشغيل", "note_apply_storage_label_to_previously_uploaded assets": "ملاحظة: لتطبيق تسمية التخزين على المحتويات التي تم رفعها مسبقًا، قم بتشغيل",
"notes": "ملاحظات", "notes": "ملاحظات",
"notification_permission_dialog_content": "لتمكين الإخطارات ، انتقل إلى الإعدادات و اختار السماح.", "notification_permission_dialog_content": "لتمكين الإخطارات ، انتقل إلى الإعدادات و اختار السماح.",
@@ -1148,12 +1261,14 @@
"notification_toggle_setting_description": "تفعيل إشعارات البريد الإلكتروني", "notification_toggle_setting_description": "تفعيل إشعارات البريد الإلكتروني",
"notifications": "إشعارات", "notifications": "إشعارات",
"notifications_setting_description": "إدارة الإشعارات", "notifications_setting_description": "إدارة الإشعارات",
"oauth": "OAuth",
"official_immich_resources": "الموارد الرسمية لشركة Immich", "official_immich_resources": "الموارد الرسمية لشركة Immich",
"offline": "غير متصل", "offline": "غير متصل",
"offline_paths": "مسارات غير متصلة", "offline_paths": "مسارات غير متصلة",
"offline_paths_description": "قد تكون هذه النتائج بسبب الحذف اليدوي للملفات التي لا تشكل جزءًا من مكتبة خارجية.", "offline_paths_description": "قد تكون هذه النتائج بسبب الحذف اليدوي للملفات التي لا تشكل جزءًا من مكتبة خارجية.",
"ok": "نعم", "ok": "نعم",
"oldest_first": "الأقدم أولا", "oldest_first": "الأقدم أولا",
"on_this_device": "On this device",
"onboarding": "الإعداد الأولي", "onboarding": "الإعداد الأولي",
"onboarding_privacy_description": "تعتمد الميزات التالية (اختياري) على خدمات خارجية، ويمكن تعطيلها في أي وقت في إعدادات الإدارة.", "onboarding_privacy_description": "تعتمد الميزات التالية (اختياري) على خدمات خارجية، ويمكن تعطيلها في أي وقت في إعدادات الإدارة.",
"onboarding_theme_description": "اختر نسق الألوان للنسخة الخاصة بك. يمكنك تغيير ذلك لاحقًا في إعداداتك.", "onboarding_theme_description": "اختر نسق الألوان للنسخة الخاصة بك. يمكنك تغيير ذلك لاحقًا في إعداداتك.",
@@ -1177,12 +1292,14 @@
"partner_can_access": "يستطيع {partner} الوصول", "partner_can_access": "يستطيع {partner} الوصول",
"partner_can_access_assets": "جميع الصور ومقاطع الفيديو الخاصة بك باستثناء تلك الموجودة في المؤرشفة والمحذوفة", "partner_can_access_assets": "جميع الصور ومقاطع الفيديو الخاصة بك باستثناء تلك الموجودة في المؤرشفة والمحذوفة",
"partner_can_access_location": "الموقع الذي تم التقاط صورك فيه", "partner_can_access_location": "الموقع الذي تم التقاط صورك فيه",
"partner_list_user_photos": "{user}'s photos",
"partner_list_view_all": "عرض الكل", "partner_list_view_all": "عرض الكل",
"partner_page_empty_message": "لم يتم مشاركة صورك بعد مع أي شريك.", "partner_page_empty_message": "لم يتم مشاركة صورك بعد مع أي شريك.",
"partner_page_no_more_users": "لا مزيد من المستخدمين لإضافة", "partner_page_no_more_users": "لا مزيد من المستخدمين لإضافة",
"partner_page_partner_add_failed": "فشل في إضافة شريك", "partner_page_partner_add_failed": "فشل في إضافة شريك",
"partner_page_select_partner": "حدد شريكًا", "partner_page_select_partner": "حدد شريكًا",
"partner_page_shared_to_title": "مشترك ل", "partner_page_shared_to_title": "مشترك ل",
"partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
"partner_sharing": "مشاركة الشركاء", "partner_sharing": "مشاركة الشركاء",
"partners": "الشركاء", "partners": "الشركاء",
"password": "كلمة المرور", "password": "كلمة المرور",
@@ -1228,9 +1345,6 @@
"photos_count": "{count, plural, one {{count, number} صورة} other {{count, number} صور}}", "photos_count": "{count, plural, one {{count, number} صورة} other {{count, number} صور}}",
"photos_from_previous_years": "صور من السنوات السابقة", "photos_from_previous_years": "صور من السنوات السابقة",
"pick_a_location": "اختر موقعًا", "pick_a_location": "اختر موقعًا",
"pin_code_changed_successfully": "تم تغير الرقم السري",
"pin_code_reset_successfully": "تم اعادة تعيين الرقم السري",
"pin_code_setup_successfully": "تم انشاء رقم سري",
"place": "مكان", "place": "مكان",
"places": "الأماكن", "places": "الأماكن",
"places_count": "{count, plural, one {{count, number} مكان} other {{count, number} أماكن}}", "places_count": "{count, plural, one {{count, number} مكان} other {{count, number} أماكن}}",
@@ -1239,6 +1353,7 @@
"play_motion_photo": "تشغيل الصور المتحركة", "play_motion_photo": "تشغيل الصور المتحركة",
"play_or_pause_video": "تشغيل الفيديو أو إيقافه مؤقتًا", "play_or_pause_video": "تشغيل الفيديو أو إيقافه مؤقتًا",
"port": "المنفذ", "port": "المنفذ",
"preferences_settings_subtitle": "Manage the app's preferences",
"preferences_settings_title": "التفضيلات", "preferences_settings_title": "التفضيلات",
"preset": "الإعداد المسبق", "preset": "الإعداد المسبق",
"preview": "معاينة", "preview": "معاينة",
@@ -1260,7 +1375,7 @@
"public_share": "مشاركة عامة", "public_share": "مشاركة عامة",
"purchase_account_info": "داعم", "purchase_account_info": "داعم",
"purchase_activated_subtitle": "شكرًا لك على دعمك لـ Immich والبرمجيات مفتوحة المصدر", "purchase_activated_subtitle": "شكرًا لك على دعمك لـ Immich والبرمجيات مفتوحة المصدر",
"purchase_activated_time": "تم التفعيل في {date}", "purchase_activated_time": "تم التفعيل في {date, date}",
"purchase_activated_title": "لقد تم تفعيل مفتاحك بنجاح", "purchase_activated_title": "لقد تم تفعيل مفتاحك بنجاح",
"purchase_button_activate": "تنشيط", "purchase_button_activate": "تنشيط",
"purchase_button_buy": "شراء", "purchase_button_buy": "شراء",
@@ -1303,6 +1418,7 @@
"recent": "حديث", "recent": "حديث",
"recent-albums": "ألبومات الحديثة", "recent-albums": "ألبومات الحديثة",
"recent_searches": "عمليات البحث الأخيرة", "recent_searches": "عمليات البحث الأخيرة",
"recently_added": "Recently added",
"recently_added_page_title": "أضيف مؤخرا", "recently_added_page_title": "أضيف مؤخرا",
"refresh": "تحديث", "refresh": "تحديث",
"refresh_encoded_videos": "تحديث مقاطع الفيديو المشفرة", "refresh_encoded_videos": "تحديث مقاطع الفيديو المشفرة",
@@ -1360,6 +1476,7 @@
"role_editor": "المحرر", "role_editor": "المحرر",
"role_viewer": "العارض", "role_viewer": "العارض",
"save": "حفظ", "save": "حفظ",
"save_to_gallery": "Save to gallery",
"saved_api_key": "تم حفظ مفتاح الـ API", "saved_api_key": "تم حفظ مفتاح الـ API",
"saved_profile": "تم حفظ الملف", "saved_profile": "تم حفظ الملف",
"saved_settings": "تم حفظ الإعدادات", "saved_settings": "تم حفظ الإعدادات",
@@ -1381,17 +1498,31 @@
"search_city": "البحث حسب المدينة...", "search_city": "البحث حسب المدينة...",
"search_country": "البحث حسب الدولة...", "search_country": "البحث حسب الدولة...",
"search_filter_apply": "اختار الفلتر ", "search_filter_apply": "اختار الفلتر ",
"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_not_in_album": "ليس في الألبوم", "search_filter_display_option_not_in_album": "ليس في الألبوم",
"search_filter_display_options": "Display Options",
"search_filter_filename": "Search by file name",
"search_filter_location": "Location",
"search_filter_location_title": "Select location",
"search_filter_media_type": "Media Type",
"search_filter_media_type_title": "Select media type",
"search_filter_people_title": "Select people",
"search_for": "البحث عن", "search_for": "البحث عن",
"search_for_existing_person": "البحث عن شخص موجود", "search_for_existing_person": "البحث عن شخص موجود",
"search_no_more_result": "No more results",
"search_no_people": "لا يوجد أشخاص", "search_no_people": "لا يوجد أشخاص",
"search_no_people_named": "لا يوجد أشخاص بالاسم \"{name}\"", "search_no_people_named": "لا يوجد أشخاص بالاسم \"{name}\"",
"search_no_result": "No results found, try a different search term or combination",
"search_options": "خيارات البحث", "search_options": "خيارات البحث",
"search_page_categories": "فئات", "search_page_categories": "فئات",
"search_page_motion_photos": "الصور المتحركه", "search_page_motion_photos": "الصور المتحركه",
"search_page_no_objects": "لا توجد معلومات عن أشياء متاحة", "search_page_no_objects": "لا توجد معلومات عن أشياء متاحة",
"search_page_no_places": "لا توجد معلومات متوفرة للأماكن", "search_page_no_places": "لا توجد معلومات متوفرة للأماكن",
"search_page_screenshots": "لقطات الشاشة", "search_page_screenshots": "لقطات الشاشة",
"search_page_search_photos_videos": "Search for your photos and videos",
"search_page_selfies": " صور ذاتيه", "search_page_selfies": " صور ذاتيه",
"search_page_things": "أشياء", "search_page_things": "أشياء",
"search_page_view_all_button": "عرض الكل", "search_page_view_all_button": "عرض الكل",
@@ -1430,6 +1561,7 @@
"selected_count": "{count, plural, other {# محددة }}", "selected_count": "{count, plural, other {# محددة }}",
"send_message": "‏إرسال رسالة", "send_message": "‏إرسال رسالة",
"send_welcome_email": "إرسال بريدًا إلكترونيًا ترحيبيًا", "send_welcome_email": "إرسال بريدًا إلكترونيًا ترحيبيًا",
"server_endpoint": "Server Endpoint",
"server_info_box_app_version": "نسخة التطبيق", "server_info_box_app_version": "نسخة التطبيق",
"server_info_box_server_url": "عنوان URL الخادم", "server_info_box_server_url": "عنوان URL الخادم",
"server_offline": "الخادم غير متصل", "server_offline": "الخادم غير متصل",
@@ -1450,21 +1582,28 @@
"setting_image_viewer_preview_title": "تحميل صورة معاينة", "setting_image_viewer_preview_title": "تحميل صورة معاينة",
"setting_image_viewer_title": "الصور", "setting_image_viewer_title": "الصور",
"setting_languages_apply": "تغيير الإعدادات", "setting_languages_apply": "تغيير الإعدادات",
"setting_languages_subtitle": "Change the app's language",
"setting_languages_title": "اللغات", "setting_languages_title": "اللغات",
"setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
"setting_notifications_notify_hours": "{} hours",
"setting_notifications_notify_immediately": "في الحال", "setting_notifications_notify_immediately": "في الحال",
"setting_notifications_notify_minutes": "{} minutes",
"setting_notifications_notify_never": "أبداً", "setting_notifications_notify_never": "أبداً",
"setting_notifications_notify_seconds": "{} seconds",
"setting_notifications_single_progress_subtitle": "معلومات التقدم التفصيلية تحميل لكل أصل", "setting_notifications_single_progress_subtitle": "معلومات التقدم التفصيلية تحميل لكل أصل",
"setting_notifications_single_progress_title": "إظهار تقدم التفاصيل الاحتياطية الخلفية", "setting_notifications_single_progress_title": "إظهار تقدم التفاصيل الاحتياطية الخلفية",
"setting_notifications_subtitle": "اضبط تفضيلات الإخطار", "setting_notifications_subtitle": "اضبط تفضيلات الإخطار",
"setting_notifications_total_progress_subtitle": "التقدم التحميل العام (تم القيام به/إجمالي الأصول)", "setting_notifications_total_progress_subtitle": "التقدم التحميل العام (تم القيام به/إجمالي الأصول)",
"setting_notifications_total_progress_title": "إظهار النسخ الاحتياطي الخلفية التقدم المحرز", "setting_notifications_total_progress_title": "إظهار النسخ الاحتياطي الخلفية التقدم المحرز",
"setting_video_viewer_looping_title": "تكرار مقطع فيديو تلقائيًا", "setting_video_viewer_looping_title": "تكرار مقطع فيديو تلقائيًا",
"setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.",
"setting_video_viewer_original_video_title": "Force original video",
"settings": "الإعدادات", "settings": "الإعدادات",
"settings_require_restart": "يرجى إعادة تشغيل لتطبيق هذا الإعداد", "settings_require_restart": "يرجى إعادة تشغيل لتطبيق هذا الإعداد",
"settings_saved": "تم حفظ الإعدادات", "settings_saved": "تم حفظ الإعدادات",
"setup_pin_code": "تحديد رقم سري",
"share": "مشاركة", "share": "مشاركة",
"share_add_photos": "إضافة الصور", "share_add_photos": "إضافة الصور",
"share_assets_selected": "{} selected",
"share_dialog_preparing": "تحضير...", "share_dialog_preparing": "تحضير...",
"shared": "مُشتَرك", "shared": "مُشتَرك",
"shared_album_activities_input_disable": "التعليق معطل", "shared_album_activities_input_disable": "التعليق معطل",
@@ -1478,22 +1617,40 @@
"shared_by_user": "تمت المشاركة بواسطة {user}", "shared_by_user": "تمت المشاركة بواسطة {user}",
"shared_by_you": "تمت مشاركته من قِبلك", "shared_by_you": "تمت مشاركته من قِبلك",
"shared_from_partner": "صور من {partner}", "shared_from_partner": "صور من {partner}",
"shared_intent_upload_button_progress_text": "{} / {} Uploaded",
"shared_link_app_bar_title": "روابط مشتركة", "shared_link_app_bar_title": "روابط مشتركة",
"shared_link_clipboard_copied_massage": "نسخ إلى الحافظة", "shared_link_clipboard_copied_massage": "نسخ إلى الحافظة",
"shared_link_clipboard_text": "Link: {}\nPassword: {}",
"shared_link_create_error": "خطأ أثناء إنشاء رابط مشترك", "shared_link_create_error": "خطأ أثناء إنشاء رابط مشترك",
"shared_link_edit_description_hint": "أدخل وصف المشاركة", "shared_link_edit_description_hint": "أدخل وصف المشاركة",
"shared_link_edit_expire_after_option_day": "يوم 1", "shared_link_edit_expire_after_option_day": "يوم 1",
"shared_link_edit_expire_after_option_days": "{} days",
"shared_link_edit_expire_after_option_hour": "1 ساعة", "shared_link_edit_expire_after_option_hour": "1 ساعة",
"shared_link_edit_expire_after_option_hours": "{} hours",
"shared_link_edit_expire_after_option_minute": "1 دقيقة", "shared_link_edit_expire_after_option_minute": "1 دقيقة",
"shared_link_edit_expire_after_option_minutes": "{} minutes",
"shared_link_edit_expire_after_option_months": "{} months",
"shared_link_edit_expire_after_option_year": "{} year",
"shared_link_edit_password_hint": "أدخل كلمة مرور المشاركة", "shared_link_edit_password_hint": "أدخل كلمة مرور المشاركة",
"shared_link_edit_submit_button": "تحديث الرابط", "shared_link_edit_submit_button": "تحديث الرابط",
"shared_link_error_server_url_fetch": "لا يمكن جلب عنوان الخادم", "shared_link_error_server_url_fetch": "لا يمكن جلب عنوان الخادم",
"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": "تنتهي ∞", "shared_link_expires_never": "تنتهي ∞",
"shared_link_expires_second": "Expires in {} second",
"shared_link_expires_seconds": "Expires in {} seconds",
"shared_link_individual_shared": "Individual shared",
"shared_link_info_chip_metadata": "EXIF",
"shared_link_manage_links": "إدارة الروابط المشتركة", "shared_link_manage_links": "إدارة الروابط المشتركة",
"shared_link_options": "خيارات الرابط المشترك", "shared_link_options": "خيارات الرابط المشترك",
"shared_links": "روابط مشتركة", "shared_links": "روابط مشتركة",
"shared_links_description": "وصف الروابط المشتركة", "shared_links_description": "وصف الروابط المشتركة",
"shared_photos_and_videos_count": "{assetCount, plural, other {# الصور ومقاطع الفيديو المُشارَكة.}}", "shared_photos_and_videos_count": "{assetCount, plural, other {# الصور ومقاطع الفيديو المُشارَكة.}}",
"shared_with_me": "Shared with me",
"shared_with_partner": "تمت المشاركة مع {partner}", "shared_with_partner": "تمت المشاركة مع {partner}",
"sharing": "مشاركة", "sharing": "مشاركة",
"sharing_enter_password": "الرجاء إدخال كلمة المرور لعرض هذه الصفحة.", "sharing_enter_password": "الرجاء إدخال كلمة المرور لعرض هذه الصفحة.",
@@ -1569,6 +1726,9 @@
"support_third_party_description": "تم حزم تثبيت immich الخاص بك بواسطة جهة خارجية. قد تكون المشكلات التي تواجهها ناجمة عن هذه الحزمة، لذا يرجى طرح المشكلات معهم في المقام الأول باستخدام الروابط أدناه.", "support_third_party_description": "تم حزم تثبيت immich الخاص بك بواسطة جهة خارجية. قد تكون المشكلات التي تواجهها ناجمة عن هذه الحزمة، لذا يرجى طرح المشكلات معهم في المقام الأول باستخدام الروابط أدناه.",
"swap_merge_direction": "تبديل اتجاه الدمج", "swap_merge_direction": "تبديل اتجاه الدمج",
"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",
"tag": "العلامة", "tag": "العلامة",
"tag_assets": "أصول العلامة", "tag_assets": "أصول العلامة",
"tag_created": "تم إنشاء العلامة: {tag}", "tag_created": "تم إنشاء العلامة: {tag}",
@@ -1583,8 +1743,14 @@
"theme_selection": "اختيار السمة", "theme_selection": "اختيار السمة",
"theme_selection_description": "قم بتعيين السمة تلقائيًا على اللون الفاتح أو الداكن بناءً على تفضيلات نظام المتصفح الخاص بك", "theme_selection_description": "قم بتعيين السمة تلقائيًا على اللون الفاتح أو الداكن بناءً على تفضيلات نظام المتصفح الخاص بك",
"theme_setting_asset_list_storage_indicator_title": "عرض مؤشر التخزين على بلاط الأصول", "theme_setting_asset_list_storage_indicator_title": "عرض مؤشر التخزين على بلاط الأصول",
"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_image_viewer_quality_subtitle": "اضبط جودة عارض الصورة التفصيلية", "theme_setting_image_viewer_quality_subtitle": "اضبط جودة عارض الصورة التفصيلية",
"theme_setting_image_viewer_quality_title": "جودة عارض الصورة", "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_system_theme_switch": "تلقائي (اتبع إعداد النظام)", "theme_setting_system_theme_switch": "تلقائي (اتبع إعداد النظام)",
"theme_setting_theme_subtitle": "اختر إعدادات مظهر التطبيق", "theme_setting_theme_subtitle": "اختر إعدادات مظهر التطبيق",
"theme_setting_three_stage_loading_subtitle": "قد يزيد التحميل من ثلاث مراحل من أداء التحميل ولكنه يسبب تحميل شبكة أعلى بكثير", "theme_setting_three_stage_loading_subtitle": "قد يزيد التحميل من ثلاث مراحل من أداء التحميل ولكنه يسبب تحميل شبكة أعلى بكثير",
@@ -1608,16 +1774,17 @@
"trash_all": "نقل الكل إلى سلة المهملات", "trash_all": "نقل الكل إلى سلة المهملات",
"trash_count": "سلة المحملات {count, number}", "trash_count": "سلة المحملات {count, number}",
"trash_delete_asset": "حذف/نقل المحتوى إلى سلة المهملات", "trash_delete_asset": "حذف/نقل المحتوى إلى سلة المهملات",
"trash_emptied": "Emptied trash",
"trash_no_results_message": "ستظهر هنا الصور ومقاطع الفيديو المحذوفة.", "trash_no_results_message": "ستظهر هنا الصور ومقاطع الفيديو المحذوفة.",
"trash_page_delete_all": "حذف الكل", "trash_page_delete_all": "حذف الكل",
"trash_page_empty_trash_dialog_content": "هل تريد تفريغ أصولك المهملة؟ ستتم إزالة هذه العناصر نهائيًا من التطبيق", "trash_page_empty_trash_dialog_content": "هل تريد تفريغ أصولك المهملة؟ ستتم إزالة هذه العناصر نهائيًا من التطبيق",
"trash_page_info": "Trashed items will be permanently deleted after {} days",
"trash_page_no_assets": "لا توجد اصول في سله المهملات", "trash_page_no_assets": "لا توجد اصول في سله المهملات",
"trash_page_restore_all": "استعادة الكل", "trash_page_restore_all": "استعادة الكل",
"trash_page_select_assets_btn": "اختر الأصول ", "trash_page_select_assets_btn": "اختر الأصول ",
"trash_page_title": "Trash ({})",
"trashed_items_will_be_permanently_deleted_after": "سيتم حذفُ العناصر المحذوفة نِهائيًا بعد {days, plural, one {# يوم} other {# أيام }}.", "trashed_items_will_be_permanently_deleted_after": "سيتم حذفُ العناصر المحذوفة نِهائيًا بعد {days, plural, one {# يوم} other {# أيام }}.",
"type": "النوع", "type": "النوع",
"unable_to_change_pin_code": "تفيير الرقم السري غير ممكن",
"unable_to_setup_pin_code": "انشاء الرقم السري غير ممكن",
"unarchive": "أخرج من الأرشيف", "unarchive": "أخرج من الأرشيف",
"unarchived_count": "{count, plural, other {غير مؤرشفة #}}", "unarchived_count": "{count, plural, other {غير مؤرشفة #}}",
"unfavorite": "أزل التفضيل", "unfavorite": "أزل التفضيل",
@@ -1653,14 +1820,15 @@
"upload_status_errors": "الأخطاء", "upload_status_errors": "الأخطاء",
"upload_status_uploaded": "تم الرفع", "upload_status_uploaded": "تم الرفع",
"upload_success": "تم الرفع بنجاح، قم بتحديث الصفحة لرؤية المحتويات المرفوعة الجديدة.", "upload_success": "تم الرفع بنجاح، قم بتحديث الصفحة لرؤية المحتويات المرفوعة الجديدة.",
"upload_to_immich": "Upload to Immich ({})",
"uploading": "Uploading",
"url": "عنوان URL", "url": "عنوان URL",
"usage": "الاستخدام", "usage": "الاستخدام",
"use_current_connection": "use current connection",
"use_custom_date_range": "استخدم النطاق الزمني المخصص بدلاً من ذلك", "use_custom_date_range": "استخدم النطاق الزمني المخصص بدلاً من ذلك",
"user": "مستخدم", "user": "مستخدم",
"user_id": "معرف المستخدم", "user_id": "معرف المستخدم",
"user_liked": "قام {user} بالإعجاب {type, select, photo {بهذه الصورة} video {بهذا الفيديو} asset {بهذا المحتوى} other {بها}}", "user_liked": "قام {user} بالإعجاب {type, select, photo {بهذه الصورة} video {بهذا الفيديو} asset {بهذا المحتوى} other {بها}}",
"user_pin_code_settings": "الرقم السري",
"user_pin_code_settings_description": "تغير الرقم السري",
"user_purchase_settings": "الشراء", "user_purchase_settings": "الشراء",
"user_purchase_settings_description": "إدارة عملية الشراء الخاصة بك", "user_purchase_settings_description": "إدارة عملية الشراء الخاصة بك",
"user_role_set": "قم بتعيين {user} كـ {role}", "user_role_set": "قم بتعيين {user} كـ {role}",
@@ -1671,6 +1839,7 @@
"users": "المستخدمين", "users": "المستخدمين",
"utilities": "أدوات", "utilities": "أدوات",
"validate": "تحقْق", "validate": "تحقْق",
"validate_endpoint_error": "Please enter a valid URL",
"variables": "المتغيرات", "variables": "المتغيرات",
"version": "الإصدار", "version": "الإصدار",
"version_announcement_closing": "صديقك، أليكس", "version_announcement_closing": "صديقك، أليكس",
@@ -1678,6 +1847,7 @@
"version_announcement_overlay_release_notes": "ملاحظات الإصدار", "version_announcement_overlay_release_notes": "ملاحظات الإصدار",
"version_announcement_overlay_text_1": "مرحبًا يا صديقي ، هناك إصدار جديد", "version_announcement_overlay_text_1": "مرحبًا يا صديقي ، هناك إصدار جديد",
"version_announcement_overlay_text_2": "من فضلك خذ وقتك لزيارة", "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": "نسخه جديده متاحه للخادم ", "version_announcement_overlay_title": "نسخه جديده متاحه للخادم ",
"version_history": "تاريخ الإصدار", "version_history": "تاريخ الإصدار",
"version_history_item": "تم تثبيت {version} في {date}", "version_history_item": "تم تثبيت {version} في {date}",
+1
View File
@@ -76,6 +76,7 @@
"library_watching_settings_description": "Dəyişdirilən faylları avtomatik olaraq yoxla", "library_watching_settings_description": "Dəyişdirilən faylları avtomatik olaraq yoxla",
"logging_enable_description": "Jurnalı aktivləşdir", "logging_enable_description": "Jurnalı aktivləşdir",
"logging_level_description": "Aktiv edildikdə hansı jurnal səviyyəsi istifadə olunur.", "logging_level_description": "Aktiv edildikdə hansı jurnal səviyyəsi istifadə olunur.",
"logging_settings": "",
"machine_learning_clip_model": "CLIP modeli", "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_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": "Dublikat Aşkarlama",
+2 -17
View File
@@ -4,7 +4,6 @@
"account_settings": "Налады ўліковага запісу", "account_settings": "Налады ўліковага запісу",
"acknowledge": "Пацвердзіць", "acknowledge": "Пацвердзіць",
"action": "Дзеянне", "action": "Дзеянне",
"action_common_update": "Абнавіць",
"actions": "Дзеянні", "actions": "Дзеянні",
"active": "Актыўны", "active": "Актыўны",
"activity": "Актыўнасць", "activity": "Актыўнасць",
@@ -14,7 +13,6 @@
"add_a_location": "Дадаць месца", "add_a_location": "Дадаць месца",
"add_a_name": "Дадаць імя", "add_a_name": "Дадаць імя",
"add_a_title": "Дадаць загаловак", "add_a_title": "Дадаць загаловак",
"add_endpoint": "Дадаць кропку доступу",
"add_exclusion_pattern": "Дадаць шаблон выключэння", "add_exclusion_pattern": "Дадаць шаблон выключэння",
"add_import_path": "Дадаць шлях імпарту", "add_import_path": "Дадаць шлях імпарту",
"add_location": "Дадайце месца", "add_location": "Дадайце месца",
@@ -22,10 +20,8 @@
"add_partner": "Дадаць партнёра", "add_partner": "Дадаць партнёра",
"add_path": "Дадаць шлях", "add_path": "Дадаць шлях",
"add_photos": "Дадаць фота", "add_photos": "Дадаць фота",
"add_to": "Дадаць у", "add_to": "Дадаць у...",
"add_to_album": "Дадаць у альбом", "add_to_album": "Дадаць у альбом",
"add_to_album_bottom_sheet_added": "Дададзена да {album}",
"add_to_album_bottom_sheet_already_exists": "Ужо знаходзіцца ў {album}",
"add_to_shared_album": "Дадаць у агульны альбом", "add_to_shared_album": "Дадаць у агульны альбом",
"add_url": "Дадаць URL", "add_url": "Дадаць URL",
"added_to_archive": "Дададзена ў архіў", "added_to_archive": "Дададзена ў архіў",
@@ -43,9 +39,8 @@
"backup_database_enable_description": "Уключыць рэзерваванне базы даных", "backup_database_enable_description": "Уключыць рэзерваванне базы даных",
"backup_keep_last_amount": "Колькасць папярэдніх рэзервовых копій для захавання", "backup_keep_last_amount": "Колькасць папярэдніх рэзервовых копій для захавання",
"backup_settings": "Налады рэзервовага капіявання", "backup_settings": "Налады рэзервовага капіявання",
"backup_settings_description": "Кіраванне наладамі дампа базы дадзеных. Заўвага: гэтыя задачы не кантралююцца, і ў выпадку няўдачы паведамленне адпраўлена не будзе.", "backup_settings_description": "Кіраванне наладкамі рэзервовага капіявання базы даных",
"check_all": "Праверыць усе", "check_all": "Праверыць усе",
"cleanup": "Ачыстка",
"cleared_jobs": "Ачышчаны заданні для: {job}", "cleared_jobs": "Ачышчаны заданні для: {job}",
"config_set_by_file": "Канфігурацыя ў зараз усталявана праз файл канфігурацыі", "config_set_by_file": "Канфігурацыя ў зараз усталявана праз файл канфігурацыі",
"confirm_delete_library": "Вы ўпэўнены што жадаеце выдаліць {library} бібліятэку?", "confirm_delete_library": "Вы ўпэўнены што жадаеце выдаліць {library} бібліятэку?",
@@ -63,18 +58,8 @@
"external_library_created_at": "Знешняя бібліятэка (створана {date})", "external_library_created_at": "Знешняя бібліятэка (створана {date})",
"external_library_management": "Кіраванне знешняй бібліятэкай", "external_library_management": "Кіраванне знешняй бібліятэкай",
"face_detection": "Выяўленне твараў", "face_detection": "Выяўленне твараў",
"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": "Фармат",
"image_format_description": "WebP стварае меншыя файлы, чым JPEG, але павольней кадуе.",
"image_fullsize_description": "Выява ў поўным памеры без метаданых, выкарыстоўваецца пры павелічэнні",
"image_fullsize_enabled": "Уключыць стварэнне выявы ў поўным памеры",
"image_fullsize_enabled_description": "Ствараць выяву ў поўным памеры для фарматаў, што не прыдатныя для вэб. Калі ўключана опцыя \"Аддаваць перавагу ўбудаванай праяве\", прагляды выкарыстоўваюцца непасрэдна без канвертацыі. Не ўплывае на вэб-прыдатныя фарматы, такія як JPEG.",
"image_fullsize_quality_description": "Якасць выявы ў поўным памеры ад 1 да 100. Больш высокае значэнне лепшае, але прыводзіць да павелічэння памеру файла.",
"image_fullsize_title": "Налады выявы ў поўным памеры",
"image_preview_title": "Налады папярэдняга прагляду", "image_preview_title": "Налады папярэдняга прагляду",
"image_quality": "Якасць", "image_quality": "Якасць",
"image_resolution": "Раздзяляльнасць", "image_resolution": "Раздзяляльнасць",
+12 -1
View File
@@ -183,13 +183,20 @@
"oauth_auto_register": "Автоматична регистрация", "oauth_auto_register": "Автоматична регистрация",
"oauth_auto_register_description": "Автоматично регистриране на нови потребители след влизане с OAuth", "oauth_auto_register_description": "Автоматично регистриране на нови потребители след влизане с OAuth",
"oauth_button_text": "Текст на бутона", "oauth_button_text": "Текст на бутона",
"oauth_client_id": "Клиентски ID",
"oauth_client_secret": "Клиентска тайна",
"oauth_enable_description": "Влизане с OAuth", "oauth_enable_description": "Влизане с OAuth",
"oauth_issuer_url": "URL на издателя",
"oauth_mobile_redirect_uri": "URI за мобилно пренасочване", "oauth_mobile_redirect_uri": "URI за мобилно пренасочване",
"oauth_mobile_redirect_uri_override": "URI пренасочване за мобилни устройства", "oauth_mobile_redirect_uri_override": "URI пренасочване за мобилни устройства",
"oauth_mobile_redirect_uri_override_description": "Разреши когато доставчика за OAuth удостоверяване не позволява за мобилни URI идентификатори, като '{callback}'", "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": "OAuth",
"oauth_settings_description": "Управление на настройките за вход с OAuth", "oauth_settings_description": "Управление на настройките за вход с OAuth",
"oauth_settings_more_details": "За повече информация за функционалността, се потърсете в <link>docs</link>.", "oauth_settings_more_details": "За повече информация за функционалността, се потърсете в <link>docs</link>.",
"oauth_signing_algorithm": "Алгоритъм за вписване",
"oauth_storage_label_claim": "Заявка за етикет за съхранение", "oauth_storage_label_claim": "Заявка за етикет за съхранение",
"oauth_storage_label_claim_description": "Автоматично задайте етикета за съхранение на потребителя със стойността от тази заявка.", "oauth_storage_label_claim_description": "Автоматично задайте етикета за съхранение на потребителя със стойността от тази заявка.",
"oauth_storage_quota_claim": "Заявка за квота за съхранение", "oauth_storage_quota_claim": "Заявка за квота за съхранение",
@@ -546,6 +553,7 @@
"direction": "Посока", "direction": "Посока",
"disabled": "Изключено", "disabled": "Изключено",
"disallow_edits": "Забраняване на редакциите", "disallow_edits": "Забраняване на редакциите",
"discord": "Discord",
"discover": "Открий", "discover": "Открий",
"dismiss_all_errors": "Отхвърляне на всички грешки", "dismiss_all_errors": "Отхвърляне на всички грешки",
"dismiss_error": "Отхвърляне на грешка", "dismiss_error": "Отхвърляне на грешка",
@@ -726,6 +734,7 @@
"unable_to_update_user": "Неуспешно обновяване на потребителя", "unable_to_update_user": "Неуспешно обновяване на потребителя",
"unable_to_upload_file": "Неуспешно качване на файл" "unable_to_upload_file": "Неуспешно качване на файл"
}, },
"exif": "Exif",
"exit_slideshow": "Изход от слайдшоуто", "exit_slideshow": "Изход от слайдшоуто",
"expand_all": "Разшири всички", "expand_all": "Разшири всички",
"expire_after": "Изтича след", "expire_after": "Изтича след",
@@ -917,6 +926,7 @@
"notification_toggle_setting_description": "Активиране на имейл известия", "notification_toggle_setting_description": "Активиране на имейл известия",
"notifications": "Известия", "notifications": "Известия",
"notifications_setting_description": "Управление на известията", "notifications_setting_description": "Управление на известията",
"oauth": "OAuth",
"official_immich_resources": "Официална информация за Immich", "official_immich_resources": "Официална информация за Immich",
"offline": "Офлайн", "offline": "Офлайн",
"offline_paths": "Офлайн пътища", "offline_paths": "Офлайн пътища",
@@ -1003,7 +1013,7 @@
"public_share": "Публично споделяне", "public_share": "Публично споделяне",
"purchase_account_info": "Поддръжник", "purchase_account_info": "Поддръжник",
"purchase_activated_subtitle": "Благодарим ви, че подкрепяте Immich и софтуера с отворен код", "purchase_activated_subtitle": "Благодарим ви, че подкрепяте Immich и софтуера с отворен код",
"purchase_activated_time": "Активиран на {date}", "purchase_activated_time": "Активиран на {date, date}",
"purchase_activated_title": "Вашият ключ беше успешно активиран", "purchase_activated_title": "Вашият ключ беше успешно активиран",
"purchase_button_activate": "Активирай", "purchase_button_activate": "Активирай",
"purchase_button_buy": "Купи", "purchase_button_buy": "Купи",
@@ -1313,6 +1323,7 @@
"upload_status_errors": "Грешки", "upload_status_errors": "Грешки",
"upload_status_uploaded": "Качено", "upload_status_uploaded": "Качено",
"upload_success": "Качването е успешно, опреснете страницата, за да видите новите файлове.", "upload_success": "Качването е успешно, опреснете страницата, за да видите новите файлове.",
"url": "URL",
"usage": "Потребление", "usage": "Потребление",
"use_custom_date_range": "Използвайте собствен диапазон от дати вместо това", "use_custom_date_range": "Използвайте собствен диапазон от дати вместо това",
"user": "Потребител", "user": "Потребител",
+848 -1
View File
@@ -3,6 +3,8 @@
"account": "Akaont", "account": "Akaont",
"account_settings": "Seting blo Akaont", "account_settings": "Seting blo Akaont",
"acknowledge": "Akcept", "acknowledge": "Akcept",
"action": "",
"actions": "",
"active": "Stap Mekem", "active": "Stap Mekem",
"activity": "Wanem hemi Mekem", "activity": "Wanem hemi Mekem",
"activity_changed": "WAnem hemi Mekem hemi", "activity_changed": "WAnem hemi Mekem hemi",
@@ -14,5 +16,850 @@
"add_exclusion_pattern": "Putem wan paten wae hemi karem aot", "add_exclusion_pattern": "Putem wan paten wae hemi karem aot",
"add_import_path": "Putem wan pat blo import", "add_import_path": "Putem wan pat blo import",
"add_location": "Putem wan place blo hem", "add_location": "Putem wan place blo hem",
"add_more_users": "Putem mor man" "add_more_users": "Putem mor man",
"add_partner": "",
"add_path": "",
"add_photos": "",
"add_to": "",
"add_to_album": "",
"add_to_shared_album": "",
"admin": {
"add_exclusion_pattern_description": "",
"authentication_settings": "",
"authentication_settings_description": "",
"background_task_job": "",
"check_all": "",
"config_set_by_file": "",
"confirm_delete_library": "",
"confirm_delete_library_assets": "",
"confirm_email_below": "",
"confirm_reprocess_all_faces": "",
"confirm_user_password_reset": "",
"disable_login": "",
"duplicate_detection_job_description": "",
"exclusion_pattern_description": "",
"external_library_created_at": "",
"external_library_management": "",
"face_detection": "",
"face_detection_description": "",
"facial_recognition_job_description": "",
"force_delete_user_warning": "",
"forcing_refresh_library_files": "",
"image_format_description": "",
"image_prefer_embedded_preview": "",
"image_prefer_embedded_preview_setting_description": "",
"image_prefer_wide_gamut": "",
"image_prefer_wide_gamut_setting_description": "",
"image_quality": "",
"image_settings": "",
"image_settings_description": "",
"job_concurrency": "",
"job_not_concurrency_safe": "",
"job_settings": "",
"job_settings_description": "",
"job_status": "",
"jobs_delayed": "",
"jobs_failed": "",
"library_created": "",
"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": "",
"machine_learning_duplicate_detection": "",
"machine_learning_duplicate_detection_enabled_description": "",
"machine_learning_duplicate_detection_setting_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_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_concurrency": "",
"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": "",
"metadata_extraction_job_description": "",
"migration_job": "",
"migration_job_description": "",
"no_paths_added": "",
"no_pattern_added": "",
"note_apply_storage_label_previous_assets": "",
"note_cannot_be_changed_later": "",
"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": "",
"offline_paths": "",
"offline_paths_description": "",
"password_enable_description": "",
"password_settings": "",
"password_settings_description": "",
"paths_validated_successfully": "",
"quota_size_gib": "",
"refreshing_all_libraries": "",
"repair_all": "",
"repair_matched_items": "",
"repaired_items": "",
"require_password_change_on_login": "",
"reset_settings_to_default": "",
"reset_settings_to_recent_saved": "",
"send_welcome_email": "",
"server_external_domain_settings": "",
"server_external_domain_settings_description": "",
"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_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": "",
"theme_custom_css_settings_description": "",
"theme_settings": "",
"theme_settings_description": "",
"these_files_matched_by_checksum": "",
"thumbnail_generation_job": "",
"thumbnail_generation_job_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_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": "",
"asset_offline": "",
"assets": "",
"authorized_devices": "",
"back": "",
"backward": "",
"blurred_background": "",
"camera": "",
"camera_brand": "",
"camera_model": "",
"cancel": "",
"cancel_search": "",
"cannot_merge_people": "",
"cannot_update_the_description": "",
"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": "",
"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_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_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": "",
"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": "",
"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": "",
"forward": "",
"general": "",
"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_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": "",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "",
"notifications_setting_description": "",
"oauth": "",
"offline": "",
"offline_paths": "",
"offline_paths_description": "",
"ok": "",
"oldest_first": "",
"online": "",
"only_favorites": "",
"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_deleted_assets": "",
"remove_from_album": "",
"remove_from_favorites": "",
"remove_from_shared_link": "",
"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_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_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": "",
"sunrise_on_the_beach": "",
"swap_merge_direction": "",
"sync": "",
"template": "",
"theme": "",
"theme_selection": "",
"theme_selection_description": "",
"time_based_memories": "",
"timezone": "",
"to_archive": "",
"to_favorite": "",
"toggle_settings": "",
"toggle_theme": "",
"total_usage": "",
"trash": "",
"trash_all": "",
"trash_no_results_message": "",
"trashed_items_will_be_permanently_deleted_after": "",
"type": "",
"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": "",
"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": ""
} }
+1 -17
View File
@@ -1,17 +1 @@
{ {}
"about": "সম্পর্কে",
"account": "অ্যাকাউন্ট",
"account_settings": "অ্যাকাউন্ট সেটিংস",
"acknowledge": "স্বীকৃতি",
"action": "কার্য",
"action_common_update": "আপডেট",
"actions": "কর্ম",
"active": "সচল",
"activity": "কার্যকলাপ",
"add": "যোগ করুন",
"add_a_description": "একটি বিবরণ যোগ করুন",
"add_a_location": "একটি অবস্থান যোগ করুন",
"add_a_name": "একটি নাম যোগ করুন",
"add_a_title": "একটি শিরোনাম যোগ করুন",
"add_endpoint": "এন্ডপয়েন্ট যোগ করুন"
}
+118 -124
View File
@@ -39,11 +39,11 @@
"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_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>.", "authentication_settings_reenable": "Per a tornar a habilitar, empra una <link>Comanda de Servidor</link>.",
"background_task_job": "Tasques en segon pla", "background_task_job": "Tasques en segon pla",
"backup_database": "Fer un bolcat de la base de dades", "backup_database": "Còpia de la base de dades",
"backup_database_enable_description": "Habilitar bolcat de la base de dades", "backup_database_enable_description": "Habilitar còpies de la base de dades",
"backup_keep_last_amount": "Quantitat de bolcats anteriors per conservar", "backup_keep_last_amount": "Quantitat de còpies de seguretat anteriors per conservar",
"backup_settings": "Configuració dels bolcats", "backup_settings": "Ajustes de les còpies de seguretat",
"backup_settings_description": "Gestionar la configuració bolcats de la base de dades. Nota: els treballs no es monitoritzen ni es notifiquen les fallades.", "backup_settings_description": "Gestionar la configuració de la còpia de seguretat de la base de dades",
"check_all": "Marca-ho tot", "check_all": "Marca-ho tot",
"cleanup": "Neteja", "cleanup": "Neteja",
"cleared_jobs": "Tasques esborrades per a: {job}", "cleared_jobs": "Tasques esborrades per a: {job}",
@@ -53,7 +53,6 @@
"confirm_email_below": "Per a confirmar, escriviu \"{email}\" a sota", "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_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}?", "confirm_user_password_reset": "Esteu segur que voleu reinicialitzar la contrasenya de l'usuari {user}?",
"confirm_user_pin_code_reset": "Esteu segur que voleu restablir el codi PIN de {user}?",
"create_job": "Crear tasca", "create_job": "Crear tasca",
"cron_expression": "Expressió Cron", "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_description": "Estableix l'interval d'escaneig amb el format cron. Per obtenir més informació, consulteu, p.e <link>Crontab Guru</link>",
@@ -193,22 +192,26 @@
"oauth_auto_register": "Registre automàtic", "oauth_auto_register": "Registre automàtic",
"oauth_auto_register_description": "Registra nous usuaris automàticament després d'iniciar sessió amb OAuth", "oauth_auto_register_description": "Registra nous usuaris automàticament després d'iniciar sessió amb OAuth",
"oauth_button_text": "Text del botó", "oauth_button_text": "Text del botó",
"oauth_client_secret_description": "Requerit si PKCE (Proof Key for Code Exchange) no està suportat pel proveïdor OAuth", "oauth_client_id": "ID Client",
"oauth_client_secret": "Secret de Client",
"oauth_enable_description": "Iniciar sessió amb OAuth", "oauth_enable_description": "Iniciar sessió amb OAuth",
"oauth_issuer_url": "URL de l'emissor",
"oauth_mobile_redirect_uri": "URI de redirecció mòbil", "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": "Sobreescriu l'URI de redirecció mòbil",
"oauth_mobile_redirect_uri_override_description": "Habilita quan el proveïdor d'OAuth no permet una URI mòbil, com ara '{callback}'", "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 dusuari.",
"oauth_scope": "Abast",
"oauth_settings": "OAuth", "oauth_settings": "OAuth",
"oauth_settings_description": "Gestiona la configuració de l'inici de sessió OAuth", "oauth_settings_description": "Gestiona la configuració de l'inici de sessió OAuth",
"oauth_settings_more_details": "Per a més detalls sobre aquesta funcionalitat, consulteu la <link>documentació</link>.", "oauth_settings_more_details": "Per a més detalls sobre aquesta funcionalitat, consulteu la <link>documentació</link>.",
"oauth_signing_algorithm": "Algorisme de signatura",
"oauth_storage_label_claim": "Petició d'etiquetatge d'emmagatzematge", "oauth_storage_label_claim": "Petició d'etiquetatge d'emmagatzematge",
"oauth_storage_label_claim_description": "Estableix automàticament l'etiquetatge d'emmagatzematge de l'usuari a aquest valor.", "oauth_storage_label_claim_description": "Estableix automàticament l'etiquetatge d'emmagatzematge de l'usuari a aquest valor.",
"oauth_storage_quota_claim": "Quota d'emmagatzematge reclamada", "oauth_storage_quota_claim": "Quota d'emmagatzematge reclamada",
"oauth_storage_quota_claim_description": "Estableix automàticament la quota d'emmagatzematge de l'usuari al valor d'aquest paràmetre.", "oauth_storage_quota_claim_description": "Estableix automàticament la quota d'emmagatzematge de l'usuari al valor d'aquest paràmetre.",
"oauth_storage_quota_default": "Quota d'emmagatzematge predeterminada (GiB)", "oauth_storage_quota_default": "Quota d'emmagatzematge predeterminada (GiB)",
"oauth_storage_quota_default_description": "Quota disponible en GB quan no s'estableixi cap valor (Entreu 0 per a quota il·limitada).", "oauth_storage_quota_default_description": "Quota disponible en GB quan no s'estableixi cap valor (Entreu 0 per a quota il·limitada).",
"oauth_timeout": "Solicitud caducada",
"oauth_timeout_description": "Timeout per a sol·licituds en mil·lisegons",
"offline_paths": "Rutes sense connexió", "offline_paths": "Rutes sense connexió",
"offline_paths_description": "Aquests resultats poden ser deguts a l'eliminació manual de fitxers que no formen part d'una llibreria externa.", "offline_paths_description": "Aquests resultats poden ser deguts a l'eliminació manual de fitxers que no formen part d'una llibreria externa.",
"password_enable_description": "Inicia sessió amb correu electrònic i contrasenya", "password_enable_description": "Inicia sessió amb correu electrònic i contrasenya",
@@ -349,7 +352,6 @@
"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ó.", "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ó.",
"user_delete_immediately": "El compte i els recursos de <b>{user}</b> es posaran a la cua per suprimir-los permanentment <b>immediatament</b>.", "user_delete_immediately": "El compte i els recursos de <b>{user}</b> es posaran a la cua per suprimir-los permanentment <b>immediatament</b>.",
"user_delete_immediately_checkbox": "Posa en cua l'usuari i els recursos per suprimir-los immediatament", "user_delete_immediately_checkbox": "Posa en cua l'usuari i els recursos per suprimir-los immediatament",
"user_details": "Detalls d'usuari",
"user_management": "Gestió d'usuaris", "user_management": "Gestió d'usuaris",
"user_password_has_been_reset": "La contrasenya de l'usuari ha estat restablida:", "user_password_has_been_reset": "La contrasenya de l'usuari ha estat restablida:",
"user_password_reset_description": "Si us plau, proporcioneu la contrasenya temporal a l'usuari i informeu-los que haurà de canviar la contrasenya en el proper inici de sessió.", "user_password_reset_description": "Si us plau, proporcioneu la contrasenya temporal a l'usuari i informeu-los que haurà de canviar la contrasenya en el proper inici de sessió.",
@@ -369,17 +371,13 @@
"admin_password": "Contrasenya de l'administrador", "admin_password": "Contrasenya de l'administrador",
"administration": "Administrador", "administration": "Administrador",
"advanced": "Avançat", "advanced": "Avançat",
"advanced_settings_enable_alternate_media_filter_subtitle": "Feu servir aquesta opció per filtrar els continguts multimèdia durant la sincronització segons criteris alternatius. Només proveu-ho si teniu problemes amb l'aplicació per detectar tots els àlbums.", "advanced_settings_log_level_title": "Nivell de registre: {}",
"advanced_settings_enable_alternate_media_filter_title": "Utilitza el filtre de sincronització d'àlbums de dispositius alternatius",
"advanced_settings_log_level_title": "Nivell de registre: {level}",
"advanced_settings_prefer_remote_subtitle": "Alguns dispositius són molt lents en carregar miniatures dels elements del dispositiu. Activeu aquest paràmetre per carregar imatges remotes en el seu lloc.", "advanced_settings_prefer_remote_subtitle": "Alguns dispositius són molt lents en carregar miniatures dels elements del dispositiu. Activeu aquest paràmetre per carregar imatges remotes en el seu lloc.",
"advanced_settings_prefer_remote_title": "Prefereix imatges remotes", "advanced_settings_prefer_remote_title": "Prefereix imatges remotes",
"advanced_settings_proxy_headers_subtitle": "Definiu les capçaleres de proxy que Immich per enviar amb cada sol·licitud de xarxa", "advanced_settings_proxy_headers_subtitle": "Definiu les capçaleres de proxy que Immich per enviar amb cada sol·licitud de xarxa",
"advanced_settings_proxy_headers_title": "Capçaleres de proxy", "advanced_settings_proxy_headers_title": "Capçaleres de proxy",
"advanced_settings_self_signed_ssl_subtitle": "Omet la verificació del certificat SSL del servidor. Requerit per a certificats autosignats.", "advanced_settings_self_signed_ssl_subtitle": "Omet la verificació del certificat SSL del servidor. Requerit per a certificats autosignats.",
"advanced_settings_self_signed_ssl_title": "Permet certificats SSL autosignats", "advanced_settings_self_signed_ssl_title": "Permet certificats SSL autosignats",
"advanced_settings_sync_remote_deletions_subtitle": "Suprimeix o restaura automàticament un actiu en aquest dispositiu quan es realitzi aquesta acció al web",
"advanced_settings_sync_remote_deletions_title": "Sincronitza les eliminacions remotes",
"advanced_settings_tile_subtitle": "Configuració avançada de l'usuari", "advanced_settings_tile_subtitle": "Configuració avançada de l'usuari",
"advanced_settings_troubleshooting_subtitle": "Habilita funcions addicionals per a la resolució de problemes", "advanced_settings_troubleshooting_subtitle": "Habilita funcions addicionals per a la resolució de problemes",
"advanced_settings_troubleshooting_title": "Resolució de problemes", "advanced_settings_troubleshooting_title": "Resolució de problemes",
@@ -402,9 +400,9 @@
"album_remove_user_confirmation": "Esteu segurs que voleu eliminar {user}?", "album_remove_user_confirmation": "Esteu segurs que voleu eliminar {user}?",
"album_share_no_users": "Sembla que has compartit aquest àlbum amb tots els usuaris o no tens cap usuari amb qui compartir-ho.", "album_share_no_users": "Sembla que has compartit aquest àlbum amb tots els usuaris o no tens cap usuari amb qui compartir-ho.",
"album_thumbnail_card_item": "1 element", "album_thumbnail_card_item": "1 element",
"album_thumbnail_card_items": "{count} elements", "album_thumbnail_card_items": "{} elements",
"album_thumbnail_card_shared": " · Compartit", "album_thumbnail_card_shared": " · Compartit",
"album_thumbnail_shared_by": "Compartit per {user}", "album_thumbnail_shared_by": "Compartit per {}",
"album_updated": "Àlbum actualitzat", "album_updated": "Àlbum actualitzat",
"album_updated_setting_description": "Rep una notificació per correu electrònic quan un àlbum compartit tingui recursos nous", "album_updated_setting_description": "Rep una notificació per correu electrònic quan un àlbum compartit tingui recursos nous",
"album_user_left": "Surt de {album}", "album_user_left": "Surt de {album}",
@@ -442,7 +440,7 @@
"archive": "Arxiu", "archive": "Arxiu",
"archive_or_unarchive_photo": "Arxivar o desarxivar fotografia", "archive_or_unarchive_photo": "Arxivar o desarxivar fotografia",
"archive_page_no_archived_assets": "No s'ha trobat res arxivat", "archive_page_no_archived_assets": "No s'ha trobat res arxivat",
"archive_page_title": "Arxiu({count})", "archive_page_title": "Arxiu({})",
"archive_size": "Mida de l'arxiu", "archive_size": "Mida de l'arxiu",
"archive_size_description": "Configureu la mida de l'arxiu de les descàrregues (en GiB)", "archive_size_description": "Configureu la mida de l'arxiu de les descàrregues (en GiB)",
"archived": "Arxivat", "archived": "Arxivat",
@@ -479,18 +477,18 @@
"assets_added_to_album_count": "{count, plural, one {Afegit un element} other {Afegits # elements}} a l'àlbum", "assets_added_to_album_count": "{count, plural, one {Afegit un element} other {Afegits # elements}} a l'àlbum",
"assets_added_to_name_count": "{count, plural, one {S'ha afegit # recurs} other {S'han afegit # recursos}} a {hasName, select, true {<b>{name}</b>} other {new album}}", "assets_added_to_name_count": "{count, plural, one {S'ha afegit # recurs} other {S'han afegit # recursos}} a {hasName, select, true {<b>{name}</b>} other {new album}}",
"assets_count": "{count, plural, one {# recurs} other {# recursos}}", "assets_count": "{count, plural, one {# recurs} other {# recursos}}",
"assets_deleted_permanently": "{count} element(s) esborrats permanentment", "assets_deleted_permanently": "{} element(s) esborrats permanentment",
"assets_deleted_permanently_from_server": "{count} element(s) esborrats permanentment del servidor d'Immich", "assets_deleted_permanently_from_server": "{} element(s) esborrats permanentment del servidor d'Immich",
"assets_moved_to_trash_count": "{count, plural, one {# recurs mogut} other {# recursos moguts}} a la paperera", "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_permanently_deleted_count": "{count, plural, one {# recurs esborrat} other {# recursos esborrats}} permanentment",
"assets_removed_count": "{count, plural, one {# element eliminat} other {# elements eliminats}}", "assets_removed_count": "{count, plural, one {# element eliminat} other {# elements eliminats}}",
"assets_removed_permanently_from_device": "{count} element(s) esborrat permanentment del dispositiu", "assets_removed_permanently_from_device": "{} element(s) esborrat permanentment del dispositiu",
"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_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_restored_count": "{count, plural, one {# element restaurat} other {# elements restaurats}}",
"assets_restored_successfully": "{count} element(s) recuperats correctament", "assets_restored_successfully": "{} element(s) recuperats correctament",
"assets_trashed": "{count} element(s) enviat a la paperera", "assets_trashed": "{} element(s) enviat a la paperera",
"assets_trashed_count": "{count, plural, one {# element enviat} other {# elements enviats}} a la paperera", "assets_trashed_count": "{count, plural, one {# element enviat} other {# elements enviats}} a la paperera",
"assets_trashed_from_server": "{count} element(s) enviat a la paperera del servidor d'Immich", "assets_trashed_from_server": "{} element(s) enviat a la paperera del servidor d'Immich",
"assets_were_part_of_album_count": "{count, plural, one {L'element ja és} other {Els elements ja són}} part de l'àlbum", "assets_were_part_of_album_count": "{count, plural, one {L'element ja és} other {Els elements ja són}} part de l'àlbum",
"authorized_devices": "Dispositius autoritzats", "authorized_devices": "Dispositius autoritzats",
"automatic_endpoint_switching_subtitle": "Connecteu-vos localment a través de la Wi-Fi designada quan estigui disponible i utilitzeu connexions alternatives en altres llocs", "automatic_endpoint_switching_subtitle": "Connecteu-vos localment a través de la Wi-Fi designada quan estigui disponible i utilitzeu connexions alternatives en altres llocs",
@@ -499,7 +497,7 @@
"back_close_deselect": "Tornar, tancar o anul·lar la selecció", "back_close_deselect": "Tornar, tancar o anul·lar la selecció",
"background_location_permission": "Permís d'ubicació en segon pla", "background_location_permission": "Permís d'ubicació en segon pla",
"background_location_permission_content": "Per canviar de xarxa quan s'executa en segon pla, Immich ha de *sempre* tenir accés a la ubicació precisa perquè l'aplicació pugui llegir el nom de la xarxa Wi-Fi", "background_location_permission_content": "Per canviar de xarxa quan s'executa en segon pla, Immich ha de *sempre* tenir accés a la ubicació precisa perquè l'aplicació pugui llegir el nom de la xarxa Wi-Fi",
"backup_album_selection_page_albums_device": "Àlbums al dispositiu ({count})", "backup_album_selection_page_albums_device": "Àlbums al dispositiu ({})",
"backup_album_selection_page_albums_tap": "Un toc per incloure, doble toc per excloure", "backup_album_selection_page_albums_tap": "Un toc per incloure, doble toc per excloure",
"backup_album_selection_page_assets_scatter": "Els elements poden dispersar-se en diversos àlbums. Per tant, els àlbums es poden incloure o excloure durant el procés de còpia de seguretat.", "backup_album_selection_page_assets_scatter": "Els elements poden dispersar-se en diversos àlbums. Per tant, els àlbums es poden incloure o excloure durant el procés de còpia de seguretat.",
"backup_album_selection_page_select_albums": "Selecciona àlbums", "backup_album_selection_page_select_albums": "Selecciona àlbums",
@@ -508,36 +506,37 @@
"backup_all": "Tots", "backup_all": "Tots",
"backup_background_service_backup_failed_message": "No s'ha pogut copiar els elements. Tornant a intentar…", "backup_background_service_backup_failed_message": "No s'ha pogut copiar els elements. Tornant a intentar…",
"backup_background_service_connection_failed_message": "No s'ha pogut connectar al servidor. Tornant a intentar…", "backup_background_service_connection_failed_message": "No s'ha pogut connectar al servidor. Tornant a intentar…",
"backup_background_service_current_upload_notification": "Pujant {filename}", "backup_background_service_current_upload_notification": "Pujant {}",
"backup_background_service_default_notification": "Cercant nous elements", "backup_background_service_default_notification": "Cercant nous elements...",
"backup_background_service_error_title": "Error copiant", "backup_background_service_error_title": "Error copiant",
"backup_background_service_in_progress_notification": "Copiant els teus elements", "backup_background_service_in_progress_notification": "Copiant els teus elements",
"backup_background_service_upload_failure_notification": "Error en pujar {filename}", "backup_background_service_upload_failure_notification": "Error al pujar {}",
"backup_controller_page_albums": "Copia els àlbums", "backup_controller_page_albums": "Copia els àlbums",
"backup_controller_page_background_app_refresh_disabled_content": "Activa l'actualització en segon pla de l'aplicació a Configuració > General > Actualització en segon pla per utilitzar la copia de seguretat en segon pla.", "backup_controller_page_background_app_refresh_disabled_content": "Activa l'actualització en segon pla de l'aplicació a Configuració > General > Actualització en segon pla per utilitzar la copia de seguretat en segon pla.",
"backup_controller_page_background_app_refresh_disabled_title": "Actualització en segon pla desactivada", "backup_controller_page_background_app_refresh_disabled_title": "Actualització en segon pla desactivada",
"backup_controller_page_background_app_refresh_enable_button_text": "Vés a configuració", "backup_controller_page_background_app_refresh_enable_button_text": "Vés a configuració",
"backup_controller_page_background_battery_info_link": "Mostra'm com", "backup_controller_page_background_battery_info_link": "Mostra'm com",
"backup_controller_page_background_battery_info_message": "Per obtenir la millor experiència de còpia de seguretat en segon pla, desactiveu qualsevol optimització de bateria que restringeixi l'activitat en segon pla per a Immich.\n\nAtès que això és específic del dispositiu, busqueu la informació necessària per al fabricant del vostre dispositiu.", "backup_controller_page_background_battery_info_message": "Per obtenir la millor experiència de copia de seguretat en segon pla, desactiveu qualsevol optimització de bateria que restringeixi l'activitat en segon pla per a Immich.\n\nAtès que això és específic del dispositiu, busqueu la informació necessària per al fabricant del vostre dispositiu",
"backup_controller_page_background_battery_info_ok": "D'acord", "backup_controller_page_background_battery_info_ok": "D'acord",
"backup_controller_page_background_battery_info_title": "Optimitzacions de bateria", "backup_controller_page_background_battery_info_title": "Optimitzacions de bateria",
"backup_controller_page_background_charging": "Només mentre es carrega", "backup_controller_page_background_charging": "Només mentre es carrega",
"backup_controller_page_background_configure_error": "No s'ha pogut configurar el servei en segon pla", "backup_controller_page_background_configure_error": "No s'ha pogut configurar el servei en segon pla",
"backup_controller_page_background_delay": "Retard en la còpia de seguretat de nous elements: {duration}", "backup_controller_page_background_delay": "Retard en la copia de seguretat de nous elements: {}",
"backup_controller_page_background_description": "Activeu el servei en segon pla per copiar automàticament tots els nous elements sense haver d'obrir l'aplicació", "backup_controller_page_background_description": "Activeu el servei en segon pla per copiar automàticament tots els nous elements sense haver d'obrir l'aplicació.",
"backup_controller_page_background_is_off": "La còpia automàtica en segon pla està desactivada", "backup_controller_page_background_is_off": "La còpia automàtica en segon pla està desactivada",
"backup_controller_page_background_is_on": "La còpia automàtica en segon pla està activada", "backup_controller_page_background_is_on": "La còpia automàtica en segon pla està activada",
"backup_controller_page_background_turn_off": "Desactiva el servei en segon pla", "backup_controller_page_background_turn_off": "Desactiva el servei en segon pla",
"backup_controller_page_background_turn_on": "Activa el servei en segon pla", "backup_controller_page_background_turn_on": "Activa el servei en segon pla",
"backup_controller_page_background_wifi": "Només amb Wi-Fi", "backup_controller_page_background_wifi": "Només amb WiFi",
"backup_controller_page_backup": "Còpia", "backup_controller_page_backup": "Còpia",
"backup_controller_page_backup_selected": "Seleccionat: ", "backup_controller_page_backup_selected": "Seleccionat: ",
"backup_controller_page_backup_sub": "Fotografies i vídeos copiats", "backup_controller_page_backup_sub": "Fotografies i vídeos copiats",
"backup_controller_page_created": "Creat el: {date}", "backup_controller_page_created": "Creat el: {}",
"backup_controller_page_desc_backup": "Activeu la còpia de seguretat per pujar automàticament els nous elements al servidor en obrir l'aplicació.", "backup_controller_page_desc_backup": "Activeu la còpia de seguretat per pujar automàticament els nous elements al servidor en obrir l'aplicació.",
"backup_controller_page_excluded": "Exclosos: ", "backup_controller_page_excluded": "Exclosos:",
"backup_controller_page_failed": "Fallats ({count})", "backup_controller_page_failed": "Fallats ({})",
"backup_controller_page_filename": "Nom de l'arxiu: {filename} [{size}]", "backup_controller_page_filename": "Nom de l'arxiu: {} [{}]",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Informació de la còpia", "backup_controller_page_info": "Informació de la còpia",
"backup_controller_page_none_selected": "Cap seleccionat", "backup_controller_page_none_selected": "Cap seleccionat",
"backup_controller_page_remainder": "Restant", "backup_controller_page_remainder": "Restant",
@@ -546,7 +545,7 @@
"backup_controller_page_start_backup": "Inicia la còpia", "backup_controller_page_start_backup": "Inicia la còpia",
"backup_controller_page_status_off": "La copia de seguretat està desactivada", "backup_controller_page_status_off": "La copia de seguretat està desactivada",
"backup_controller_page_status_on": "La copia de seguretat està activada", "backup_controller_page_status_on": "La copia de seguretat està activada",
"backup_controller_page_storage_format": "{used} de {total} utilitzats", "backup_controller_page_storage_format": "{} de {} utilitzats",
"backup_controller_page_to_backup": "Àlbums a copiar", "backup_controller_page_to_backup": "Àlbums a copiar",
"backup_controller_page_total_sub": "Totes les fotografies i vídeos dels àlbums seleccionats", "backup_controller_page_total_sub": "Totes les fotografies i vídeos dels àlbums seleccionats",
"backup_controller_page_turn_off": "Desactiva la còpia de seguretat", "backup_controller_page_turn_off": "Desactiva la còpia de seguretat",
@@ -571,21 +570,21 @@
"bulk_keep_duplicates_confirmation": "Esteu segur que voleu mantenir {count, plural, one {# recurs duplicat} other {# recursos duplicats}}? Això resoldrà tots els grups duplicats sense eliminar res.", "bulk_keep_duplicates_confirmation": "Esteu segur que voleu mantenir {count, plural, one {# recurs duplicat} other {# recursos duplicats}}? Això resoldrà tots els grups duplicats sense eliminar res.",
"bulk_trash_duplicates_confirmation": "Esteu segur que voleu enviar a les escombraries {count, plural, one {# recurs duplicat} other {# recursos duplicats}}? Això mantindrà el recurs més gran de cada grup i eliminarà la resta de duplicats.", "bulk_trash_duplicates_confirmation": "Esteu segur que voleu enviar a les escombraries {count, plural, one {# recurs duplicat} other {# recursos duplicats}}? Això mantindrà el recurs més gran de cada grup i eliminarà la resta de duplicats.",
"buy": "Comprar Immich", "buy": "Comprar Immich",
"cache_settings_album_thumbnails": "Miniatures de la pàgina de la biblioteca ({count} elements)", "cache_settings_album_thumbnails": "Miniatures de la pàgina de la biblioteca ({} elements)",
"cache_settings_clear_cache_button": "Neteja la memòria cau", "cache_settings_clear_cache_button": "Neteja la memòria cau",
"cache_settings_clear_cache_button_title": "Neteja la memòria cau de l'aplicació. Això impactarà significativament el rendiment fins que la memòria cau es torni a reconstruir.", "cache_settings_clear_cache_button_title": "Neteja la memòria cau de l'aplicació. Això impactarà significativament el rendiment fins que la memòria cau es torni a reconstruir.",
"cache_settings_duplicated_assets_clear_button": "NETEJA", "cache_settings_duplicated_assets_clear_button": "NETEJA",
"cache_settings_duplicated_assets_subtitle": "Fotos i vídeos que estan a la llista negra de l'aplicació", "cache_settings_duplicated_assets_subtitle": "Fotos i vídeos que estan a la llista negra de l'aplicació.",
"cache_settings_duplicated_assets_title": "Elements duplicats ({count})", "cache_settings_duplicated_assets_title": "Elements duplicats ({})",
"cache_settings_image_cache_size": "Mida de la memòria cau d'imatges ({count} elements)", "cache_settings_image_cache_size": "Mida de la memòria cau de imatges ({} elements)",
"cache_settings_statistics_album": "Miniatures de la biblioteca", "cache_settings_statistics_album": "Miniatures de la biblioteca",
"cache_settings_statistics_assets": "{count} elements ({size})", "cache_settings_statistics_assets": "{} elements ({})",
"cache_settings_statistics_full": "Imatges completes", "cache_settings_statistics_full": "Imatges completes",
"cache_settings_statistics_shared": "Miniatures d'àlbums compartits", "cache_settings_statistics_shared": "Miniatures d'àlbums compartits",
"cache_settings_statistics_thumbnail": "Miniatures", "cache_settings_statistics_thumbnail": "Miniatures",
"cache_settings_statistics_title": "Ús de memòria cau", "cache_settings_statistics_title": "Ús de memòria cau",
"cache_settings_subtitle": "Controla el comportament de la memòria cau de l'aplicació mòbil Immich", "cache_settings_subtitle": "Controla el comportament de la memòria cau de l'aplicació mòbil Immich",
"cache_settings_thumbnail_size": "Mida de la memòria cau de les miniatures ({count} elements)", "cache_settings_thumbnail_size": "Mida de la memòria cau de les miniatures ({} elements)",
"cache_settings_tile_subtitle": "Controla el comportament de l'emmagatzematge local", "cache_settings_tile_subtitle": "Controla el comportament de l'emmagatzematge local",
"cache_settings_tile_title": "Emmagatzematge local", "cache_settings_tile_title": "Emmagatzematge local",
"cache_settings_title": "Configuració de la memòria cau", "cache_settings_title": "Configuració de la memòria cau",
@@ -611,7 +610,6 @@
"change_password_form_new_password": "Nova contrasenya", "change_password_form_new_password": "Nova contrasenya",
"change_password_form_password_mismatch": "Les contrasenyes no coincideixen", "change_password_form_password_mismatch": "Les contrasenyes no coincideixen",
"change_password_form_reenter_new_password": "Torna a introduir la nova contrasenya", "change_password_form_reenter_new_password": "Torna a introduir la nova contrasenya",
"change_pin_code": "Canviar el codi PIN",
"change_your_password": "Canvia la teva contrasenya", "change_your_password": "Canvia la teva contrasenya",
"changed_visibility_successfully": "Visibilitat canviada amb èxit", "changed_visibility_successfully": "Visibilitat canviada amb èxit",
"check_all": "Marqueu-ho tot", "check_all": "Marqueu-ho tot",
@@ -626,6 +624,7 @@
"clear_all_recent_searches": "Esborra totes les cerques recents", "clear_all_recent_searches": "Esborra totes les cerques recents",
"clear_message": "Neteja el missatge", "clear_message": "Neteja el missatge",
"clear_value": "Neteja el valor", "clear_value": "Neteja el valor",
"client_cert_dialog_msg_confirm": "OK",
"client_cert_enter_password": "Introdueix la contrasenya", "client_cert_enter_password": "Introdueix la contrasenya",
"client_cert_import": "Importar", "client_cert_import": "Importar",
"client_cert_import_success_msg": "S'ha importat el certificat del client", "client_cert_import_success_msg": "S'ha importat el certificat del client",
@@ -637,6 +636,7 @@
"close": "Tanca", "close": "Tanca",
"collapse": "Tanca", "collapse": "Tanca",
"collapse_all": "Redueix-ho tot", "collapse_all": "Redueix-ho tot",
"color": "Color",
"color_theme": "Tema de color", "color_theme": "Tema de color",
"comment_deleted": "Comentari esborrat", "comment_deleted": "Comentari esborrat",
"comment_options": "Opcions de comentari", "comment_options": "Opcions de comentari",
@@ -650,11 +650,11 @@
"confirm_delete_face": "Estàs segur que vols eliminar la cara de {name} de les cares reconegudes?", "confirm_delete_face": "Estàs segur que vols eliminar la cara de {name} de les cares reconegudes?",
"confirm_delete_shared_link": "Esteu segurs que voleu eliminar aquest enllaç compartit?", "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_keep_this_delete_others": "Excepte aquest element, tots els altres de la pila se suprimiran. Esteu segur que voleu continuar?",
"confirm_new_pin_code": "Confirma el nou codi PIN",
"confirm_password": "Confirmació de contrasenya", "confirm_password": "Confirmació de contrasenya",
"contain": "Contingut", "contain": "Contingut",
"context": "Context",
"continue": "Continuar", "continue": "Continuar",
"control_bottom_app_bar_album_info_shared": "{count} elements - Compartits", "control_bottom_app_bar_album_info_shared": "{} elements - Compartits",
"control_bottom_app_bar_create_new_album": "Crea un àlbum nou", "control_bottom_app_bar_create_new_album": "Crea un àlbum nou",
"control_bottom_app_bar_delete_from_immich": "Suprimeix del Immich", "control_bottom_app_bar_delete_from_immich": "Suprimeix del Immich",
"control_bottom_app_bar_delete_from_local": "Suprimeix del dispositiu", "control_bottom_app_bar_delete_from_local": "Suprimeix del dispositiu",
@@ -692,11 +692,9 @@
"create_tag_description": "Crear una nova etiqueta. Per les etiquetes aniuades, escriu la ruta comperta de l'etiqueta, incloses les barres diagonals.", "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", "create_user": "Crea un usuari",
"created": "Creat", "created": "Creat",
"created_at": "Creat",
"crop": "Retalla", "crop": "Retalla",
"curated_object_page_title": "Coses", "curated_object_page_title": "Coses",
"current_device": "Dispositiu actual", "current_device": "Dispositiu actual",
"current_pin_code": "Codi PIN actual",
"current_server_address": "Adreça actual del servidor", "current_server_address": "Adreça actual del servidor",
"custom_locale": "Localització personalitzada", "custom_locale": "Localització personalitzada",
"custom_locale_description": "Format de dates i números segons la llengua i regió", "custom_locale_description": "Format de dates i números segons la llengua i regió",
@@ -720,7 +718,7 @@
"delete": "Esborra", "delete": "Esborra",
"delete_album": "Esborra l'àlbum", "delete_album": "Esborra l'àlbum",
"delete_api_key_prompt": "Esteu segurs que voleu eliminar aquesta clau API?", "delete_api_key_prompt": "Esteu segurs que voleu eliminar aquesta clau API?",
"delete_dialog_alert": "Aquests elements seran eliminats de manera permanent d'Immich i del vostre dispositiu", "delete_dialog_alert": "Aquests elements seran eliminats de manera permanent d'Immich i del vostre dispositiu.",
"delete_dialog_alert_local": "Aquests elements s'eliminaran permanentment del vostre dispositiu, però encara estaran disponibles al servidor Immich", "delete_dialog_alert_local": "Aquests elements s'eliminaran permanentment del vostre dispositiu, però encara estaran disponibles al servidor Immich",
"delete_dialog_alert_local_non_backed_up": "Alguns dels elements no tenen còpia de seguretat a Immich i s'eliminaran permanentment del dispositiu", "delete_dialog_alert_local_non_backed_up": "Alguns dels elements no tenen còpia de seguretat a Immich i s'eliminaran permanentment del dispositiu",
"delete_dialog_alert_remote": "Aquests elements s'eliminaran permanentment del servidor Immich", "delete_dialog_alert_remote": "Aquests elements s'eliminaran permanentment del servidor Immich",
@@ -748,6 +746,7 @@
"direction": "Direcció", "direction": "Direcció",
"disabled": "Desactivat", "disabled": "Desactivat",
"disallow_edits": "No permetre les edicions", "disallow_edits": "No permetre les edicions",
"discord": "Discord",
"discover": "Descobreix", "discover": "Descobreix",
"dismiss_all_errors": "Descarta tots els errors", "dismiss_all_errors": "Descarta tots els errors",
"dismiss_error": "Descarta l'error", "dismiss_error": "Descarta l'error",
@@ -764,7 +763,7 @@
"download_enqueue": "Descàrrega en cua", "download_enqueue": "Descàrrega en cua",
"download_error": "Error de descàrrega", "download_error": "Error de descàrrega",
"download_failed": "Descàrrega ha fallat", "download_failed": "Descàrrega ha fallat",
"download_filename": "arxiu: {filename}", "download_filename": "arxiu: {}",
"download_finished": "Descàrrega acabada", "download_finished": "Descàrrega acabada",
"download_include_embedded_motion_videos": "Vídeos incrustats", "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_include_embedded_motion_videos_description": "Incloure vídeos incrustats en fotografies en moviment com un arxiu separat",
@@ -802,12 +801,12 @@
"edit_title": "Edita títol", "edit_title": "Edita títol",
"edit_user": "Edita l'usuari", "edit_user": "Edita l'usuari",
"edited": "Editat", "edited": "Editat",
"editor": "Editor",
"editor_close_without_save_prompt": "No es desaran els canvis", "editor_close_without_save_prompt": "No es desaran els canvis",
"editor_close_without_save_title": "Tancar l'editor?", "editor_close_without_save_title": "Tancar l'editor?",
"editor_crop_tool_h2_aspect_ratios": "Relació d'aspecte", "editor_crop_tool_h2_aspect_ratios": "Relació d'aspecte",
"editor_crop_tool_h2_rotation": "Rotació", "editor_crop_tool_h2_rotation": "Rotació",
"email": "Correu electrònic", "email": "Correu electrònic",
"email_notifications": "Correu electrònic de notificacions",
"empty_folder": "Aquesta carpeta és buida", "empty_folder": "Aquesta carpeta és buida",
"empty_trash": "Buidar la paperera", "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ó!", "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ó!",
@@ -815,10 +814,12 @@
"enabled": "Activat", "enabled": "Activat",
"end_date": "Data final", "end_date": "Data final",
"enqueued": "En cua", "enqueued": "En cua",
"enter_wifi_name": "Introdueix el nom de Wi-Fi", "enter_wifi_name": "Introdueix el nom de WiFi",
"error": "Error",
"error_change_sort_album": "No s'ha pogut canviar l'ordre d'ordenació dels àlbums", "error_change_sort_album": "No s'ha pogut canviar l'ordre d'ordenació dels àlbums",
"error_delete_face": "Error esborrant cara de les cares reconegudes", "error_delete_face": "Error esborrant cara de les cares reconegudes",
"error_loading_image": "Error carregant la imatge", "error_loading_image": "Error carregant la imatge",
"error_saving_image": "Error: {}",
"error_title": "Error - Quelcom ha anat malament", "error_title": "Error - Quelcom ha anat malament",
"errors": { "errors": {
"cannot_navigate_next_asset": "No es pot navegar a l'element següent", "cannot_navigate_next_asset": "No es pot navegar a l'element següent",
@@ -848,12 +849,10 @@
"failed_to_keep_this_delete_others": "No s'ha pogut conservar aquest element i suprimir els altres", "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_asset": "No s'ha pogut carregar l'element",
"failed_to_load_assets": "No s'han pogut carregar els elements", "failed_to_load_assets": "No s'han pogut carregar els elements",
"failed_to_load_notifications": "Error en carregar les notificacions",
"failed_to_load_people": "No s'han pogut carregar les persones", "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_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_stack_assets": "No s'han pogut apilar els elements",
"failed_to_unstack_assets": "No s'han pogut desapilar els elements", "failed_to_unstack_assets": "No s'han pogut desapilar els elements",
"failed_to_update_notification_status": "Error en actualitzar l'estat de les notificacions",
"import_path_already_exists": "Aquesta ruta d'importació ja existeix.", "import_path_already_exists": "Aquesta ruta d'importació ja existeix.",
"incorrect_email_or_password": "Correu electrònic o contrasenya incorrectes", "incorrect_email_or_password": "Correu electrònic o contrasenya incorrectes",
"paths_validation_failed": "{paths, plural, one {# ruta} other {# rutes}} no ha pogut validar", "paths_validation_failed": "{paths, plural, one {# ruta} other {# rutes}} no ha pogut validar",
@@ -921,7 +920,6 @@
"unable_to_remove_reaction": "No es pot eliminar la reacció", "unable_to_remove_reaction": "No es pot eliminar la reacció",
"unable_to_repair_items": "No es poden reparar els elements", "unable_to_repair_items": "No es poden reparar els elements",
"unable_to_reset_password": "No es pot restablir la contrasenya", "unable_to_reset_password": "No es pot restablir la contrasenya",
"unable_to_reset_pin_code": "No es pot restablir el codi PIN",
"unable_to_resolve_duplicate": "No es pot resoldre el duplicat", "unable_to_resolve_duplicate": "No es pot resoldre el duplicat",
"unable_to_restore_assets": "No es poden restaurar els recursos", "unable_to_restore_assets": "No es poden restaurar els recursos",
"unable_to_restore_trash": "No es pot restaurar la paperera", "unable_to_restore_trash": "No es pot restaurar la paperera",
@@ -949,20 +947,22 @@
"unable_to_update_user": "No es pot actualitzar l'usuari", "unable_to_update_user": "No es pot actualitzar l'usuari",
"unable_to_upload_file": "No es pot carregar el fitxer" "unable_to_upload_file": "No es pot carregar el fitxer"
}, },
"exif_bottom_sheet_description": "Afegeix descripció...", "exif": "Exif",
"exif_bottom_sheet_description": "Afegeix descripció",
"exif_bottom_sheet_details": "DETALLS", "exif_bottom_sheet_details": "DETALLS",
"exif_bottom_sheet_location": "UBICACIÓ", "exif_bottom_sheet_location": "UBICACIÓ",
"exif_bottom_sheet_people": "PERSONES", "exif_bottom_sheet_people": "PERSONES",
"exif_bottom_sheet_person_add_person": "Afegir nom", "exif_bottom_sheet_person_add_person": "Afegir nom",
"exif_bottom_sheet_person_age": "Edat {age}", "exif_bottom_sheet_person_age": "Edat {}",
"exif_bottom_sheet_person_age_months": "Edat {months} mesos", "exif_bottom_sheet_person_age_months": "Edat {} mesos",
"exif_bottom_sheet_person_age_year_months": "Edat 1 any, {months} mesos", "exif_bottom_sheet_person_age_year_months": "Edat 1 any, {} mesos",
"exif_bottom_sheet_person_age_years": "Edat {years}", "exif_bottom_sheet_person_age_years": "Edat {}",
"exit_slideshow": "Surt de la presentació de diapositives", "exit_slideshow": "Surt de la presentació de diapositives",
"expand_all": "Ampliar-ho tot", "expand_all": "Ampliar-ho tot",
"experimental_settings_new_asset_list_subtitle": "Treball en curs", "experimental_settings_new_asset_list_subtitle": "Treball en curs",
"experimental_settings_new_asset_list_title": "Habilita la graella de fotos experimental", "experimental_settings_new_asset_list_title": "Habilita la graella de fotos experimental",
"experimental_settings_subtitle": "Utilitzeu-ho sota la vostra responsabilitat!", "experimental_settings_subtitle": "Utilitzeu-ho sota la vostra responsabilitat!",
"experimental_settings_title": "Experimental",
"expire_after": "Caduca després de", "expire_after": "Caduca després de",
"expired": "Caducat", "expired": "Caducat",
"expires_date": "Caduca el {date}", "expires_date": "Caduca el {date}",
@@ -974,7 +974,7 @@
"external": "Extern", "external": "Extern",
"external_libraries": "Llibreries externes", "external_libraries": "Llibreries externes",
"external_network": "Xarxa externa", "external_network": "Xarxa externa",
"external_network_sheet_info": "Quan no estigui a la xarxa Wi-Fi preferida, l'aplicació es connectarà al servidor mitjançant el primer dels URL següents a què pot arribar, començant de dalt a baix", "external_network_sheet_info": "Quan no estigui a la xarxa WiFi preferida, l'aplicació es connectarà al servidor mitjançant el primer dels URL següents a què pot arribar, començant de dalt a baix.",
"face_unassigned": "Sense assignar", "face_unassigned": "Sense assignar",
"failed": "Fallat", "failed": "Fallat",
"failed_to_load_assets": "Error carregant recursos", "failed_to_load_assets": "Error carregant recursos",
@@ -992,7 +992,6 @@
"filetype": "Tipus d'arxiu", "filetype": "Tipus d'arxiu",
"filter": "Filtrar", "filter": "Filtrar",
"filter_people": "Filtra persones", "filter_people": "Filtra persones",
"filter_places": "Filtrar per llocs",
"find_them_fast": "Trobeu-los ràpidament pel nom amb la cerca", "find_them_fast": "Trobeu-los ràpidament pel nom amb la cerca",
"fix_incorrect_match": "Corregiu la coincidència incorrecta", "fix_incorrect_match": "Corregiu la coincidència incorrecta",
"folder": "Carpeta", "folder": "Carpeta",
@@ -1000,6 +999,7 @@
"folders": "Carpetes", "folders": "Carpetes",
"folders_feature_description": "Explorar la vista de carpetes per les fotos i vídeos del sistema d'arxius", "folders_feature_description": "Explorar la vista de carpetes per les fotos i vídeos del sistema d'arxius",
"forward": "Endavant", "forward": "Endavant",
"general": "General",
"get_help": "Aconseguir ajuda", "get_help": "Aconseguir ajuda",
"get_wifiname_error": "No s'ha pogut obtenir el nom de la Wi-Fi. Assegureu-vos que heu concedit els permisos necessaris i que esteu connectat a una xarxa Wi-Fi", "get_wifiname_error": "No s'ha pogut obtenir el nom de la Wi-Fi. Assegureu-vos que heu concedit els permisos necessaris i que esteu connectat a una xarxa Wi-Fi",
"getting_started": "Començant", "getting_started": "Començant",
@@ -1040,7 +1040,7 @@
"home_page_delete_remote_err_local": "Elements locals a la selecció d'eliminació remota, ometent", "home_page_delete_remote_err_local": "Elements locals a la selecció d'eliminació remota, ometent",
"home_page_favorite_err_local": "Encara no es pot afegir a preferits elements locals, ometent", "home_page_favorite_err_local": "Encara no es pot afegir a preferits elements locals, ometent",
"home_page_favorite_err_partner": "Encara no es pot afegir a preferits elements de companys, ometent", "home_page_favorite_err_partner": "Encara no es pot afegir a preferits elements de companys, ometent",
"home_page_first_time_notice": "Si és la primera vegada que utilitzes l'app, si us plau, assegura't d'escollir un àlbum de còpia de seguretat perquè la línia de temps pugui carregar fotos i vídeos als àlbums", "home_page_first_time_notice": "Si és la primera vegada que utilitzes l'app, si us plau, assegura't d'escollir un àlbum de còpia de seguretat perquè la línia de temps pugui carregar fotos i vídeos als àlbums.",
"home_page_share_err_local": "No es poden compartir els elements locals a través d'un enllaç, ometent", "home_page_share_err_local": "No es poden compartir els elements locals a través d'un enllaç, ometent",
"home_page_upload_err_limit": "Només es poden pujar un màxim de 30 elements alhora, ometent", "home_page_upload_err_limit": "Només es poden pujar un màxim de 30 elements alhora, ometent",
"host": "Amfitrió", "host": "Amfitrió",
@@ -1120,7 +1120,7 @@
"local_network": "Xarxa local", "local_network": "Xarxa local",
"local_network_sheet_info": "L'aplicació es connectarà al servidor mitjançant aquest URL quan utilitzeu la xarxa Wi-Fi especificada", "local_network_sheet_info": "L'aplicació es connectarà al servidor mitjançant aquest URL quan utilitzeu la xarxa Wi-Fi especificada",
"location_permission": "Permís d'ubicació", "location_permission": "Permís d'ubicació",
"location_permission_content": "Per utilitzar la funció de canvi automàtic, Immich necessita un permís d'ubicació precisa perquè pugui llegir el nom de la xarxa Wi-Fi actual", "location_permission_content": "Per utilitzar la funció de canvi automàtic, Immich necessita un permís de ubicació precisa perquè pugui llegir el nom de la xarxa WiFi actual",
"location_picker_choose_on_map": "Escollir en el mapa", "location_picker_choose_on_map": "Escollir en el mapa",
"location_picker_latitude_error": "Introdueix una latitud vàlida", "location_picker_latitude_error": "Introdueix una latitud vàlida",
"location_picker_latitude_hint": "Introdueix aquí la latitud", "location_picker_latitude_hint": "Introdueix aquí la latitud",
@@ -1144,7 +1144,7 @@
"login_form_err_trailing_whitespace": "Espai en blanc al final", "login_form_err_trailing_whitespace": "Espai en blanc al final",
"login_form_failed_get_oauth_server_config": "Error en iniciar sessió amb OAuth, comprova l'URL del servidor", "login_form_failed_get_oauth_server_config": "Error en iniciar sessió amb OAuth, comprova l'URL del servidor",
"login_form_failed_get_oauth_server_disable": "La funcionalitat OAuth no està disponible en aquest servidor", "login_form_failed_get_oauth_server_disable": "La funcionalitat OAuth no està disponible en aquest servidor",
"login_form_failed_login": "Error en iniciar sessió, comprova l'URL del servidor, el correu electrònic i la contrasenya", "login_form_failed_login": "Error en iniciar sessió, comprova l'URL del servidor, el correu electrònic i la contrasenya.",
"login_form_handshake_exception": "S'ha produït una excepció de handshake amb el servidor. Activa el suport per certificats autofirmats a la configuració si estàs fent servir un certificat autofirmat.", "login_form_handshake_exception": "S'ha produït una excepció de handshake amb el servidor. Activa el suport per certificats autofirmats a la configuració si estàs fent servir un certificat autofirmat.",
"login_form_password_hint": "contrasenya", "login_form_password_hint": "contrasenya",
"login_form_save_login": "Mantingues identificat", "login_form_save_login": "Mantingues identificat",
@@ -1170,8 +1170,8 @@
"manage_your_devices": "Gestioneu els vostres dispositius connectats", "manage_your_devices": "Gestioneu els vostres dispositius connectats",
"manage_your_oauth_connection": "Gestioneu la vostra connexió OAuth", "manage_your_oauth_connection": "Gestioneu la vostra connexió OAuth",
"map": "Mapa", "map": "Mapa",
"map_assets_in_bound": "{count} foto", "map_assets_in_bound": "{} foto",
"map_assets_in_bounds": "{count} fotos", "map_assets_in_bounds": "{} fotos",
"map_cannot_get_user_location": "No es pot obtenir la ubicació de l'usuari", "map_cannot_get_user_location": "No es pot obtenir la ubicació de l'usuari",
"map_location_dialog_yes": "Sí", "map_location_dialog_yes": "Sí",
"map_location_picker_page_use_location": "Utilitzar aquesta ubicació", "map_location_picker_page_use_location": "Utilitzar aquesta ubicació",
@@ -1185,18 +1185,15 @@
"map_settings": "Paràmetres de mapa", "map_settings": "Paràmetres de mapa",
"map_settings_dark_mode": "Mode fosc", "map_settings_dark_mode": "Mode fosc",
"map_settings_date_range_option_day": "Últimes 24 hores", "map_settings_date_range_option_day": "Últimes 24 hores",
"map_settings_date_range_option_days": "Darrers {days} dies", "map_settings_date_range_option_days": "Darrers {} dies",
"map_settings_date_range_option_year": "Any passat", "map_settings_date_range_option_year": "Any passat",
"map_settings_date_range_option_years": "Darrers {years} anys", "map_settings_date_range_option_years": "Darrers {} anys",
"map_settings_dialog_title": "Configuració del mapa", "map_settings_dialog_title": "Configuració del mapa",
"map_settings_include_show_archived": "Incloure arxivats", "map_settings_include_show_archived": "Incloure arxivats",
"map_settings_include_show_partners": "Incloure companys", "map_settings_include_show_partners": "Incloure companys",
"map_settings_only_show_favorites": "Mostra només preferits", "map_settings_only_show_favorites": "Mostra només preferits",
"map_settings_theme_settings": "Tema del Mapa", "map_settings_theme_settings": "Tema del Mapa",
"map_zoom_to_see_photos": "Allunya per veure fotos", "map_zoom_to_see_photos": "Allunya per veure fotos",
"mark_all_as_read": "Marcar-ho tot com a llegit",
"mark_as_read": "Marcar com ha llegit",
"marked_all_as_read": "Marcat tot com a llegit",
"matches": "Coincidències", "matches": "Coincidències",
"media_type": "Tipus de mitjà", "media_type": "Tipus de mitjà",
"memories": "Records", "memories": "Records",
@@ -1205,6 +1202,8 @@
"memories_setting_description": "Gestiona el que veus als teus records", "memories_setting_description": "Gestiona el que veus als teus records",
"memories_start_over": "Torna a començar", "memories_start_over": "Torna a començar",
"memories_swipe_to_close": "Llisca per tancar", "memories_swipe_to_close": "Llisca per tancar",
"memories_year_ago": "Fa un any",
"memories_years_ago": "Fa {} anys",
"memory": "Record", "memory": "Record",
"memory_lane_title": "Línia de records {title}", "memory_lane_title": "Línia de records {title}",
"menu": "Menú", "menu": "Menú",
@@ -1217,13 +1216,13 @@
"minimize": "Minimitza", "minimize": "Minimitza",
"minute": "Minut", "minute": "Minut",
"missing": "Restants", "missing": "Restants",
"model": "Model",
"month": "Mes", "month": "Mes",
"monthly_title_text_date_format": "MMMM y",
"more": "Més", "more": "Més",
"moved_to_archive": "S'han mogut {count, plural, one {# asset} other {# assets}} a l'arxiu",
"moved_to_library": "S'ha mogut {count, plural, one {# asset} other {# assets}} a la llibreria",
"moved_to_trash": "S'ha mogut a la paperera", "moved_to_trash": "S'ha mogut a la paperera",
"multiselect_grid_edit_date_time_err_read_only": "No es pot canviar la data del fitxer(s) de només lectura, ometent", "multiselect_grid_edit_date_time_err_read_only": "No es pot canviar la data del fitxer(s) de només lectura, ometent",
"multiselect_grid_edit_gps_err_read_only": "No es pot canviar la localització de fitxers de només lectura, saltant", "multiselect_grid_edit_gps_err_read_only": "No es pot canviar la localització de fitxers de només lectura. Saltant.",
"mute_memories": "Silenciar records", "mute_memories": "Silenciar records",
"my_albums": "Els meus àlbums", "my_albums": "Els meus àlbums",
"name": "Nom", "name": "Nom",
@@ -1235,12 +1234,12 @@
"new_api_key": "Nova clau de l'API", "new_api_key": "Nova clau de l'API",
"new_password": "Nova contrasenya", "new_password": "Nova contrasenya",
"new_person": "Persona nova", "new_person": "Persona nova",
"new_pin_code": "Nou codi PIN",
"new_user_created": "Nou usuari creat", "new_user_created": "Nou usuari creat",
"new_version_available": "NOVA VERSIÓ DISPONIBLE", "new_version_available": "NOVA VERSIÓ DISPONIBLE",
"newest_first": "El més nou primer", "newest_first": "El més nou primer",
"next": "Següent", "next": "Següent",
"next_memory": "Següent record", "next_memory": "Següent record",
"no": "No",
"no_albums_message": "Creeu un àlbum per organitzar les vostres fotos i vídeos", "no_albums_message": "Creeu un àlbum per organitzar les vostres fotos i vídeos",
"no_albums_with_name_yet": "Sembla que encara no tens cap àlbum amb aquest nom.", "no_albums_with_name_yet": "Sembla que encara no tens cap àlbum amb aquest nom.",
"no_albums_yet": "Sembla que encara no tens cap àlbum.", "no_albums_yet": "Sembla que encara no tens cap àlbum.",
@@ -1253,8 +1252,6 @@
"no_favorites_message": "Afegiu preferits per trobar les millors fotos i vídeos a l'instant", "no_favorites_message": "Afegiu preferits per trobar les millors fotos i vídeos a l'instant",
"no_libraries_message": "Creeu una llibreria externa per veure les vostres fotos i vídeos", "no_libraries_message": "Creeu una llibreria externa per veure les vostres fotos i vídeos",
"no_name": "Sense nom", "no_name": "Sense nom",
"no_notifications": "No hi ha notificacions",
"no_people_found": "No s'han trobat coincidències de persones",
"no_places": "No hi ha llocs", "no_places": "No hi ha llocs",
"no_results": "Sense resultats", "no_results": "Sense resultats",
"no_results_description": "Proveu un sinònim o una paraula clau més general", "no_results_description": "Proveu un sinònim o una paraula clau més general",
@@ -1262,6 +1259,7 @@
"not_in_any_album": "En cap àlbum", "not_in_any_album": "En cap àlbum",
"not_selected": "No seleccionat", "not_selected": "No seleccionat",
"note_apply_storage_label_to_previously_uploaded assets": "Nota: per aplicar l'etiqueta d'emmagatzematge als actius penjats anteriorment, executeu el", "note_apply_storage_label_to_previously_uploaded assets": "Nota: per aplicar l'etiqueta d'emmagatzematge als actius penjats anteriorment, executeu el",
"notes": "Notes",
"notification_permission_dialog_content": "Per activar les notificacions, aneu a Configuració i seleccioneu permet.", "notification_permission_dialog_content": "Per activar les notificacions, aneu a Configuració i seleccioneu permet.",
"notification_permission_list_tile_content": "Atorga permís per a activar les notificacions.", "notification_permission_list_tile_content": "Atorga permís per a activar les notificacions.",
"notification_permission_list_tile_enable_button": "Activa les notificacions", "notification_permission_list_tile_enable_button": "Activa les notificacions",
@@ -1269,6 +1267,7 @@
"notification_toggle_setting_description": "Activa les notificacions per correu electrònic", "notification_toggle_setting_description": "Activa les notificacions per correu electrònic",
"notifications": "Notificacions", "notifications": "Notificacions",
"notifications_setting_description": "Gestiona les notificacions", "notifications_setting_description": "Gestiona les notificacions",
"oauth": "OAuth",
"official_immich_resources": "Recursos oficials d'Immich", "official_immich_resources": "Recursos oficials d'Immich",
"offline": "Fora de línia", "offline": "Fora de línia",
"offline_paths": "Rutes fora de línia", "offline_paths": "Rutes fora de línia",
@@ -1283,13 +1282,13 @@
"onboarding_welcome_user": "Benvingut, {user}", "onboarding_welcome_user": "Benvingut, {user}",
"online": "En línia", "online": "En línia",
"only_favorites": "Només preferits", "only_favorites": "Només preferits",
"open": "Obrir",
"open_in_map_view": "Obrir a la vista del mapa", "open_in_map_view": "Obrir a la vista del mapa",
"open_in_openstreetmap": "Obre a OpenStreetMap", "open_in_openstreetmap": "Obre a OpenStreetMap",
"open_the_search_filters": "Obriu els filtres de cerca", "open_the_search_filters": "Obriu els filtres de cerca",
"options": "Opcions", "options": "Opcions",
"or": "o", "or": "o",
"organize_your_library": "Organitzeu la llibreria", "organize_your_library": "Organitzeu la llibreria",
"original": "original",
"other": "Altres", "other": "Altres",
"other_devices": "Altres dispositius", "other_devices": "Altres dispositius",
"other_variables": "Altres variables", "other_variables": "Altres variables",
@@ -1306,7 +1305,7 @@
"partner_page_partner_add_failed": "No s'ha pogut afegir el company", "partner_page_partner_add_failed": "No s'ha pogut afegir el company",
"partner_page_select_partner": "Escull company", "partner_page_select_partner": "Escull company",
"partner_page_shared_to_title": "Compartit amb", "partner_page_shared_to_title": "Compartit amb",
"partner_page_stop_sharing_content": "{partner} ja no podrà accedir a les teves fotos.", "partner_page_stop_sharing_content": "{} ja no podrà accedir a les teves fotos.",
"partner_sharing": "Compartició amb companys", "partner_sharing": "Compartició amb companys",
"partners": "Companys", "partners": "Companys",
"password": "Contrasenya", "password": "Contrasenya",
@@ -1352,9 +1351,6 @@
"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 d'anys anteriors", "photos_from_previous_years": "Fotos d'anys anteriors",
"pick_a_location": "Triar una ubicació", "pick_a_location": "Triar una ubicació",
"pin_code_changed_successfully": "Codi PIN canviat correctament",
"pin_code_reset_successfully": "S'ha restablert correctament el codi PIN",
"pin_code_setup_successfully": "S'ha configurat correctament un codi PIN",
"place": "Lloc", "place": "Lloc",
"places": "Llocs", "places": "Llocs",
"places_count": "{count, plural, one {{count, number} Lloc} other {{count, number} Llocs}}", "places_count": "{count, plural, one {{count, number} Lloc} other {{count, number} Llocs}}",
@@ -1362,6 +1358,7 @@
"play_memories": "Reproduir records", "play_memories": "Reproduir records",
"play_motion_photo": "Reproduir Fotos en Moviment", "play_motion_photo": "Reproduir Fotos en Moviment",
"play_or_pause_video": "Reproduir o posar en pausa el vídeo", "play_or_pause_video": "Reproduir o posar en pausa el vídeo",
"port": "Port",
"preferences_settings_subtitle": "Gestiona les preferències de l'aplicació", "preferences_settings_subtitle": "Gestiona les preferències de l'aplicació",
"preferences_settings_title": "Preferències", "preferences_settings_title": "Preferències",
"preset": "Preestablert", "preset": "Preestablert",
@@ -1371,11 +1368,11 @@
"previous_or_next_photo": "Foto anterior o següent", "previous_or_next_photo": "Foto anterior o següent",
"primary": "Primària", "primary": "Primària",
"privacy": "Privacitat", "privacy": "Privacitat",
"profile": "Perfil",
"profile_drawer_app_logs": "Registres", "profile_drawer_app_logs": "Registres",
"profile_drawer_client_out_of_date_major": "L'aplicació mòbil està desactualitzada. Si us plau, actualitzeu a l'última versió major.", "profile_drawer_client_out_of_date_major": "L'aplicació mòbil està desactualitzada. Si us plau, actualitzeu a l'última versió major.",
"profile_drawer_client_out_of_date_minor": "L'aplicació mòbil està desactualitzada. Si us plau, actualitzeu a l'última versió menor.", "profile_drawer_client_out_of_date_minor": "L'aplicació mòbil està desactualitzada. Si us plau, actualitzeu a l'última versió menor.",
"profile_drawer_client_server_up_to_date": "El Client i el Servidor estan actualitzats", "profile_drawer_client_server_up_to_date": "El Client i el Servidor estan actualitzats",
"profile_drawer_github": "GitHub",
"profile_drawer_server_out_of_date_major": "L'aplicació mòbil està desactualitzada. Si us plau, actualitzeu a l'última versió major.", "profile_drawer_server_out_of_date_major": "L'aplicació mòbil està desactualitzada. Si us plau, actualitzeu a l'última versió major.",
"profile_drawer_server_out_of_date_minor": "L'aplicació mòbil està desactualitzada. Si us plau, actualitzeu a l'última versió menor.", "profile_drawer_server_out_of_date_minor": "L'aplicació mòbil està desactualitzada. Si us plau, actualitzeu a l'última versió menor.",
"profile_image_of_user": "Imatge de perfil de {user}", "profile_image_of_user": "Imatge de perfil de {user}",
@@ -1384,7 +1381,7 @@
"public_share": "Compartit públicament", "public_share": "Compartit públicament",
"purchase_account_info": "Contribuent", "purchase_account_info": "Contribuent",
"purchase_activated_subtitle": "Gràcies per donar suport a Immich i al programari de codi obert", "purchase_activated_subtitle": "Gràcies per donar suport a Immich i al programari de codi obert",
"purchase_activated_time": "Activat el {date}", "purchase_activated_time": "Activat el {date, date}",
"purchase_activated_title": "La teva clau s'ha activat correctament", "purchase_activated_title": "La teva clau s'ha activat correctament",
"purchase_button_activate": "Activar", "purchase_button_activate": "Activar",
"purchase_button_buy": "Comprar", "purchase_button_buy": "Comprar",
@@ -1396,6 +1393,7 @@
"purchase_failed_activation": "No s'ha pogut activar! Si us plau, comproveu el vostre correu electrònic per trobar la clau de producte correcta!", "purchase_failed_activation": "No s'ha pogut activar! Si us plau, comproveu el vostre correu electrònic per trobar la clau de producte correcta!",
"purchase_individual_description_1": "Per a un particular", "purchase_individual_description_1": "Per a un particular",
"purchase_individual_description_2": "Estat de la contribució", "purchase_individual_description_2": "Estat de la contribució",
"purchase_individual_title": "Individual",
"purchase_input_suggestion": "Tens una clau de producte? Introduïu la clau a continuació", "purchase_input_suggestion": "Tens una clau de producte? Introduïu la clau a continuació",
"purchase_license_subtitle": "Compra Immich per donar suport al desenvolupament continuat del servei", "purchase_license_subtitle": "Compra Immich per donar suport al desenvolupament continuat del servei",
"purchase_lifetime_description": "Compra de per vida", "purchase_lifetime_description": "Compra de per vida",
@@ -1423,12 +1421,11 @@
"reassigned_assets_to_existing_person": "{count, plural, one {S'ha reassignat # recurs} other {S'han reassignat # recursos}} a {name, select, null {una persona existent} other {{name}}}", "reassigned_assets_to_existing_person": "{count, plural, one {S'ha reassignat # recurs} other {S'han reassignat # recursos}} a {name, select, null {una persona existent} other {{name}}}",
"reassigned_assets_to_new_person": "{count, plural, one {S'ha reassignat # recurs} other {S'han reassignat # recursos}} a una persona nova", "reassigned_assets_to_new_person": "{count, plural, one {S'ha reassignat # recurs} other {S'han reassignat # recursos}} a una persona nova",
"reassing_hint": "Assignar els elements seleccionats a una persona existent", "reassing_hint": "Assignar els elements seleccionats a una persona existent",
"recent": "Recent",
"recent-albums": "Àlbums recents", "recent-albums": "Àlbums recents",
"recent_searches": "Cerques recents", "recent_searches": "Cerques recents",
"recently_added": "Afegit recentment", "recently_added": "Afegit recentment",
"recently_added_page_title": "Afegit recentment", "recently_added_page_title": "Afegit recentment",
"recently_taken": "Fet recentment",
"recently_taken_page_title": "Fet recentment",
"refresh": "Actualitzar", "refresh": "Actualitzar",
"refresh_encoded_videos": "Actualitza vídeos codificats", "refresh_encoded_videos": "Actualitza vídeos codificats",
"refresh_faces": "Actualitzar cares", "refresh_faces": "Actualitzar cares",
@@ -1471,7 +1468,6 @@
"reset": "Restablir", "reset": "Restablir",
"reset_password": "Restablir contrasenya", "reset_password": "Restablir contrasenya",
"reset_people_visibility": "Restablir la visibilitat de les persones", "reset_people_visibility": "Restablir la visibilitat de les persones",
"reset_pin_code": "Restablir el codi PIN",
"reset_to_default": "Restableix els valors predeterminats", "reset_to_default": "Restableix els valors predeterminats",
"resolve_duplicates": "Resoldre duplicats", "resolve_duplicates": "Resoldre duplicats",
"resolved_all_duplicates": "Tots els duplicats resolts", "resolved_all_duplicates": "Tots els duplicats resolts",
@@ -1483,6 +1479,7 @@
"retry_upload": "Torna a provar de pujar", "retry_upload": "Torna a provar de pujar",
"review_duplicates": "Revisar duplicats", "review_duplicates": "Revisar duplicats",
"role": "Rol", "role": "Rol",
"role_editor": "Editor",
"role_viewer": "Visor", "role_viewer": "Visor",
"save": "Desa", "save": "Desa",
"save_to_gallery": "Desa a galeria", "save_to_gallery": "Desa a galeria",
@@ -1526,6 +1523,7 @@
"search_no_people_named": "Cap persona anomenada \"{name}\"", "search_no_people_named": "Cap persona anomenada \"{name}\"",
"search_no_result": "No s'han trobat resultats, proveu un terme de cerca o una combinació diferents", "search_no_result": "No s'han trobat resultats, proveu un terme de cerca o una combinació diferents",
"search_options": "Opcions de cerca", "search_options": "Opcions de cerca",
"search_page_categories": "Categories",
"search_page_motion_photos": "Fotografies animades", "search_page_motion_photos": "Fotografies animades",
"search_page_no_objects": "No hi ha informació d'objectes disponibles", "search_page_no_objects": "No hi ha informació d'objectes disponibles",
"search_page_no_places": "No hi ha informació de llocs disponibles", "search_page_no_places": "No hi ha informació de llocs disponibles",
@@ -1562,7 +1560,6 @@
"select_keep_all": "Mantén tota la selecció", "select_keep_all": "Mantén tota la selecció",
"select_library_owner": "Selecciona el propietari de la bilbioteca", "select_library_owner": "Selecciona el propietari de la bilbioteca",
"select_new_face": "Selecciona nova cara", "select_new_face": "Selecciona nova cara",
"select_person_to_tag": "Selecciona una persona per etiquetar",
"select_photos": "Tria fotografies", "select_photos": "Tria fotografies",
"select_trash_all": "Envia la selecció a la paperera", "select_trash_all": "Envia la selecció a la paperera",
"select_user_for_sharing_page_err_album": "Error al crear l'àlbum", "select_user_for_sharing_page_err_album": "Error al crear l'àlbum",
@@ -1593,12 +1590,12 @@
"setting_languages_apply": "Aplicar", "setting_languages_apply": "Aplicar",
"setting_languages_subtitle": "Canvia el llenguatge de l'aplicació", "setting_languages_subtitle": "Canvia el llenguatge de l'aplicació",
"setting_languages_title": "Idiomes", "setting_languages_title": "Idiomes",
"setting_notifications_notify_failures_grace_period": "Notifica les fallades de la còpia de seguretat en segon pla: {duration}", "setting_notifications_notify_failures_grace_period": "Notifica les fallades de la còpia de seguretat en segon pla: {}",
"setting_notifications_notify_hours": "{count} hores", "setting_notifications_notify_hours": "{} hores",
"setting_notifications_notify_immediately": "immediatament", "setting_notifications_notify_immediately": "immediatament",
"setting_notifications_notify_minutes": "{count} minuts", "setting_notifications_notify_minutes": "{} minuts",
"setting_notifications_notify_never": "mai", "setting_notifications_notify_never": "mai",
"setting_notifications_notify_seconds": "{count} segons", "setting_notifications_notify_seconds": "{} segons",
"setting_notifications_single_progress_subtitle": "Informació detallada del progrés de la pujada de cada fitxer", "setting_notifications_single_progress_subtitle": "Informació detallada del progrés de la pujada de cada fitxer",
"setting_notifications_single_progress_title": "Mostra el progrés detallat de la còpia de seguretat en segon pla", "setting_notifications_single_progress_title": "Mostra el progrés detallat de la còpia de seguretat en segon pla",
"setting_notifications_subtitle": "Ajusta les preferències de notificació", "setting_notifications_subtitle": "Ajusta les preferències de notificació",
@@ -1610,10 +1607,9 @@
"settings": "Configuració", "settings": "Configuració",
"settings_require_restart": "Si us plau, reinicieu Immich per a aplicar aquest canvi", "settings_require_restart": "Si us plau, reinicieu Immich per a aplicar aquest canvi",
"settings_saved": "Configuració desada", "settings_saved": "Configuració desada",
"setup_pin_code": "Configurar un codi PIN",
"share": "Comparteix", "share": "Comparteix",
"share_add_photos": "Afegeix fotografies", "share_add_photos": "Afegeix fotografies",
"share_assets_selected": "{count} seleccionats", "share_assets_selected": "{} seleccionats",
"share_dialog_preparing": "S'està preparant...", "share_dialog_preparing": "S'està preparant...",
"shared": "Compartit", "shared": "Compartit",
"shared_album_activities_input_disable": "Els comentaris estan desactivats", "shared_album_activities_input_disable": "Els comentaris estan desactivats",
@@ -1627,33 +1623,34 @@
"shared_by_user": "Compartit per {user}", "shared_by_user": "Compartit per {user}",
"shared_by_you": "Compartit per tu", "shared_by_you": "Compartit per tu",
"shared_from_partner": "Fotos de {partner}", "shared_from_partner": "Fotos de {partner}",
"shared_intent_upload_button_progress_text": "{current} / {total} Pujat", "shared_intent_upload_button_progress_text": "{} / {} Pujat",
"shared_link_app_bar_title": "Enllaços compartits", "shared_link_app_bar_title": "Enllaços compartits",
"shared_link_clipboard_copied_massage": "S'ha copiat al porta-retalls", "shared_link_clipboard_copied_massage": "S'ha copiat al porta-retalls",
"shared_link_clipboard_text": "Enllaç: {link}\nContrasenya: {password}", "shared_link_clipboard_text": "Enllaç: {}\nContrasenya: {}",
"shared_link_create_error": "S'ha produït un error en crear l'enllaç compartit", "shared_link_create_error": "S'ha produït un error en crear l'enllaç compartit",
"shared_link_edit_description_hint": "Introduïu la descripció de compartició", "shared_link_edit_description_hint": "Introduïu la descripció de compartició",
"shared_link_edit_expire_after_option_day": "1 dia", "shared_link_edit_expire_after_option_day": "1 dia",
"shared_link_edit_expire_after_option_days": "{count} dies", "shared_link_edit_expire_after_option_days": "{} dies",
"shared_link_edit_expire_after_option_hour": "1 hora", "shared_link_edit_expire_after_option_hour": "1 hora",
"shared_link_edit_expire_after_option_hours": "{count} hores", "shared_link_edit_expire_after_option_hours": "{} hores",
"shared_link_edit_expire_after_option_minute": "1 minut", "shared_link_edit_expire_after_option_minute": "1 minut",
"shared_link_edit_expire_after_option_minutes": "{count} minuts", "shared_link_edit_expire_after_option_minutes": "{} minuts",
"shared_link_edit_expire_after_option_months": "{count} mesos", "shared_link_edit_expire_after_option_months": "{} mesos",
"shared_link_edit_expire_after_option_year": "any {count}", "shared_link_edit_expire_after_option_year": "any {}",
"shared_link_edit_password_hint": "Introduïu la contrasenya de compartició", "shared_link_edit_password_hint": "Introduïu la contrasenya de compartició",
"shared_link_edit_submit_button": "Actualitza l'enllaç", "shared_link_edit_submit_button": "Actualitza l'enllaç",
"shared_link_error_server_url_fetch": "No s'ha pogut obtenir l'URL del servidor", "shared_link_error_server_url_fetch": "No s'ha pogut obtenir l'URL del servidor",
"shared_link_expires_day": "Caduca d'aquí a {count} dia", "shared_link_expires_day": "Caduca d'aquí a {} dia",
"shared_link_expires_days": "Caduca d'aquí a {count} dies", "shared_link_expires_days": "Caduca d'aquí a {} dies",
"shared_link_expires_hour": "Caduca d'aquí a {count} hora", "shared_link_expires_hour": "Caduca d'aquí a {} hora",
"shared_link_expires_hours": "Caduca d'aquí a {count} hores", "shared_link_expires_hours": "Caduca d'aquí a {} hores",
"shared_link_expires_minute": "Caduca d'aquí a {count} minut", "shared_link_expires_minute": "Caduca d'aquí a {} minut",
"shared_link_expires_minutes": "Caduca d'aquí a {count} minuts", "shared_link_expires_minutes": "Caduca d'aquí a {} minuts",
"shared_link_expires_never": "Caduca ∞", "shared_link_expires_never": "Caduca ∞",
"shared_link_expires_second": "Caduca d'aquí a {count} segon", "shared_link_expires_second": "Caduca d'aquí a {} segon",
"shared_link_expires_seconds": "Caduca d'aquí a {count} segons", "shared_link_expires_seconds": "Caduca d'aquí a {} segons",
"shared_link_individual_shared": "Individual compartit", "shared_link_individual_shared": "Individual compartit",
"shared_link_info_chip_metadata": "EXIF",
"shared_link_manage_links": "Gestiona els enllaços compartits", "shared_link_manage_links": "Gestiona els enllaços compartits",
"shared_link_options": "Opcions d'enllaços compartits", "shared_link_options": "Opcions d'enllaços compartits",
"shared_links": "Enllaços compartits", "shared_links": "Enllaços compartits",
@@ -1726,7 +1723,6 @@
"stop_sharing_photos_with_user": "Deixa de compartir les fotos amb aquest usuari", "stop_sharing_photos_with_user": "Deixa de compartir les fotos amb aquest usuari",
"storage": "Emmagatzematge", "storage": "Emmagatzematge",
"storage_label": "Etiquetatge d'emmagatzematge", "storage_label": "Etiquetatge d'emmagatzematge",
"storage_quota": "Quota d'emmagatzematge",
"storage_usage": "{used} de {available} en ús", "storage_usage": "{used} de {available} en ús",
"submit": "Envia", "submit": "Envia",
"suggestions": "Suggeriments", "suggestions": "Suggeriments",
@@ -1753,7 +1749,7 @@
"theme_selection": "Selecció de 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", "theme_selection_description": "Activa automàticament el tema fosc o clar en funció de les preferències del sistema del navegador",
"theme_setting_asset_list_storage_indicator_title": "Mostra l'indicador d'emmagatzematge als títols dels elements", "theme_setting_asset_list_storage_indicator_title": "Mostra l'indicador d'emmagatzematge als títols dels elements",
"theme_setting_asset_list_tiles_per_row_title": "Nombre d'elements per fila ({count})", "theme_setting_asset_list_tiles_per_row_title": "Nombre d'elements per fila ({})",
"theme_setting_colorful_interface_subtitle": "Apliqueu color primari a les superfícies de fons.", "theme_setting_colorful_interface_subtitle": "Apliqueu color primari a les superfícies de fons.",
"theme_setting_colorful_interface_title": "Interfície colorida", "theme_setting_colorful_interface_title": "Interfície colorida",
"theme_setting_image_viewer_quality_subtitle": "Ajusta la qualitat del visor de detalls d'imatges", "theme_setting_image_viewer_quality_subtitle": "Ajusta la qualitat del visor de detalls d'imatges",
@@ -1778,6 +1774,7 @@
"to_trash": "Paperera", "to_trash": "Paperera",
"toggle_settings": "Canvia configuració", "toggle_settings": "Canvia configuració",
"toggle_theme": "Alternar tema", "toggle_theme": "Alternar tema",
"total": "Total",
"total_usage": "Ús total", "total_usage": "Ús total",
"trash": "Paperera", "trash": "Paperera",
"trash_all": "Envia-ho tot a la paperera", "trash_all": "Envia-ho tot a la paperera",
@@ -1787,15 +1784,13 @@
"trash_no_results_message": "Les imatges i vídeos que s'enviïn a la paperera es mostraran aquí.", "trash_no_results_message": "Les imatges i vídeos que s'enviïn a la paperera es mostraran aquí.",
"trash_page_delete_all": "Eliminar-ho tot", "trash_page_delete_all": "Eliminar-ho tot",
"trash_page_empty_trash_dialog_content": "Segur que voleu eliminar els elements? Aquests elements seran eliminats permanentment de Immich", "trash_page_empty_trash_dialog_content": "Segur que voleu eliminar els elements? Aquests elements seran eliminats permanentment de Immich",
"trash_page_info": "Els elements que s'enviïn a la paperera s'eliminaran permanentment després de {days} dies", "trash_page_info": "Els elements que s'enviïn a la paperera s'eliminaran permanentment després de {} dies",
"trash_page_no_assets": "No hi ha elements a la paperera", "trash_page_no_assets": "No hi ha elements a la paperera",
"trash_page_restore_all": "Restaura-ho tot", "trash_page_restore_all": "Restaura-ho tot",
"trash_page_select_assets_btn": "Selecciona elements", "trash_page_select_assets_btn": "Selecciona elements",
"trash_page_title": "Paperera ({count})", "trash_page_title": "Paperera ({})",
"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}}.", "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", "type": "Tipus",
"unable_to_change_pin_code": "No es pot canviar el codi PIN",
"unable_to_setup_pin_code": "No s'ha pogut configurar el codi PIN",
"unarchive": "Desarxivar", "unarchive": "Desarxivar",
"unarchived_count": "{count, plural, other {# elements desarxivats}}", "unarchived_count": "{count, plural, other {# elements desarxivats}}",
"unfavorite": "Reverteix preferit", "unfavorite": "Reverteix preferit",
@@ -1819,7 +1814,6 @@
"untracked_files": "Fitxers no monitoritzats", "untracked_files": "Fitxers no monitoritzats",
"untracked_files_decription": "Aquests fitxers no estan monitoritzats per l'aplicació. Poden ser el resultat de moviments errats, descàrregues interrompudes o deixats enrere per error", "untracked_files_decription": "Aquests fitxers no estan monitoritzats per l'aplicació. Poden ser el resultat de moviments errats, descàrregues interrompudes o deixats enrere per error",
"up_next": "Pròxim", "up_next": "Pròxim",
"updated_at": "Actualitzat",
"updated_password": "Contrasenya actualitzada", "updated_password": "Contrasenya actualitzada",
"upload": "Pujar", "upload": "Pujar",
"upload_concurrency": "Concurrència de pujades", "upload_concurrency": "Concurrència de pujades",
@@ -1829,19 +1823,18 @@
"upload_progress": "Restant {remaining, number} - Processat {processed, number}/{total, number}", "upload_progress": "Restant {remaining, number} - Processat {processed, number}/{total, number}",
"upload_skipped_duplicates": "{count, plural, one {S'ha omès # recurs duplicat} other {S'han omès # recursos duplicats}}", "upload_skipped_duplicates": "{count, plural, one {S'ha omès # recurs duplicat} other {S'han omès # recursos duplicats}}",
"upload_status_duplicates": "Duplicats", "upload_status_duplicates": "Duplicats",
"upload_status_errors": "Errors",
"upload_status_uploaded": "Carregat", "upload_status_uploaded": "Carregat",
"upload_success": "Pujada correcta, actualitza la pàgina per veure nous recursos de pujada.", "upload_success": "Pujada correcta, actualitza la pàgina per veure nous recursos de pujada.",
"upload_to_immich": "Puja a Immich ({count})", "upload_to_immich": "Puja a Immich ({})",
"uploading": "Pujant", "uploading": "Pujant",
"url": "URL",
"usage": "Ús", "usage": "Ús",
"use_current_connection": "utilitzar la connexió actual", "use_current_connection": "utilitzar la connexió actual",
"use_custom_date_range": "Fes servir un rang de dates personalitzat", "use_custom_date_range": "Fes servir un rang de dates personalitzat",
"user": "Usuari", "user": "Usuari",
"user_has_been_deleted": "Aquest usuari ha sigut eliminat.",
"user_id": "ID d'usuari", "user_id": "ID d'usuari",
"user_liked": "A {user} li ha agradat {type, select, photo {aquesta foto} video {aquest vídeo} asset {aquest recurs} other {}}", "user_liked": "A {user} li ha agradat {type, select, photo {aquesta foto} video {aquest vídeo} asset {aquest recurs} other {}}",
"user_pin_code_settings": "Codi PIN",
"user_pin_code_settings_description": "Gestiona el teu codi PIN",
"user_purchase_settings": "Compra", "user_purchase_settings": "Compra",
"user_purchase_settings_description": "Gestiona la teva compra", "user_purchase_settings_description": "Gestiona la teva compra",
"user_role_set": "Establir {user} com a {role}", "user_role_set": "Establir {user} com a {role}",
@@ -1853,6 +1846,7 @@
"utilities": "Utilitats", "utilities": "Utilitats",
"validate": "Valida", "validate": "Valida",
"validate_endpoint_error": "Per favor introdueix un URL vàlid", "validate_endpoint_error": "Per favor introdueix un URL vàlid",
"variables": "Variables",
"version": "Versió", "version": "Versió",
"version_announcement_closing": "El teu amic Alex", "version_announcement_closing": "El teu amic Alex",
"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_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.",
@@ -1889,11 +1883,11 @@
"week": "Setmana", "week": "Setmana",
"welcome": "Benvingut", "welcome": "Benvingut",
"welcome_to_immich": "Benvingut a immich", "welcome_to_immich": "Benvingut a immich",
"wifi_name": "Nom Wi-Fi", "wifi_name": "Nom WiFi",
"year": "Any", "year": "Any",
"years_ago": "Fa {years, plural, one {# any} other {# anys}}", "years_ago": "Fa {years, plural, one {# any} other {# anys}}",
"yes": "Sí", "yes": "Sí",
"you_dont_have_any_shared_links": "No tens cap enllaç compartit", "you_dont_have_any_shared_links": "No tens cap enllaç compartit",
"your_wifi_name": "Nom del teu Wi-Fi", "your_wifi_name": "El teu nom WiFi",
"zoom_image": "Ampliar Imatge" "zoom_image": "Ampliar Imatge"
} }
+114 -159
View File
@@ -26,7 +26,6 @@
"add_to_album": "Přidat do alba", "add_to_album": "Přidat do alba",
"add_to_album_bottom_sheet_added": "Přidáno do {album}", "add_to_album_bottom_sheet_added": "Přidáno do {album}",
"add_to_album_bottom_sheet_already_exists": "Je již v {album}", "add_to_album_bottom_sheet_already_exists": "Je již v {album}",
"add_to_locked_folder": "Přidat do uzamčené složky",
"add_to_shared_album": "Přidat do sdíleného alba", "add_to_shared_album": "Přidat do sdíleného alba",
"add_url": "Přidat URL", "add_url": "Přidat URL",
"added_to_archive": "Přidáno do archivu", "added_to_archive": "Přidáno do archivu",
@@ -40,11 +39,11 @@
"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_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>.", "authentication_settings_reenable": "Pro opětovné povolení použijte příkaz <link>Příkaz serveru</link>.",
"background_task_job": "Úkoly na pozadí", "background_task_job": "Úkoly na pozadí",
"backup_database": "Vytvořit výpis databáze", "backup_database": "Zálohování databáze",
"backup_database_enable_description": "Povolit výpisy z databáze", "backup_database_enable_description": "Povolit zálohování databáze",
"backup_keep_last_amount": "Počet předchozích výpisů, které se mají ponechat", "backup_keep_last_amount": "Počet předchozích záloh k uchování",
"backup_settings": "Nastavení výpisu databáze", "backup_settings": "Nastavení zálohování",
"backup_settings_description": "Správa nastavení výpisu databáze. Poznámka: Tyto úlohy nejsou monitorovány a nebudete upozorněni na jejich selhání.", "backup_settings_description": "Správa nastavení zálohování databáze",
"check_all": "Vše zkontrolovat", "check_all": "Vše zkontrolovat",
"cleanup": "Vyčištění", "cleanup": "Vyčištění",
"cleared_jobs": "Hotové úlohy pro: {job}", "cleared_jobs": "Hotové úlohy pro: {job}",
@@ -54,7 +53,6 @@
"confirm_email_below": "Pro potvrzení zadejte níže \"{email}\"", "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_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}?", "confirm_user_password_reset": "Opravdu chcete obnovit heslo uživatele {user}?",
"confirm_user_pin_code_reset": "Opravdu chcete resetovat PIN kód uživatele {user}?",
"create_job": "Vytvořit úlohu", "create_job": "Vytvořit úlohu",
"cron_expression": "Výraz cron", "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_description": "Nastavte interval prohledávání pomocí cron formátu. Další informace naleznete např. v <link>Crontab Guru</link>",
@@ -194,22 +192,26 @@
"oauth_auto_register": "Automatická registrace", "oauth_auto_register": "Automatická registrace",
"oauth_auto_register_description": "Automaticky registrovat nové uživatele po přihlášení pomocí OAuth", "oauth_auto_register_description": "Automaticky registrovat nové uživatele po přihlášení pomocí OAuth",
"oauth_button_text": "Text tlačítka", "oauth_button_text": "Text tlačítka",
"oauth_client_secret_description": "Vyžaduje se, pokud poskytovatel OAuth nepodporuje PKCE (Proof Key for Code Exchange)", "oauth_client_id": "Client ID",
"oauth_client_secret": "Client Secret",
"oauth_enable_description": "Přihlásit pomocí OAuth", "oauth_enable_description": "Přihlásit pomocí OAuth",
"oauth_issuer_url": "URL vydavatele",
"oauth_mobile_redirect_uri": "Mobilní přesměrování URI", "oauth_mobile_redirect_uri": "Mobilní přesměrování URI",
"oauth_mobile_redirect_uri_override": "Přepsat mobilní přesměrování URI", "oauth_mobile_redirect_uri_override": "Přepsat mobilní přesměrování URI",
"oauth_mobile_redirect_uri_override_description": "Povolit, pokud poskytovatel OAuth nepovoluje mobilní URI, například '{callback}'", "oauth_mobile_redirect_uri_override_description": "Povolit, pokud poskytovatel OAuth nepovoluje mobilní URI, například '{callback}'",
"oauth_profile_signing_algorithm": "Algoritmus podepisování profilu",
"oauth_profile_signing_algorithm_description": "Algoritmus použitý k podepsání profilu uživatele.",
"oauth_scope": "Rozsah",
"oauth_settings": "OAuth", "oauth_settings": "OAuth",
"oauth_settings_description": "Správa nastavení OAuth přihlášení", "oauth_settings_description": "Správa nastavení OAuth přihlášení",
"oauth_settings_more_details": "Další podrobnosti o této funkci naleznete v <link>dokumentaci</link>.", "oauth_settings_more_details": "Další podrobnosti o této funkci naleznete v <link>dokumentaci</link>.",
"oauth_signing_algorithm": "Algoritmus podepisování",
"oauth_storage_label_claim": "Deklarace štítku úložiště", "oauth_storage_label_claim": "Deklarace štítku úložiště",
"oauth_storage_label_claim_description": "Automaticky nastavit štítek úložiště uživatele na hodnotu této deklarace.", "oauth_storage_label_claim_description": "Automaticky nastavit štítek úložiště uživatele na hodnotu této deklarace.",
"oauth_storage_quota_claim": "Deklarace kvóty úložiště", "oauth_storage_quota_claim": "Deklarace kvóty úložiště",
"oauth_storage_quota_claim_description": "Automaticky nastavit kvótu úložiště uživatele na hodnotu této deklarace.", "oauth_storage_quota_claim_description": "Automaticky nastavit kvótu úložiště uživatele na hodnotu této deklarace.",
"oauth_storage_quota_default": "Výchozí kvóta úložiště (GiB)", "oauth_storage_quota_default": "Výchozí kvóta úložiště (GiB)",
"oauth_storage_quota_default_description": "Kvóta v GiB, která se použije, pokud není poskytnuta žádná deklarace (pro neomezenou kvótu zadejte 0).", "oauth_storage_quota_default_description": "Kvóta v GiB, která se použije, pokud není poskytnuta žádná deklarace (pro neomezenou kvótu zadejte 0).",
"oauth_timeout": "Časový limit požadavku",
"oauth_timeout_description": "Časový limit pro požadavky v milisekundách",
"offline_paths": "Cesty offline", "offline_paths": "Cesty offline",
"offline_paths_description": "Tyto výsledky mohou být způsobeny ručním odstraněním souborů, které nejsou součástí externí knihovny.", "offline_paths_description": "Tyto výsledky mohou být způsobeny ručním odstraněním souborů, které nejsou součástí externí knihovny.",
"password_enable_description": "Přihlášení pomocí e-mailu a hesla", "password_enable_description": "Přihlášení pomocí e-mailu a hesla",
@@ -350,7 +352,6 @@
"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í.", "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í.",
"user_delete_immediately": "Účet a položky uživatele <b>{user}</b> budou zařazeny do fronty k trvalému smazání <b>okamžitě</b>.", "user_delete_immediately": "Účet a položky uživatele <b>{user}</b> budou zařazeny do fronty k trvalému smazání <b>okamžitě</b>.",
"user_delete_immediately_checkbox": "Uživatele a položky zařadit do fronty k okamžitému smazání", "user_delete_immediately_checkbox": "Uživatele a položky zařadit do fronty k okamžitému smazání",
"user_details": "Podrobnosti o uživateli",
"user_management": "Správa uživatelů", "user_management": "Správa uživatelů",
"user_password_has_been_reset": "Heslo uživatele bylo obnoveno:", "user_password_has_been_reset": "Heslo uživatele bylo obnoveno:",
"user_password_reset_description": "Poskytněte uživateli dočasné heslo a informujte ho, že si ho bude muset při příštím přihlášení změnit.", "user_password_reset_description": "Poskytněte uživateli dočasné heslo a informujte ho, že si ho bude muset při příštím přihlášení změnit.",
@@ -370,22 +371,18 @@
"admin_password": "Heslo správce", "admin_password": "Heslo správce",
"administration": "Administrace", "administration": "Administrace",
"advanced": "Pokročilé", "advanced": "Pokročilé",
"advanced_settings_enable_alternate_media_filter_subtitle": "Tuto možnost použijte k filtrování médií během synchronizace na základě alternativních kritérií. Tuto možnost vyzkoušejte pouze v případě, že máte problémy s detekcí všech alb v aplikaci.", "advanced_settings_log_level_title": "Úroveň protokolování: {}",
"advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTÁLNÍ] Použít alternativní filtr pro synchronizaci alb zařízení",
"advanced_settings_log_level_title": "Úroveň protokolování: {level}",
"advanced_settings_prefer_remote_subtitle": "U některých zařízení je načítání miniatur z prostředků v zařízení velmi pomalé. Aktivujte toto nastavení, aby se místo toho načítaly vzdálené obrázky.", "advanced_settings_prefer_remote_subtitle": "U některých zařízení je načítání miniatur z prostředků v zařízení velmi pomalé. Aktivujte toto nastavení, aby se místo toho načítaly vzdálené obrázky.",
"advanced_settings_prefer_remote_title": "Preferovat vzdálené obrázky", "advanced_settings_prefer_remote_title": "Preferovat vzdálené obrázky",
"advanced_settings_proxy_headers_subtitle": "Definice hlaviček proxy serveru, které by měl Immich odesílat s každým síťovým požadavkem", "advanced_settings_proxy_headers_subtitle": "Definice hlaviček proxy serveru, které by měl Immich odesílat s každým síťovým požadavkem",
"advanced_settings_proxy_headers_title": "Proxy hlavičky", "advanced_settings_proxy_headers_title": "Proxy hlavičky",
"advanced_settings_self_signed_ssl_subtitle": "Vynechá ověření SSL certifikátu serveru. Vyžadováno pro self-signed certifikáty.", "advanced_settings_self_signed_ssl_subtitle": "Vynechá ověření SSL certifikátu serveru. Vyžadováno pro self-signed certifikáty.",
"advanced_settings_self_signed_ssl_title": "Povolit self-signed SSL certifikáty", "advanced_settings_self_signed_ssl_title": "Povolit self-signed SSL certifikáty",
"advanced_settings_sync_remote_deletions_subtitle": "Automaticky odstranit nebo obnovit položku v tomto zařízení, když je tato akce provedena na webu",
"advanced_settings_sync_remote_deletions_title": "Synchronizace vzdáleného mazání [EXPERIMENTÁLNÍ]",
"advanced_settings_tile_subtitle": "Pokročilé uživatelské nastavení", "advanced_settings_tile_subtitle": "Pokročilé uživatelské nastavení",
"advanced_settings_troubleshooting_subtitle": "Zobrazit dodatečné vlastnosti pro řešení problémů", "advanced_settings_troubleshooting_subtitle": "Zobrazit dodatečné vlastnosti pro řešení problémů",
"advanced_settings_troubleshooting_title": "Řešení problémů", "advanced_settings_troubleshooting_title": "Řešení problémů",
"age_months": "Věk {months, plural, one {# měsíc} few {# měsíce} other {# měsíců}}", "age_months": "{months, plural, one {# měsíc} few {# měsíce} other {# měsíců}}",
"age_year_months": "Věk 1 rok, {months, plural, one {# měsíc} other {# měsíce}}", "age_year_months": "1 rok a {months, plural, one {# měsíc} few {# měsíce} other {# měsíců}}",
"age_years": "{years, plural, one {# rok} few {# roky} other {# let}}", "age_years": "{years, plural, one {# rok} few {# roky} other {# let}}",
"album_added": "Přidáno album", "album_added": "Přidáno album",
"album_added_notification_setting_description": "Dostávat e-mailové oznámení, když jste přidáni do sdíleného alba", "album_added_notification_setting_description": "Dostávat e-mailové oznámení, když jste přidáni do sdíleného alba",
@@ -403,9 +400,9 @@
"album_remove_user_confirmation": "Opravdu chcete odebrat uživatele {user}?", "album_remove_user_confirmation": "Opravdu chcete odebrat uživatele {user}?",
"album_share_no_users": "Zřejmě jste toto album sdíleli se všemi uživateli, nebo nemáte žádného uživatele, se kterým byste ho mohli sdílet.", "album_share_no_users": "Zřejmě jste toto album sdíleli se všemi uživateli, nebo nemáte žádného uživatele, se kterým byste ho mohli sdílet.",
"album_thumbnail_card_item": "1 položka", "album_thumbnail_card_item": "1 položka",
"album_thumbnail_card_items": "{count} položek", "album_thumbnail_card_items": "{} položek",
"album_thumbnail_card_shared": " · Sdíleno", "album_thumbnail_card_shared": " · Sdíleno",
"album_thumbnail_shared_by": "Sdílel(a) {user}", "album_thumbnail_shared_by": "Sdílel(a) {}",
"album_updated": "Album aktualizováno", "album_updated": "Album aktualizováno",
"album_updated_setting_description": "Dostávat e-mailová oznámení o nových položkách sdíleného alba", "album_updated_setting_description": "Dostávat e-mailová oznámení o nových položkách sdíleného alba",
"album_user_left": "Opustil {album}", "album_user_left": "Opustil {album}",
@@ -413,7 +410,7 @@
"album_viewer_appbar_delete_confirm": "Opravdu chcete toto album odstranit ze svého účtu?", "album_viewer_appbar_delete_confirm": "Opravdu chcete toto album odstranit ze svého účtu?",
"album_viewer_appbar_share_err_delete": "Nepodařilo se smazat album", "album_viewer_appbar_share_err_delete": "Nepodařilo se smazat album",
"album_viewer_appbar_share_err_leave": "Nepodařilo se opustit album", "album_viewer_appbar_share_err_leave": "Nepodařilo se opustit album",
"album_viewer_appbar_share_err_remove": "Při odstraňování položek z alba se vyskytly problémy", "album_viewer_appbar_share_err_remove": "Při odstraňování položek z alba se vyskytly problémy.",
"album_viewer_appbar_share_err_title": "Nepodařilo se změnit název alba", "album_viewer_appbar_share_err_title": "Nepodařilo se změnit název alba",
"album_viewer_appbar_share_leave": "Opustit album", "album_viewer_appbar_share_leave": "Opustit album",
"album_viewer_appbar_share_to": "Sdílet na", "album_viewer_appbar_share_to": "Sdílet na",
@@ -443,7 +440,7 @@
"archive": "Archiv", "archive": "Archiv",
"archive_or_unarchive_photo": "Archivovat nebo odarchivovat fotku", "archive_or_unarchive_photo": "Archivovat nebo odarchivovat fotku",
"archive_page_no_archived_assets": "Nebyla nalezena žádná archivovaná média", "archive_page_no_archived_assets": "Nebyla nalezena žádná archivovaná média",
"archive_page_title": "Archiv ({count})", "archive_page_title": "Archiv ({})",
"archive_size": "Velikost archivu", "archive_size": "Velikost archivu",
"archive_size_description": "Nastavte velikost archivu pro stahování (v GiB)", "archive_size_description": "Nastavte velikost archivu pro stahování (v GiB)",
"archived": "Archiv", "archived": "Archiv",
@@ -480,18 +477,18 @@
"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_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_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_count": "{count, plural, one {# položka} few {# položky} other {# položek}}",
"assets_deleted_permanently": "{count} položek trvale odstraněno", "assets_deleted_permanently": "{} položek trvale odstraněno",
"assets_deleted_permanently_from_server": "{count} položek trvale odstraněno z Immich serveru", "assets_deleted_permanently_from_server": "{} položek trvale odstraněno z Immich serveru",
"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_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_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_removed_count": "{count, plural, one {Odstraněna # položka} few {Odstraněny # položky} other {Odstraněno # položek}}",
"assets_removed_permanently_from_device": "{count} položek trvale odstraněno z vašeho zařízení", "assets_removed_permanently_from_device": "{} položek trvale odstraněno z vašeho zařízení",
"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_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_restored_count": "{count, plural, one {Obnovena # položka} few {Obnoveny # položky} other {Obnoveno # položek}}",
"assets_restored_successfully": "{count} položek úspěšně obnoveno", "assets_restored_successfully": "{} položek úspěšně obnoveno",
"assets_trashed": "{count} položek vyhozeno do koše", "assets_trashed": "{} položek vyhozeno do koše",
"assets_trashed_count": "{count, plural, one {Vyhozena # položka} few {Vyhozeny # položky} other {Vyhozeno # položek}}", "assets_trashed_count": "{count, plural, one {Vyhozena # položka} few {Vyhozeny # položky} other {Vyhozeno # položek}}",
"assets_trashed_from_server": "{count} položek vyhozeno do koše na Immich serveru", "assets_trashed_from_server": "{} položek vyhozeno do koše na Immich serveru",
"assets_were_part_of_album_count": "{count, plural, one {Položka byla} other {Položky byly}} součástí alba", "assets_were_part_of_album_count": "{count, plural, one {Položka byla} other {Položky byly}} součástí alba",
"authorized_devices": "Autorizovaná zařízení", "authorized_devices": "Autorizovaná zařízení",
"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_subtitle": "Připojit se místně přes určenou Wi-Fi, pokud je k dispozici, a používat alternativní připojení jinde",
@@ -500,30 +497,31 @@
"back_close_deselect": "Zpět, zavřít nebo zrušit výběr", "back_close_deselect": "Zpět, zavřít nebo zrušit výběr",
"background_location_permission": "Povolení polohy na pozadí", "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ě", "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í ({count})", "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_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í.", "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í.",
"backup_album_selection_page_select_albums": "Vybraná alba", "backup_album_selection_page_select_albums": "Vybraná alba",
"backup_album_selection_page_selection_info": "Informace o výběru", "backup_album_selection_page_selection_info": "Informace o výběru",
"backup_album_selection_page_total_assets": "Celkový počet jedinečných položek", "backup_album_selection_page_total_assets": "Celkový počet jedinečných položek",
"backup_all": "Vše", "backup_all": "Vše",
"backup_background_service_backup_failed_message": "Zálohování médií selhalo. Zkouším to znovu", "backup_background_service_backup_failed_message": "Zálohování médií selhalo. Zkouším to znovu...",
"backup_background_service_connection_failed_message": "Nepodařilo se připojit k serveru. Zkouším to znovu", "backup_background_service_connection_failed_message": "Nepodařilo se připojit k serveru. Zkouším to znovu...",
"backup_background_service_current_upload_notification": "Nahrávání {filename}", "backup_background_service_current_upload_notification": "Nahrávání {}",
"backup_background_service_default_notification": "Kontrola nových médií…", "backup_background_service_default_notification": "Kontrola nových médií…",
"backup_background_service_error_title": "Chyba zálohování", "backup_background_service_error_title": "Chyba zálohování",
"backup_background_service_in_progress_notification": "Zálohování vašich médií", "backup_background_service_in_progress_notification": "Zálohování vašich médií...",
"backup_background_service_upload_failure_notification": "Nepodařilo se nahrát {filename}", "backup_background_service_upload_failure_notification": "Nepodařilo se nahrát {}",
"backup_controller_page_albums": "Zálohovaná alba", "backup_controller_page_albums": "Zálohovaná alba",
"backup_controller_page_background_app_refresh_disabled_content": "Povolte obnovení aplikace na pozadí v Nastavení > Obecné > Obnovení aplikace na pozadí, abyste mohli používat zálohování na pozadí.", "backup_controller_page_background_app_refresh_disabled_content": "Povolte obnovení aplikace na pozadí v Nastavení > Obecné > Obnovení aplikace na pozadí, abyste mohli používat zálohování na pozadí.",
"backup_controller_page_background_app_refresh_disabled_title": "Obnovování aplikací na pozadí je vypnuté", "backup_controller_page_background_app_refresh_disabled_title": " Obnovování aplikací na pozadí je vypnuté",
"backup_controller_page_background_app_refresh_enable_button_text": "Přejít do nastavení", "backup_controller_page_background_app_refresh_enable_button_text": "Přejít do nastavení",
"backup_controller_page_background_battery_info_link": "Ukaž mi jak", "backup_controller_page_background_battery_info_link": "Ukaž mi jak",
"backup_controller_page_background_battery_info_message": "Chcete-li dosáhnout nejlepších výsledků při zálohování na pozadí, vypněte všechny optimalizace baterie, které omezují aktivitu na pozadí pro Immich ve vašem zařízení. \n\nJelikož je to závislé na typu zařízení, vyhledejte požadované informace pro výrobce vašeho zařízení.", "backup_controller_page_background_battery_info_message": "Chcete-li dosáhnout nejlepších výsledků při zálohování na pozadí, vypněte všechny optimalizace baterie, které omezují aktivitu na pozadí pro Immich ve vašem zařízení. \n\nJelikož je to závislé na typu zařízení, vyhledejte požadované informace pro výrobce vašeho zařízení.",
"backup_controller_page_background_battery_info_ok": "OK",
"backup_controller_page_background_battery_info_title": "Optimalizace baterie", "backup_controller_page_background_battery_info_title": "Optimalizace baterie",
"backup_controller_page_background_charging": "Pouze během nabíjení", "backup_controller_page_background_charging": "Pouze během nabíjení",
"backup_controller_page_background_configure_error": "Nepodařilo se nakonfigurovat službu na pozadí", "backup_controller_page_background_configure_error": "Nepodařilo se nakonfigurovat službu na pozadí",
"backup_controller_page_background_delay": "Zpoždění zálohování nových médií: {duration}", "backup_controller_page_background_delay": "Zpoždění zálohování nových médií: {}",
"backup_controller_page_background_description": "Povolte službu na pozadí pro automatické zálohování všech nových položek bez nutnosti otevření aplikace", "backup_controller_page_background_description": "Povolte službu na pozadí pro automatické zálohování všech nových položek bez nutnosti otevření aplikace",
"backup_controller_page_background_is_off": "Automatické zálohování na pozadí je vypnuto", "backup_controller_page_background_is_off": "Automatické zálohování na pozadí je vypnuto",
"backup_controller_page_background_is_on": "Automatické zálohování na pozadí je zapnuto", "backup_controller_page_background_is_on": "Automatické zálohování na pozadí je zapnuto",
@@ -533,11 +531,12 @@
"backup_controller_page_backup": "Zálohování", "backup_controller_page_backup": "Zálohování",
"backup_controller_page_backup_selected": "Vybrané: ", "backup_controller_page_backup_selected": "Vybrané: ",
"backup_controller_page_backup_sub": "Zálohované fotografie a videa", "backup_controller_page_backup_sub": "Zálohované fotografie a videa",
"backup_controller_page_created": "Vytvořeno: {date}", "backup_controller_page_created": "Vytvořeno: {}",
"backup_controller_page_desc_backup": "Zapněte zálohování na popředí, aby se nové položky automaticky nahrávaly na server při otevření aplikace.", "backup_controller_page_desc_backup": "Zapněte zálohování na popředí, aby se nové položky automaticky nahrávaly na server při otevření aplikace.",
"backup_controller_page_excluded": "Vyloučeno: ", "backup_controller_page_excluded": "Vyloučeno: ",
"backup_controller_page_failed": "Nepodařilo se ({count})", "backup_controller_page_failed": "Nepodařilo se ({})",
"backup_controller_page_filename": "Název souboru: {filename} [{size}]", "backup_controller_page_filename": "Název souboru: {} [{}]",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Informace o zálohování", "backup_controller_page_info": "Informace o zálohování",
"backup_controller_page_none_selected": "Žádné vybrané", "backup_controller_page_none_selected": "Žádné vybrané",
"backup_controller_page_remainder": "Zbývá", "backup_controller_page_remainder": "Zbývá",
@@ -546,7 +545,7 @@
"backup_controller_page_start_backup": "Spustit zálohování", "backup_controller_page_start_backup": "Spustit zálohování",
"backup_controller_page_status_off": "Automatické zálohování na popředí je vypnuto", "backup_controller_page_status_off": "Automatické zálohování na popředí je vypnuto",
"backup_controller_page_status_on": "Automatické zálohování na popředí je zapnuto", "backup_controller_page_status_on": "Automatické zálohování na popředí je zapnuto",
"backup_controller_page_storage_format": "{used} z {total} použitých", "backup_controller_page_storage_format": "{} z {} použitých",
"backup_controller_page_to_backup": "Alba, která mají být zálohována", "backup_controller_page_to_backup": "Alba, která mají být zálohována",
"backup_controller_page_total_sub": "Všechny jedinečné fotografie a videa z vybraných alb", "backup_controller_page_total_sub": "Všechny jedinečné fotografie a videa z vybraných alb",
"backup_controller_page_turn_off": "Vypnout zálohování na popředí", "backup_controller_page_turn_off": "Vypnout zálohování na popředí",
@@ -561,10 +560,6 @@
"backup_options_page_title": "Nastavení záloh", "backup_options_page_title": "Nastavení záloh",
"backup_setting_subtitle": "Správa nastavení zálohování na pozadí a na popředí", "backup_setting_subtitle": "Správa nastavení zálohování na pozadí a na popředí",
"backward": "Pozpátku", "backward": "Pozpátku",
"biometric_auth_enabled": "Biometrické ověřování je povoleno",
"biometric_locked_out": "Jste vyloučeni z biometrického ověřování",
"biometric_no_options": "Biometrické možnosti nejsou k dispozici",
"biometric_not_available": "Biometrické ověřování není na tomto zařízení k dispozici",
"birthdate_saved": "Datum narození úspěšně uloženo", "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.", "birthdate_set_description": "Datum narození se používá k výpočtu věku osoby v době pořízení fotografie.",
"blurred_background": "Rozmazané pozadí", "blurred_background": "Rozmazané pozadí",
@@ -575,21 +570,21 @@
"bulk_keep_duplicates_confirmation": "Opravdu si chcete ponechat {count, plural, one {# duplicitní položku} few {# duplicitní položky} other {# duplicitních položek}}? Tím se vyřeší všechny duplicitní skupiny, aniž by se cokoli odstranilo.", "bulk_keep_duplicates_confirmation": "Opravdu si chcete ponechat {count, plural, one {# duplicitní položku} few {# duplicitní položky} other {# duplicitních položek}}? Tím se vyřeší všechny duplicitní skupiny, aniž by se cokoli odstranilo.",
"bulk_trash_duplicates_confirmation": "Opravdu chcete hromadně vyhodit {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í duplikáty se vyhodí.", "bulk_trash_duplicates_confirmation": "Opravdu chcete hromadně vyhodit {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í duplikáty se vyhodí.",
"buy": "Zakoupit Immich", "buy": "Zakoupit Immich",
"cache_settings_album_thumbnails": "Náhledy stránek knihovny ({count} položek)", "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": "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í.", "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í.",
"cache_settings_duplicated_assets_clear_button": "VYMAZAT", "cache_settings_duplicated_assets_clear_button": "VYMAZAT",
"cache_settings_duplicated_assets_subtitle": "Fotografie a videa, které aplikace zařadila na černou listinu", "cache_settings_duplicated_assets_subtitle": "Fotografie a videa, které aplikace zařadila na černou listinu",
"cache_settings_duplicated_assets_title": "Duplicitní položky ({count})", "cache_settings_duplicated_assets_title": "Duplicitní položky ({})",
"cache_settings_image_cache_size": "Velikost vyrovnávací paměti ({count} položek)", "cache_settings_image_cache_size": "Velikost vyrovnávací paměti (položek {})",
"cache_settings_statistics_album": "Knihovna náhledů", "cache_settings_statistics_album": "Knihovna náhledů",
"cache_settings_statistics_assets": "{count} položek ({size})", "cache_settings_statistics_assets": "{} položky ({})",
"cache_settings_statistics_full": "Kompletní fotografie", "cache_settings_statistics_full": "Kompletní fotografie",
"cache_settings_statistics_shared": "Sdílené náhledy alb", "cache_settings_statistics_shared": "Sdílené náhledy alb",
"cache_settings_statistics_thumbnail": "Náhledy", "cache_settings_statistics_thumbnail": "Náhledy",
"cache_settings_statistics_title": "Použití vyrovnávací paměti", "cache_settings_statistics_title": "Použití vyrovnávací paměti",
"cache_settings_subtitle": "Ovládání chování mobilní aplikace Immich v mezipaměti", "cache_settings_subtitle": "Ovládání chování mobilní aplikace Immich v mezipaměti",
"cache_settings_thumbnail_size": "Velikost vyrovnávací paměti náhledů ({count} položek)", "cache_settings_thumbnail_size": "Velikost vyrovnávací paměti náhledů (položek {})",
"cache_settings_tile_subtitle": "Ovládání chování místního úložiště", "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_tile_title": "Místní úložiště",
"cache_settings_title": "Nastavení vyrovnávací paměti", "cache_settings_title": "Nastavení vyrovnávací paměti",
@@ -602,9 +597,7 @@
"cannot_merge_people": "Nelze sloučit osoby", "cannot_merge_people": "Nelze sloučit osoby",
"cannot_undo_this_action": "Tuto akci nelze vrátit zpět!", "cannot_undo_this_action": "Tuto akci nelze vrátit zpět!",
"cannot_update_the_description": "Nelze aktualizovat popis", "cannot_update_the_description": "Nelze aktualizovat popis",
"cast": "Přenášet",
"change_date": "Změnit datum", "change_date": "Změnit datum",
"change_description": "Změnit popis",
"change_display_order": "Změnit pořadí zobrazení", "change_display_order": "Změnit pořadí zobrazení",
"change_expiration_time": "Změna konce platnosti", "change_expiration_time": "Změna konce platnosti",
"change_location": "Změna polohy", "change_location": "Změna polohy",
@@ -617,7 +610,6 @@
"change_password_form_new_password": "Nové heslo", "change_password_form_new_password": "Nové heslo",
"change_password_form_password_mismatch": "Hesla se neshodují", "change_password_form_password_mismatch": "Hesla se neshodují",
"change_password_form_reenter_new_password": "Znovu zadejte nové heslo", "change_password_form_reenter_new_password": "Znovu zadejte nové heslo",
"change_pin_code": "Změnit PIN kód",
"change_your_password": "Změna vašeho hesla", "change_your_password": "Změna vašeho hesla",
"changed_visibility_successfully": "Změna viditelnosti proběhla úspěšně", "changed_visibility_successfully": "Změna viditelnosti proběhla úspěšně",
"check_all": "Zkontrolovat vše", "check_all": "Zkontrolovat vše",
@@ -632,6 +624,7 @@
"clear_all_recent_searches": "Vymazat všechna nedávná vyhledávání", "clear_all_recent_searches": "Vymazat všechna nedávná vyhledávání",
"clear_message": "Vymazat zprávu", "clear_message": "Vymazat zprávu",
"clear_value": "Vymazat hodnotu", "clear_value": "Vymazat hodnotu",
"client_cert_dialog_msg_confirm": "OK",
"client_cert_enter_password": "Zadejte heslo", "client_cert_enter_password": "Zadejte heslo",
"client_cert_import": "Importovat", "client_cert_import": "Importovat",
"client_cert_import_success_msg": "Klientský certifikát je importován", "client_cert_import_success_msg": "Klientský certifikát je importován",
@@ -657,13 +650,11 @@
"confirm_delete_face": "Opravdu chcete z položky odstranit obličej osoby {name}?", "confirm_delete_face": "Opravdu chcete z položky odstranit obličej osoby {name}?",
"confirm_delete_shared_link": "Opravdu chcete odstranit tento sdílený odkaz?", "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_keep_this_delete_others": "Všechny ostatní položky v tomto uskupení mimo této budou odstraněny. Opravdu chcete pokračovat?",
"confirm_new_pin_code": "Potvrzení nového PIN kódu",
"confirm_password": "Potvrzení hesla", "confirm_password": "Potvrzení hesla",
"connected_to": "Připojeno k",
"contain": "Obsah", "contain": "Obsah",
"context": "Kontext", "context": "Kontext",
"continue": "Pokračovat", "continue": "Pokračovat",
"control_bottom_app_bar_album_info_shared": "{count} položek · Sdíleno", "control_bottom_app_bar_album_info_shared": "{} položky sdílené",
"control_bottom_app_bar_create_new_album": "Vytvořit nové album", "control_bottom_app_bar_create_new_album": "Vytvořit nové album",
"control_bottom_app_bar_delete_from_immich": "Smazat ze serveru Immich", "control_bottom_app_bar_delete_from_immich": "Smazat ze serveru Immich",
"control_bottom_app_bar_delete_from_local": "Smazat ze zařízení", "control_bottom_app_bar_delete_from_local": "Smazat ze zařízení",
@@ -701,11 +692,9 @@
"create_tag_description": "Vytvoření nové značky. U vnořených značek zadejte celou cestu ke značce včetně dopředných lomítek.", "create_tag_description": "Vytvoření nové značky. U vnořených značek zadejte celou cestu ke značce včetně dopředných lomítek.",
"create_user": "Vytvořit uživatele", "create_user": "Vytvořit uživatele",
"created": "Vytvořeno", "created": "Vytvořeno",
"created_at": "Vytvořeno",
"crop": "Oříznout", "crop": "Oříznout",
"curated_object_page_title": "Věci", "curated_object_page_title": "Věci",
"current_device": "Současné zařízení", "current_device": "Současné zařízení",
"current_pin_code": "Aktuální PIN kód",
"current_server_address": "Aktuální adresa serveru", "current_server_address": "Aktuální adresa serveru",
"custom_locale": "Vlastní lokalizace", "custom_locale": "Vlastní lokalizace",
"custom_locale_description": "Formátovat datumy a čísla podle jazyka a oblasti", "custom_locale_description": "Formátovat datumy a čísla podle jazyka a oblasti",
@@ -732,7 +721,7 @@
"delete_dialog_alert": "Tyto položky budou trvale smazány z aplikace Immich i z vašeho zařízení", "delete_dialog_alert": "Tyto položky budou trvale smazány z aplikace Immich i z vašeho zařízení",
"delete_dialog_alert_local": "Tyto položky budou z vašeho zařízení trvale smazány, ale budou stále k dispozici na Immich serveru", "delete_dialog_alert_local": "Tyto položky budou z vašeho zařízení trvale smazány, ale budou stále k dispozici na Immich serveru",
"delete_dialog_alert_local_non_backed_up": "Některé položky nejsou zálohovány na Immich server a budou ze zařízení trvale smazány", "delete_dialog_alert_local_non_backed_up": "Některé položky nejsou zálohovány na Immich server a budou ze zařízení trvale smazány",
"delete_dialog_alert_remote": "Tyto položky budou trvale smazány z Immich serveru", "delete_dialog_alert_remote": "Tyto položky budou trvale smazány z Immich serveru ",
"delete_dialog_ok_force": "Přesto smazat", "delete_dialog_ok_force": "Přesto smazat",
"delete_dialog_title": "Smazat trvale", "delete_dialog_title": "Smazat trvale",
"delete_duplicates_confirmation": "Opravdu chcete tyto duplicity trvale odstranit?", "delete_duplicates_confirmation": "Opravdu chcete tyto duplicity trvale odstranit?",
@@ -757,6 +746,7 @@
"direction": "Směr", "direction": "Směr",
"disabled": "Zakázáno", "disabled": "Zakázáno",
"disallow_edits": "Zakázat úpravy", "disallow_edits": "Zakázat úpravy",
"discord": "Discord",
"discover": "Objevit", "discover": "Objevit",
"dismiss_all_errors": "Zrušit všechny chyby", "dismiss_all_errors": "Zrušit všechny chyby",
"dismiss_error": "Zrušit chybu", "dismiss_error": "Zrušit chybu",
@@ -773,7 +763,7 @@
"download_enqueue": "Stahování ve frontě", "download_enqueue": "Stahování ve frontě",
"download_error": "Chyba při stahování", "download_error": "Chyba při stahování",
"download_failed": "Stahování selhalo", "download_failed": "Stahování selhalo",
"download_filename": "soubor: {filename}", "download_filename": "soubor: {}",
"download_finished": "Stahování dokončeno", "download_finished": "Stahování dokončeno",
"download_include_embedded_motion_videos": "Vložená videa", "download_include_embedded_motion_videos": "Vložená videa",
"download_include_embedded_motion_videos_description": "Zahrnout videa vložená do pohyblivých fotografií jako samostatný soubor", "download_include_embedded_motion_videos_description": "Zahrnout videa vložená do pohyblivých fotografií jako samostatný soubor",
@@ -797,8 +787,6 @@
"edit_avatar": "Upravit avatar", "edit_avatar": "Upravit avatar",
"edit_date": "Upravit datum", "edit_date": "Upravit datum",
"edit_date_and_time": "Upravit datum a čas", "edit_date_and_time": "Upravit datum a čas",
"edit_description": "Upravit popis",
"edit_description_prompt": "Vyberte nový popis:",
"edit_exclusion_pattern": "Upravit vzor vyloučení", "edit_exclusion_pattern": "Upravit vzor vyloučení",
"edit_faces": "Upravit obličeje", "edit_faces": "Upravit obličeje",
"edit_import_path": "Upravit cestu importu", "edit_import_path": "Upravit cestu importu",
@@ -813,28 +801,25 @@
"edit_title": "Upravit název", "edit_title": "Upravit název",
"edit_user": "Upravit uživatele", "edit_user": "Upravit uživatele",
"edited": "Upraveno", "edited": "Upraveno",
"editor": "Editor",
"editor_close_without_save_prompt": "Změny nebudou uloženy", "editor_close_without_save_prompt": "Změny nebudou uloženy",
"editor_close_without_save_title": "Zavřít editor?", "editor_close_without_save_title": "Zavřít editor?",
"editor_crop_tool_h2_aspect_ratios": "Poměr stran", "editor_crop_tool_h2_aspect_ratios": "Poměr stran",
"editor_crop_tool_h2_rotation": "Otočení", "editor_crop_tool_h2_rotation": "Otočení",
"email": "E-mail", "email": "E-mail",
"email_notifications": "E-mailová oznámení",
"empty_folder": "Tato složka je prázdná", "empty_folder": "Tato složka je prázdná",
"empty_trash": "Vyprázdnit koš", "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!", "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", "enable": "Povolit",
"enable_biometric_auth_description": "Zadejte váš PIN kód pro povolení biometrického ověřování",
"enabled": "Povoleno", "enabled": "Povoleno",
"end_date": "Konečné datum", "end_date": "Konečné datum",
"enqueued": "Ve frontě", "enqueued": "Ve frontě",
"enter_wifi_name": "Zadejte název Wi-Fi", "enter_wifi_name": "Zadejte název WiFi",
"enter_your_pin_code": "Zadejte PIN kód",
"enter_your_pin_code_subtitle": "Zadejte PIN kód pro přístup k uzamčené složce",
"error": "Chyba", "error": "Chyba",
"error_change_sort_album": "Nepodařilo se změnit pořadí alba", "error_change_sort_album": "Nepodařilo se změnit pořadí alba",
"error_delete_face": "Chyba při odstraňování obličeje z položky", "error_delete_face": "Chyba při odstraňování obličeje z položky",
"error_loading_image": "Chyba při načítání obrázku", "error_loading_image": "Chyba při načítání obrázku",
"error_saving_image": "Chyba: {error}", "error_saving_image": "Chyba: {}",
"error_title": "Chyba - Něco se pokazilo", "error_title": "Chyba - Něco se pokazilo",
"errors": { "errors": {
"cannot_navigate_next_asset": "Nelze přejít na další položku", "cannot_navigate_next_asset": "Nelze přejít na další položku",
@@ -864,12 +849,10 @@
"failed_to_keep_this_delete_others": "Nepodařilo se zachovat tuto položku a odstranit ostatní položky", "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_asset": "Nepodařilo se načíst položku",
"failed_to_load_assets": "Nepodařilo se načíst položky", "failed_to_load_assets": "Nepodařilo se načíst položky",
"failed_to_load_notifications": "Nepodařilo se načíst oznámení",
"failed_to_load_people": "Chyba načítání osob", "failed_to_load_people": "Chyba načítání osob",
"failed_to_remove_product_key": "Nepodařilo se odebrat klíč produktu", "failed_to_remove_product_key": "Nepodařilo se odebrat klíč produktu",
"failed_to_stack_assets": "Nepodařilo se seskupit položky", "failed_to_stack_assets": "Nepodařilo se seskupit položky",
"failed_to_unstack_assets": "Nepodařilo se rozložit položky", "failed_to_unstack_assets": "Nepodařilo se rozložit položky",
"failed_to_update_notification_status": "Nepodařilo se aktualizovat stav oznámení",
"import_path_already_exists": "Tato cesta importu již existuje.", "import_path_already_exists": "Tato cesta importu již existuje.",
"incorrect_email_or_password": "Nesprávný e-mail nebo heslo", "incorrect_email_or_password": "Nesprávný e-mail nebo heslo",
"paths_validation_failed": "{paths, plural, one {# cesta neprošla} few {# cesty neprošly} other {# cest neprošlo}} kontrolou", "paths_validation_failed": "{paths, plural, one {# cesta neprošla} few {# cesty neprošly} other {# cest neprošlo}} kontrolou",
@@ -887,7 +870,6 @@
"unable_to_archive_unarchive": "Nelze {archived, select, true {archivovat} other {odarchivovat}}", "unable_to_archive_unarchive": "Nelze {archived, select, true {archivovat} other {odarchivovat}}",
"unable_to_change_album_user_role": "Nelze změnit roli uživatele alba", "unable_to_change_album_user_role": "Nelze změnit roli uživatele alba",
"unable_to_change_date": "Nelze změnit datum", "unable_to_change_date": "Nelze změnit datum",
"unable_to_change_description": "Nelze změnit popis",
"unable_to_change_favorite": "Nelze změnit oblíbení položky", "unable_to_change_favorite": "Nelze změnit oblíbení položky",
"unable_to_change_location": "Nelze změnit polohu", "unable_to_change_location": "Nelze změnit polohu",
"unable_to_change_password": "Nelze změnit heslo", "unable_to_change_password": "Nelze změnit heslo",
@@ -925,7 +907,6 @@
"unable_to_log_out_all_devices": "Nelze odhlásit všechna zařízení", "unable_to_log_out_all_devices": "Nelze odhlásit všechna zařízení",
"unable_to_log_out_device": "Nelze odhlásit zařízení", "unable_to_log_out_device": "Nelze odhlásit zařízení",
"unable_to_login_with_oauth": "Nelze se přihlásit pomocí OAuth", "unable_to_login_with_oauth": "Nelze se přihlásit pomocí OAuth",
"unable_to_move_to_locked_folder": "Nelze přesunout do uzamčené složky",
"unable_to_play_video": "Nelze přehrát video", "unable_to_play_video": "Nelze přehrát video",
"unable_to_reassign_assets_existing_person": "Nelze přeřadit položky na {name, select, null {existující osobu} other {{name}}}", "unable_to_reassign_assets_existing_person": "Nelze přeřadit položky na {name, select, null {existující osobu} other {{name}}}",
"unable_to_reassign_assets_new_person": "Nelze přeřadit položku na novou osobu", "unable_to_reassign_assets_new_person": "Nelze přeřadit položku na novou osobu",
@@ -939,7 +920,6 @@
"unable_to_remove_reaction": "Nelze odstranit reakci", "unable_to_remove_reaction": "Nelze odstranit reakci",
"unable_to_repair_items": "Nelze opravit položky", "unable_to_repair_items": "Nelze opravit položky",
"unable_to_reset_password": "Nelze obnovit heslo", "unable_to_reset_password": "Nelze obnovit heslo",
"unable_to_reset_pin_code": "Nelze resetovat PIN kód",
"unable_to_resolve_duplicate": "Nelze vyřešit duplicitu", "unable_to_resolve_duplicate": "Nelze vyřešit duplicitu",
"unable_to_restore_assets": "Nelze obnovit položky", "unable_to_restore_assets": "Nelze obnovit položky",
"unable_to_restore_trash": "Nelze obnovit koš", "unable_to_restore_trash": "Nelze obnovit koš",
@@ -967,15 +947,16 @@
"unable_to_update_user": "Nelze aktualizovat uživatele", "unable_to_update_user": "Nelze aktualizovat uživatele",
"unable_to_upload_file": "Nepodařilo se nahrát soubor" "unable_to_upload_file": "Nepodařilo se nahrát soubor"
}, },
"exif": "Exif",
"exif_bottom_sheet_description": "Přidat popis...", "exif_bottom_sheet_description": "Přidat popis...",
"exif_bottom_sheet_details": "PODROBNOSTI", "exif_bottom_sheet_details": "PODROBNOSTI",
"exif_bottom_sheet_location": "POLOHA", "exif_bottom_sheet_location": "POLOHA",
"exif_bottom_sheet_people": "LIDÉ", "exif_bottom_sheet_people": "LIDÉ",
"exif_bottom_sheet_person_add_person": "Přidat jméno", "exif_bottom_sheet_person_add_person": "Přidat jméno",
"exif_bottom_sheet_person_age": "Věk {age}", "exif_bottom_sheet_person_age": "{} let",
"exif_bottom_sheet_person_age_months": "{months} měsíců", "exif_bottom_sheet_person_age_months": "{} měsíců",
"exif_bottom_sheet_person_age_year_months": "1 rok a {months} měsíců", "exif_bottom_sheet_person_age_year_months": "1 rok a {} měsíců",
"exif_bottom_sheet_person_age_years": "{years} let", "exif_bottom_sheet_person_age_years": "{} let",
"exit_slideshow": "Ukončit prezentaci", "exit_slideshow": "Ukončit prezentaci",
"expand_all": "Rozbalit vše", "expand_all": "Rozbalit vše",
"experimental_settings_new_asset_list_subtitle": "Zpracovávám", "experimental_settings_new_asset_list_subtitle": "Zpracovávám",
@@ -987,15 +968,15 @@
"expires_date": "Platnost končí {date}", "expires_date": "Platnost končí {date}",
"explore": "Prozkoumat", "explore": "Prozkoumat",
"explorer": "Průzkumník", "explorer": "Průzkumník",
"export": "Export",
"export_as_json": "Exportovat jako JSON", "export_as_json": "Exportovat jako JSON",
"extension": "Přípona", "extension": "Přípona",
"external": "Externí", "external": "Externí",
"external_libraries": "Externí knihovny", "external_libraries": "Externí knihovny",
"external_network": "Externí síť", "external_network": "Externí síť",
"external_network_sheet_info": "Pokud nejste v preferované síti Wi-Fi, 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ů", "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ů",
"face_unassigned": "Nepřiřazena", "face_unassigned": "Nepřiřazena",
"failed": "Selhalo", "failed": "Selhalo",
"failed_to_authenticate": "Ověření se nezdařilo",
"failed_to_load_assets": "Nepodařilo se načíst položky", "failed_to_load_assets": "Nepodařilo se načíst položky",
"failed_to_load_folder": "Nepodařilo se načíst složku", "failed_to_load_folder": "Nepodařilo se načíst složku",
"favorite": "Oblíbit", "favorite": "Oblíbit",
@@ -1011,7 +992,6 @@
"filetype": "Typ souboru", "filetype": "Typ souboru",
"filter": "Filtr", "filter": "Filtr",
"filter_people": "Filtrovat lidi", "filter_people": "Filtrovat lidi",
"filter_places": "Filtrovat místa",
"find_them_fast": "Najděte je rychle vyhledáním jejich jména", "find_them_fast": "Najděte je rychle vyhledáním jejich jména",
"fix_incorrect_match": "Opravit nesprávnou shodu", "fix_incorrect_match": "Opravit nesprávnou shodu",
"folder": "Složka", "folder": "Složka",
@@ -1060,9 +1040,7 @@
"home_page_delete_remote_err_local": "Místní položky ve vzdáleném výběru pro smazání, přeskakuji", "home_page_delete_remote_err_local": "Místní položky ve vzdáleném výběru pro smazání, přeskakuji",
"home_page_favorite_err_local": "Zatím není možné zařadit lokální média mezi oblíbená, přeskakuji", "home_page_favorite_err_local": "Zatím není možné zařadit lokální média mezi oblíbená, přeskakuji",
"home_page_favorite_err_partner": "Položky partnera nelze označit jako oblíbené, přeskakuji", "home_page_favorite_err_partner": "Položky partnera nelze označit jako oblíbené, přeskakuji",
"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_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_locked_error_local": "Místní položky nelze přesunout do uzamčené složky, přeskočí se",
"home_page_locked_error_partner": "Položky partnera nelze přesunout do uzamčené složky, přeskočí se",
"home_page_share_err_local": "Nelze sdílet místní položky prostřednictvím odkazu, přeskakuji", "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", "home_page_upload_err_limit": "Lze nahrát nejvýše 30 položek najednou, přeskakuji",
"host": "Hostitel", "host": "Hostitel",
@@ -1084,6 +1062,7 @@
"image_viewer_page_state_provider_download_started": "Stahování zahájeno", "image_viewer_page_state_provider_download_started": "Stahování zahájeno",
"image_viewer_page_state_provider_download_success": "Stahování bylo úspěšné", "image_viewer_page_state_provider_download_success": "Stahování bylo úspěšné",
"image_viewer_page_state_provider_share_error": "Chyba sdílení", "image_viewer_page_state_provider_share_error": "Chyba sdílení",
"immich_logo": "Immich Logo",
"immich_web_interface": "Webové rozhraní Immich", "immich_web_interface": "Webové rozhraní Immich",
"import_from_json": "Import z JSONu", "import_from_json": "Import z JSONu",
"import_path": "Cesta importu", "import_path": "Cesta importu",
@@ -1141,14 +1120,12 @@
"local_network": "Místní síť", "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", "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": "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í sítě Wi-Fi", "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_choose_on_map": "Vyberte na mapě",
"location_picker_latitude_error": "Zadejte platnou zeměpisnou šířku", "location_picker_latitude_error": "Zadejte platnou zeměpisnou šířku",
"location_picker_latitude_hint": "Zadejte vlastní zeměpisnou šířku", "location_picker_latitude_hint": "Zadejte vlastní zeměpisnou šířku",
"location_picker_longitude_error": "Zadejte platnou zeměpisnou délku", "location_picker_longitude_error": "Zadejte platnou zeměpisnou délku",
"location_picker_longitude_hint": "Zadejte vlastní zeměpisnou délku", "location_picker_longitude_hint": "Zadejte vlastní zeměpisnou délku",
"lock": "Zamknout",
"locked_folder": "Uzamčená složka",
"log_out": "Odhlásit", "log_out": "Odhlásit",
"log_out_all_devices": "Odhlásit všechna zařízení", "log_out_all_devices": "Odhlásit všechna zařízení",
"logged_out_all_devices": "Všechna zařízení odhlášena", "logged_out_all_devices": "Všechna zařízení odhlášena",
@@ -1167,7 +1144,7 @@
"login_form_err_trailing_whitespace": "Koncová mezera", "login_form_err_trailing_whitespace": "Koncová mezera",
"login_form_failed_get_oauth_server_config": "Chyba přihlášení pomocí OAuth, zkontrolujte adresu URL serveru", "login_form_failed_get_oauth_server_config": "Chyba přihlášení pomocí OAuth, zkontrolujte adresu URL serveru",
"login_form_failed_get_oauth_server_disable": "Funkce OAuth není na tomto serveru dostupná", "login_form_failed_get_oauth_server_disable": "Funkce OAuth není na tomto serveru dostupná",
"login_form_failed_login": "Chyba přihlášení, zkontrolujte URL adresu serveru, e-mail a heslo", "login_form_failed_login": "Chyba přihlášení, zkontrolujte URL adresu serveru, e-mail a heslo.",
"login_form_handshake_exception": "Došlo k výjimce Handshake se serverem. Pokud používáte self-signed certifikát, povolte v nastavení podporu self-signed certifikátu.", "login_form_handshake_exception": "Došlo k výjimce Handshake se serverem. Pokud používáte self-signed certifikát, povolte v nastavení podporu self-signed certifikátu.",
"login_form_password_hint": "heslo", "login_form_password_hint": "heslo",
"login_form_save_login": "Zůstat přihlášen", "login_form_save_login": "Zůstat přihlášen",
@@ -1193,8 +1170,8 @@
"manage_your_devices": "Správa přihlášených zařízení", "manage_your_devices": "Správa přihlášených zařízení",
"manage_your_oauth_connection": "Správa OAuth propojení", "manage_your_oauth_connection": "Správa OAuth propojení",
"map": "Mapa", "map": "Mapa",
"map_assets_in_bound": "{count} fotka", "map_assets_in_bound": "{} fotka",
"map_assets_in_bounds": "{count} fotek", "map_assets_in_bounds": "{} fotek",
"map_cannot_get_user_location": "Nelze zjistit polohu uživatele", "map_cannot_get_user_location": "Nelze zjistit polohu uživatele",
"map_location_dialog_yes": "Ano", "map_location_dialog_yes": "Ano",
"map_location_picker_page_use_location": "Použít tuto polohu", "map_location_picker_page_use_location": "Použít tuto polohu",
@@ -1208,18 +1185,15 @@
"map_settings": "Nastavení mapy", "map_settings": "Nastavení mapy",
"map_settings_dark_mode": "Tmavý režim", "map_settings_dark_mode": "Tmavý režim",
"map_settings_date_range_option_day": "Posledních 24 hodin", "map_settings_date_range_option_day": "Posledních 24 hodin",
"map_settings_date_range_option_days": "Posledních {days} dní", "map_settings_date_range_option_days": "Posledních {} dní",
"map_settings_date_range_option_year": "Poslední rok", "map_settings_date_range_option_year": "Poslední rok",
"map_settings_date_range_option_years": "Poslední {years} roky", "map_settings_date_range_option_years": "Poslední {} roky",
"map_settings_dialog_title": "Nastavení map", "map_settings_dialog_title": "Nastavení map",
"map_settings_include_show_archived": "Zahrnout archivované", "map_settings_include_show_archived": "Zahrnout archivované",
"map_settings_include_show_partners": "Včetně partnerů", "map_settings_include_show_partners": "Včetně partnerů",
"map_settings_only_show_favorites": "Zobrazit pouze oblíbené", "map_settings_only_show_favorites": "Zobrazit pouze oblíbené",
"map_settings_theme_settings": "Motiv mapy", "map_settings_theme_settings": "Motiv mapy",
"map_zoom_to_see_photos": "Oddálit pro zobrazení fotografií", "map_zoom_to_see_photos": "Oddálit pro zobrazení fotografií",
"mark_all_as_read": "Označit vše jako přečtené",
"mark_as_read": "Označit jako přečtené",
"marked_all_as_read": "Vše označeno jako přečtené",
"matches": "Shody", "matches": "Shody",
"media_type": "Typ média", "media_type": "Typ média",
"memories": "Vzpomínky", "memories": "Vzpomínky",
@@ -1228,6 +1202,8 @@
"memories_setting_description": "Správa toho, co vidíte ve svých vzpomínkách", "memories_setting_description": "Správa toho, co vidíte ve svých vzpomínkách",
"memories_start_over": "Začít znovu", "memories_start_over": "Začít znovu",
"memories_swipe_to_close": "Přejetím nahoru zavřete", "memories_swipe_to_close": "Přejetím nahoru zavřete",
"memories_year_ago": "Před rokem",
"memories_years_ago": "Před {} lety",
"memory": "Vzpomínka", "memory": "Vzpomínka",
"memory_lane_title": "Řada vzpomínek {title}", "memory_lane_title": "Řada vzpomínek {title}",
"menu": "Nabídka", "menu": "Nabídka",
@@ -1240,15 +1216,10 @@
"minimize": "Minimalizovat", "minimize": "Minimalizovat",
"minute": "Minuta", "minute": "Minuta",
"missing": "Chybějící", "missing": "Chybějící",
"model": "Model",
"month": "Měsíc", "month": "Měsíc",
"monthly_title_text_date_format": "LLLL y", "monthly_title_text_date_format": "LLLL y",
"more": "Více", "more": "Více",
"move": "Přesunout",
"move_off_locked_folder": "Přesunout z uzamčené složky",
"move_to_locked_folder": "Přesunout do uzamčené složky",
"move_to_locked_folder_confirmation": "Tyto fotky a videa budou odstraněny ze všech alb a bude je možné zobrazit pouze v uzamčené složce",
"moved_to_archive": "{count, plural, one {Přesunuta # položka} few {Přesunuty # položky} other {Přesunuto # položek}} do archivu",
"moved_to_library": "{count, plural, one {Přesunuta # položka} few {Přesunuty # položky} other {Přesunuto # položek}} do knihovny",
"moved_to_trash": "Přesunuto do koše", "moved_to_trash": "Přesunuto do koše",
"multiselect_grid_edit_date_time_err_read_only": "Nelze upravit datum položek pouze pro čtení, přeskakuji", "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", "multiselect_grid_edit_gps_err_read_only": "Nelze upravit polohu položek pouze pro čtení, přeskakuji",
@@ -1263,8 +1234,6 @@
"new_api_key": "Nový API klíč", "new_api_key": "Nový API klíč",
"new_password": "Nové heslo", "new_password": "Nové heslo",
"new_person": "Nová osoba", "new_person": "Nová osoba",
"new_pin_code": "Nový PIN kód",
"new_pin_code_subtitle": "Poprvé přistupujete k uzamčené složce. Vytvořte si kód PIN pro bezpečný přístup na tuto stránku",
"new_user_created": "Vytvořen nový uživatel", "new_user_created": "Vytvořen nový uživatel",
"new_version_available": "NOVÁ VERZE K DISPOZICI", "new_version_available": "NOVÁ VERZE K DISPOZICI",
"newest_first": "Nejnovější první", "newest_first": "Nejnovější první",
@@ -1282,10 +1251,7 @@
"no_explore_results_message": "Nahrajte další fotografie a prozkoumejte svou sbírku.", "no_explore_results_message": "Nahrajte další fotografie a prozkoumejte svou sbírku.",
"no_favorites_message": "Přidejte si oblíbené položky a rychle najděte své nejlepší obrázky a videa", "no_favorites_message": "Přidejte si oblíbené položky a rychle najděte své nejlepší obrázky a videa",
"no_libraries_message": "Vytvořte si externí knihovnu pro zobrazení fotografií a videí", "no_libraries_message": "Vytvořte si externí knihovnu pro zobrazení fotografií a videí",
"no_locked_photos_message": "Fotky a videa v uzamčené složce jsou skryté a při procházení knihovny se nezobrazují.",
"no_name": "Bez jména", "no_name": "Bez jména",
"no_notifications": "Žádná oznámení",
"no_people_found": "Nebyli nalezeni žádní odpovídající lidé",
"no_places": "Žádná místa", "no_places": "Žádná místa",
"no_results": "Žádné výsledky", "no_results": "Žádné výsledky",
"no_results_description": "Zkuste použít synonymum nebo obecnější klíčové slovo", "no_results_description": "Zkuste použít synonymum nebo obecnější klíčové slovo",
@@ -1294,7 +1260,6 @@
"not_selected": "Není vybráno", "not_selected": "Není vybráno",
"note_apply_storage_label_to_previously_uploaded assets": "Upozornění: Chcete-li použít štítek úložiště na dříve nahrané položky, spusťte příkaz", "note_apply_storage_label_to_previously_uploaded assets": "Upozornění: Chcete-li použít štítek úložiště na dříve nahrané položky, spusťte příkaz",
"notes": "Poznámky", "notes": "Poznámky",
"nothing_here_yet": "Zatím zde nic není",
"notification_permission_dialog_content": "Chcete-li povolit oznámení, přejděte do nastavení a vyberte možnost povolit.", "notification_permission_dialog_content": "Chcete-li povolit oznámení, přejděte do nastavení a vyberte možnost povolit.",
"notification_permission_list_tile_content": "Udělte oprávnění k aktivaci oznámení.", "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_enable_button": "Povolit oznámení",
@@ -1302,9 +1267,12 @@
"notification_toggle_setting_description": "Povolení e-mailových oznámení", "notification_toggle_setting_description": "Povolení e-mailových oznámení",
"notifications": "Oznámení", "notifications": "Oznámení",
"notifications_setting_description": "Správa oznámení", "notifications_setting_description": "Správa oznámení",
"oauth": "OAuth",
"official_immich_resources": "Oficiální zdroje Immich", "official_immich_resources": "Oficiální zdroje Immich",
"offline": "Offline",
"offline_paths": "Offline cesty", "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.", "offline_paths_description": "Tyto výsledky mohou být způsobeny ručním odstraněním souborů, které nejsou součástí externí knihovny.",
"ok": "Ok",
"oldest_first": "Nejstarší první", "oldest_first": "Nejstarší první",
"on_this_device": "V tomto zařízení", "on_this_device": "V tomto zařízení",
"onboarding": "Zahájení", "onboarding": "Zahájení",
@@ -1312,8 +1280,8 @@
"onboarding_theme_description": "Zvolte si barevný motiv pro svou instanci. Můžete to později změnit v nastavení.", "onboarding_theme_description": "Zvolte si barevný motiv 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_description": "Nastavíme vaši instanci pomocí několika běžných nastavení.",
"onboarding_welcome_user": "Vítej, {user}", "onboarding_welcome_user": "Vítej, {user}",
"online": "Online",
"only_favorites": "Pouze oblíbené", "only_favorites": "Pouze oblíbené",
"open": "Otevřít",
"open_in_map_view": "Otevřít v zobrazení mapy", "open_in_map_view": "Otevřít v zobrazení mapy",
"open_in_openstreetmap": "Otevřít v OpenStreetMap", "open_in_openstreetmap": "Otevřít v OpenStreetMap",
"open_the_search_filters": "Otevřít vyhledávací filtry", "open_the_search_filters": "Otevřít vyhledávací filtry",
@@ -1326,6 +1294,7 @@
"other_variables": "Další proměnné", "other_variables": "Další proměnné",
"owned": "Vlastní", "owned": "Vlastní",
"owner": "Vlastník", "owner": "Vlastník",
"partner": "Partner",
"partner_can_access": "{partner} má přístup", "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_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_can_access_location": "Místo, kde byly vaše fotografie pořízeny",
@@ -1336,7 +1305,7 @@
"partner_page_partner_add_failed": "Nepodařilo se přidat partnera", "partner_page_partner_add_failed": "Nepodařilo se přidat partnera",
"partner_page_select_partner": "Vyberte partnera", "partner_page_select_partner": "Vyberte partnera",
"partner_page_shared_to_title": "Sdíleno", "partner_page_shared_to_title": "Sdíleno",
"partner_page_stop_sharing_content": "{partner} již nebude mít přístup k vašim fotografiím.", "partner_page_stop_sharing_content": "{} již nebude mít přístup k vašim fotografiím.",
"partner_sharing": "Sdílení mezi partnery", "partner_sharing": "Sdílení mezi partnery",
"partners": "Partneři", "partners": "Partneři",
"password": "Heslo", "password": "Heslo",
@@ -1374,7 +1343,7 @@
"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_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í.", "permission_onboarding_request": "Immich potřebuje přístup k zobrazení vašich fotek a videí.",
"person": "Osoba", "person": "Osoba",
"person_birthdate": "Narozen(a) {date}", "person_birthdate": "Narozen/a {date}",
"person_hidden": "{name}{hidden, select, true { (skryto)} other {}}", "person_hidden": "{name}{hidden, select, true { (skryto)} other {}}",
"photo_shared_all_users": "Vypadá to, že jste fotky sdíleli se všemi uživateli, nebo nemáte žádného uživatele, se kterým byste je mohli sdílet.", "photo_shared_all_users": "Vypadá to, že jste fotky sdíleli se všemi uživateli, nebo nemáte žádného uživatele, se kterým byste je mohli sdílet.",
"photos": "Fotky", "photos": "Fotky",
@@ -1382,10 +1351,6 @@
"photos_count": "{count, plural, one {{count, number} fotka} few {{count, number} fotky} other {{count, number} fotek}}", "photos_count": "{count, plural, one {{count, number} fotka} few {{count, number} fotky} other {{count, number} fotek}}",
"photos_from_previous_years": "Fotky z předchozích let", "photos_from_previous_years": "Fotky z předchozích let",
"pick_a_location": "Vyberte polohu", "pick_a_location": "Vyberte polohu",
"pin_code_changed_successfully": "PIN kód byl úspěšně změněn",
"pin_code_reset_successfully": "PIN kód úspěšně resetován",
"pin_code_setup_successfully": "PIN kód úspěšně nastaven",
"pin_verification": "Ověření PIN kódu",
"place": "Místo", "place": "Místo",
"places": "Místa", "places": "Místa",
"places_count": "{count, plural, one {{count, number} místo} few {{count, number} místa} other {{count, number} míst}}", "places_count": "{count, plural, one {{count, number} místo} few {{count, number} místa} other {{count, number} míst}}",
@@ -1393,7 +1358,7 @@
"play_memories": "Přehrát vzpomníky", "play_memories": "Přehrát vzpomníky",
"play_motion_photo": "Přehrát pohybovou fotografii", "play_motion_photo": "Přehrát pohybovou fotografii",
"play_or_pause_video": "Přehrát nebo pozastavit video", "play_or_pause_video": "Přehrát nebo pozastavit video",
"please_auth_to_access": "Pro přístup se prosím ověřte", "port": "Port",
"preferences_settings_subtitle": "Správa předvoleb aplikace", "preferences_settings_subtitle": "Správa předvoleb aplikace",
"preferences_settings_title": "Předvolby", "preferences_settings_title": "Předvolby",
"preset": "Přednastavení", "preset": "Přednastavení",
@@ -1403,11 +1368,11 @@
"previous_or_next_photo": "Předchozí nebo další fotka", "previous_or_next_photo": "Předchozí nebo další fotka",
"primary": "Primární", "primary": "Primární",
"privacy": "Soukromí", "privacy": "Soukromí",
"profile": "Profil",
"profile_drawer_app_logs": "Logy", "profile_drawer_app_logs": "Logy",
"profile_drawer_client_out_of_date_major": "Mobilní aplikace je zastaralá. Aktualizujte ji na nejnovější hlavní verzi.", "profile_drawer_client_out_of_date_major": "Mobilní aplikace je zastaralá. Aktualizujte ji na nejnovější hlavní verzi.",
"profile_drawer_client_out_of_date_minor": "Mobilní aplikace je zastaralá. Aktualizujte ji na nejnovější verzi.", "profile_drawer_client_out_of_date_minor": "Mobilní aplikace je zastaralá. Aktualizujte ji na nejnovější verzi.",
"profile_drawer_client_server_up_to_date": "Klient a server jsou aktuální", "profile_drawer_client_server_up_to_date": "Klient a server jsou aktuální",
"profile_drawer_github": "GitHub",
"profile_drawer_server_out_of_date_major": "Server je zastaralý. Aktualizujte na nejnovější hlavní verzi.", "profile_drawer_server_out_of_date_major": "Server je zastaralý. Aktualizujte na nejnovější hlavní verzi.",
"profile_drawer_server_out_of_date_minor": "Server je zastaralý. Aktualizujte je na nejnovější verzi.", "profile_drawer_server_out_of_date_minor": "Server je zastaralý. Aktualizujte je na nejnovější verzi.",
"profile_image_of_user": "Profilový obrázek uživatele {user}", "profile_image_of_user": "Profilový obrázek uživatele {user}",
@@ -1416,7 +1381,7 @@
"public_share": "Veřejné sdílení", "public_share": "Veřejné sdílení",
"purchase_account_info": "Podporovatel", "purchase_account_info": "Podporovatel",
"purchase_activated_subtitle": "Děkujeme vám za podporu aplikace Immich a softwaru s otevřeným zdrojovým kódem", "purchase_activated_subtitle": "Děkujeme vám za podporu aplikace Immich a softwaru s otevřeným zdrojovým kódem",
"purchase_activated_time": "Aktivováno dne {date}", "purchase_activated_time": "Aktivováno dne {date, date}",
"purchase_activated_title": "Váš klíč byl úspěšně aktivován", "purchase_activated_title": "Váš klíč byl úspěšně aktivován",
"purchase_button_activate": "Aktivovat", "purchase_button_activate": "Aktivovat",
"purchase_button_buy": "Koupit", "purchase_button_buy": "Koupit",
@@ -1444,6 +1409,7 @@
"purchase_remove_server_product_key_prompt": "Opravdu chcete odebrat serverový produktový klíč?", "purchase_remove_server_product_key_prompt": "Opravdu chcete odebrat serverový produktový klíč?",
"purchase_server_description_1": "Pro celý server", "purchase_server_description_1": "Pro celý server",
"purchase_server_description_2": "Stav podporovatele", "purchase_server_description_2": "Stav podporovatele",
"purchase_server_title": "Server",
"purchase_settings_server_activated": "Produktový klíč serveru spravuje správce", "purchase_settings_server_activated": "Produktový klíč serveru spravuje správce",
"rating": "Hodnocení hvězdičkami", "rating": "Hodnocení hvězdičkami",
"rating_clear": "Vyčistit hodnocení", "rating_clear": "Vyčistit hodnocení",
@@ -1460,8 +1426,6 @@
"recent_searches": "Nedávná vyhledávání", "recent_searches": "Nedávná vyhledávání",
"recently_added": "Nedávno přidané", "recently_added": "Nedávno přidané",
"recently_added_page_title": "Nedávno přidané", "recently_added_page_title": "Nedávno přidané",
"recently_taken": "Nedávno pořízené",
"recently_taken_page_title": "Nedávno pořízené",
"refresh": "Obnovit", "refresh": "Obnovit",
"refresh_encoded_videos": "Obnovit kódovaná videa", "refresh_encoded_videos": "Obnovit kódovaná videa",
"refresh_faces": "Obnovit obličeje", "refresh_faces": "Obnovit obličeje",
@@ -1481,8 +1445,6 @@
"remove_deleted_assets": "Odstranit offline soubory", "remove_deleted_assets": "Odstranit offline soubory",
"remove_from_album": "Odstranit z alba", "remove_from_album": "Odstranit z alba",
"remove_from_favorites": "Odstranit z oblíbených", "remove_from_favorites": "Odstranit z oblíbených",
"remove_from_locked_folder": "Odstranit z uzamčené složky",
"remove_from_locked_folder_confirmation": "Opravdu chcete tyto fotky a videa přesunout z uzamčené složky? Budou viditelné ve vaší knihovně.",
"remove_from_shared_link": "Odstranit ze sdíleného odkazu", "remove_from_shared_link": "Odstranit ze sdíleného odkazu",
"remove_memory": "Odstranit vzpomínku", "remove_memory": "Odstranit vzpomínku",
"remove_photo_from_memory": "Odstranit fotografii z této vzpomínky", "remove_photo_from_memory": "Odstranit fotografii z této vzpomínky",
@@ -1506,7 +1468,6 @@
"reset": "Výchozí", "reset": "Výchozí",
"reset_password": "Obnovit heslo", "reset_password": "Obnovit heslo",
"reset_people_visibility": "Obnovit viditelnost lidí", "reset_people_visibility": "Obnovit viditelnost lidí",
"reset_pin_code": "Resetovat PIN kód",
"reset_to_default": "Obnovit výchozí nastavení", "reset_to_default": "Obnovit výchozí nastavení",
"resolve_duplicates": "Vyřešit duplicity", "resolve_duplicates": "Vyřešit duplicity",
"resolved_all_duplicates": "Vyřešeny všechny duplicity", "resolved_all_duplicates": "Vyřešeny všechny duplicity",
@@ -1517,6 +1478,8 @@
"resume": "Pokračovat", "resume": "Pokračovat",
"retry_upload": "Opakování nahrávání", "retry_upload": "Opakování nahrávání",
"review_duplicates": "Kontrola duplicit", "review_duplicates": "Kontrola duplicit",
"role": "Role",
"role_editor": "Editor",
"role_viewer": "Divák", "role_viewer": "Divák",
"save": "Uložit", "save": "Uložit",
"save_to_gallery": "Uložit do galerie", "save_to_gallery": "Uložit do galerie",
@@ -1597,7 +1560,6 @@
"select_keep_all": "Vybrat ponechat vše", "select_keep_all": "Vybrat ponechat vše",
"select_library_owner": "Vyberte vlastníka knihovny", "select_library_owner": "Vyberte vlastníka knihovny",
"select_new_face": "Výběr nového obličeje", "select_new_face": "Výběr nového obličeje",
"select_person_to_tag": "Vyberte osobu, kterou chcete označit",
"select_photos": "Vybrat fotky", "select_photos": "Vybrat fotky",
"select_trash_all": "Vybrat vyhodit vše", "select_trash_all": "Vybrat vyhodit vše",
"select_user_for_sharing_page_err_album": "Nepodařilo se vytvořit album", "select_user_for_sharing_page_err_album": "Nepodařilo se vytvořit album",
@@ -1628,12 +1590,12 @@
"setting_languages_apply": "Použít", "setting_languages_apply": "Použít",
"setting_languages_subtitle": "Změna jazyka aplikace", "setting_languages_subtitle": "Změna jazyka aplikace",
"setting_languages_title": "Jazyk", "setting_languages_title": "Jazyk",
"setting_notifications_notify_failures_grace_period": "Oznámení o selhání zálohování na pozadí: {duration}", "setting_notifications_notify_failures_grace_period": "Oznámení o selhání zálohování na pozadí: {}",
"setting_notifications_notify_hours": "{count} hodin", "setting_notifications_notify_hours": "{} hodin",
"setting_notifications_notify_immediately": "okamžitě", "setting_notifications_notify_immediately": "okamžitě",
"setting_notifications_notify_minutes": "{count} minut", "setting_notifications_notify_minutes": "{} minut",
"setting_notifications_notify_never": "nikdy", "setting_notifications_notify_never": "nikdy",
"setting_notifications_notify_seconds": "{count} sekund", "setting_notifications_notify_seconds": "{} sekundy",
"setting_notifications_single_progress_subtitle": "Podrobné informace o průběhu nahrávání položky", "setting_notifications_single_progress_subtitle": "Podrobné informace o průběhu nahrávání položky",
"setting_notifications_single_progress_title": "Zobrazit průběh detailů zálohování na pozadí", "setting_notifications_single_progress_title": "Zobrazit průběh detailů zálohování na pozadí",
"setting_notifications_subtitle": "Přizpůsobení předvoleb oznámení", "setting_notifications_subtitle": "Přizpůsobení předvoleb oznámení",
@@ -1645,12 +1607,10 @@
"settings": "Nastavení", "settings": "Nastavení",
"settings_require_restart": "Pro použití tohoto nastavení restartujte Immich", "settings_require_restart": "Pro použití tohoto nastavení restartujte Immich",
"settings_saved": "Nastavení uloženo", "settings_saved": "Nastavení uloženo",
"setup_pin_code": "Nastavení PIN kódu",
"share": "Sdílet", "share": "Sdílet",
"share_add_photos": "Přidat fotografie", "share_add_photos": "Přidat fotografie",
"share_assets_selected": "{count} vybráno", "share_assets_selected": "{} vybráno",
"share_dialog_preparing": "Připravuji...", "share_dialog_preparing": "Připravuji...",
"share_link": "Sdílet odkaz",
"shared": "Sdílené", "shared": "Sdílené",
"shared_album_activities_input_disable": "Komentář je vypnutý", "shared_album_activities_input_disable": "Komentář je vypnutý",
"shared_album_activity_remove_content": "Chcete odstranit tuto aktivitu?", "shared_album_activity_remove_content": "Chcete odstranit tuto aktivitu?",
@@ -1663,33 +1623,34 @@
"shared_by_user": "Sdílel(a) {user}", "shared_by_user": "Sdílel(a) {user}",
"shared_by_you": "Sdíleli jste", "shared_by_you": "Sdíleli jste",
"shared_from_partner": "Fotky od {partner}", "shared_from_partner": "Fotky od {partner}",
"shared_intent_upload_button_progress_text": "{current} / {total} nahráno", "shared_intent_upload_button_progress_text": "{} / {} nahráno",
"shared_link_app_bar_title": "Sdílené odkazy", "shared_link_app_bar_title": "Sdílené odkazy",
"shared_link_clipboard_copied_massage": "Zkopírováno do schránky", "shared_link_clipboard_copied_massage": "Zkopírováno do schránky",
"shared_link_clipboard_text": "Odkaz: {link}\nHeslo: {password}", "shared_link_clipboard_text": "Odkaz: {}\nHeslo: {}",
"shared_link_create_error": "Chyba při vytváření sdíleného odkazu", "shared_link_create_error": "Chyba při vytváření sdíleného odkazu",
"shared_link_edit_description_hint": "Zadejte popis sdílení", "shared_link_edit_description_hint": "Zadejte popis sdílení",
"shared_link_edit_expire_after_option_day": "1 den", "shared_link_edit_expire_after_option_day": "1 den",
"shared_link_edit_expire_after_option_days": "{count} dní", "shared_link_edit_expire_after_option_days": "{} dní",
"shared_link_edit_expire_after_option_hour": "1 hodina", "shared_link_edit_expire_after_option_hour": "1 hodina",
"shared_link_edit_expire_after_option_hours": "{count} hodin", "shared_link_edit_expire_after_option_hours": "{} hodin",
"shared_link_edit_expire_after_option_minute": "1 minuta", "shared_link_edit_expire_after_option_minute": "1 minuta",
"shared_link_edit_expire_after_option_minutes": "{count} minut", "shared_link_edit_expire_after_option_minutes": "{} minut",
"shared_link_edit_expire_after_option_months": "{count} měsíce", "shared_link_edit_expire_after_option_months": "{} měsíce",
"shared_link_edit_expire_after_option_year": "{count} rok", "shared_link_edit_expire_after_option_year": "{} rok",
"shared_link_edit_password_hint": "Zadejte heslo pro sdílení", "shared_link_edit_password_hint": "Zadejte heslo pro sdílení",
"shared_link_edit_submit_button": "Aktualizovat odkaz", "shared_link_edit_submit_button": "Aktualizovat odkaz",
"shared_link_error_server_url_fetch": "Nelze načíst url serveru", "shared_link_error_server_url_fetch": "Nelze načíst url serveru",
"shared_link_expires_day": "Vyprší za {count} den", "shared_link_expires_day": "Vyprší za {} den",
"shared_link_expires_days": "Vyprší za {count} dní", "shared_link_expires_days": "Vyprší za {} dní",
"shared_link_expires_hour": "Vyprší za {count} hodinu", "shared_link_expires_hour": "Vyprší za {} hodinu",
"shared_link_expires_hours": "Vyprší za {count} hodin", "shared_link_expires_hours": "Vyprší za {} hodin",
"shared_link_expires_minute": "Vyprší za {count} minutu", "shared_link_expires_minute": "Vyprší za {} minutu",
"shared_link_expires_minutes": "Vyprší za {count} minut", "shared_link_expires_minutes": "Vyprší za {} minut",
"shared_link_expires_never": "Platnost ∞", "shared_link_expires_never": "Platnost ∞",
"shared_link_expires_second": "Vyprší za {count} sekundu", "shared_link_expires_second": "Vyprší za {} sekundu",
"shared_link_expires_seconds": "Vyprší za {count} sekund", "shared_link_expires_seconds": "Vyprší za {} sekund",
"shared_link_individual_shared": "Individuální sdílení", "shared_link_individual_shared": "Individuální sdílení",
"shared_link_info_chip_metadata": "EXIF",
"shared_link_manage_links": "Spravovat sdílené odkazy", "shared_link_manage_links": "Spravovat sdílené odkazy",
"shared_link_options": "Možnosti sdíleného odkazu", "shared_link_options": "Možnosti sdíleného odkazu",
"shared_links": "Sdílené odkazy", "shared_links": "Sdílené odkazy",
@@ -1752,6 +1713,7 @@
"stack_selected_photos": "Seskupení vybraných fotografií", "stack_selected_photos": "Seskupení vybraných fotografií",
"stacked_assets_count": "{count, plural, one {Seskupena # položka} few {Seskupeny # položky} other {Seskupeno # položek}}", "stacked_assets_count": "{count, plural, one {Seskupena # položka} few {Seskupeny # položky} other {Seskupeno # položek}}",
"stacktrace": "Výpis zásobníku", "stacktrace": "Výpis zásobníku",
"start": "Start",
"start_date": "Počáteční datum", "start_date": "Počáteční datum",
"state": "Stát", "state": "Stát",
"status": "Stav", "status": "Stav",
@@ -1761,7 +1723,6 @@
"stop_sharing_photos_with_user": "Přestat sdílet své fotky s tímto uživatelem", "stop_sharing_photos_with_user": "Přestat sdílet své fotky s tímto uživatelem",
"storage": "Velikost úložiště", "storage": "Velikost úložiště",
"storage_label": "Štítek úložiště", "storage_label": "Štítek úložiště",
"storage_quota": "Kvóta úložiště",
"storage_usage": "Využito {used} z {available}", "storage_usage": "Využito {used} z {available}",
"submit": "Odeslat", "submit": "Odeslat",
"suggestions": "Návrhy", "suggestions": "Návrhy",
@@ -1788,7 +1749,7 @@
"theme_selection": "Výběr motivu", "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", "theme_selection_description": "Automatické nastavení světlého nebo tmavého motivu podle systémových preferencí prohlížeče",
"theme_setting_asset_list_storage_indicator_title": "Zobrazit indikátor úložiště na dlaždicích položek", "theme_setting_asset_list_storage_indicator_title": "Zobrazit indikátor úložiště na dlaždicích položek",
"theme_setting_asset_list_tiles_per_row_title": "Počet položek na řádek ({count})", "theme_setting_asset_list_tiles_per_row_title": "Počet položek na řádek ({})",
"theme_setting_colorful_interface_subtitle": "Použít hlavní barvu na povrchy pozadí.", "theme_setting_colorful_interface_subtitle": "Použít hlavní barvu na povrchy pozadí.",
"theme_setting_colorful_interface_title": "Barevné rozhraní", "theme_setting_colorful_interface_title": "Barevné rozhraní",
"theme_setting_image_viewer_quality_subtitle": "Přizpůsobení kvality detailů prohlížeče obrázků", "theme_setting_image_viewer_quality_subtitle": "Přizpůsobení kvality detailů prohlížeče obrázků",
@@ -1798,7 +1759,7 @@
"theme_setting_system_primary_color_title": "Použití systémové barvy", "theme_setting_system_primary_color_title": "Použití systémové barvy",
"theme_setting_system_theme_switch": "Automaticky (podle systemového nastavení)", "theme_setting_system_theme_switch": "Automaticky (podle systemového nastavení)",
"theme_setting_theme_subtitle": "Vyberte nastavení tématu aplikace", "theme_setting_theme_subtitle": "Vyberte nastavení tématu aplikace",
"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_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í", "theme_setting_three_stage_loading_title": "Povolení třístupňového načítání",
"they_will_be_merged_together": "Budou sloučeny dohromady", "they_will_be_merged_together": "Budou sloučeny dohromady",
"third_party_resources": "Zdroje třetích stran", "third_party_resources": "Zdroje třetích stran",
@@ -1823,15 +1784,13 @@
"trash_no_results_message": "Zde se zobrazí odstraněné fotky a videa.", "trash_no_results_message": "Zde se zobrazí odstraněné fotky a videa.",
"trash_page_delete_all": "Smazat všechny", "trash_page_delete_all": "Smazat všechny",
"trash_page_empty_trash_dialog_content": "Chcete vyprázdnit svoje vyhozené položky? Tyto položky budou trvale odstraněny z aplikace", "trash_page_empty_trash_dialog_content": "Chcete vyprázdnit svoje vyhozené položky? Tyto položky budou trvale odstraněny z aplikace",
"trash_page_info": "Vyhozené položky budou trvale smazány po {days} dnech", "trash_page_info": "Vyhozené položky budou trvale smazány po {} dnech",
"trash_page_no_assets": "Žádné vyhozené položky", "trash_page_no_assets": "Žádné vyhozené položky",
"trash_page_restore_all": "Obnovit všechny", "trash_page_restore_all": "Obnovit všechny",
"trash_page_select_assets_btn": "Vybrat položky", "trash_page_select_assets_btn": "Vybrat položky",
"trash_page_title": "Koš ({count})", "trash_page_title": "Koš ({})",
"trashed_items_will_be_permanently_deleted_after": "Smazané položky budou trvale odstraněny po {days, plural, one {# dni} other {# dnech}}.", "trashed_items_will_be_permanently_deleted_after": "Smazané položky budou trvale odstraněny po {days, plural, one {# dni} other {# dnech}}.",
"type": "Typ", "type": "Typ",
"unable_to_change_pin_code": "Nelze změnit PIN kód",
"unable_to_setup_pin_code": "Nelze nastavit PIN kód",
"unarchive": "Odarchivovat", "unarchive": "Odarchivovat",
"unarchived_count": "{count, plural, one {Odarchivována #} few {Odarchivovány #} other {Odarchivováno #}}", "unarchived_count": "{count, plural, one {Odarchivována #} few {Odarchivovány #} other {Odarchivováno #}}",
"unfavorite": "Zrušit oblíbení", "unfavorite": "Zrušit oblíbení",
@@ -1855,7 +1814,6 @@
"untracked_files": "Nesledované soubory", "untracked_files": "Nesledované soubory",
"untracked_files_decription": "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ě", "untracked_files_decription": "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ě",
"up_next": "To je prozatím vše", "up_next": "To je prozatím vše",
"updated_at": "Aktualizováno",
"updated_password": "Heslo aktualizováno", "updated_password": "Heslo aktualizováno",
"upload": "Nahrát", "upload": "Nahrát",
"upload_concurrency": "Souběžnost nahrávání", "upload_concurrency": "Souběžnost nahrávání",
@@ -1868,18 +1826,15 @@
"upload_status_errors": "Chyby", "upload_status_errors": "Chyby",
"upload_status_uploaded": "Nahráno", "upload_status_uploaded": "Nahráno",
"upload_success": "Nahrání proběhlo úspěšně, obnovením stránky se zobrazí nově nahrané položky.", "upload_success": "Nahrání proběhlo úspěšně, obnovením stránky se zobrazí nově nahrané položky.",
"upload_to_immich": "Nahrát do Immich ({count})", "upload_to_immich": "Nahrát do Immiche ({})",
"uploading": "Nahrávání", "uploading": "Nahrávání",
"url": "URL",
"usage": "Využití", "usage": "Využití",
"use_biometric": "Použít biometrické údaje",
"use_current_connection": "použít aktuální připojení", "use_current_connection": "použít aktuální připojení",
"use_custom_date_range": "Použít vlastní rozsah dat", "use_custom_date_range": "Použít vlastní rozsah dat",
"user": "Uživatel", "user": "Uživatel",
"user_has_been_deleted": "Tento uživatel byl smazán.",
"user_id": "ID uživatele", "user_id": "ID uživatele",
"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_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_pin_code_settings": "PIN kód",
"user_pin_code_settings_description": "Správa vašeho PIN kódu",
"user_purchase_settings": "Nákup", "user_purchase_settings": "Nákup",
"user_purchase_settings_description": "Správa vašeho nákupu", "user_purchase_settings_description": "Správa vašeho nákupu",
"user_role_set": "Uživatel {user} nastaven jako {role}", "user_role_set": "Uživatel {user} nastaven jako {role}",
@@ -1902,6 +1857,7 @@
"version_announcement_overlay_title": "K dispozici je nová verze serveru 🎉", "version_announcement_overlay_title": "K dispozici je nová verze serveru 🎉",
"version_history": "Historie verzí", "version_history": "Historie verzí",
"version_history_item": "Nainstalováno {version} dne {date}", "version_history_item": "Nainstalováno {version} dne {date}",
"video": "Video",
"video_hover_setting": "Přehrávat miniaturu videa po najetí myší", "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í.", "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í.",
"videos": "Videa", "videos": "Videa",
@@ -1927,12 +1883,11 @@
"week": "Týden", "week": "Týden",
"welcome": "Vítejte", "welcome": "Vítejte",
"welcome_to_immich": "Vítejte v Immichi", "welcome_to_immich": "Vítejte v Immichi",
"wifi_name": "Název Wi-Fi", "wifi_name": "Název WiFi",
"wrong_pin_code": "Chybný PIN kód",
"year": "Rok", "year": "Rok",
"years_ago": "Před {years, plural, one {rokem} other {# lety}}", "years_ago": "Před {years, plural, one {rokem} other {# lety}}",
"yes": "Ano", "yes": "Ano",
"you_dont_have_any_shared_links": "Nemáte žádné sdílené odkazy", "you_dont_have_any_shared_links": "Nemáte žádné sdílené odkazy",
"your_wifi_name": "Název vaší Wi-Fi", "your_wifi_name": "Váš název WiFi",
"zoom_image": "Zvětšit obrázek" "zoom_image": "Zvětšit obrázek"
} }
+179 -204
View File
File diff suppressed because it is too large Load Diff
+187 -214
View File
File diff suppressed because it is too large Load Diff

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