mirror of
https://github.com/immich-app/immich.git
synced 2025-12-10 23:01:03 -08:00
Compare commits
425 Commits
feat/integ
...
refact/ass
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab988f3be6 | ||
|
|
b8dc1a4b1f | ||
|
|
769d0aed87 | ||
|
|
09cbc5d3f4 | ||
|
|
a2a9797fab | ||
|
|
3d35e65f27 | ||
|
|
df76735f4a | ||
|
|
6feca56da8 | ||
|
|
97aabe466e | ||
|
|
72a53f43c8 | ||
|
|
30b4f334d8 | ||
|
|
6c6a32c63e | ||
|
|
6fed223405 | ||
|
|
3105094a3d | ||
|
|
b96c95beda | ||
|
|
926ff075a3 | ||
|
|
934649c8df | ||
|
|
a43159f4ba | ||
|
|
ea3a14ed25 | ||
|
|
24a4cba953 | ||
|
|
fda22c83b9 | ||
|
|
2a8019726c | ||
|
|
5f76cdddc7 | ||
|
|
48be10e48b | ||
|
|
6c11ef62e8 | ||
|
|
65dce58aa4 | ||
|
|
64cc7239fe | ||
|
|
5f89c2d111 | ||
|
|
4621ec5ea2 | ||
|
|
881a96cdf9 | ||
|
|
b001ba44f5 | ||
|
|
afb444c92c | ||
|
|
027c4a8b34 | ||
|
|
eca9b56847 | ||
|
|
5b0575b956 | ||
|
|
05064f87f0 | ||
|
|
522cdbac99 | ||
|
|
9240bbc6ff | ||
|
|
3751f8bc57 | ||
|
|
88b8afb8d6 | ||
|
|
2e13543d5d | ||
|
|
bcfc967d77 | ||
|
|
7d0e8f50f7 | ||
|
|
c759233d8c | ||
|
|
bfe32c2bb9 | ||
|
|
6c7b2e4b5c | ||
|
|
7edbeb2ed6 | ||
|
|
4e59a55c1d | ||
|
|
c2d7337d12 | ||
|
|
c1b82bed9b | ||
|
|
9ca31abae9 | ||
|
|
ebcf133bea | ||
|
|
1923f1a887 | ||
|
|
ce14324c97 | ||
|
|
6a309129b7 | ||
|
|
bcb1bf4692 | ||
|
|
7f89999abe | ||
|
|
813186e618 | ||
|
|
20d9204ada | ||
|
|
3a9e79a452 | ||
|
|
03966146fe | ||
|
|
ecc58a8971 | ||
|
|
c705a7b280 | ||
|
|
ef278b4fb0 | ||
|
|
4cd633dc68 | ||
|
|
a18c6fa910 | ||
|
|
90aa0dc14d | ||
|
|
ce8c80dad0 | ||
|
|
81eb98d4e5 | ||
|
|
2b03802e9c | ||
|
|
484311e9bb | ||
|
|
366539bc4c | ||
|
|
69b1331026 | ||
|
|
af30d97668 | ||
|
|
9b047d30e4 | ||
|
|
6a5597b36b | ||
|
|
c10b795e99 | ||
|
|
b606d4fe73 | ||
|
|
4c2ad44303 | ||
|
|
698d3004b4 | ||
|
|
fe4d6edbdc | ||
|
|
798debfde3 | ||
|
|
6563fa608a | ||
|
|
1a90fc8e58 | ||
|
|
c707f9cef4 | ||
|
|
6fda863c08 | ||
|
|
373b654156 | ||
|
|
a5d84ba552 | ||
|
|
1dc8fa2979 | ||
|
|
0426699f13 | ||
|
|
8154ec29df | ||
|
|
3024cd343b | ||
|
|
0b44d4b6f2 | ||
|
|
a04c6ed80d | ||
|
|
1c50e19894 | ||
|
|
e61d7f2616 | ||
|
|
a6b0869714 | ||
|
|
9c25b8ba7d | ||
|
|
3c72f489d8 | ||
|
|
1f2c779b36 | ||
|
|
5c74f634b7 | ||
|
|
ecc99bfd16 | ||
|
|
ff4d70e351 | ||
|
|
42c2389eb5 | ||
|
|
33c9f88ba4 | ||
|
|
11c469907f | ||
|
|
7c43e6c3c8 | ||
|
|
00aa385972 | ||
|
|
a5ed453929 | ||
|
|
dd8969cb7d | ||
|
|
bce4f93c90 | ||
|
|
a4c0dc5007 | ||
|
|
d233a7d97a | ||
|
|
5cdbb65d28 | ||
|
|
3434544864 | ||
|
|
269bf4b344 | ||
|
|
f9435a538b | ||
|
|
10e2ec2841 | ||
|
|
fe91b44ab9 | ||
|
|
747a72120e | ||
|
|
910661e75c | ||
|
|
c8a135a7ae | ||
|
|
08d1cf5bde | ||
|
|
3e62497fd0 | ||
|
|
a1bc862a32 | ||
|
|
75bf3aa1be | ||
|
|
38e68d16f9 | ||
|
|
caf11fbb96 | ||
|
|
f99c6feac5 | ||
|
|
5122512f19 | ||
|
|
49ed212af8 | ||
|
|
e29103b69f | ||
|
|
14b771d7c7 | ||
|
|
07aa51638c | ||
|
|
0a9a520ed2 | ||
|
|
de81006367 | ||
|
|
e0144b4ece | ||
|
|
65e8d75e82 | ||
|
|
023bcffdb8 | ||
|
|
06f1d0dc4d | ||
|
|
c6641d4859 | ||
|
|
91cbd56c1c | ||
|
|
35280b94cc | ||
|
|
4c69511225 | ||
|
|
0684a3ada4 | ||
|
|
a0f44f147b | ||
|
|
15c488ccd9 | ||
|
|
bc062da11b | ||
|
|
8038ae1e7a | ||
|
|
f28c0d912c | ||
|
|
bd70824961 | ||
|
|
749f63e4a0 | ||
|
|
db68d1af9b | ||
|
|
864fe3d0d6 | ||
|
|
00536bf074 | ||
|
|
0d3efe229d | ||
|
|
3b0a803089 | ||
|
|
bcda2c6e22 | ||
|
|
7347f64958 | ||
|
|
176d53c1b3 | ||
|
|
5fc448bc97 | ||
|
|
3d0c851636 | ||
|
|
16fcb657b7 | ||
|
|
32b57bcbfc | ||
|
|
7f56443b24 | ||
|
|
189442e9c4 | ||
|
|
523fe5bef7 | ||
|
|
77a362f0c0 | ||
|
|
5f5308631e | ||
|
|
004c2f2496 | ||
|
|
e2dfbd66c3 | ||
|
|
de756d9497 | ||
|
|
103b83d2d6 | ||
|
|
f54cfa7a5a | ||
|
|
ed5b260eeb | ||
|
|
8923d5b0a3 | ||
|
|
2f3d4e15d2 | ||
|
|
c9bcae813b | ||
|
|
bddb43e1d4 | ||
|
|
176656b5f4 | ||
|
|
5cd186d3d4 | ||
|
|
144cc8ab6d | ||
|
|
0322a8b1d9 | ||
|
|
94e9adf625 | ||
|
|
24edf23bc8 | ||
|
|
d784c7737a | ||
|
|
fdc7a154c0 | ||
|
|
e5219f1f31 | ||
|
|
22eef5f3c5 | ||
|
|
5179c5badf | ||
|
|
4c5cd14270 | ||
|
|
38ad15af4c | ||
|
|
7a001d27a5 | ||
|
|
08e2b22db8 | ||
|
|
5dd3a6e13f | ||
|
|
bedcf50196 | ||
|
|
c03e72c1da | ||
|
|
b50d9fa448 | ||
|
|
4b4ee5abf3 | ||
|
|
6499057b4c | ||
|
|
e88bd74fd2 | ||
|
|
16745e77d4 | ||
|
|
c0ed2210b4 | ||
|
|
f9ed314b37 | ||
|
|
160ca28253 | ||
|
|
9380625762 | ||
|
|
ade7cd258d | ||
|
|
63996f4dd3 | ||
|
|
360f68b86b | ||
|
|
bf212bf235 | ||
|
|
b890440f6b | ||
|
|
16f83c0aa9 | ||
|
|
2572413b2b | ||
|
|
14d785cec9 | ||
|
|
242817c49a | ||
|
|
74f79cae69 | ||
|
|
ac0e94c003 | ||
|
|
047c7821a3 | ||
|
|
ccb0e711f0 | ||
|
|
3fb2c3a7bf | ||
|
|
197a1886c3 | ||
|
|
adac30c9a1 | ||
|
|
ae04a62030 | ||
|
|
02246cdd1f | ||
|
|
2d05a5482f | ||
|
|
7b2237b86b | ||
|
|
84024f6cdc | ||
|
|
5574b2dd39 | ||
|
|
e88eb44aba | ||
|
|
75c24f0023 | ||
|
|
e376366b7b | ||
|
|
48e16f0a5a | ||
|
|
e8ba9dd208 | ||
|
|
a932cbae38 | ||
|
|
526206b2a5 | ||
|
|
de2115d11e | ||
|
|
e0ac588ca8 | ||
|
|
0c965ae2ea | ||
|
|
28e05537bd | ||
|
|
acca040524 | ||
|
|
b0a0ae6cd3 | ||
|
|
fb4be6e231 | ||
|
|
ecb16d9907 | ||
|
|
737fedd527 | ||
|
|
b557f3b7f2 | ||
|
|
ce6631f7e0 | ||
|
|
b46e066cc2 | ||
|
|
55f4e93456 | ||
|
|
81423420c8 | ||
|
|
a9bd651692 | ||
|
|
afda7b9525 | ||
|
|
86f64fd0bf | ||
|
|
19013af58f | ||
|
|
e746d27f5e | ||
|
|
90c8fdba96 | ||
|
|
e2ffc9d5a1 | ||
|
|
f64a3003af | ||
|
|
a26d703335 | ||
|
|
5d0ad853f4 | ||
|
|
19ff39c2b9 | ||
|
|
8733d1e554 | ||
|
|
1fb8861e35 | ||
|
|
70b9a4c8f1 | ||
|
|
2da94439c7 | ||
|
|
3d3e5dc547 | ||
|
|
daf1bee7ac | ||
|
|
6b4d5e3beb | ||
|
|
6b9233c71a | ||
|
|
b4a798c39f | ||
|
|
edae9c2d3d | ||
|
|
246d593c9d | ||
|
|
e4322ae0a2 | ||
|
|
e506c7fb19 | ||
|
|
393e8d50b2 | ||
|
|
74438f5bd8 | ||
|
|
e7d7886f44 | ||
|
|
97e86e409a | ||
|
|
72401aa6b1 | ||
|
|
fb94fd3132 | ||
|
|
a02e1f5e7c | ||
|
|
d544053c67 | ||
|
|
df927dd3ce | ||
|
|
d48702f943 | ||
|
|
fa22e865a4 | ||
|
|
b5c3a675b2 | ||
|
|
5589616921 | ||
|
|
a53d033622 | ||
|
|
36506250c4 | ||
|
|
31af44dd2a | ||
|
|
c89ac5b5e5 | ||
|
|
daf1a48b54 | ||
|
|
091a101f39 | ||
|
|
d118b46c3f | ||
|
|
ad3f58bcda | ||
|
|
0711a9006f | ||
|
|
9c18fef9b2 | ||
|
|
d00c872dc1 | ||
|
|
3a5fed99e1 | ||
|
|
e2defbc49a | ||
|
|
f4e4e6628e | ||
|
|
9d04853b34 | ||
|
|
97503d11c5 | ||
|
|
cbf68b006e | ||
|
|
4b9a7b2ce0 | ||
|
|
b854a3dd47 | ||
|
|
aebd68e24e | ||
|
|
0f42babb6b | ||
|
|
dbdb64f6c5 | ||
|
|
2b1b20ab0b | ||
|
|
44d49b9671 | ||
|
|
0e81c20cbb | ||
|
|
1f18a09061 | ||
|
|
0257f1a743 | ||
|
|
6f39a706b2 | ||
|
|
10181defb1 | ||
|
|
8ea40973a7 | ||
|
|
be247395db | ||
|
|
78224961d1 | ||
|
|
b054e9dc2c | ||
|
|
f0d881b4f8 | ||
|
|
9677eb37e1 | ||
|
|
dc23bc4d55 | ||
|
|
e9f8d68f62 | ||
|
|
3f08768854 | ||
|
|
f029910dc7 | ||
|
|
b5593823a2 | ||
|
|
a40d35555f | ||
|
|
0205e89e34 | ||
|
|
a231d7be64 | ||
|
|
219f5b25a4 | ||
|
|
486bb47ddb | ||
|
|
58ae77ec92 | ||
|
|
4794a1a092 | ||
|
|
6abcfaef99 | ||
|
|
f6903696cb | ||
|
|
724a081bb5 | ||
|
|
4e332db2fb | ||
|
|
0712183a18 | ||
|
|
d004c03990 | ||
|
|
fff651f8a5 | ||
|
|
e2720e85bb | ||
|
|
5fdc8c9481 | ||
|
|
a3404cf420 | ||
|
|
5268dc4ee2 | ||
|
|
ef060e97b6 | ||
|
|
a9851df8d1 | ||
|
|
099a1e4210 | ||
|
|
79d760ccd7 | ||
|
|
369d3dfa38 | ||
|
|
93e53f6d74 | ||
|
|
d8f0a69dc8 | ||
|
|
09d9fa9755 | ||
|
|
118dc8cf5a | ||
|
|
9557395991 | ||
|
|
a5d63d6953 | ||
|
|
5ee4a43e74 | ||
|
|
c3aeb6c497 | ||
|
|
d22fb2d5db | ||
|
|
c4df96bd72 | ||
|
|
40e7b58ba4 | ||
|
|
4743a085f1 | ||
|
|
911c877e72 | ||
|
|
806000e671 | ||
|
|
54bafccbf9 | ||
|
|
e61c575b01 | ||
|
|
e12c67742c | ||
|
|
4878c500a5 | ||
|
|
2fa7a40996 | ||
|
|
963dd3210a | ||
|
|
4fdf75311c | ||
|
|
529359de2d | ||
|
|
2d7377a5e9 | ||
|
|
8fcf47e5cb | ||
|
|
c7dc31151d | ||
|
|
065f7c7d5d | ||
|
|
15877ddf1f | ||
|
|
1b8fa51315 | ||
|
|
1f84cbe7e5 | ||
|
|
b194aee754 | ||
|
|
91b961642a | ||
|
|
c61ea483ba | ||
|
|
c278bb0e5b | ||
|
|
bc8e08f5e8 | ||
|
|
0b8fc7b493 | ||
|
|
7bb25a5c8d | ||
|
|
58c1b92816 | ||
|
|
55adc136c8 | ||
|
|
cd288533a1 | ||
|
|
58af574241 | ||
|
|
6954b11be1 | ||
|
|
bc906f7343 | ||
|
|
760b08506a | ||
|
|
6b31e333bb | ||
|
|
493b9b7a54 | ||
|
|
188188a844 | ||
|
|
b2ef8ea7dd | ||
|
|
a6c4bd1555 | ||
|
|
a02fe89ec9 | ||
|
|
98e998e814 | ||
|
|
b83b28cd73 | ||
|
|
9771e48049 | ||
|
|
86db0aafe5 | ||
|
|
12b7a079c1 | ||
|
|
53420b7c02 | ||
|
|
c05aa445d8 | ||
|
|
bdf19ce331 | ||
|
|
895e0eacfe | ||
|
|
e7b60a9278 | ||
|
|
4e2fc9f017 | ||
|
|
d1e6682df0 | ||
|
|
965498d19b | ||
|
|
62f24a79f4 | ||
|
|
495a959879 | ||
|
|
a6a4dfcfd3 | ||
|
|
0d773af6c3 | ||
|
|
fe71894308 | ||
|
|
397808dd1a | ||
|
|
e7edbcdf04 | ||
|
|
59f666b115 | ||
|
|
dc8962f2bc | ||
|
|
00a77c2d6a | ||
|
|
c8641d24f6 | ||
|
|
2431e04a09 | ||
|
|
9e47093501 | ||
|
|
230c286b97 |
2
.devcontainer/.gitignore
vendored
2
.devcontainer/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
.env
|
||||
library
|
||||
@@ -1,16 +0,0 @@
|
||||
ARG BASEIMAGE=mcr.microsoft.com/devcontainers/typescript-node:22@sha256:a20b8a3538313487ac9266875bbf733e544c1aa2091df2bb99ab592a6d4f7399
|
||||
FROM ${BASEIMAGE}
|
||||
|
||||
# Flutter SDK
|
||||
# https://flutter.dev/docs/development/tools/sdk/releases?tab=linux
|
||||
ENV FLUTTER_CHANNEL="stable"
|
||||
ENV FLUTTER_VERSION="3.29.1"
|
||||
ENV FLUTTER_HOME=/flutter
|
||||
ENV PATH=${PATH}:${FLUTTER_HOME}/bin
|
||||
|
||||
# Flutter SDK
|
||||
RUN mkdir -p ${FLUTTER_HOME} \
|
||||
&& curl -C - --output flutter.tar.xz https://storage.googleapis.com/flutter_infra_release/releases/${FLUTTER_CHANNEL}/linux/flutter_linux_${FLUTTER_VERSION}-${FLUTTER_CHANNEL}.tar.xz \
|
||||
&& tar -xf flutter.tar.xz --strip-components=1 -C ${FLUTTER_HOME} \
|
||||
&& rm flutter.tar.xz \
|
||||
&& chown -R 1000:1000 ${FLUTTER_HOME}
|
||||
@@ -1,26 +1,67 @@
|
||||
{
|
||||
"name": "Immich",
|
||||
"service": "immich-devcontainer",
|
||||
"name": "Immich - Backend, Frontend and ML",
|
||||
"service": "immich-server",
|
||||
"runServices": [
|
||||
"immich-server",
|
||||
"redis",
|
||||
"database",
|
||||
"immich-machine-learning"
|
||||
],
|
||||
"dockerComposeFile": [
|
||||
"docker-compose.yml",
|
||||
"../docker/docker-compose.dev.yml"
|
||||
"../docker/docker-compose.dev.yml",
|
||||
"./server/container-compose-overrides.yml"
|
||||
],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"Dart-Code.dart-code",
|
||||
"Dart-Code.flutter",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"dcmdev.dcm-vscode-extension",
|
||||
"esbenp.prettier-vscode",
|
||||
"svelte.svelte-vscode"
|
||||
"svelte.svelte-vscode",
|
||||
"ms-vscode-remote.remote-containers",
|
||||
"foxundermoon.shell-format",
|
||||
"timonwong.shellcheck",
|
||||
"rvest.vs-code-prettier-eslint",
|
||||
"bluebrown.yamlfmt",
|
||||
"vkrishna04.cspell-sync",
|
||||
"vitest.explorer",
|
||||
"ms-playwright.playwright",
|
||||
"ms-azuretools.vscode-docker"
|
||||
]
|
||||
}
|
||||
},
|
||||
"forwardPorts": [],
|
||||
"initializeCommand": "bash .devcontainer/scripts/initializeCommand.sh",
|
||||
"onCreateCommand": "bash .devcontainer/scripts/onCreateCommand.sh",
|
||||
"forwardPorts": [3000, 9231, 9230, 2283],
|
||||
"portsAttributes": {
|
||||
"3000": {
|
||||
"label": "Immich - Frontend HTTP",
|
||||
"description": "The frontend of the Immich project",
|
||||
"onAutoForward": "openBrowserOnce"
|
||||
},
|
||||
"2283": {
|
||||
"label": "Immich - API Server - HTTP",
|
||||
"description": "The API server of the Immich project"
|
||||
},
|
||||
"9231": {
|
||||
"label": "Immich - API Server - DEBUG",
|
||||
"description": "The API server of the Immich project"
|
||||
},
|
||||
"9230": {
|
||||
"label": "Immich - Workers - DEBUG",
|
||||
"description": "The workers of the Immich project"
|
||||
}
|
||||
},
|
||||
"overrideCommand": true,
|
||||
"workspaceFolder": "/immich",
|
||||
"remoteUser": "node"
|
||||
"workspaceFolder": "/workspaces/immich",
|
||||
"remoteUser": "node",
|
||||
"userEnvProbe": "loginInteractiveShell",
|
||||
"remoteEnv": {
|
||||
// The location where your uploaded files are stored
|
||||
"UPLOAD_LOCATION": "${localEnv:UPLOAD_LOCATION:./library}",
|
||||
// Connection secret for postgres. You should change it to a random password
|
||||
// Please use only the characters `A-Za-z0-9`, without special characters or spaces
|
||||
"DB_PASSWORD": "${localEnv:DB_PASSWORD:postgres}",
|
||||
// The database username
|
||||
"DB_USERNAME": "${localEnv:DB_USERNAME:postgres}",
|
||||
// The database name
|
||||
"DB_DATABASE_NAME": "${localEnv:DB_DATABASE_NAME:immich}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
services:
|
||||
immich-devcontainer:
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
extra_hosts:
|
||||
- 'host.docker.internal:host-gateway'
|
||||
volumes:
|
||||
- ..:/immich:cached
|
||||
34
.devcontainer/mobile/container-compose-overrides.yml
Normal file
34
.devcontainer/mobile/container-compose-overrides.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
services:
|
||||
immich-server:
|
||||
build:
|
||||
target: dev-container-mobile
|
||||
environment:
|
||||
- IMMICH_SERVER_URL=http://127.0.0.1:2283/
|
||||
volumes: !override # bind mount host to /workspaces/immich
|
||||
- ..:/workspaces/immich
|
||||
- cli_node_modules:/workspaces/immich/cli/node_modules
|
||||
- e2e_node_modules:/workspaces/immich/e2e/node_modules
|
||||
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
|
||||
- server_node_modules:/workspaces/immich/server/node_modules
|
||||
- web_node_modules:/workspaces/immich/web/node_modules
|
||||
- ${UPLOAD_LOCATION}/photos:/workspaces/immich/server/upload
|
||||
- ${UPLOAD_LOCATION}/photos/upload:/workspaces/immich/server/upload/upload
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
|
||||
database:
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
# Node modules for each service to avoid conflicts and ensure consistent dependencies
|
||||
cli_node_modules:
|
||||
e2e_node_modules:
|
||||
open_api_node_modules:
|
||||
server_node_modules:
|
||||
web_node_modules:
|
||||
|
||||
# UPLOAD_LOCATION must be set to a absolute path or vol-upload
|
||||
vol-upload:
|
||||
|
||||
# DB_DATA_LOCATION must be set to a absolute path or vol-database
|
||||
vol-database:
|
||||
52
.devcontainer/mobile/devcontainer.json
Normal file
52
.devcontainer/mobile/devcontainer.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "Immich - Mobile",
|
||||
"service": "immich-server",
|
||||
"runServices": [
|
||||
"immich-server",
|
||||
"redis",
|
||||
"database",
|
||||
"immich-machine-learning"
|
||||
],
|
||||
"dockerComposeFile": [
|
||||
"../../docker/docker-compose.dev.yml",
|
||||
"./container-compose-overrides.yml"
|
||||
],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"Dart-Code.dart-code",
|
||||
"Dart-Code.flutter",
|
||||
"dcmdev.dcm-vscode-extension",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"svelte.svelte-vscode",
|
||||
"ms-vscode-remote.remote-containers",
|
||||
"foxundermoon.shell-format",
|
||||
"timonwong.shellcheck",
|
||||
"rvest.vs-code-prettier-eslint",
|
||||
"bluebrown.yamlfmt",
|
||||
"vkrishna04.cspell-sync",
|
||||
"vitest.explorer",
|
||||
"ms-playwright.playwright",
|
||||
"ms-azuretools.vscode-docker"
|
||||
]
|
||||
}
|
||||
},
|
||||
"forwardPorts": [],
|
||||
"overrideCommand": true,
|
||||
"workspaceFolder": "/workspaces/immich",
|
||||
"remoteUser": "node",
|
||||
"userEnvProbe": "loginInteractiveShell",
|
||||
"remoteEnv": {
|
||||
// The location where your uploaded files are stored
|
||||
"UPLOAD_LOCATION": "${localEnv:UPLOAD_LOCATION:./Library}",
|
||||
// Connection secret for postgres. You should change it to a random password
|
||||
// Please use only the characters `A-Za-z0-9`, without special characters or spaces
|
||||
"DB_PASSWORD": "${localEnv:DB_PASSWORD:postgres}",
|
||||
// The database username
|
||||
"DB_USERNAME": "${localEnv:DB_USERNAME:postgres}",
|
||||
// The database name
|
||||
"DB_DATABASE_NAME": "${localEnv:DB_DATABASE_NAME:immich}"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# If .env file does not exist, create it by copying example.env from the docker folder
|
||||
if [ ! -f ".devcontainer/.env" ]; then
|
||||
cp docker/example.env .devcontainer/.env
|
||||
fi
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Enable multiarch for arm64 if necessary
|
||||
if [ "$(dpkg --print-architecture)" = "arm64" ]; then
|
||||
sudo dpkg --add-architecture amd64 && \
|
||||
sudo apt-get update && \
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
qemu-user-static \
|
||||
libc6:amd64 \
|
||||
libstdc++6:amd64 \
|
||||
libgcc1:amd64
|
||||
fi
|
||||
|
||||
# Install DCM
|
||||
wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg
|
||||
sudo echo 'deb [signed-by=/usr/share/keyrings/dcm.gpg arch=amd64] https://dcm.dev/debian stable main' | sudo tee /etc/apt/sources.list.d/dart_stable.list
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install dcm
|
||||
|
||||
dart --disable-analytics
|
||||
|
||||
# Install immich
|
||||
cd /immich || exit
|
||||
make install-all
|
||||
82
.devcontainer/server/container-common.sh
Executable file
82
.devcontainer/server/container-common.sh
Executable file
@@ -0,0 +1,82 @@
|
||||
#!/bin/bash
|
||||
export IMMICH_PORT="${DEV_SERVER_PORT:-2283}"
|
||||
export DEV_PORT="${DEV_PORT:-3000}"
|
||||
|
||||
# search for immich directory inside workspace.
|
||||
# /workspaces/immich is the bind mount, but other directories can be mounted if runing
|
||||
# Devcontainer: Clone [repository|pull request] in container volumne
|
||||
WORKSPACES_DIR="/workspaces"
|
||||
IMMICH_DIR="$WORKSPACES_DIR/immich"
|
||||
IMMICH_DEVCONTAINER_LOG="$HOME/immich-devcontainer.log"
|
||||
|
||||
log() {
|
||||
# Display command on console, log with timestamp to file
|
||||
echo "$*"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >>"$IMMICH_DEVCONTAINER_LOG"
|
||||
}
|
||||
|
||||
run_cmd() {
|
||||
# Ensure log directory exists
|
||||
mkdir -p "$(dirname "$IMMICH_DEVCONTAINER_LOG")"
|
||||
|
||||
log "$@"
|
||||
|
||||
# Execute command: display normally on console, log with timestamps to file
|
||||
"$@" 2>&1 | tee >(while IFS= read -r line; do
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $line" >>"$IMMICH_DEVCONTAINER_LOG"
|
||||
done)
|
||||
|
||||
# Preserve exit status
|
||||
return "${PIPESTATUS[0]}"
|
||||
}
|
||||
|
||||
# Find directories excluding /workspaces/immich
|
||||
mapfile -t other_dirs < <(find "$WORKSPACES_DIR" -mindepth 1 -maxdepth 1 -type d ! -path "$IMMICH_DIR" ! -name ".*")
|
||||
|
||||
if [ ${#other_dirs[@]} -gt 1 ]; then
|
||||
log "Error: More than one directory found in $WORKSPACES_DIR other than $IMMICH_DIR."
|
||||
exit 1
|
||||
elif [ ${#other_dirs[@]} -eq 1 ]; then
|
||||
export IMMICH_WORKSPACE="${other_dirs[0]}"
|
||||
else
|
||||
export IMMICH_WORKSPACE="$IMMICH_DIR"
|
||||
fi
|
||||
|
||||
log "Found immich workspace in $IMMICH_WORKSPACE"
|
||||
log ""
|
||||
|
||||
fix_permissions() {
|
||||
|
||||
log "Fixing permissions for ${IMMICH_WORKSPACE}"
|
||||
|
||||
run_cmd sudo find "${IMMICH_WORKSPACE}/server/upload" -not -path "${IMMICH_WORKSPACE}/server/upload/postgres/*" -not -path "${IMMICH_WORKSPACE}/server/upload/postgres" -exec chown node {} +
|
||||
|
||||
# Change ownership for directories that exist
|
||||
for dir in "${IMMICH_WORKSPACE}/.vscode" \
|
||||
"${IMMICH_WORKSPACE}/cli/node_modules" \
|
||||
"${IMMICH_WORKSPACE}/e2e/node_modules" \
|
||||
"${IMMICH_WORKSPACE}/open-api/typescript-sdk/node_modules" \
|
||||
"${IMMICH_WORKSPACE}/server/node_modules" \
|
||||
"${IMMICH_WORKSPACE}/server/dist" \
|
||||
"${IMMICH_WORKSPACE}/web/node_modules" \
|
||||
"${IMMICH_WORKSPACE}/web/dist"; do
|
||||
if [ -d "$dir" ]; then
|
||||
run_cmd sudo chown node -R "$dir"
|
||||
fi
|
||||
done
|
||||
|
||||
log ""
|
||||
}
|
||||
|
||||
install_dependencies() {
|
||||
|
||||
log "Installing dependencies"
|
||||
(
|
||||
cd "${IMMICH_WORKSPACE}" || exit 1
|
||||
run_cmd make ci-server
|
||||
run_cmd make ci-sdk
|
||||
run_cmd make build-sdk
|
||||
run_cmd make ci-web
|
||||
)
|
||||
log ""
|
||||
}
|
||||
49
.devcontainer/server/container-compose-overrides.yml
Normal file
49
.devcontainer/server/container-compose-overrides.yml
Normal file
@@ -0,0 +1,49 @@
|
||||
services:
|
||||
immich-server:
|
||||
build:
|
||||
target: dev-container-server
|
||||
env_file: !reset []
|
||||
hostname: immich-dev
|
||||
environment:
|
||||
- IMMICH_SERVER_URL=http://127.0.0.1:2283/
|
||||
volumes: !override
|
||||
- ..:/workspaces/immich
|
||||
- cli_node_modules:/workspaces/immich/cli/node_modules
|
||||
- e2e_node_modules:/workspaces/immich/e2e/node_modules
|
||||
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
|
||||
- server_node_modules:/workspaces/immich/server/node_modules
|
||||
- web_node_modules:/workspaces/immich/web/node_modules
|
||||
- ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/workspaces/immich/server/upload
|
||||
- ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/workspaces/immich/server/upload/upload
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
|
||||
immich-web:
|
||||
env_file: !reset []
|
||||
|
||||
immich-machine-learning:
|
||||
env_file: !reset []
|
||||
|
||||
database:
|
||||
env_file: !reset []
|
||||
environment: !override
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD-postgres}
|
||||
POSTGRES_USER: ${DB_USERNAME-postgres}
|
||||
POSTGRES_DB: ${DB_DATABASE_NAME-immich}
|
||||
POSTGRES_INITDB_ARGS: '--data-checksums'
|
||||
POSTGRES_HOST_AUTH_METHOD: md5
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION:-postgres-devcontainer-volume}${UPLOAD_LOCATION:+/postgres}:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
env_file: !reset []
|
||||
|
||||
volumes:
|
||||
# Node modules for each service to avoid conflicts and ensure consistent dependencies
|
||||
cli_node_modules:
|
||||
e2e_node_modules:
|
||||
open_api_node_modules:
|
||||
server_node_modules:
|
||||
web_node_modules:
|
||||
upload1-devcontainer-volume:
|
||||
upload2-devcontainer-volume:
|
||||
postgres-devcontainer-volume:
|
||||
17
.devcontainer/server/container-start-backend.sh
Executable file
17
.devcontainer/server/container-start-backend.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
# shellcheck source=common.sh
|
||||
# shellcheck disable=SC1091
|
||||
source /immich-devcontainer/container-common.sh
|
||||
|
||||
log "Starting Nest API Server"
|
||||
log ""
|
||||
cd "${IMMICH_WORKSPACE}/server" || (
|
||||
log "Immich workspace not found"
|
||||
exit 1
|
||||
)
|
||||
|
||||
while true; do
|
||||
run_cmd node ./node_modules/.bin/nest start --debug "0.0.0.0:9230" --watch
|
||||
log "Nest API Server crashed with exit code $?. Respawning in 3s ..."
|
||||
sleep 3
|
||||
done
|
||||
22
.devcontainer/server/container-start-frontend.sh
Executable file
22
.devcontainer/server/container-start-frontend.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
# shellcheck source=common.sh
|
||||
# shellcheck disable=SC1091
|
||||
source /immich-devcontainer/container-common.sh
|
||||
|
||||
log "Starting Immich Web Frontend"
|
||||
log ""
|
||||
cd "${IMMICH_WORKSPACE}/web" || (
|
||||
log "Immich Workspace not found"
|
||||
exit 1
|
||||
)
|
||||
|
||||
until curl --output /dev/null --silent --head --fail "http://127.0.0.1:${IMMICH_PORT}/api/server/config"; do
|
||||
log "Waiting for api server..."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
while true; do
|
||||
run_cmd node ./node_modules/.bin/vite dev --host 0.0.0.0 --port "${DEV_PORT}"
|
||||
log "Web crashed with exit code $?. Respawning in 3s ..."
|
||||
sleep 3
|
||||
done
|
||||
20
.devcontainer/server/container-start.sh
Executable file
20
.devcontainer/server/container-start.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# shellcheck source=common.sh
|
||||
# shellcheck disable=SC1091
|
||||
source /immich-devcontainer/container-common.sh
|
||||
|
||||
log "Setting up Immich dev container..."
|
||||
fix_permissions
|
||||
|
||||
log "Installing npm dependencies (node_modules)..."
|
||||
install_dependencies
|
||||
|
||||
log "Setup complete, please wait while backend and frontend services automatically start"
|
||||
log
|
||||
log "If necessary, the services may be manually started using"
|
||||
log
|
||||
log "$ /immich-devcontainer/container-start-backend.sh"
|
||||
log "$ /immich-devcontainer/container-start-frontend.sh"
|
||||
log
|
||||
log "From different terminal windows, as these scripts automatically restart the server"
|
||||
log "on error, and will continuously run in a loop"
|
||||
@@ -6,7 +6,11 @@ design/
|
||||
docker/
|
||||
!docker/scripts
|
||||
docs/
|
||||
!docs/package.json
|
||||
!docs/package-lock.json
|
||||
e2e/
|
||||
!e2e/package.json
|
||||
!e2e/package-lock.json
|
||||
fastlane/
|
||||
machine-learning/
|
||||
misc/
|
||||
|
||||
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -9,6 +9,9 @@ mobile/lib/**/*.g.dart linguist-generated=true
|
||||
mobile/lib/**/*.drift.dart -diff -merge
|
||||
mobile/lib/**/*.drift.dart linguist-generated=true
|
||||
|
||||
mobile/drift_schemas/main/drift_schema_*.json -diff -merge
|
||||
mobile/drift_schemas/main/drift_schema_*.json linguist-generated=true
|
||||
|
||||
open-api/typescript-sdk/fetch-client.ts -diff -merge
|
||||
open-api/typescript-sdk/fetch-client.ts linguist-generated=true
|
||||
|
||||
|
||||
2
.github/.nvmrc
vendored
2
.github/.nvmrc
vendored
@@ -1 +1 @@
|
||||
22.15.0
|
||||
22.16.0
|
||||
|
||||
@@ -14,7 +14,6 @@ body:
|
||||
label: I have searched the existing feature requests, both open and closed, to make sure this is not a duplicate request.
|
||||
options:
|
||||
- label: 'Yes'
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: feature
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -6,7 +6,6 @@ body:
|
||||
label: I have searched the existing issues, both open and closed, to make sure this is not a duplicate report.
|
||||
options:
|
||||
- label: 'Yes'
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
118
.github/actions/image-build/action.yml
vendored
118
.github/actions/image-build/action.yml
vendored
@@ -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@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.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
|
||||
76
.github/workflows/build-mobile.yml
vendored
76
.github/workflows/build-mobile.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
contents: read
|
||||
# Skip when PR from a fork
|
||||
if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' && needs.pre-job.outputs.should_run == 'true' }}
|
||||
runs-on: macos-14
|
||||
runs-on: mich
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@@ -66,24 +66,46 @@ jobs:
|
||||
ref: ${{ inputs.ref || github.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
cache: 'gradle'
|
||||
|
||||
- name: Setup Flutter SDK
|
||||
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version-file: ./mobile/pubspec.yaml
|
||||
cache: true
|
||||
- name: Install missing deps
|
||||
run: |
|
||||
sudo add-apt-repository ppa:rmescandon/yq
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y yq xz-utils ninja-build zstd
|
||||
|
||||
- name: Create the Keystore
|
||||
env:
|
||||
KEY_JKS: ${{ secrets.KEY_JKS }}
|
||||
working-directory: ./mobile
|
||||
run: echo $KEY_JKS | base64 -d > android/key.jks
|
||||
run: printf "%s" $KEY_JKS | base64 -d > android/key.jks
|
||||
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
- name: Restore Gradle Cache
|
||||
id: cache-gradle-restore
|
||||
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
~/.android/sdk
|
||||
mobile/android/.gradle
|
||||
mobile/.dart_tool
|
||||
key: build-mobile-gradle-${{ runner.os }}-main
|
||||
|
||||
- name: Setup Flutter SDK
|
||||
uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version-file: ./mobile/pubspec.yaml
|
||||
cache: true
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2
|
||||
with:
|
||||
packages: ''
|
||||
|
||||
- name: Get Packages
|
||||
working-directory: ./mobile
|
||||
@@ -93,18 +115,40 @@ jobs:
|
||||
run: make translation
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Generate platform APIs
|
||||
run: make pigeon
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Build Android App Bundle
|
||||
working-directory: ./mobile
|
||||
env:
|
||||
ALIAS: ${{ secrets.ALIAS }}
|
||||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
|
||||
IS_MAIN: ${{ github.ref == 'refs/heads/main' }}
|
||||
run: |
|
||||
flutter build apk --release
|
||||
flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
|
||||
if [[ $IS_MAIN == 'true' ]]; then
|
||||
flutter build apk --release
|
||||
flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
|
||||
else
|
||||
flutter build apk --debug --split-per-abi --target-platform android-arm64
|
||||
fi
|
||||
|
||||
- name: Publish Android Artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: release-apk-signed
|
||||
path: mobile/build/app/outputs/flutter-apk/*.apk
|
||||
|
||||
- name: Save Gradle Cache
|
||||
id: cache-gradle-save
|
||||
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
if: github.ref == 'refs/heads/main'
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
~/.android/sdk
|
||||
mobile/android/.gradle
|
||||
mobile/.dart_tool
|
||||
key: ${{ steps.cache-gradle-restore.outputs.cache-primary-key }}
|
||||
|
||||
7
.github/workflows/cli.yml
vendored
7
.github/workflows/cli.yml
vendored
@@ -38,6 +38,9 @@ jobs:
|
||||
with:
|
||||
node-version-file: './cli/.nvmrc'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Prepare SDK
|
||||
run: npm ci --prefix ../open-api/typescript-sdk/
|
||||
- name: Build SDK
|
||||
@@ -67,7 +70,7 @@ jobs:
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
@@ -96,7 +99,7 @@ jobs:
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
file: cli/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
|
||||
uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# 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).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
|
||||
uses: github/codeql-action/autobuild@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
|
||||
# ℹ️ 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
|
||||
@@ -76,6 +76,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
|
||||
uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
|
||||
24
.github/workflows/docker.yml
vendored
24
.github/workflows/docker.yml
vendored
@@ -131,7 +131,7 @@ jobs:
|
||||
tag-suffix: '-rocm'
|
||||
platforms: linux/amd64
|
||||
runner-mapping: '{"linux/amd64": "mich"}'
|
||||
uses: ./.github/workflows/multi-runner-build.yml
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@094bfb927b8cd75b343abaac27b3241be0fccfe9 # multi-runner-build-workflow-0.1.0
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
name: Build and Push Server
|
||||
needs: pre-job
|
||||
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
||||
uses: ./.github/workflows/multi-runner-build.yml
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@094bfb927b8cd75b343abaac27b3241be0fccfe9 # multi-runner-build-workflow-0.1.0
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
@@ -177,13 +177,9 @@ jobs:
|
||||
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) }}"
|
||||
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
|
||||
with:
|
||||
needs: ${{ toJSON(needs) }}
|
||||
|
||||
success-check-ml:
|
||||
name: Docker Build & Push ML Success
|
||||
@@ -192,10 +188,6 @@ jobs:
|
||||
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) }}"
|
||||
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
|
||||
with:
|
||||
needs: ${{ toJSON(needs) }}
|
||||
|
||||
2
.github/workflows/docs-build.yml
vendored
2
.github/workflows/docs-build.yml
vendored
@@ -57,6 +57,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './docs/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run npm install
|
||||
run: npm ci
|
||||
|
||||
6
.github/workflows/docs-deploy.yml
vendored
6
.github/workflows/docs-deploy.yml
vendored
@@ -150,7 +150,7 @@ jobs:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
|
||||
uses: gruntwork-io/terragrunt-action@9559e51d05873b0ea467c42bbabcb5c067642ccc # v2.1.5
|
||||
uses: gruntwork-io/terragrunt-action@aee21a7df999be8b471c2a8564c6cd853cb674e1 # v2.1.8
|
||||
with:
|
||||
tg_version: '0.58.12'
|
||||
tofu_version: '1.7.1'
|
||||
@@ -165,7 +165,7 @@ jobs:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
|
||||
uses: gruntwork-io/terragrunt-action@9559e51d05873b0ea467c42bbabcb5c067642ccc # v2.1.5
|
||||
uses: gruntwork-io/terragrunt-action@aee21a7df999be8b471c2a8564c6cd853cb674e1 # v2.1.8
|
||||
with:
|
||||
tg_version: '0.58.12'
|
||||
tofu_version: '1.7.1'
|
||||
@@ -199,7 +199,7 @@ jobs:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
|
||||
uses: gruntwork-io/terragrunt-action@9559e51d05873b0ea467c42bbabcb5c067642ccc # v2.1.5
|
||||
uses: gruntwork-io/terragrunt-action@aee21a7df999be8b471c2a8564c6cd853cb674e1 # v2.1.8
|
||||
with:
|
||||
tg_version: '0.58.12'
|
||||
tofu_version: '1.7.1'
|
||||
|
||||
2
.github/workflows/docs-destroy.yml
vendored
2
.github/workflows/docs-destroy.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
|
||||
uses: gruntwork-io/terragrunt-action@9559e51d05873b0ea467c42bbabcb5c067642ccc # v2.1.5
|
||||
uses: gruntwork-io/terragrunt-action@aee21a7df999be8b471c2a8564c6cd853cb674e1 # v2.1.8
|
||||
with:
|
||||
tg_version: '0.58.12'
|
||||
tofu_version: '1.7.1'
|
||||
|
||||
2
.github/workflows/fix-format.yml
vendored
2
.github/workflows/fix-format.yml
vendored
@@ -32,6 +32,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Fix formatting
|
||||
run: make install-all && make format-all
|
||||
|
||||
185
.github/workflows/multi-runner-build.yml
vendored
185
.github/workflows/multi-runner-build.yml
vendored
@@ -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@95815c38cf2ff2164869cbab79da8d1f422bc89e # 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
.github/workflows/pr-label-validation.yml
vendored
2
.github/workflows/pr-label-validation.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Require PR to have a changelog label
|
||||
uses: mheap/github-action-required-labels@388fd6af37b34cdfe5a23b37060e763217e58b03 # v5.5.0
|
||||
uses: mheap/github-action-required-labels@fb29a14a076b0f74099f6198f77750e8fc236016 # v5.5.0
|
||||
with:
|
||||
mode: exactly
|
||||
count: 1
|
||||
|
||||
2
.github/workflows/prepare-release.yml
vendored
2
.github/workflows/prepare-release.yml
vendored
@@ -100,7 +100,7 @@ jobs:
|
||||
name: release-apk-signed
|
||||
|
||||
- name: Create draft release
|
||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2
|
||||
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
|
||||
with:
|
||||
draft: true
|
||||
tag_name: ${{ env.IMMICH_VERSION }}
|
||||
|
||||
2
.github/workflows/sdk.yml
vendored
2
.github/workflows/sdk.yml
vendored
@@ -25,6 +25,8 @@ jobs:
|
||||
with:
|
||||
node-version-file: './open-api/typescript-sdk/.nvmrc'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
- name: Install deps
|
||||
run: npm ci
|
||||
- name: Build
|
||||
|
||||
22
.github/workflows/static_analysis.yml
vendored
22
.github/workflows/static_analysis.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Flutter SDK
|
||||
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0
|
||||
uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version-file: ./mobile/pubspec.yaml
|
||||
@@ -58,14 +58,26 @@ jobs:
|
||||
run: dart pub get
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Install DCM
|
||||
run: |
|
||||
sudo apt-get update
|
||||
wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg
|
||||
echo 'deb [signed-by=/usr/share/keyrings/dcm.gpg arch=amd64] https://dcm.dev/debian stable main' | sudo tee /etc/apt/sources.list.d/dart_stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install dcm
|
||||
|
||||
- name: Generate translation file
|
||||
run: make translation; dart format lib/generated/codegen_loader.g.dart
|
||||
run: make translation
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Run Build Runner
|
||||
run: make build
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Generate platform API
|
||||
run: make pigeon
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Find file changes
|
||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
||||
id: verify-changed-files
|
||||
@@ -96,6 +108,10 @@ jobs:
|
||||
run: dart run custom_lint
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Run DCM
|
||||
run: dcm analyze lib --fatal-style --fatal-warnings
|
||||
working-directory: ./mobile
|
||||
|
||||
zizmor:
|
||||
name: zizmor
|
||||
runs-on: ubuntu-latest
|
||||
@@ -118,7 +134,7 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload SARIF file
|
||||
uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
|
||||
uses: github/codeql-action/upload-sarif@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
category: zizmor
|
||||
|
||||
58
.github/workflows/test.yml
vendored
58
.github/workflows/test.yml
vendored
@@ -84,6 +84,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run npm install
|
||||
run: npm ci
|
||||
@@ -101,7 +103,7 @@ jobs:
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run small tests & coverage
|
||||
run: npm run test:cov
|
||||
run: npm test
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
cli-unit-tests:
|
||||
@@ -125,6 +127,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './cli/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Setup typescript-sdk
|
||||
run: npm ci && npm run build
|
||||
@@ -146,7 +150,7 @@ jobs:
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run unit tests & coverage
|
||||
run: npm run test:cov
|
||||
run: npm run test
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
cli-unit-tests-win:
|
||||
@@ -170,6 +174,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './cli/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Setup typescript-sdk
|
||||
run: npm ci && npm run build
|
||||
@@ -184,7 +190,7 @@ jobs:
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run unit tests & coverage
|
||||
run: npm run test:cov
|
||||
run: npm run test
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
web-lint:
|
||||
@@ -208,6 +214,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './web/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run setup typescript-sdk
|
||||
run: npm ci && npm run build
|
||||
@@ -249,6 +257,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './web/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run setup typescript-sdk
|
||||
run: npm ci && npm run build
|
||||
@@ -262,7 +272,7 @@ jobs:
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run unit tests & coverage
|
||||
run: npm run test:cov
|
||||
run: npm run test
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
i18n-tests:
|
||||
@@ -282,6 +292,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './web/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm --prefix=web ci
|
||||
@@ -326,6 +338,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './e2e/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run setup typescript-sdk
|
||||
run: npm ci && npm run build
|
||||
@@ -369,6 +383,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run npm install
|
||||
run: npm ci
|
||||
@@ -402,6 +418,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './e2e/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run setup typescript-sdk
|
||||
run: npm ci && npm run build
|
||||
@@ -450,6 +468,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './e2e/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run setup typescript-sdk
|
||||
run: npm ci && npm run build
|
||||
@@ -461,7 +481,7 @@ jobs:
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps chromium
|
||||
run: npx playwright install chromium --only-shell
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Docker build
|
||||
@@ -479,13 +499,9 @@ jobs:
|
||||
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) }}"
|
||||
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
|
||||
with:
|
||||
needs: ${{ toJSON(needs) }}
|
||||
|
||||
mobile-unit-tests:
|
||||
name: Unit Test Mobile
|
||||
@@ -500,10 +516,15 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Flutter SDK
|
||||
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0
|
||||
uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version-file: ./mobile/pubspec.yaml
|
||||
|
||||
- name: Generate translation file
|
||||
run: make translation
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Run tests
|
||||
working-directory: ./mobile
|
||||
run: flutter test -j 1
|
||||
@@ -567,6 +588,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './.github/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run npm install
|
||||
run: npm ci
|
||||
@@ -586,7 +609,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run ShellCheck
|
||||
uses: ludeeus/action-shellcheck@master
|
||||
uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0
|
||||
with:
|
||||
ignore_paths: >-
|
||||
**/open-api/**
|
||||
@@ -608,6 +631,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Install server dependencies
|
||||
run: npm --prefix=server ci
|
||||
@@ -643,7 +668,7 @@ jobs:
|
||||
contents: read
|
||||
services:
|
||||
postgres:
|
||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3@sha256:1f5583fe3397210a0fbc7f11b0cec18bacc4a99e3e8ea0548e9bd6bcf26ec37a
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
@@ -669,6 +694,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Install server dependencies
|
||||
run: npm ci
|
||||
@@ -721,6 +748,7 @@ jobs:
|
||||
run: |
|
||||
echo "ERROR: Generated SQL files not up to date!"
|
||||
echo "Changed files: ${CHANGED_FILES}"
|
||||
git diff
|
||||
exit 1
|
||||
|
||||
# mobile-integration-tests:
|
||||
|
||||
10
.github/workflows/weblate-lock.yml
vendored
10
.github/workflows/weblate-lock.yml
vendored
@@ -52,10 +52,6 @@ jobs:
|
||||
permissions: {}
|
||||
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) }}"
|
||||
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
|
||||
with:
|
||||
needs: ${{ toJSON(needs) }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,6 +3,7 @@
|
||||
.DS_Store
|
||||
.vscode/*
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
|
||||
docker/upload
|
||||
|
||||
10
.vscode/extensions.json
vendored
Normal file
10
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"svelte.svelte-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"dart-code.flutter",
|
||||
"dart-code.dart-code",
|
||||
"dcmdev.dcm-vscode-extension"
|
||||
]
|
||||
}
|
||||
72
.vscode/tasks.json
vendored
Normal file
72
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Fix Permissions, Install Dependencies",
|
||||
"type": "shell",
|
||||
"command": "[ -f /immich-devcontainer/container-start.sh ] && /immich-devcontainer/container-start.sh || exit 0",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "dedicated",
|
||||
"showReuseMessage": true,
|
||||
"clear": false,
|
||||
"group": "Devcontainer tasks",
|
||||
"close": true
|
||||
},
|
||||
"runOptions": {
|
||||
"runOn": "default"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Immich API Server (Nest)",
|
||||
"dependsOn": ["Fix Permissions, Install Dependencies"],
|
||||
"type": "shell",
|
||||
"command": "[ -f /immich-devcontainer/container-start-backend.sh ] && /immich-devcontainer/container-start-backend.sh || exit 0",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "dedicated",
|
||||
"showReuseMessage": true,
|
||||
"clear": false,
|
||||
"group": "Devcontainer tasks",
|
||||
"close": true
|
||||
},
|
||||
"runOptions": {
|
||||
"runOn": "default"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Immich Web Server (Vite)",
|
||||
"dependsOn": ["Fix Permissions, Install Dependencies"],
|
||||
"type": "shell",
|
||||
"command": "[ -f /immich-devcontainer/container-start-frontend.sh ] && /immich-devcontainer/container-start-frontend.sh || exit 0",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "dedicated",
|
||||
"showReuseMessage": true,
|
||||
"clear": false,
|
||||
"group": "Devcontainer tasks",
|
||||
"close": true
|
||||
},
|
||||
"runOptions": {
|
||||
"runOn": "default"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Immich Server and Web",
|
||||
"dependsOn": ["Immich Web Server (Vite)", "Immich API Server (Nest)"],
|
||||
"runOptions": {
|
||||
"runOn": "folderOpen"
|
||||
},
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
5
Makefile
5
Makefile
@@ -48,6 +48,8 @@ audit-%:
|
||||
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) audit fix
|
||||
install-%:
|
||||
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) i
|
||||
ci-%:
|
||||
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) ci
|
||||
build-cli: build-sdk
|
||||
build-web: build-sdk
|
||||
build-%: install-%
|
||||
@@ -81,7 +83,8 @@ test-medium-dev:
|
||||
docker exec -it immich_server /bin/sh -c "npm run test:medium"
|
||||
|
||||
build-all: $(foreach M,$(filter-out e2e .github,$(MODULES)),build-$M) ;
|
||||
install-all: $(foreach M,$(MODULES),install-$M) ;
|
||||
install-all: $(foreach M,$(MODULES),install-$M) ;
|
||||
ci-all: $(foreach M,$(filter-out .github,$(MODULES)),ci-$M) ;
|
||||
check-all: $(foreach M,$(filter-out sdk cli docs .github,$(MODULES)),check-$M) ;
|
||||
lint-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),lint-$M) ;
|
||||
format-all: $(foreach M,$(filter-out sdk,$(MODULES)),format-$M) ;
|
||||
|
||||
@@ -1 +1 @@
|
||||
22.15.0
|
||||
22.16.0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:22.15.0-alpine3.20@sha256:686b8892b69879ef5bfd6047589666933508f9a5451c67320df3070ba0e9807b AS core
|
||||
FROM node:22.16.0-alpine3.20@sha256:2289fb1fba0f4633b08ec47b94a89c7e20b829fc5679f9b7b298eaa2f1ed8b7e AS core
|
||||
|
||||
WORKDIR /usr/src/open-api/typescript-sdk
|
||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||
|
||||
1502
cli/package-lock.json
generated
1502
cli/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.65",
|
||||
"version": "2.2.72",
|
||||
"description": "Command Line Interface (CLI) for Immich",
|
||||
"type": "module",
|
||||
"exports": "./dist/index.js",
|
||||
@@ -21,7 +21,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.15.16",
|
||||
"@types/node": "^22.15.32",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
@@ -29,7 +29,7 @@
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^57.0.0",
|
||||
"eslint-plugin-unicorn": "^59.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"prettier": "^3.2.5",
|
||||
@@ -69,6 +69,6 @@
|
||||
"micromatch": "^4.0.8"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.15.0"
|
||||
"node": "22.16.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface UploadOptionsDto {
|
||||
concurrency: number;
|
||||
progress?: boolean;
|
||||
watch?: boolean;
|
||||
jsonOutput?: boolean;
|
||||
}
|
||||
|
||||
class UploadFile extends File {
|
||||
@@ -65,8 +66,14 @@ class UploadFile extends File {
|
||||
const uploadBatch = async (files: string[], options: UploadOptionsDto) => {
|
||||
const { newFiles, duplicates } = await checkForDuplicates(files, options);
|
||||
const newAssets = await uploadFiles(newFiles, options);
|
||||
if (options.jsonOutput) {
|
||||
console.log(JSON.stringify({ newFiles, duplicates, newAssets }, undefined, 4));
|
||||
}
|
||||
await updateAlbums([...newAssets, ...duplicates], options);
|
||||
await deleteFiles(newFiles, options);
|
||||
await deleteFiles(
|
||||
newAssets.map(({ filepath }) => filepath),
|
||||
options,
|
||||
);
|
||||
};
|
||||
|
||||
export const startWatch = async (
|
||||
|
||||
@@ -68,6 +68,11 @@ program
|
||||
.env('IMMICH_UPLOAD_CONCURRENCY')
|
||||
.default(4),
|
||||
)
|
||||
.addOption(
|
||||
new Option('-j, --json-output', 'Output detailed information in json format')
|
||||
.env('IMMICH_JSON_OUTPUT')
|
||||
.default(false),
|
||||
)
|
||||
.addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS'))
|
||||
.addOption(new Option('--no-progress', 'Hide progress bars').env('IMMICH_PROGRESS_BAR').default(true))
|
||||
.addOption(
|
||||
|
||||
@@ -48,7 +48,7 @@ services:
|
||||
IMMICH_THIRD_PARTY_SOURCE_URL: https://github.com/immich-app/immich/
|
||||
IMMICH_THIRD_PARTY_BUG_FEATURE_URL: https://github.com/immich-app/immich/issues
|
||||
IMMICH_THIRD_PARTY_DOCUMENTATION_URL: https://immich.app/docs
|
||||
IMMICH_THIRD_PARTY_SUPPORT_URL: https://immich.app/docs/third-party
|
||||
IMMICH_THIRD_PARTY_SUPPORT_URL: https://immich.app/docs/community-guides
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 1048576
|
||||
@@ -116,13 +116,13 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:8-bookworm@sha256:ff21bc0f8194dc9c105b769aeabf9585fea6a8ed649c0781caeac5cb3c247884
|
||||
image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
|
||||
database:
|
||||
container_name: immich_postgres
|
||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:5f6a838e4e44c8e0e019d0ebfe3ee8952b69afc2809b2c25f7b0119641978e91
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
@@ -134,25 +134,7 @@ services:
|
||||
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
|
||||
ports:
|
||||
- 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
|
||||
|
||||
shm_size: 128mb
|
||||
# set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics
|
||||
# immich-prometheus:
|
||||
# container_name: immich_prometheus
|
||||
|
||||
@@ -56,14 +56,14 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:8-bookworm@sha256:ff21bc0f8194dc9c105b769aeabf9585fea6a8ed649c0781caeac5cb3c247884
|
||||
image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
|
||||
database:
|
||||
container_name: immich_postgres
|
||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:5f6a838e4e44c8e0e019d0ebfe3ee8952b69afc2809b2c25f7b0119641978e91
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
@@ -75,24 +75,7 @@ services:
|
||||
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
|
||||
ports:
|
||||
- 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
|
||||
shm_size: 128mb
|
||||
restart: always
|
||||
|
||||
# set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics
|
||||
@@ -100,7 +83,7 @@ services:
|
||||
container_name: immich_prometheus
|
||||
ports:
|
||||
- 9090:9090
|
||||
image: prom/prometheus@sha256:e2b8aa62b64855956e3ec1e18b4f9387fb6203174a4471936f4662f437f04405
|
||||
image: prom/prometheus@sha256:9abc6cf6aea7710d163dbb28d8eeb7dc5baef01e38fa4cd146a406dd9f07f70d
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus-data:/prometheus
|
||||
@@ -109,10 +92,10 @@ services:
|
||||
# add data source for http://immich-prometheus:9090 to get started
|
||||
immich-grafana:
|
||||
container_name: immich_grafana
|
||||
command: [ './run.sh', '-disable-reporting' ]
|
||||
command: ['./run.sh', '-disable-reporting']
|
||||
ports:
|
||||
- 3000:3000
|
||||
image: grafana/grafana:11.6.1-ubuntu@sha256:6fc273288470ef499dd3c6b36aeade093170d4f608f864c5dd3a7fabeae77b50
|
||||
image: grafana/grafana:12.0.2-ubuntu@sha256:0512d81cdeaaff0e370a9aa66027b465d1f1f04379c3a9c801a905fabbdbc7a5
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
|
||||
|
||||
@@ -49,40 +49,25 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:8-bookworm@sha256:ff21bc0f8194dc9c105b769aeabf9585fea6a8ed649c0781caeac5cb3c247884
|
||||
image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
|
||||
database:
|
||||
container_name: immich_postgres
|
||||
image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:5f6a838e4e44c8e0e019d0ebfe3ee8952b69afc2809b2c25f7b0119641978e91
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_USER: ${DB_USERNAME}
|
||||
POSTGRES_DB: ${DB_DATABASE_NAME}
|
||||
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:
|
||||
# Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file
|
||||
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: >-
|
||||
pg_isready --dbname="$${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
|
||||
shm_size: 128mb
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
|
||||
@@ -1 +1 @@
|
||||
22.15.0
|
||||
22.16.0
|
||||
|
||||
@@ -490,7 +490,7 @@ You can also scan the Postgres database file structure for errors:
|
||||
<details>
|
||||
<summary>Scan for file structure errors</summary>
|
||||
```bash
|
||||
docker exec -it immich_postgres pg_amcheck --username=postgres --heapallindexed --parent-check --rootdescend --progress --all --install-missing
|
||||
docker exec -it immich_postgres pg_amcheck --username=<DB_USERNAME> --heapallindexed --parent-check --rootdescend --progress --all --install-missing
|
||||
```
|
||||
|
||||
A normal result will end something like this and return with an exit code of `0`:
|
||||
|
||||
@@ -57,7 +57,7 @@ Then please follow the steps in the following section for restoring the database
|
||||
<TabItem value="Linux system" label="Linux system" default>
|
||||
|
||||
```bash title='Backup'
|
||||
docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres | gzip > "/path/to/backup/dump.sql.gz"
|
||||
docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=<DB_USERNAME> | gzip > "/path/to/backup/dump.sql.gz"
|
||||
```
|
||||
|
||||
```bash title='Restore'
|
||||
@@ -79,7 +79,7 @@ docker compose up -d # Start remainder of Immich apps
|
||||
<TabItem value="Windows system (PowerShell)" label="Windows system (PowerShell)">
|
||||
|
||||
```powershell title='Backup'
|
||||
[System.IO.File]::WriteAllLines("C:\absolute\path\to\backup\dump.sql", (docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres))
|
||||
[System.IO.File]::WriteAllLines("C:\absolute\path\to\backup\dump.sql", (docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=<DB_USERNAME>))
|
||||
```
|
||||
|
||||
```powershell title='Restore'
|
||||
@@ -219,3 +219,10 @@ When you turn off the storage template engine, it will leave the assets in `UPLO
|
||||
Do not touch the files inside these folders under any circumstances except taking a backup. Changing or removing an asset can cause untracked and missing files.
|
||||
You can think of it as App-Which-Must-Not-Be-Named, the only access to viewing, changing and deleting assets is only through the mobile or browser interface.
|
||||
:::
|
||||
|
||||
## Backup ordering
|
||||
|
||||
A backup of Immich should contain both the database and the asset files. When backing these up it's possible for them to get out of sync, potentially resulting in broken assets after you restore.
|
||||
The best way of dealing with this is to stop the immich-server container while you take a backup. If nothing is changing then the backup will always be in sync.
|
||||
|
||||
If stopping the container is not an option, then the recommended order is to back up the database first, and the filesystem second. This way, the worst case scenario is that there are files on the filesystem that the database doesn't know about. If necessary, these can be (re)uploaded manually after a restore. If the backup is done the other way around, with the filesystem first and the database second, it's possible for the restored database to reference files that aren't in the filesystem backup, thus resulting in broken assets.
|
||||
|
||||
@@ -93,6 +93,7 @@ The `.well-known/openid-configuration` part of the url is optional and will be a
|
||||
## Auto Launch
|
||||
|
||||
When Auto Launch is enabled, the login page will automatically redirect the user to the OAuth authorization url, to login with OAuth. To access the login screen again, use the browser's back button, or navigate directly to `/auth/login?autoLaunch=0`.
|
||||
Auto Launch can also be enabled on a per-request basis by navigating to `/auth/login?authLaunch=1`, this can be useful in situations where Immich is called from e.g. Nextcloud using the _External sites_ app and the _oidc_ app so as to enable users to directly interact with a logged-in instance of Immich.
|
||||
|
||||
## Mobile Redirect URI
|
||||
|
||||
|
||||
@@ -10,12 +10,16 @@ Running with a pre-existing Postgres server can unlock powerful administrative f
|
||||
|
||||
## Prerequisites
|
||||
|
||||
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'`.
|
||||
You must install `pgvector` (`>= 0.7.0, < 1.0.0`), as it is a prerequisite for `vchord`.
|
||||
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
|
||||
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.
|
||||
Immich is known to work with Postgres versions `>= 14, < 18`.
|
||||
|
||||
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`.
|
||||
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.5.0`.
|
||||
:::
|
||||
|
||||
## Specifying the connection URL
|
||||
@@ -53,21 +57,99 @@ CREATE DATABASE <immichdatabasename>;
|
||||
\c <immichdatabasename>
|
||||
BEGIN;
|
||||
ALTER DATABASE <immichdatabasename> OWNER TO <immichdbusername>;
|
||||
CREATE EXTENSION vectors;
|
||||
CREATE EXTENSION vchord CASCADE;
|
||||
CREATE EXTENSION earthdistance CASCADE;
|
||||
ALTER DATABASE <immichdatabasename> SET search_path TO "$user", public, vectors;
|
||||
ALTER SCHEMA vectors OWNER TO <immichdbusername>;
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
### Updating pgvecto.rs
|
||||
### Updating VectorChord
|
||||
|
||||
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;`.
|
||||
When installing a new version of VectorChord, you will need to manually update the extension and reindex by connecting to the Immich database and running:
|
||||
|
||||
### Common errors
|
||||
```
|
||||
ALTER EXTENSION vchord UPDATE;
|
||||
REINDEX INDEX face_index;
|
||||
REINDEX INDEX clip_index;
|
||||
```
|
||||
|
||||
#### Permission denied for view
|
||||
## Migrating to VectorChord
|
||||
|
||||
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>;`.
|
||||
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.
|
||||
|
||||
[vectors-install]: https://docs.vectorchord.ai/getting-started/installation.html
|
||||
### Migrating from pgvecto.rs
|
||||
|
||||
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.
|
||||
|
||||
The easiest option is to have both extensions installed during the migration:
|
||||
|
||||
<details>
|
||||
<summary>Migration steps (automatic)</summary>
|
||||
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`
|
||||
|
||||
</details>
|
||||
|
||||
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:
|
||||
|
||||
<details>
|
||||
<summary>Migration steps (manual)</summary>
|
||||
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
|
||||
|
||||
</details>
|
||||
|
||||
### Migrating from pgvector
|
||||
|
||||
<details>
|
||||
<summary>Migration steps</summary>
|
||||
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
|
||||
|
||||
</details>
|
||||
|
||||
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
|
||||
|
||||
481
docs/docs/developer/devcontainers.md
Normal file
481
docs/docs/developer/devcontainers.md
Normal file
@@ -0,0 +1,481 @@
|
||||
---
|
||||
title: Devcontainers
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Development with Dev Containers
|
||||
|
||||
Dev Containers provide a consistent, reproducible development environment using Docker containers. With a single click, you can get started with an Immich development environment on Mac, Linux, Windows, or in the cloud using GitHub Codespaces.
|
||||
|
||||
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/immich-app/immich/)
|
||||
|
||||
[](https://codespaces.new/immich-app/immich/)
|
||||
|
||||
[Learn more about Dev Containers](https://docs.github.com/en/codespaces/setting-up-your-project-for-codespaces/adding-a-dev-container-configuration/introduction-to-dev-containers)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before getting started, ensure you have:
|
||||
|
||||
- **Docker Desktop** (latest version)
|
||||
- [Mac](https://docs.docker.com/desktop/install/mac-install/)
|
||||
- [Windows](https://docs.docker.com/desktop/install/windows-install/) (with WSL2 backend recommended)
|
||||
- [Linux](https://docs.docker.com/desktop/install/linux-install/)
|
||||
- **Visual Studio Code** with the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
|
||||
- **Git** for cloning the repository
|
||||
- At least **8GB of RAM** (16GB recommended)
|
||||
- **20GB of free disk space**
|
||||
|
||||
:::tip Alternative Development Environments
|
||||
While this guide focuses on VS Code, you have many options for Dev Container development:
|
||||
|
||||
**Local Editors:**
|
||||
|
||||
- [IntelliJ IDEA](https://www.jetbrains.com/help/idea/connect-to-devcontainer.html) - Full JetBrains IDE support
|
||||
- [neovim](https://github.com/jamestthompson3/nvim-remote-containers) - Lightweight terminal-based editor
|
||||
- [Emacs](https://github.com/emacs-lsp/lsp-docker) - Extensible text editor
|
||||
- [DevContainer CLI](https://github.com/devcontainers/cli) - Command-line interface
|
||||
|
||||
**Cloud-Based Solutions:**
|
||||
|
||||
- [GitHub Codespaces](https://github.com/features/codespaces) - Fully integrated with GitHub, excellent devcontainer.json support
|
||||
- [GitPod](https://www.gitpod.io) - SaaS platform with recent Dev Container support (historically used gitpod.yml)
|
||||
|
||||
**Self-Hostable Options:**
|
||||
|
||||
- [Coder](https://coder.com) - Enterprise-focused, requires Terraform knowledge, self-managed
|
||||
- [DevPod](https://devpod.sh) - Client-only tool with excellent devcontainer.json support, works with any provider (local, cloud, or on-premise)
|
||||
:::
|
||||
|
||||
## Dev Container Services
|
||||
|
||||
The Dev Container environment consists of the following services:
|
||||
|
||||
| Service | Container Name | Description | Ports |
|
||||
| ---------------- | ------------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| Server & Web | `immich-server` | Runs both API server and web frontend in development mode | 2283 (API)<br/>3000 (Web)<br/>9230 (Workers Debug)<br/>9231 (API Debug) |
|
||||
| Database | `database` | PostgreSQL database | 5432 |
|
||||
| Cache | `redis` | Valkey cache server | 6379 |
|
||||
| Machine Learning | `immich-machine-learning` | Immich ML model inference server | 3003 |
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Step 1: Clone the Repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/immich-app/immich.git
|
||||
cd immich
|
||||
```
|
||||
|
||||
### Step 2: Configure Environment Variables
|
||||
|
||||
The immich dev containers read environment variables from your shell environment, not from `.env` files. This allows them to work in cloud environments without pre-configuration.
|
||||
|
||||
:::important Required Configuration
|
||||
When running locally, and if you want to create (or use an existing) DB and/or photo storage folder, you must set the `UPLOAD_LOCATION` variable in your shell environment before launching the Dev Container. This determines where uploaded files are stored and also where the DB stores it data.
|
||||
|
||||
```bash
|
||||
# Set temporarily for current session
|
||||
export UPLOAD_LOCATION=/opt/dev_upload_folder
|
||||
|
||||
# Or add to your shell profile for persistence
|
||||
# (~/.bashrc, ~/.zshrc, ~/.bash_profile, etc.)
|
||||
echo 'export UPLOAD_LOCATION=/opt/dev_upload_folder' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Step 3: Launch the Dev Container
|
||||
|
||||
#### Using VS Code UI:
|
||||
|
||||
1. Open the cloned repository in VS Code
|
||||
2. Press `F1` or `Ctrl/Cmd+Shift+P` to open the command palette
|
||||
3. Type and select "Dev Containers: Rebuild and Reopen in Container"
|
||||
4. Select "Immich - Backend, Frontend and ML" from the list
|
||||
5. Wait for the container to build and start (this may take several minutes on first run)
|
||||
|
||||
#### Using VS Code Quick Actions:
|
||||
|
||||
1. Open the repository in VS Code
|
||||
2. You should see a popup asking if you want to reopen in a container
|
||||
3. Click "Reopen in Container"
|
||||
|
||||
#### Using Command Line:
|
||||
|
||||
```bash
|
||||
# Using the DevContainer CLI
|
||||
devcontainer up --workspace-folder .
|
||||
```
|
||||
|
||||
## Environment Variable Details
|
||||
|
||||
### How Dev Containers Handle Environment Variables
|
||||
|
||||
Unlike the Immich developer setup based on Docker Compose which uses `.env` files, Immich Dev Containers read environment variables from your shell environment. This is configured in `.devcontainer/devcontainer.json`:
|
||||
|
||||
```json
|
||||
"remoteEnv": {
|
||||
"UPLOAD_LOCATION": "${localEnv:UPLOAD_LOCATION:./Library}",
|
||||
"DB_PASSWORD": "${localEnv:DB_PASSWORD:postgres}",
|
||||
"DB_USERNAME": "${localEnv:DB_USERNAME:postgres}",
|
||||
"DB_DATABASE_NAME": "${localEnv:DB_DATABASE_NAME:immich}"
|
||||
}
|
||||
```
|
||||
|
||||
The `${localEnv:VARIABLE:default}` syntax reads from your shell environment with optional defaults.
|
||||
|
||||
### Upload Location Path Resolution
|
||||
|
||||
The `UPLOAD_LOCATION` environment variable controls where files are stored:
|
||||
|
||||
**Default:** `./Library` (relative to the `docker` directory)
|
||||
**Resolved to:** `<immich-root>/docker/Library`
|
||||
|
||||
**Bind Mounts Created:**
|
||||
|
||||
```yaml
|
||||
# From .devcontainer/server/container-compose-overrides.yml
|
||||
- ${UPLOAD_LOCATION-./Library}/photos:/workspaces/immich/server/upload
|
||||
- ${UPLOAD_LOCATION-./Library}/postgres:/var/lib/postgresql/data
|
||||
```
|
||||
|
||||
### Database Configuration
|
||||
|
||||
These variables have sensible defaults (for development) but can be customized:
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------ | ---------- | ------------------- |
|
||||
| `DB_PASSWORD` | `postgres` | PostgreSQL password |
|
||||
| `DB_USERNAME` | `postgres` | PostgreSQL username |
|
||||
| `DB_DATABASE_NAME` | `immich` | Database name |
|
||||
|
||||
### Setting Environment Variables
|
||||
|
||||
Add these to your shell profile (`~/.bashrc`, `~/.zshrc`, `~/.bash_profile`, etc.):
|
||||
|
||||
```bash
|
||||
# Required
|
||||
export UPLOAD_LOCATION=./Library # or absolute path
|
||||
|
||||
# Optional (only if using non-default values)
|
||||
export DB_PASSWORD=your_password
|
||||
export DB_USERNAME=your_username
|
||||
export DB_DATABASE_NAME=your_database
|
||||
```
|
||||
|
||||
Remember to reload your shell configuration:
|
||||
|
||||
```bash
|
||||
source ~/.bashrc # or ~/.zshrc, etc.
|
||||
```
|
||||
|
||||
## Git Configuration
|
||||
|
||||
### SSH Keys and Authentication
|
||||
|
||||
To use your SSH keys for GitHub access inside the Dev Container:
|
||||
|
||||
1. **Start SSH Agent** on your host machine:
|
||||
|
||||
```bash
|
||||
eval "$(ssh-agent -s)"
|
||||
ssh-add ~/.ssh/id_rsa # or your key path
|
||||
```
|
||||
|
||||
2. **VS Code automatically forwards your SSH agent** to the container
|
||||
|
||||
For detailed instructions, see the [VS Code guide on sharing Git credentials](https://code.visualstudio.com/remote/advancedcontainers/sharing-git-credentials).
|
||||
|
||||
### Commit Signing
|
||||
|
||||
To use your SSH key for commit signing, see the [GitHub guide on SSH commit signing](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key#telling-git-about-your-ssh-key).
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Automatic Setup
|
||||
|
||||
When the Dev Container starts, it automatically:
|
||||
|
||||
1. **Runs post-create script** (`container-server-post-create.sh`):
|
||||
|
||||
- Adjusts file permissions for the `node` user
|
||||
- Installs dependencies: `npm install` in all packages
|
||||
- Builds TypeScript SDK: `npm run build` in `open-api/typescript-sdk`
|
||||
|
||||
2. **Starts development servers** via VS Code tasks:
|
||||
|
||||
- `Immich API Server (Nest)` - API server with hot-reloading on port 2283
|
||||
- `Immich Web Server (Vite)` - Web frontend with hot-reloading on port 3000
|
||||
- Both servers watch for file changes and recompile automatically
|
||||
|
||||
3. **Configures port forwarding**:
|
||||
- Web UI: http://localhost:3000 (opens automatically)
|
||||
- API: http://localhost:2283
|
||||
- Debug ports: 9230 (workers), 9231 (API)
|
||||
|
||||
:::info
|
||||
The Dev Container setup replaces the `make dev` command from the traditional setup. All services start automatically when you open the container.
|
||||
:::
|
||||
|
||||
### Accessing Services
|
||||
|
||||
Once running, you can access:
|
||||
|
||||
| Service | URL | Description |
|
||||
| -------- | --------------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| Web UI | http://localhost:3000 | Main web interface |
|
||||
| API | http://localhost:2283 | REST API endpoints (Not used directly, web UI will expose this over http://localhost:3000/api) |
|
||||
| Database | localhost:5432 | PostgreSQL (username: `postgres`) (Not used directly) |
|
||||
|
||||
### Connecting Mobile Apps
|
||||
|
||||
To connect the mobile app to your Dev Container:
|
||||
|
||||
1. Find your machine's IP address
|
||||
2. In the mobile app, use: `http://YOUR_IP:3000/api`
|
||||
3. Ensure your firewall allows connections on port 2283
|
||||
|
||||
### Making Code Changes
|
||||
|
||||
- **Server code** (`/server`): Changes trigger automatic restart
|
||||
- **Web code** (`/web`): Changes trigger hot module replacement
|
||||
- **Database migrations**: Run `npm run sync:sql` in the server directory
|
||||
- **API changes**: Regenerate TypeScript SDK with `make open-api`
|
||||
|
||||
## Testing
|
||||
|
||||
### Running Tests
|
||||
|
||||
The Dev Container supports multiple ways to run tests:
|
||||
|
||||
#### Using Make Commands (Recommended)
|
||||
|
||||
```bash
|
||||
# Run tests for specific components
|
||||
make test-server # Server unit tests
|
||||
make test-web # Web unit tests
|
||||
make test-e2e # End-to-end tests
|
||||
make test-cli # CLI tests
|
||||
|
||||
# Run all tests
|
||||
make test-all # Runs tests for all components
|
||||
|
||||
# Medium tests (integration tests)
|
||||
make test-medium-dev # End-to-end tests
|
||||
```
|
||||
|
||||
#### Using NPM Directly
|
||||
|
||||
```bash
|
||||
# Server tests
|
||||
cd /workspaces/immich/server
|
||||
npm test # Run all tests
|
||||
npm run test:watch # Watch mode
|
||||
npm run test:cov # Coverage report
|
||||
|
||||
# Web tests
|
||||
cd /workspaces/immich/web
|
||||
npm test # Run all tests
|
||||
npm run test:watch # Watch mode
|
||||
|
||||
# E2E tests
|
||||
cd /workspaces/immich/e2e
|
||||
npm run test # Run API tests
|
||||
npm run test:web # Run web UI tests
|
||||
```
|
||||
|
||||
### Code Quality Commands
|
||||
|
||||
```bash
|
||||
# Linting
|
||||
make lint-server # Lint server code
|
||||
make lint-web # Lint web code
|
||||
make lint-all # Lint all components
|
||||
|
||||
# Formatting
|
||||
make format-server # Format server code
|
||||
make format-web # Format web code
|
||||
make format-all # Format all code
|
||||
|
||||
# Type checking
|
||||
make check-server # Type check server
|
||||
make check-web # Type check web
|
||||
make check-all # Check all components
|
||||
|
||||
# Complete hygiene check
|
||||
make hygiene-all # Runs lint, format, check, SQL sync, and audit
|
||||
```
|
||||
|
||||
### Additional Make Commands
|
||||
|
||||
```bash
|
||||
# Build commands
|
||||
make build-server # Build server
|
||||
make build-web # Build web app
|
||||
make build-all # Build everything
|
||||
|
||||
# API generation
|
||||
make open-api # Generate OpenAPI specs
|
||||
make open-api-typescript # Generate TypeScript SDK
|
||||
make open-api-dart # Generate Dart SDK
|
||||
|
||||
# Database
|
||||
make sql # Sync database schema
|
||||
|
||||
# Dependencies
|
||||
make install-server # Install server dependencies
|
||||
make install-web # Install web dependencies
|
||||
make install-all # Install all dependencies
|
||||
```
|
||||
|
||||
### Debugging
|
||||
|
||||
The Dev Container is pre-configured for debugging:
|
||||
|
||||
1. **API Server Debugging**:
|
||||
|
||||
- Set breakpoints in VS Code
|
||||
- Press `F5` or use "Run and Debug" panel
|
||||
- Select "Attach to Server" configuration
|
||||
- Debug port: 9231
|
||||
|
||||
2. **Worker Debugging**:
|
||||
|
||||
- Use "Attach to Workers" configuration
|
||||
- Debug port: 9230
|
||||
|
||||
3. **Web Debugging**:
|
||||
- Use browser DevTools
|
||||
- VS Code debugger for Chrome/Edge extensions supported
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Permission Errors
|
||||
|
||||
**Problem**: `EACCES` or permission denied errors
|
||||
**Solution**:
|
||||
|
||||
- The Dev Container runs as the `node` user (UID 1000)
|
||||
- If your host UID differs, you may see permission issues
|
||||
- Try rebuilding the container: "Dev Containers: Rebuild Container"
|
||||
|
||||
#### Container Won't Start
|
||||
|
||||
**Problem**: Dev Container fails to start or build
|
||||
**Solution**:
|
||||
|
||||
1. Check Docker is running: `docker ps`
|
||||
2. Clean Docker resources: `docker system prune -a`
|
||||
3. Check available disk space
|
||||
4. Review Docker Desktop resource limits
|
||||
|
||||
#### Port Already in Use
|
||||
|
||||
**Problem**: "Port 3000/2283 is already in use"
|
||||
**Solution**:
|
||||
|
||||
1. Check for conflicting services: `lsof -i :3000` (macOS/Linux)
|
||||
2. Stop conflicting services or change port mappings
|
||||
3. Restart Docker Desktop
|
||||
|
||||
#### Upload Location Not Set
|
||||
|
||||
**Problem**: Errors about missing UPLOAD_LOCATION
|
||||
**Solution**:
|
||||
|
||||
1. Set the environment variable: `export UPLOAD_LOCATION=./Library`
|
||||
2. Add to your shell profile for persistence
|
||||
3. Restart your terminal and VS Code
|
||||
|
||||
#### Database Connection Failed
|
||||
|
||||
**Problem**: Cannot connect to PostgreSQL
|
||||
**Solution**:
|
||||
|
||||
1. Ensure all containers are running: `docker ps`
|
||||
2. Check logs: "Dev Containers: Show Container Log"
|
||||
3. Verify database credentials match environment variables
|
||||
|
||||
### Getting Help
|
||||
|
||||
If you encounter issues:
|
||||
|
||||
1. Check container logs: View → Output → Select "Dev Containers"
|
||||
2. Rebuild without cache: "Dev Containers: Rebuild Container Without Cache"
|
||||
3. Review [common Docker issues](https://docs.docker.com/desktop/troubleshoot/)
|
||||
4. Ask in [Discord](https://discord.immich.app) `#help-desk-support` channel
|
||||
|
||||
## Mobile Development
|
||||
|
||||
While the Dev Container focuses on server and web development, you can connect mobile apps for testing:
|
||||
|
||||
### Connecting iOS/Android Apps
|
||||
|
||||
1. **Ensure API is accessible**:
|
||||
|
||||
```bash
|
||||
# Find your machine's IP
|
||||
# macOS
|
||||
ipconfig getifaddr en0
|
||||
# Linux
|
||||
hostname -I
|
||||
# Windows (in WSL2)
|
||||
ip addr show eth0
|
||||
```
|
||||
|
||||
2. **Configure mobile app**:
|
||||
|
||||
- Server URL: `http://YOUR_IP:2283/api`
|
||||
- Ensure firewall allows port 2283
|
||||
|
||||
3. **For full mobile development**, see the [mobile development guide](/docs/developer/setup) which covers:
|
||||
- Flutter setup
|
||||
- Running on simulators/devices
|
||||
- Mobile-specific debugging
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Custom VS Code Extensions
|
||||
|
||||
Add extensions to `.devcontainer/devcontainer.json`:
|
||||
|
||||
```json
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"your.extension-id"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Additional Services
|
||||
|
||||
To add services (e.g., Redis Commander), modify:
|
||||
|
||||
1. `/docker/docker-compose.dev.yml` - Add service definition
|
||||
2. `/.devcontainer/server/container-compose-overrides.yml` - Add overrides if needed
|
||||
|
||||
### Resource Limits
|
||||
|
||||
Adjust Docker Desktop resources:
|
||||
|
||||
- **macOS/Windows**: Docker Desktop → Settings → Resources
|
||||
- **Linux**: Modify Docker daemon configuration
|
||||
|
||||
Recommended minimums:
|
||||
|
||||
- CPU: 4 cores
|
||||
- Memory: 8GB
|
||||
- Disk: 20GB
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Read the [architecture overview](/docs/developer/architecture)
|
||||
- Learn about [database migrations](/docs/developer/database-migrations)
|
||||
- Explore [API documentation](/docs/api)
|
||||
- Join `#immich` on [Discord](https://discord.immich.app)
|
||||
@@ -75,11 +75,12 @@ npm run dev
|
||||
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. Build the `@immich/ui` project via `npm run build`
|
||||
1. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yaml` file (`../../ui:/usr/ui`)
|
||||
1. Uncomment the corresponding alias in the `web/vite.config.js` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui')`)
|
||||
1. Start up the stack via `make dev`
|
||||
1. After making changes in `@immich/ui`, rebuild it (`npm run build`)
|
||||
2. 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`)
|
||||
4. 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';`
|
||||
6. Start up the stack via `make dev`
|
||||
7. After making changes in `@immich/ui`, rebuild it (`npm run build`)
|
||||
|
||||
### Mobile app
|
||||
|
||||
@@ -114,32 +115,72 @@ Note: Activating the license is not required.
|
||||
|
||||
### VSCode
|
||||
|
||||
Install `Flutter`, `DCM`, `Prettier`, `ESLint` and `Svelte` extensions.
|
||||
Install `Flutter`, `DCM`, `Prettier`, `ESLint` and `Svelte` extensions. These extensions are listed in the `extensions.json` file under `.vscode/` and should appear as workspace recommendations.
|
||||
|
||||
in User `settings.json` (`cmd + shift + p` and search for `Open User Settings JSON`) add the following:
|
||||
Here are the settings we use, they should be active as workspace settings (`settings.json`):
|
||||
|
||||
```json title="settings.json"
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"[javascript][typescript][css]": {
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.tabSize": 2,
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"[svelte]": {
|
||||
"editor.defaultFormatter": "svelte.svelte-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.tabSize": 2
|
||||
},
|
||||
"svelte.enable-ts-plugin": true,
|
||||
"eslint.validate": ["javascript", "svelte"],
|
||||
"[dart]": {
|
||||
"editor.defaultFormatter": "Dart-Code.dart-code",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.selectionHighlight": false,
|
||||
"editor.suggest.snippetsPreventQuickSuggestions": false,
|
||||
"editor.suggestSelection": "first",
|
||||
"editor.tabCompletion": "onlySnippets",
|
||||
"editor.wordBasedSuggestions": "off",
|
||||
"editor.defaultFormatter": "Dart-Code.dart-code"
|
||||
}
|
||||
"editor.wordBasedSuggestions": "off"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"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.patterns": {
|
||||
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart",
|
||||
"*.ts": "${capture}.spec.ts,${capture}.mock.ts"
|
||||
},
|
||||
"svelte.enable-ts-plugin": true,
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative"
|
||||
}
|
||||
```
|
||||
|
||||
19
docs/docs/features/casting.md
Normal file
19
docs/docs/features/casting.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# 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.
|
||||
|
||||
## Enable Google Cast Support
|
||||
|
||||
Google Cast support is disabled by default. The web UI uses Google-provided scripts and must retreive them from Google servers when the page loads. This is a privacy concern for some and is thus opt-in.
|
||||
|
||||
You can enable Google Cast support through `Account Settings > Features > Cast > Google Cast`
|
||||
|
||||
<img src={require('./img/gcast-enable.webp').default} width="70%" title='Enable Google Cast Support' />
|
||||
|
||||
## 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)
|
||||
@@ -90,19 +90,22 @@ Usage: immich upload [paths...] [options]
|
||||
Upload assets
|
||||
|
||||
Arguments:
|
||||
paths One or more paths to assets to be uploaded
|
||||
paths One or more paths to assets to be uploaded
|
||||
|
||||
Options:
|
||||
-r, --recursive Recursive (default: false, env: IMMICH_RECURSIVE)
|
||||
-i, --ignore [paths...] Paths to ignore (default: [], env: IMMICH_IGNORE_PATHS)
|
||||
-h, --skip-hash Don't hash files before upload (default: false, env: IMMICH_SKIP_HASH)
|
||||
-H, --include-hidden Include hidden folders (default: false, env: IMMICH_INCLUDE_HIDDEN)
|
||||
-a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM)
|
||||
-A, --album-name <name> Add all assets to specified album (env: IMMICH_ALBUM_NAME)
|
||||
-n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN)
|
||||
-c, --concurrency <number> Number of assets to upload at the same time (default: 4, env: IMMICH_UPLOAD_CONCURRENCY)
|
||||
--delete Delete local assets after upload (env: IMMICH_DELETE_ASSETS)
|
||||
--help display help for command
|
||||
-r, --recursive Recursive (default: false, env: IMMICH_RECURSIVE)
|
||||
-i, --ignore <pattern> Pattern to ignore (env: IMMICH_IGNORE_PATHS)
|
||||
-h, --skip-hash Don't hash files before upload (default: false, env: IMMICH_SKIP_HASH)
|
||||
-H, --include-hidden Include hidden folders (default: false, env: IMMICH_INCLUDE_HIDDEN)
|
||||
-a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM)
|
||||
-A, --album-name <name> Add all assets to specified album (env: IMMICH_ALBUM_NAME)
|
||||
-n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN)
|
||||
-c, --concurrency <number> Number of assets to upload at the same time (default: 4, env: IMMICH_UPLOAD_CONCURRENCY)
|
||||
-j, --json-output Output detailed information in json format (default: false, env: IMMICH_JSON_OUTPUT)
|
||||
--delete Delete local assets after upload (env: IMMICH_DELETE_ASSETS)
|
||||
--no-progress Hide progress bars (env: IMMICH_PROGRESS_BAR)
|
||||
--watch Watch for changes and upload automatically (default: false, env: IMMICH_WATCH_CHANGES)
|
||||
--help display help for command
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -172,6 +175,16 @@ By default, hidden files are skipped. If you want to include hidden files, use t
|
||||
immich upload --include-hidden --recursive directory/
|
||||
```
|
||||
|
||||
You can use the `--json-output` option to get a json printed which includes
|
||||
three keys: `newFiles`, `duplicates` and `newAssets`. Due to some logging
|
||||
output you will need to strip the first three lines of output to get the json.
|
||||
For example to get a list of files that would be uploaded for further
|
||||
processing:
|
||||
|
||||
```bash
|
||||
immich upload --dry-run . | tail -n +4 | jq .newFiles[]
|
||||
```
|
||||
|
||||
### Obtain the API Key
|
||||
|
||||
The API key can be obtained in the user setting panel on the web interface.
|
||||
|
||||
BIN
docs/docs/features/img/gcast-enable.webp
Normal file
BIN
docs/docs/features/img/gcast-enable.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -112,12 +112,15 @@ _Remember to run `docker compose up -d` to register the changes. Make sure you c
|
||||
|
||||
These actions must be performed by the Immich administrator.
|
||||
|
||||
- Click on Administration -> Libraries
|
||||
- Click on Create External Library
|
||||
- Click on your avatar on the upper right corner
|
||||
- Click on Administration -> External Libraries
|
||||
- Click on Create an external library…
|
||||
- Select which user owns the library, this can not be changed later
|
||||
- Enter `/mnt/media/christmas-trip` then click Add
|
||||
- Click on Save
|
||||
- Click the drop-down menu on the newly created library
|
||||
- Click on Scan
|
||||
- Click the drop-down menu on the newly created library
|
||||
- Click on Rename Library and rename it to "Christmas Trip"
|
||||
|
||||
NOTE: We have to use the `/mnt/media/christmas-trip` path and not the `/mnt/nas/christmas-trip` path since all paths have to be what the Docker containers see.
|
||||
|
||||
@@ -5,7 +5,7 @@ import TabItem from '@theme/TabItem';
|
||||
|
||||
Immich uses Postgres as its search database for both metadata and contextual CLIP search.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Advanced Search Filters
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 10 KiB |
@@ -52,9 +52,9 @@ REMOTE_BACKUP_PATH="/path/to/remote/backup/directory"
|
||||
### Local
|
||||
|
||||
# Backup Immich database
|
||||
docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres > "$UPLOAD_LOCATION"/database-backup/immich-database.sql
|
||||
docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=<DB_USERNAME> > "$UPLOAD_LOCATION"/database-backup/immich-database.sql
|
||||
# For deduplicating backup programs such as Borg or Restic, compressing the content can increase backup size by making it harder to deduplicate. If you are using a different program or still prefer to compress, you can use the following command instead:
|
||||
# docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres | /usr/bin/gzip --rsyncable > "$UPLOAD_LOCATION"/database-backup/immich-database.sql.gz
|
||||
# docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=<DB_USERNAME> | /usr/bin/gzip --rsyncable > "$UPLOAD_LOCATION"/database-backup/immich-database.sql.gz
|
||||
|
||||
### Append to local Borg repository
|
||||
borg create "$BACKUP_PATH/immich-borg::{now}" "$UPLOAD_LOCATION" --exclude "$UPLOAD_LOCATION"/thumbs/ --exclude "$UPLOAD_LOCATION"/encoded-video/
|
||||
|
||||
@@ -123,7 +123,7 @@ The default configuration looks like this:
|
||||
"buttonText": "Login with OAuth",
|
||||
"clientId": "",
|
||||
"clientSecret": "",
|
||||
"defaultStorageQuota": 0,
|
||||
"defaultStorageQuota": null,
|
||||
"enabled": false,
|
||||
"issuerUrl": "",
|
||||
"mobileOverrideEnabled": false,
|
||||
|
||||
@@ -72,21 +72,21 @@ Information on the current workers can be found [here](/docs/administration/jobs
|
||||
|
||||
## Database
|
||||
|
||||
| Variable | Description | Default | Containers |
|
||||
| :---------------------------------- | :----------------------------------------------------------------------- | :----------: | :----------------------------- |
|
||||
| `DB_URL` | Database URL | | server |
|
||||
| `DB_HOSTNAME` | Database host | `database` | server |
|
||||
| `DB_PORT` | Database port | `5432` | server |
|
||||
| `DB_USERNAME` | Database user | `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_SSL_MODE` | Database SSL mode | | server |
|
||||
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`pgvector`, `pgvecto.rs`]) | `pgvecto.rs` | server |
|
||||
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
|
||||
| Variable | Description | Default | Containers |
|
||||
| :---------------------------------- | :--------------------------------------------------------------------------- | :--------: | :----------------------------- |
|
||||
| `DB_URL` | Database URL | | server |
|
||||
| `DB_HOSTNAME` | Database host | `database` | server |
|
||||
| `DB_PORT` | Database port | `5432` | server |
|
||||
| `DB_USERNAME` | Database user | `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_SSL_MODE` | Database SSL mode | | 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 |
|
||||
|
||||
\*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: This setting cannot be changed after the server has successfully started up.
|
||||
\*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.
|
||||
|
||||
:::info
|
||||
|
||||
|
||||
@@ -39,8 +39,8 @@ alt="Dot Env Example"
|
||||
/>
|
||||
|
||||
- Change the default `DB_PASSWORD`, and add custom database connection information if necessary.
|
||||
- Change `DB_DATA_LOCATION` to a folder where the database will be saved to disk.
|
||||
- Change `UPLOAD_LOCATION` to a folder where media (uploaded and generated) will be stored.
|
||||
- Change `DB_DATA_LOCATION` to a folder (absolute path) where the database will be saved to disk.
|
||||
- Change `UPLOAD_LOCATION` to a folder (absolute path) where media (uploaded and generated) will be stored.
|
||||
|
||||
11. Click on "**Deploy the stack**".
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ When you're all done, you should have the following:
|
||||
- `./docker/immich-app/postgres`
|
||||
- `./docker/immich-app/library`
|
||||
|
||||
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) to your computer. Upload the files to the `./docker/immich-app` directory.
|
||||
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) to your computer. Upload the files to the `./docker/immich-app` directory, and rename `example.env` to `.env`.
|
||||
|
||||
## Step 2 - Populate the .env file with custom values
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ This is a community contribution and not officially supported by the Immich team
|
||||
|
||||
Community support can be found in the dedicated channel on the [Discord Server](https://discord.immich.app/).
|
||||
|
||||
**Please report app issues to the corresponding [Github Repository](https://github.com/truenas/charts/tree/master/community/immich).**
|
||||
**Please report app issues to the corresponding [Github Repository](https://github.com/truenas/apps/tree/master/trains/community/immich).**
|
||||
:::
|
||||
|
||||
Immich can easily be installed on TrueNAS Community Edition via the **Community** train application.
|
||||
|
||||
@@ -57,6 +57,6 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.15.0"
|
||||
"node": "22.16.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,11 +44,6 @@ const projects: CommunityProjectProps[] = [
|
||||
'Lightroom plugin to publish, export photos from Lightroom to Immich. Import from Immich to Lightroom is also supported.',
|
||||
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',
|
||||
description: 'Script to search for and remove TikTok videos from your Immich library.',
|
||||
|
||||
@@ -13,6 +13,9 @@ import {
|
||||
mdiTrashCan,
|
||||
mdiWeb,
|
||||
mdiWrap,
|
||||
mdiCloudKeyOutline,
|
||||
mdiRegex,
|
||||
mdiCodeJson,
|
||||
} from '@mdi/js';
|
||||
import Layout from '@theme/Layout';
|
||||
import React from 'react';
|
||||
@@ -23,6 +26,30 @@ const withLanguage = (date: Date) => (language: string) => date.toLocaleDateStri
|
||||
type Item = Omit<TimelineItem, 'done' | 'getDateLabel'> & { date: Date };
|
||||
|
||||
const items: Item[] = [
|
||||
{
|
||||
icon: mdiRegex,
|
||||
iconColor: 'purple',
|
||||
title: 'Zitadel Actions are cursed',
|
||||
description:
|
||||
"Zitadel is cursed because its custom scripting feature is executed with a JS engine that doesn't support regex named capture groups.",
|
||||
link: {
|
||||
url: 'https://github.com/dop251/goja',
|
||||
text: 'Go JS engine',
|
||||
},
|
||||
date: new Date(2025, 5, 4),
|
||||
},
|
||||
{
|
||||
icon: mdiCloudKeyOutline,
|
||||
iconColor: '#0078d4',
|
||||
title: 'Entra is cursed',
|
||||
description:
|
||||
"Microsoft Entra supports PKCE, but doesn't include it in its OpenID discovery document. This leads to clients thinking PKCE isn't available.",
|
||||
link: {
|
||||
url: 'https://github.com/immich-app/immich/pull/18725',
|
||||
text: '#18725',
|
||||
},
|
||||
date: new Date(2025, 4, 30),
|
||||
},
|
||||
{
|
||||
icon: mdiCrop,
|
||||
iconColor: 'tomato',
|
||||
@@ -33,7 +60,18 @@ const items: Item[] = [
|
||||
url: 'https://github.com/immich-app/immich/pull/17974',
|
||||
text: '#17974',
|
||||
},
|
||||
date: new Date(2025, 5, 5),
|
||||
date: new Date(2025, 4, 5),
|
||||
},
|
||||
{
|
||||
icon: mdiCodeJson,
|
||||
iconColor: 'yellow',
|
||||
title: 'YAML whitespace is cursed',
|
||||
description: 'YAML whitespaces are often handled in unintuitive ways.',
|
||||
link: {
|
||||
url: 'https://github.com/immich-app/immich/pull/17309',
|
||||
text: '#17309',
|
||||
},
|
||||
date: new Date(2025, 3, 1),
|
||||
},
|
||||
{
|
||||
icon: mdiMicrosoftWindows,
|
||||
|
||||
@@ -78,12 +78,14 @@ import {
|
||||
mdiLinkEdit,
|
||||
mdiTagFaces,
|
||||
mdiMovieOpenPlayOutline,
|
||||
mdiCast,
|
||||
} from '@mdi/js';
|
||||
import Layout from '@theme/Layout';
|
||||
import React from 'react';
|
||||
import { Item, Timeline } from '../components/timeline';
|
||||
|
||||
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),
|
||||
@@ -216,14 +218,6 @@ const roadmap: Item[] = [
|
||||
iconColor: 'indianred',
|
||||
title: 'Stable release',
|
||||
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',
|
||||
},
|
||||
{
|
||||
@@ -245,6 +239,20 @@ const roadmap: 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',
|
||||
|
||||
28
docs/static/archived-versions.json
vendored
28
docs/static/archived-versions.json
vendored
@@ -1,4 +1,32 @@
|
||||
[
|
||||
{
|
||||
"label": "v1.135.3",
|
||||
"url": "https://v1.135.3.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.135.2",
|
||||
"url": "https://v1.135.2.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.135.1",
|
||||
"url": "https://v1.135.1.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.135.0",
|
||||
"url": "https://v1.135.0.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.134.0",
|
||||
"url": "https://v1.134.0.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
|
||||
@@ -1 +1 @@
|
||||
22.15.0
|
||||
22.16.0
|
||||
|
||||
@@ -28,8 +28,10 @@ services:
|
||||
extra_hosts:
|
||||
- 'auth-server:host-gateway'
|
||||
depends_on:
|
||||
- redis
|
||||
- database
|
||||
redis:
|
||||
condition: service_started
|
||||
database:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- 2285:2285
|
||||
|
||||
@@ -37,11 +39,17 @@ services:
|
||||
image: redis:6.2-alpine@sha256:3211c33a618c457e5d241922c975dbc4f446d0bdb2dc75694f5573ef8e2d01fa
|
||||
|
||||
database:
|
||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
|
||||
command: -c fsync=off -c shared_preload_libraries=vectors.so
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:3aef84a0a4fabbda17ef115c3019ba0c914ec73e9f6e59203674322d858b8eea
|
||||
command: -c fsync=off -c shared_preload_libraries=vchord.so -c config_file=/var/lib/postgresql/data/postgresql.conf
|
||||
environment:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: immich
|
||||
ports:
|
||||
- 5435:5432
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U postgres -d immich']
|
||||
interval: 1s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
start_period: 10s
|
||||
|
||||
2541
e2e/package-lock.json
generated
2541
e2e/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "1.132.3",
|
||||
"version": "1.135.3",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
@@ -25,8 +25,8 @@
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^22.15.16",
|
||||
"@types/oidc-provider": "^8.5.1",
|
||||
"@types/node": "^22.15.32",
|
||||
"@types/oidc-provider": "^9.0.0",
|
||||
"@types/pg": "^8.15.1",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
"@types/supertest": "^6.0.2",
|
||||
@@ -34,16 +34,17 @@
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^57.0.0",
|
||||
"eslint-plugin-unicorn": "^59.0.0",
|
||||
"exiftool-vendored": "^28.3.1",
|
||||
"globals": "^16.0.0",
|
||||
"jose": "^5.6.3",
|
||||
"luxon": "^3.4.4",
|
||||
"oidc-provider": "^8.5.1",
|
||||
"oidc-provider": "^9.0.0",
|
||||
"pg": "^8.11.3",
|
||||
"pngjs": "^7.0.0",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"sharp": "^0.33.5",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"supertest": "^7.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
@@ -52,6 +53,6 @@
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.15.0"
|
||||
"node": "22.16.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,6 +428,15 @@ describe('/albums', () => {
|
||||
order: AssetOrder.Desc,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not be able to share album with owner', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/albums')
|
||||
.send({ albumName: 'New album', albumUsers: [{ role: AlbumUserRole.Editor, userId: user1.userId }] })
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest('Cannot share album with owner'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /albums/:id/assets', () => {
|
||||
|
||||
@@ -143,7 +143,7 @@ describe('/api-keys', () => {
|
||||
const { apiKey } = await create(user.accessToken, [Permission.All]);
|
||||
const { status, body } = await request(app)
|
||||
.put(`/api-keys/${apiKey.id}`)
|
||||
.send({ name: 'new name' })
|
||||
.send({ name: 'new name', permissions: [Permission.All] })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest('API Key not found'));
|
||||
@@ -153,13 +153,16 @@ describe('/api-keys', () => {
|
||||
const { apiKey } = await create(user.accessToken, [Permission.All]);
|
||||
const { status, body } = await request(app)
|
||||
.put(`/api-keys/${apiKey.id}`)
|
||||
.send({ name: 'new name' })
|
||||
.send({
|
||||
name: 'new name',
|
||||
permissions: [Permission.ActivityCreate, Permission.ActivityRead, Permission.ActivityUpdate],
|
||||
})
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
id: expect.any(String),
|
||||
name: 'new name',
|
||||
permissions: [Permission.All],
|
||||
permissions: [Permission.ActivityCreate, Permission.ActivityRead, Permission.ActivityUpdate],
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ import { DateTime } from 'luxon';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { basename, join } from 'node:path';
|
||||
import sharp from 'sharp';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||
import { makeRandomImage } from 'src/generators';
|
||||
@@ -40,6 +41,40 @@ const today = DateTime.fromObject({
|
||||
}) as DateTime<true>;
|
||||
const yesterday = today.minus({ days: 1 });
|
||||
|
||||
const createTestImageWithExif = async (filename: string, exifData: Record<string, any>) => {
|
||||
// Generate unique color to ensure different checksums for each image
|
||||
const r = Math.floor(Math.random() * 256);
|
||||
const g = Math.floor(Math.random() * 256);
|
||||
const b = Math.floor(Math.random() * 256);
|
||||
|
||||
// Create a 100x100 solid color JPEG using Sharp
|
||||
const imageBytes = await sharp({
|
||||
create: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
channels: 3,
|
||||
background: { r, g, b },
|
||||
},
|
||||
})
|
||||
.jpeg({ quality: 90 })
|
||||
.toBuffer();
|
||||
|
||||
// Add random suffix to filename to avoid collisions
|
||||
const uniqueFilename = filename.replace('.jpg', `-${randomBytes(4).toString('hex')}.jpg`);
|
||||
const filepath = join(tempDir, uniqueFilename);
|
||||
await writeFile(filepath, imageBytes);
|
||||
|
||||
// Filter out undefined values before writing EXIF
|
||||
const cleanExifData = Object.fromEntries(Object.entries(exifData).filter(([, value]) => value !== undefined));
|
||||
|
||||
await exiftool.write(filepath, cleanExifData);
|
||||
|
||||
// Re-read the image bytes after EXIF has been written
|
||||
const finalImageBytes = await readFile(filepath);
|
||||
|
||||
return { filepath, imageBytes: finalImageBytes, filename: uniqueFilename };
|
||||
};
|
||||
|
||||
describe('/asset', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let websocket: Socket;
|
||||
@@ -1190,6 +1225,411 @@ describe('/asset', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('EXIF metadata extraction', () => {
|
||||
describe('Additional date tag extraction', () => {
|
||||
describe('Date-time vs time-only tag handling', () => {
|
||||
it('should fall back to file timestamps when only time-only tags are available', async () => {
|
||||
const { imageBytes, filename } = await createTestImageWithExif('time-only-fallback.jpg', {
|
||||
TimeCreated: '2023:11:15 14:30:00', // Time-only tag, should not be used for dateTimeOriginal
|
||||
// Exclude all date-time tags to force fallback to file timestamps
|
||||
SubSecDateTimeOriginal: undefined,
|
||||
DateTimeOriginal: undefined,
|
||||
SubSecCreateDate: undefined,
|
||||
SubSecMediaCreateDate: undefined,
|
||||
CreateDate: undefined,
|
||||
MediaCreateDate: undefined,
|
||||
CreationDate: undefined,
|
||||
DateTimeCreated: undefined,
|
||||
GPSDateTime: undefined,
|
||||
DateTimeUTC: undefined,
|
||||
SonyDateTime2: undefined,
|
||||
GPSDateStamp: undefined,
|
||||
});
|
||||
|
||||
const oldDate = new Date('2020-01-01T00:00:00.000Z');
|
||||
const asset = await utils.createAsset(admin.accessToken, {
|
||||
assetData: {
|
||||
filename,
|
||||
bytes: imageBytes,
|
||||
},
|
||||
fileCreatedAt: oldDate.toISOString(),
|
||||
fileModifiedAt: oldDate.toISOString(),
|
||||
});
|
||||
|
||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
|
||||
|
||||
const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) });
|
||||
|
||||
expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined();
|
||||
// Should fall back to file timestamps, which we set to 2020-01-01
|
||||
expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe(
|
||||
new Date('2020-01-01T00:00:00.000Z').getTime(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should prefer DateTimeOriginal over time-only tags', async () => {
|
||||
const { imageBytes, filename } = await createTestImageWithExif('datetime-over-time.jpg', {
|
||||
DateTimeOriginal: '2023:10:10 10:00:00', // Should be preferred
|
||||
TimeCreated: '2023:11:15 14:30:00', // Should be ignored (time-only)
|
||||
});
|
||||
|
||||
const asset = await utils.createAsset(admin.accessToken, {
|
||||
assetData: {
|
||||
filename,
|
||||
bytes: imageBytes,
|
||||
},
|
||||
});
|
||||
|
||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
|
||||
|
||||
const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) });
|
||||
|
||||
expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined();
|
||||
// Should use DateTimeOriginal, not TimeCreated
|
||||
expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe(
|
||||
new Date('2023-10-10T10:00:00.000Z').getTime(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GPSDateTime tag extraction', () => {
|
||||
it('should extract GPSDateTime with GPS coordinates', async () => {
|
||||
const { imageBytes, filename } = await createTestImageWithExif('gps-datetime.jpg', {
|
||||
GPSDateTime: '2023:11:15 12:30:00Z',
|
||||
GPSLatitude: 37.7749,
|
||||
GPSLongitude: -122.4194,
|
||||
// Exclude other date tags
|
||||
SubSecDateTimeOriginal: undefined,
|
||||
DateTimeOriginal: undefined,
|
||||
SubSecCreateDate: undefined,
|
||||
SubSecMediaCreateDate: undefined,
|
||||
CreateDate: undefined,
|
||||
MediaCreateDate: undefined,
|
||||
CreationDate: undefined,
|
||||
DateTimeCreated: undefined,
|
||||
TimeCreated: undefined,
|
||||
});
|
||||
|
||||
const asset = await utils.createAsset(admin.accessToken, {
|
||||
assetData: {
|
||||
filename,
|
||||
bytes: imageBytes,
|
||||
},
|
||||
});
|
||||
|
||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
|
||||
|
||||
const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) });
|
||||
|
||||
expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined();
|
||||
expect(assetInfo.exifInfo?.latitude).toBeCloseTo(37.7749, 4);
|
||||
expect(assetInfo.exifInfo?.longitude).toBeCloseTo(-122.4194, 4);
|
||||
expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe(
|
||||
new Date('2023-11-15T12:30:00.000Z').getTime(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CreateDate tag extraction', () => {
|
||||
it('should extract CreateDate when available', async () => {
|
||||
const { imageBytes, filename } = await createTestImageWithExif('create-date.jpg', {
|
||||
CreateDate: '2023:11:15 10:30:00',
|
||||
// Exclude other higher priority date tags
|
||||
SubSecDateTimeOriginal: undefined,
|
||||
DateTimeOriginal: undefined,
|
||||
SubSecCreateDate: undefined,
|
||||
SubSecMediaCreateDate: undefined,
|
||||
MediaCreateDate: undefined,
|
||||
CreationDate: undefined,
|
||||
DateTimeCreated: undefined,
|
||||
TimeCreated: undefined,
|
||||
GPSDateTime: undefined,
|
||||
});
|
||||
|
||||
const asset = await utils.createAsset(admin.accessToken, {
|
||||
assetData: {
|
||||
filename,
|
||||
bytes: imageBytes,
|
||||
},
|
||||
});
|
||||
|
||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
|
||||
|
||||
const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) });
|
||||
|
||||
expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined();
|
||||
expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe(
|
||||
new Date('2023-11-15T10:30:00.000Z').getTime(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GPSDateStamp tag extraction', () => {
|
||||
it('should fall back to file timestamps when only date-only tags are available', async () => {
|
||||
const { imageBytes, filename } = await createTestImageWithExif('gps-datestamp.jpg', {
|
||||
GPSDateStamp: '2023:11:15', // Date-only tag, should not be used for dateTimeOriginal
|
||||
// Note: NOT including GPSTimeStamp to avoid automatic GPSDateTime creation
|
||||
GPSLatitude: 51.5074,
|
||||
GPSLongitude: -0.1278,
|
||||
// Explicitly exclude all testable date-time tags to force fallback to file timestamps
|
||||
DateTimeOriginal: undefined,
|
||||
CreateDate: undefined,
|
||||
CreationDate: undefined,
|
||||
GPSDateTime: undefined,
|
||||
});
|
||||
|
||||
const oldDate = new Date('2020-01-01T00:00:00.000Z');
|
||||
const asset = await utils.createAsset(admin.accessToken, {
|
||||
assetData: {
|
||||
filename,
|
||||
bytes: imageBytes,
|
||||
},
|
||||
fileCreatedAt: oldDate.toISOString(),
|
||||
fileModifiedAt: oldDate.toISOString(),
|
||||
});
|
||||
|
||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
|
||||
|
||||
const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) });
|
||||
|
||||
expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined();
|
||||
expect(assetInfo.exifInfo?.latitude).toBeCloseTo(51.5074, 4);
|
||||
expect(assetInfo.exifInfo?.longitude).toBeCloseTo(-0.1278, 4);
|
||||
// Should fall back to file timestamps, which we set to 2020-01-01
|
||||
expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe(
|
||||
new Date('2020-01-01T00:00:00.000Z').getTime(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* NOTE: The following EXIF date tags are NOT effectively usable with JPEG test files:
|
||||
*
|
||||
* NOT WRITABLE to JPEG:
|
||||
* - MediaCreateDate: Can be read from video files but not written to JPEG
|
||||
* - DateTimeCreated: Read-only tag in JPEG format
|
||||
* - DateTimeUTC: Cannot be written to JPEG files
|
||||
* - SonyDateTime2: Proprietary Sony tag, not writable to JPEG
|
||||
* - SubSecMediaCreateDate: Tag not defined for JPEG format
|
||||
* - SourceImageCreateTime: Non-standard insta360 tag, not writable to JPEG
|
||||
*
|
||||
* WRITABLE but NOT READABLE from JPEG:
|
||||
* - SubSecDateTimeOriginal: Can be written but not read back from JPEG
|
||||
* - SubSecCreateDate: Can be written but not read back from JPEG
|
||||
*
|
||||
* EFFECTIVELY TESTABLE TAGS (writable and readable):
|
||||
* - DateTimeOriginal ✓
|
||||
* - CreateDate ✓
|
||||
* - CreationDate ✓
|
||||
* - GPSDateTime ✓
|
||||
*
|
||||
* The metadata service correctly handles non-readable tags and will fall back to
|
||||
* file timestamps when only non-readable tags are present.
|
||||
*/
|
||||
|
||||
describe('Date tag priority order', () => {
|
||||
it('should respect the complete date tag priority order', async () => {
|
||||
// Test cases using only EFFECTIVELY TESTABLE tags (writable AND readable from JPEG)
|
||||
const testCases = [
|
||||
{
|
||||
name: 'DateTimeOriginal has highest priority among testable tags',
|
||||
exifData: {
|
||||
DateTimeOriginal: '2023:04:04 04:00:00', // TESTABLE - highest priority among readable tags
|
||||
CreateDate: '2023:05:05 05:00:00', // TESTABLE
|
||||
CreationDate: '2023:07:07 07:00:00', // TESTABLE
|
||||
GPSDateTime: '2023:10:10 10:00:00', // TESTABLE
|
||||
},
|
||||
expectedDate: '2023-04-04T04:00:00.000Z',
|
||||
},
|
||||
{
|
||||
name: 'CreateDate when DateTimeOriginal missing',
|
||||
exifData: {
|
||||
CreateDate: '2023:05:05 05:00:00', // TESTABLE
|
||||
CreationDate: '2023:07:07 07:00:00', // TESTABLE
|
||||
GPSDateTime: '2023:10:10 10:00:00', // TESTABLE
|
||||
},
|
||||
expectedDate: '2023-05-05T05:00:00.000Z',
|
||||
},
|
||||
{
|
||||
name: 'CreationDate when standard EXIF tags missing',
|
||||
exifData: {
|
||||
CreationDate: '2023:07:07 07:00:00', // TESTABLE
|
||||
GPSDateTime: '2023:10:10 10:00:00', // TESTABLE
|
||||
},
|
||||
expectedDate: '2023-07-07T07:00:00.000Z',
|
||||
},
|
||||
{
|
||||
name: 'GPSDateTime when no other testable date tags present',
|
||||
exifData: {
|
||||
GPSDateTime: '2023:10:10 10:00:00', // TESTABLE
|
||||
Make: 'SONY',
|
||||
},
|
||||
expectedDate: '2023-10-10T10:00:00.000Z',
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const { imageBytes, filename } = await createTestImageWithExif(
|
||||
`${testCase.name.replaceAll(/\s+/g, '-').toLowerCase()}.jpg`,
|
||||
testCase.exifData,
|
||||
);
|
||||
|
||||
const asset = await utils.createAsset(admin.accessToken, {
|
||||
assetData: {
|
||||
filename,
|
||||
bytes: imageBytes,
|
||||
},
|
||||
});
|
||||
|
||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
|
||||
|
||||
const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) });
|
||||
|
||||
expect(assetInfo.exifInfo?.dateTimeOriginal, `Failed for: ${testCase.name}`).toBeDefined();
|
||||
expect(
|
||||
new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime(),
|
||||
`Date mismatch for: ${testCase.name}`,
|
||||
).toBe(new Date(testCase.expectedDate).getTime());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases for date tag handling', () => {
|
||||
it('should fall back to file timestamps with GPSDateStamp alone', async () => {
|
||||
const { imageBytes, filename } = await createTestImageWithExif('gps-datestamp-only.jpg', {
|
||||
GPSDateStamp: '2023:08:08', // Date-only tag, should not be used for dateTimeOriginal
|
||||
// Intentionally no GPSTimeStamp
|
||||
// Exclude all other date tags
|
||||
SubSecDateTimeOriginal: undefined,
|
||||
DateTimeOriginal: undefined,
|
||||
SubSecCreateDate: undefined,
|
||||
SubSecMediaCreateDate: undefined,
|
||||
CreateDate: undefined,
|
||||
MediaCreateDate: undefined,
|
||||
CreationDate: undefined,
|
||||
DateTimeCreated: undefined,
|
||||
TimeCreated: undefined,
|
||||
GPSDateTime: undefined,
|
||||
DateTimeUTC: undefined,
|
||||
});
|
||||
|
||||
const oldDate = new Date('2020-01-01T00:00:00.000Z');
|
||||
const asset = await utils.createAsset(admin.accessToken, {
|
||||
assetData: {
|
||||
filename,
|
||||
bytes: imageBytes,
|
||||
},
|
||||
fileCreatedAt: oldDate.toISOString(),
|
||||
fileModifiedAt: oldDate.toISOString(),
|
||||
});
|
||||
|
||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
|
||||
|
||||
const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) });
|
||||
|
||||
expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined();
|
||||
// Should fall back to file timestamps, which we set to 2020-01-01
|
||||
expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe(
|
||||
new Date('2020-01-01T00:00:00.000Z').getTime(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle all testable date tags present to verify complete priority order', async () => {
|
||||
const { imageBytes, filename } = await createTestImageWithExif('all-testable-date-tags.jpg', {
|
||||
// All TESTABLE date tags to JPEG format (writable AND readable)
|
||||
DateTimeOriginal: '2023:04:04 04:00:00', // TESTABLE - highest priority among readable tags
|
||||
CreateDate: '2023:05:05 05:00:00', // TESTABLE
|
||||
CreationDate: '2023:07:07 07:00:00', // TESTABLE
|
||||
GPSDateTime: '2023:10:10 10:00:00', // TESTABLE
|
||||
// Note: Excluded non-testable tags:
|
||||
// SubSec tags: writable but not readable from JPEG
|
||||
// Non-writable tags: MediaCreateDate, DateTimeCreated, DateTimeUTC, SonyDateTime2, etc.
|
||||
// Time-only/date-only tags: already excluded from EXIF_DATE_TAGS
|
||||
});
|
||||
|
||||
const asset = await utils.createAsset(admin.accessToken, {
|
||||
assetData: {
|
||||
filename,
|
||||
bytes: imageBytes,
|
||||
},
|
||||
});
|
||||
|
||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
|
||||
|
||||
const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) });
|
||||
|
||||
expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined();
|
||||
// Should use DateTimeOriginal as it has the highest priority among testable tags
|
||||
expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe(
|
||||
new Date('2023-04-04T04:00:00.000Z').getTime(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use CreationDate when SubSec tags are missing', async () => {
|
||||
const { imageBytes, filename } = await createTestImageWithExif('creation-date-priority.jpg', {
|
||||
CreationDate: '2023:07:07 07:00:00', // WRITABLE
|
||||
GPSDateTime: '2023:10:10 10:00:00', // WRITABLE
|
||||
// Note: DateTimeCreated, DateTimeUTC, SonyDateTime2 are NOT writable to JPEG
|
||||
// Note: TimeCreated and GPSDateStamp are excluded from EXIF_DATE_TAGS (time-only/date-only)
|
||||
// Exclude SubSec and standard EXIF tags
|
||||
SubSecDateTimeOriginal: undefined,
|
||||
DateTimeOriginal: undefined,
|
||||
SubSecCreateDate: undefined,
|
||||
CreateDate: undefined,
|
||||
});
|
||||
|
||||
const asset = await utils.createAsset(admin.accessToken, {
|
||||
assetData: {
|
||||
filename,
|
||||
bytes: imageBytes,
|
||||
},
|
||||
});
|
||||
|
||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
|
||||
|
||||
const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) });
|
||||
|
||||
expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined();
|
||||
// Should use CreationDate when available
|
||||
expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe(
|
||||
new Date('2023-07-07T07:00:00.000Z').getTime(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should skip invalid date formats and use next valid tag', async () => {
|
||||
const { imageBytes, filename } = await createTestImageWithExif('invalid-date-handling.jpg', {
|
||||
// Note: Testing invalid date handling with only WRITABLE tags
|
||||
GPSDateTime: '2023:10:10 10:00:00', // WRITABLE - Valid date
|
||||
CreationDate: '2023:13:13 13:00:00', // WRITABLE - Valid date
|
||||
// Note: TimeCreated excluded (time-only), DateTimeCreated not writable to JPEG
|
||||
// Exclude other date tags
|
||||
SubSecDateTimeOriginal: undefined,
|
||||
DateTimeOriginal: undefined,
|
||||
SubSecCreateDate: undefined,
|
||||
CreateDate: undefined,
|
||||
});
|
||||
|
||||
const asset = await utils.createAsset(admin.accessToken, {
|
||||
assetData: {
|
||||
filename,
|
||||
bytes: imageBytes,
|
||||
},
|
||||
});
|
||||
|
||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
|
||||
|
||||
const assetInfo = await getAssetInfo({ id: asset.id }, { headers: asBearerAuth(admin.accessToken) });
|
||||
|
||||
expect(assetInfo.exifInfo?.dateTimeOriginal).toBeDefined();
|
||||
// Should skip invalid dates and use the first valid one (GPSDateTime)
|
||||
expect(new Date(assetInfo.exifInfo!.dateTimeOriginal!).getTime()).toBe(
|
||||
new Date('2023-10-10T10:00:00.000Z').getTime(),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /assets/exist', () => {
|
||||
it('ignores invalid deviceAssetIds', async () => {
|
||||
const response = await utils.checkExistingAssets(user1.accessToken, {
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
import { LoginResponseDto, login, signUpAdmin } from '@immich/sdk';
|
||||
import { loginDto, signupDto } from 'src/fixtures';
|
||||
import { errorDto, loginResponseDto, signupResponseDto } from 'src/responses';
|
||||
import { app, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
const { email, password } = signupDto.admin;
|
||||
|
||||
describe(`/auth/admin-sign-up`, () => {
|
||||
beforeEach(async () => {
|
||||
await utils.resetDatabase();
|
||||
});
|
||||
|
||||
describe('POST /auth/admin-sign-up', () => {
|
||||
it(`should sign up the admin`, async () => {
|
||||
const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin);
|
||||
expect(status).toBe(201);
|
||||
expect(body).toEqual(signupResponseDto.admin);
|
||||
});
|
||||
|
||||
it('should not allow a second admin to sign up', async () => {
|
||||
await signUpAdmin({ signUpDto: signupDto.admin });
|
||||
|
||||
const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin);
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.alreadyHasAdmin);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/auth/*', () => {
|
||||
let admin: LoginResponseDto;
|
||||
|
||||
beforeEach(async () => {
|
||||
await utils.resetDatabase();
|
||||
await signUpAdmin({ signUpDto: signupDto.admin });
|
||||
admin = await login({ loginCredentialDto: loginDto.admin });
|
||||
});
|
||||
|
||||
describe(`POST /auth/login`, () => {
|
||||
it('should reject an incorrect password', async () => {
|
||||
const { status, body } = await request(app).post('/auth/login').send({ email, password: 'incorrect' });
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.incorrectLogin);
|
||||
});
|
||||
|
||||
it('should accept a correct password', async () => {
|
||||
const { status, body, headers } = await request(app).post('/auth/login').send({ email, password });
|
||||
expect(status).toBe(201);
|
||||
expect(body).toEqual(loginResponseDto.admin);
|
||||
|
||||
const token = body.accessToken;
|
||||
expect(token).toBeDefined();
|
||||
|
||||
const cookies = headers['set-cookie'];
|
||||
expect(cookies).toHaveLength(3);
|
||||
expect(cookies[0].split(';').map((item) => item.trim())).toEqual([
|
||||
`immich_access_token=${token}`,
|
||||
'Max-Age=34560000',
|
||||
'Path=/',
|
||||
expect.stringContaining('Expires='),
|
||||
'HttpOnly',
|
||||
'SameSite=Lax',
|
||||
]);
|
||||
expect(cookies[1].split(';').map((item) => item.trim())).toEqual([
|
||||
'immich_auth_type=password',
|
||||
'Max-Age=34560000',
|
||||
'Path=/',
|
||||
expect.stringContaining('Expires='),
|
||||
'HttpOnly',
|
||||
'SameSite=Lax',
|
||||
]);
|
||||
expect(cookies[2].split(';').map((item) => item.trim())).toEqual([
|
||||
'immich_is_authenticated=true',
|
||||
'Max-Age=34560000',
|
||||
'Path=/',
|
||||
expect.stringContaining('Expires='),
|
||||
'SameSite=Lax',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/validateToken', () => {
|
||||
it('should reject an invalid token', async () => {
|
||||
const { status, body } = await request(app).post(`/auth/validateToken`).set('Authorization', 'Bearer 123');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.invalidToken);
|
||||
});
|
||||
|
||||
it('should accept a valid token', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post(`/auth/validateToken`)
|
||||
.send({})
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({ authStatus: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/change-password', () => {
|
||||
it('should require the current password', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post(`/auth/change-password`)
|
||||
.send({ password: 'wrong-password', newPassword: 'Password1234' })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.wrongPassword);
|
||||
});
|
||||
|
||||
it('should change the password', async () => {
|
||||
const { status } = await request(app)
|
||||
.post(`/auth/change-password`)
|
||||
.send({ password, newPassword: 'Password1234' })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
|
||||
await login({
|
||||
loginCredentialDto: {
|
||||
email: 'admin@immich.cloud',
|
||||
password: 'Password1234',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/logout', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).post(`/auth/logout`);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should logout the user', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post(`/auth/logout`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
successful: true,
|
||||
redirectUri: '/auth/login?autoLaunch=0',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
createMemory,
|
||||
getMemory,
|
||||
} from '@immich/sdk';
|
||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||
import { createUserDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { app, asBearerAuth, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
@@ -17,7 +17,6 @@ describe('/memories', () => {
|
||||
let user: LoginResponseDto;
|
||||
let adminAsset: AssetMediaResponseDto;
|
||||
let userAsset1: AssetMediaResponseDto;
|
||||
let userAsset2: AssetMediaResponseDto;
|
||||
let userMemory: MemoryResponseDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -25,10 +24,9 @@ describe('/memories', () => {
|
||||
|
||||
admin = await utils.adminSetup();
|
||||
user = await utils.userSetup(admin.accessToken, createUserDto.user1);
|
||||
[adminAsset, userAsset1, userAsset2] = await Promise.all([
|
||||
[adminAsset, userAsset1] = await Promise.all([
|
||||
utils.createAsset(admin.accessToken),
|
||||
utils.createAsset(user.accessToken),
|
||||
utils.createAsset(user.accessToken),
|
||||
]);
|
||||
userMemory = await createMemory(
|
||||
{
|
||||
@@ -43,121 +41,7 @@ describe('/memories', () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe('GET /memories', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/memories');
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /memories', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).post('/memories');
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should validate data when type is on this day', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/memories')
|
||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||
.send({
|
||||
type: 'on_this_day',
|
||||
data: {},
|
||||
memoryAt: new Date(2021).toISOString(),
|
||||
});
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest(['data.year must be a positive number', 'data.year must be an integer number']),
|
||||
);
|
||||
});
|
||||
|
||||
it('should create a new memory', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/memories')
|
||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||
.send({
|
||||
type: 'on_this_day',
|
||||
data: { year: 2021 },
|
||||
memoryAt: new Date(2021).toISOString(),
|
||||
});
|
||||
|
||||
expect(status).toBe(201);
|
||||
expect(body).toEqual({
|
||||
id: expect.any(String),
|
||||
type: 'on_this_day',
|
||||
data: { year: 2021 },
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
isSaved: false,
|
||||
memoryAt: expect.any(String),
|
||||
ownerId: user.userId,
|
||||
assets: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a new memory (with assets)', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/memories')
|
||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||
.send({
|
||||
type: 'on_this_day',
|
||||
data: { year: 2021 },
|
||||
memoryAt: new Date(2021).toISOString(),
|
||||
assetIds: [userAsset1.id, userAsset2.id],
|
||||
});
|
||||
|
||||
expect(status).toBe(201);
|
||||
expect(body).toMatchObject({
|
||||
id: expect.any(String),
|
||||
assets: expect.arrayContaining([
|
||||
expect.objectContaining({ id: userAsset1.id }),
|
||||
expect.objectContaining({ id: userAsset2.id }),
|
||||
]),
|
||||
});
|
||||
expect(body.assets).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should create a new memory and ignore assets the user does not have access to', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/memories')
|
||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||
.send({
|
||||
type: 'on_this_day',
|
||||
data: { year: 2021 },
|
||||
memoryAt: new Date(2021).toISOString(),
|
||||
assetIds: [userAsset1.id, adminAsset.id],
|
||||
});
|
||||
|
||||
expect(status).toBe(201);
|
||||
expect(body).toMatchObject({
|
||||
id: expect.any(String),
|
||||
assets: [expect.objectContaining({ id: userAsset1.id })],
|
||||
});
|
||||
expect(body.assets).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /memories/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get(`/memories/${uuidDto.invalid}`);
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get(`/memories/${uuidDto.invalid}`)
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
||||
});
|
||||
|
||||
it('should require access', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get(`/memories/${userMemory.id}`)
|
||||
@@ -176,22 +60,6 @@ describe('/memories', () => {
|
||||
});
|
||||
|
||||
describe('PUT /memories/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).put(`/memories/${uuidDto.invalid}`).send({ isSaved: true });
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/memories/${uuidDto.invalid}`)
|
||||
.send({ isSaved: true })
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
||||
});
|
||||
|
||||
it('should require access', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/memories/${userMemory.id}`)
|
||||
@@ -218,23 +86,6 @@ describe('/memories', () => {
|
||||
});
|
||||
|
||||
describe('PUT /memories/:id/assets', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/memories/${userMemory.id}/assets`)
|
||||
.send({ ids: [userAsset1.id] });
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/memories/${uuidDto.invalid}/assets`)
|
||||
.send({ ids: [userAsset1.id] })
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
||||
});
|
||||
|
||||
it('should require access', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/memories/${userMemory.id}/assets`)
|
||||
@@ -244,15 +95,6 @@ describe('/memories', () => {
|
||||
expect(body).toEqual(errorDto.noPermission);
|
||||
});
|
||||
|
||||
it('should require a valid asset id', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/memories/${userMemory.id}/assets`)
|
||||
.send({ ids: [uuidDto.invalid] })
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID']));
|
||||
});
|
||||
|
||||
it('should require asset access', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/memories/${userMemory.id}/assets`)
|
||||
@@ -279,23 +121,6 @@ describe('/memories', () => {
|
||||
});
|
||||
|
||||
describe('DELETE /memories/:id/assets', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/memories/${userMemory.id}/assets`)
|
||||
.send({ ids: [userAsset1.id] });
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/memories/${uuidDto.invalid}/assets`)
|
||||
.send({ ids: [userAsset1.id] })
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
||||
});
|
||||
|
||||
it('should require access', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/memories/${userMemory.id}/assets`)
|
||||
@@ -305,15 +130,6 @@ describe('/memories', () => {
|
||||
expect(body).toEqual(errorDto.noPermission);
|
||||
});
|
||||
|
||||
it('should require a valid asset id', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/memories/${userMemory.id}/assets`)
|
||||
.send({ ids: [uuidDto.invalid] })
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID']));
|
||||
});
|
||||
|
||||
it('should only remove assets in the memory', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/memories/${userMemory.id}/assets`)
|
||||
@@ -340,21 +156,6 @@ describe('/memories', () => {
|
||||
});
|
||||
|
||||
describe('DELETE /memories/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).delete(`/memories/${uuidDto.invalid}`);
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/memories/${uuidDto.invalid}`)
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
||||
});
|
||||
|
||||
it('should require access', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/memories/${userMemory.id}`)
|
||||
|
||||
@@ -11,11 +11,32 @@ describe('/people', () => {
|
||||
let hiddenPerson: PersonResponseDto;
|
||||
let multipleAssetsPerson: PersonResponseDto;
|
||||
|
||||
let nameAlicePerson: PersonResponseDto;
|
||||
let nameBobPerson: PersonResponseDto;
|
||||
let nameCharliePerson: PersonResponseDto;
|
||||
let nameNullPerson4Assets: PersonResponseDto;
|
||||
let nameNullPerson3Assets: PersonResponseDto;
|
||||
let nameNullPerson1Asset: PersonResponseDto;
|
||||
let nameBillPersonFavourite: PersonResponseDto;
|
||||
let nameFreddyPersonFavourite: PersonResponseDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
admin = await utils.adminSetup();
|
||||
|
||||
[visiblePerson, hiddenPerson, multipleAssetsPerson] = await Promise.all([
|
||||
[
|
||||
visiblePerson,
|
||||
hiddenPerson,
|
||||
multipleAssetsPerson,
|
||||
nameCharliePerson,
|
||||
nameBobPerson,
|
||||
nameAlicePerson,
|
||||
nameNullPerson4Assets,
|
||||
nameNullPerson3Assets,
|
||||
nameNullPerson1Asset,
|
||||
nameBillPersonFavourite,
|
||||
nameFreddyPersonFavourite,
|
||||
] = await Promise.all([
|
||||
utils.createPerson(admin.accessToken, {
|
||||
name: 'visible_person',
|
||||
}),
|
||||
@@ -26,10 +47,39 @@ describe('/people', () => {
|
||||
utils.createPerson(admin.accessToken, {
|
||||
name: 'multiple_assets_person',
|
||||
}),
|
||||
// --- Setup for the specific sorting test ---
|
||||
utils.createPerson(admin.accessToken, {
|
||||
name: 'Charlie',
|
||||
}),
|
||||
utils.createPerson(admin.accessToken, {
|
||||
name: 'Bob',
|
||||
}),
|
||||
utils.createPerson(admin.accessToken, {
|
||||
name: 'Alice',
|
||||
}),
|
||||
utils.createPerson(admin.accessToken, {
|
||||
name: '',
|
||||
}),
|
||||
utils.createPerson(admin.accessToken, {
|
||||
name: '',
|
||||
}),
|
||||
utils.createPerson(admin.accessToken, {
|
||||
name: '',
|
||||
}),
|
||||
utils.createPerson(admin.accessToken, {
|
||||
name: 'Bill',
|
||||
isFavorite: true,
|
||||
}),
|
||||
utils.createPerson(admin.accessToken, {
|
||||
name: 'Freddy',
|
||||
isFavorite: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const asset1 = await utils.createAsset(admin.accessToken);
|
||||
const asset2 = await utils.createAsset(admin.accessToken);
|
||||
const asset3 = await utils.createAsset(admin.accessToken);
|
||||
const asset4 = await utils.createAsset(admin.accessToken);
|
||||
|
||||
await Promise.all([
|
||||
utils.createFace({ assetId: asset1.id, personId: visiblePerson.id }),
|
||||
@@ -37,6 +87,27 @@ describe('/people', () => {
|
||||
utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }),
|
||||
utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }),
|
||||
utils.createFace({ assetId: asset2.id, personId: multipleAssetsPerson.id }),
|
||||
utils.createFace({ assetId: asset3.id, personId: multipleAssetsPerson.id }), // 4 assets
|
||||
// Named persons
|
||||
utils.createFace({ assetId: asset1.id, personId: nameCharliePerson.id }), // 1 asset
|
||||
utils.createFace({ assetId: asset1.id, personId: nameBobPerson.id }),
|
||||
utils.createFace({ assetId: asset2.id, personId: nameBobPerson.id }), // 2 assets
|
||||
utils.createFace({ assetId: asset1.id, personId: nameAlicePerson.id }), // 1 asset
|
||||
// Null-named person 4 assets
|
||||
utils.createFace({ assetId: asset1.id, personId: nameNullPerson4Assets.id }),
|
||||
utils.createFace({ assetId: asset2.id, personId: nameNullPerson4Assets.id }),
|
||||
utils.createFace({ assetId: asset3.id, personId: nameNullPerson4Assets.id }),
|
||||
utils.createFace({ assetId: asset4.id, personId: nameNullPerson4Assets.id }), // 4 assets
|
||||
// Null-named person 3 assets
|
||||
utils.createFace({ assetId: asset1.id, personId: nameNullPerson3Assets.id }),
|
||||
utils.createFace({ assetId: asset2.id, personId: nameNullPerson3Assets.id }),
|
||||
utils.createFace({ assetId: asset3.id, personId: nameNullPerson3Assets.id }), // 3 assets
|
||||
// Null-named person 1 asset
|
||||
utils.createFace({ assetId: asset3.id, personId: nameNullPerson1Asset.id }),
|
||||
// Favourite People
|
||||
utils.createFace({ assetId: asset1.id, personId: nameFreddyPersonFavourite.id }),
|
||||
utils.createFace({ assetId: asset2.id, personId: nameFreddyPersonFavourite.id }),
|
||||
utils.createFace({ assetId: asset1.id, personId: nameBillPersonFavourite.id }),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -51,27 +122,66 @@ describe('/people', () => {
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
hasNextPage: false,
|
||||
total: 3,
|
||||
total: 11,
|
||||
hidden: 1,
|
||||
people: [
|
||||
expect.objectContaining({ name: 'Freddy' }),
|
||||
expect.objectContaining({ name: 'Bill' }),
|
||||
expect.objectContaining({ name: 'multiple_assets_person' }),
|
||||
expect.objectContaining({ name: 'Bob' }),
|
||||
expect.objectContaining({ name: 'Alice' }),
|
||||
expect.objectContaining({ name: 'Charlie' }),
|
||||
expect.objectContaining({ name: 'visible_person' }),
|
||||
expect.objectContaining({ name: 'hidden_person' }),
|
||||
expect.objectContaining({ id: nameNullPerson4Assets.id, name: '' }),
|
||||
expect.objectContaining({ id: nameNullPerson3Assets.id, name: '' }),
|
||||
expect.objectContaining({ name: 'hidden_person' }), // Should really be before the null names
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should sort visible people by asset count (desc), then by name (asc, nulls last)', async () => {
|
||||
const { status, body } = await request(app).get('/people').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.hasNextPage).toBe(false);
|
||||
expect(body.total).toBe(11); // All persons
|
||||
expect(body.hidden).toBe(1); // 'hidden_person'
|
||||
|
||||
const people = body.people as PersonResponseDto[];
|
||||
|
||||
expect(people.map((p) => p.id)).toEqual([
|
||||
nameFreddyPersonFavourite.id, // name: 'Freddy', count: 2
|
||||
nameBillPersonFavourite.id, // name: 'Bill', count: 1
|
||||
multipleAssetsPerson.id, // name: 'multiple_assets_person', count: 3
|
||||
nameBobPerson.id, // name: 'Bob', count: 2
|
||||
nameAlicePerson.id, // name: 'Alice', count: 1
|
||||
nameCharliePerson.id, // name: 'Charlie', count: 1
|
||||
visiblePerson.id, // name: 'visible_person', count: 1
|
||||
nameNullPerson4Assets.id, // name: '', count: 4
|
||||
nameNullPerson3Assets.id, // name: '', count: 3
|
||||
]);
|
||||
|
||||
expect(people.some((p) => p.id === hiddenPerson.id)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return only visible people', async () => {
|
||||
const { status, body } = await request(app).get('/people').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
hasNextPage: false,
|
||||
total: 3,
|
||||
total: 11,
|
||||
hidden: 1,
|
||||
people: [
|
||||
expect.objectContaining({ name: 'Freddy' }),
|
||||
expect.objectContaining({ name: 'Bill' }),
|
||||
expect.objectContaining({ name: 'multiple_assets_person' }),
|
||||
expect.objectContaining({ name: 'Bob' }),
|
||||
expect.objectContaining({ name: 'Alice' }),
|
||||
expect.objectContaining({ name: 'Charlie' }),
|
||||
expect.objectContaining({ name: 'visible_person' }),
|
||||
expect.objectContaining({ id: nameNullPerson4Assets.id, name: '' }),
|
||||
expect.objectContaining({ id: nameNullPerson3Assets.id, name: '' }),
|
||||
],
|
||||
});
|
||||
});
|
||||
@@ -80,14 +190,14 @@ describe('/people', () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/people')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.query({ withHidden: true, page: 2, size: 1 });
|
||||
.query({ withHidden: true, page: 5, size: 1 });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
hasNextPage: true,
|
||||
total: 3,
|
||||
total: 11,
|
||||
hidden: 1,
|
||||
people: [expect.objectContaining({ name: 'visible_person' })],
|
||||
people: [expect.objectContaining({ name: 'Alice' })],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -128,7 +238,7 @@ describe('/people', () => {
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(expect.objectContaining({ assets: 2 }));
|
||||
expect(body).toEqual(expect.objectContaining({ assets: 3 }));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -119,6 +119,16 @@ describe('/shared-links', () => {
|
||||
expect(resp.header['content-type']).toContain('text/html');
|
||||
expect(resp.text).toContain(`<meta property="og:image" content="https://my.immich.app`);
|
||||
});
|
||||
|
||||
it('should return 404 for an invalid shared link', async () => {
|
||||
const resp = await request(shareUrl).get(`/invalid-key`);
|
||||
expect(resp.status).toBe(404);
|
||||
expect(resp.header['content-type']).toContain('text/html');
|
||||
expect(resp.text).not.toContain(`og:type`);
|
||||
expect(resp.text).not.toContain(`og:title`);
|
||||
expect(resp.text).not.toContain(`og:description`);
|
||||
expect(resp.text).not.toContain(`og:image`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /shared-links', () => {
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { AssetMediaResponseDto, AssetVisibility, LoginResponseDto, SharedLinkType, TimeBucketSize } from '@immich/sdk';
|
||||
import {
|
||||
AssetMediaResponseDto,
|
||||
AssetVisibility,
|
||||
LoginResponseDto,
|
||||
SharedLinkType,
|
||||
TimeBucketAssetResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
import { createUserDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
@@ -19,7 +25,8 @@ describe('/timeline', () => {
|
||||
let user: LoginResponseDto;
|
||||
let timeBucketUser: LoginResponseDto;
|
||||
|
||||
let userAssets: AssetMediaResponseDto[];
|
||||
let user1Assets: AssetMediaResponseDto[];
|
||||
let user2Assets: AssetMediaResponseDto[];
|
||||
|
||||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
@@ -29,7 +36,7 @@ describe('/timeline', () => {
|
||||
utils.userSetup(admin.accessToken, createUserDto.create('time-bucket')),
|
||||
]);
|
||||
|
||||
userAssets = await Promise.all([
|
||||
user1Assets = await Promise.all([
|
||||
utils.createAsset(user.accessToken),
|
||||
utils.createAsset(user.accessToken),
|
||||
utils.createAsset(user.accessToken, {
|
||||
@@ -42,17 +49,20 @@ describe('/timeline', () => {
|
||||
utils.createAsset(user.accessToken),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
user2Assets = await Promise.all([
|
||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-01-01').toISOString() }),
|
||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-10').toISOString() }),
|
||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }),
|
||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }),
|
||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-12').toISOString() }),
|
||||
]);
|
||||
|
||||
await utils.deleteAssets(timeBucketUser.accessToken, [user2Assets[4].id]);
|
||||
});
|
||||
|
||||
describe('GET /timeline/buckets', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/timeline/buckets').query({ size: TimeBucketSize.Month });
|
||||
const { status, body } = await request(app).get('/timeline/buckets');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
@@ -60,14 +70,13 @@ describe('/timeline', () => {
|
||||
it('should get time buckets by month', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/timeline/buckets')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Month });
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ count: 3, timeBucket: '1970-02-01T00:00:00.000Z' },
|
||||
{ count: 1, timeBucket: '1970-01-01T00:00:00.000Z' },
|
||||
{ count: 3, timeBucket: '1970-02-01' },
|
||||
{ count: 1, timeBucket: '1970-01-01' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
@@ -75,36 +84,20 @@ describe('/timeline', () => {
|
||||
it('should not allow access for unrelated shared links', async () => {
|
||||
const sharedLink = await utils.createSharedLink(user.accessToken, {
|
||||
type: SharedLinkType.Individual,
|
||||
assetIds: userAssets.map(({ id }) => id),
|
||||
assetIds: user1Assets.map(({ id }) => id),
|
||||
});
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.get('/timeline/buckets')
|
||||
.query({ key: sharedLink.key, size: TimeBucketSize.Month });
|
||||
const { status, body } = await request(app).get('/timeline/buckets').query({ key: sharedLink.key });
|
||||
|
||||
expect(status).toBe(400);
|
||||
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 () => {
|
||||
const req1 = await request(app)
|
||||
.get('/timeline/buckets')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Month, withPartners: true, visibility: AssetVisibility.Archive });
|
||||
.query({ withPartners: true, visibility: AssetVisibility.Archive });
|
||||
|
||||
expect(req1.status).toBe(400);
|
||||
expect(req1.body).toEqual(errorDto.badRequest());
|
||||
@@ -112,7 +105,7 @@ describe('/timeline', () => {
|
||||
const req2 = await request(app)
|
||||
.get('/timeline/buckets')
|
||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Month, withPartners: true, visibility: undefined });
|
||||
.query({ withPartners: true, visibility: undefined });
|
||||
|
||||
expect(req2.status).toBe(400);
|
||||
expect(req2.body).toEqual(errorDto.badRequest());
|
||||
@@ -122,7 +115,7 @@ describe('/timeline', () => {
|
||||
const req1 = await request(app)
|
||||
.get('/timeline/buckets')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: true });
|
||||
.query({ withPartners: true, isFavorite: true });
|
||||
|
||||
expect(req1.status).toBe(400);
|
||||
expect(req1.body).toEqual(errorDto.badRequest());
|
||||
@@ -130,7 +123,7 @@ describe('/timeline', () => {
|
||||
const req2 = await request(app)
|
||||
.get('/timeline/buckets')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: false });
|
||||
.query({ withPartners: true, isFavorite: false });
|
||||
|
||||
expect(req2.status).toBe(400);
|
||||
expect(req2.body).toEqual(errorDto.badRequest());
|
||||
@@ -140,7 +133,7 @@ describe('/timeline', () => {
|
||||
const req = await request(app)
|
||||
.get('/timeline/buckets')
|
||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Month, withPartners: true, isTrashed: true });
|
||||
.query({ withPartners: true, isTrashed: true });
|
||||
|
||||
expect(req.status).toBe(400);
|
||||
expect(req.body).toEqual(errorDto.badRequest());
|
||||
@@ -150,7 +143,6 @@ describe('/timeline', () => {
|
||||
describe('GET /timeline/bucket', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/timeline/bucket').query({
|
||||
size: TimeBucketSize.Month,
|
||||
timeBucket: '1900-01-01',
|
||||
});
|
||||
|
||||
@@ -161,11 +153,28 @@ describe('/timeline', () => {
|
||||
it('should handle 5 digit years', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/timeline/bucket')
|
||||
.query({ size: TimeBucketSize.Month, timeBucket: '012345-01-01' })
|
||||
.query({ timeBucket: '012345-01-01' })
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual([]);
|
||||
expect(body).toEqual({
|
||||
city: [],
|
||||
country: [],
|
||||
duration: [],
|
||||
id: [],
|
||||
visibility: [],
|
||||
isFavorite: [],
|
||||
isImage: [],
|
||||
isTrashed: [],
|
||||
livePhotoVideoId: [],
|
||||
fileCreatedAt: [],
|
||||
localOffsetHours: [],
|
||||
ownerId: [],
|
||||
projectionType: [],
|
||||
ratio: [],
|
||||
status: [],
|
||||
thumbhash: [],
|
||||
});
|
||||
});
|
||||
|
||||
// TODO enable date string validation while still accepting 5 digit years
|
||||
@@ -173,7 +182,7 @@ describe('/timeline', () => {
|
||||
// const { status, body } = await request(app)
|
||||
// .get('/timeline/bucket')
|
||||
// .set('Authorization', `Bearer ${user.accessToken}`)
|
||||
// .query({ size: TimeBucketSize.Month, timeBucket: 'foo' });
|
||||
// .query({ timeBucket: 'foo' });
|
||||
|
||||
// expect(status).toBe(400);
|
||||
// expect(body).toEqual(errorDto.badRequest);
|
||||
@@ -183,10 +192,39 @@ describe('/timeline', () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/timeline/bucket')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Month, timeBucket: '1970-02-10' });
|
||||
.query({ timeBucket: '1970-02-10' });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual([]);
|
||||
expect(body).toEqual({
|
||||
city: [],
|
||||
country: [],
|
||||
duration: [],
|
||||
id: [],
|
||||
visibility: [],
|
||||
isFavorite: [],
|
||||
isImage: [],
|
||||
isTrashed: [],
|
||||
livePhotoVideoId: [],
|
||||
fileCreatedAt: [],
|
||||
localOffsetHours: [],
|
||||
ownerId: [],
|
||||
projectionType: [],
|
||||
ratio: [],
|
||||
status: [],
|
||||
thumbhash: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return time bucket in trash', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/timeline/bucket')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ timeBucket: '1970-02-01T00:00:00.000Z', isTrashed: true });
|
||||
|
||||
expect(status).toBe(200);
|
||||
|
||||
const timeBucket: TimeBucketAssetResponseDto = body;
|
||||
expect(timeBucket.isTrashed).toEqual([true]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,7 +118,7 @@ describe('/admin/users', () => {
|
||||
});
|
||||
}
|
||||
|
||||
it('should ignore `isAdmin`', async () => {
|
||||
it('should accept `isAdmin`', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post(`/admin/users`)
|
||||
.send({
|
||||
@@ -130,7 +130,7 @@ describe('/admin/users', () => {
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(body).toMatchObject({
|
||||
email: 'user5@immich.cloud',
|
||||
isAdmin: false,
|
||||
isAdmin: true,
|
||||
shouldChangePassword: true,
|
||||
});
|
||||
expect(status).toBe(201);
|
||||
@@ -163,14 +163,15 @@ describe('/admin/users', () => {
|
||||
});
|
||||
}
|
||||
|
||||
it('should not allow a non-admin to become an admin', async () => {
|
||||
it('should allow a non-admin to become an admin', async () => {
|
||||
const user = await utils.userSetup(admin.accessToken, createUserDto.create('admin2'));
|
||||
const { status, body } = await request(app)
|
||||
.put(`/admin/users/${nonAdmin.userId}`)
|
||||
.put(`/admin/users/${user.userId}`)
|
||||
.send({ isAdmin: true })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({ isAdmin: false });
|
||||
expect(body).toMatchObject({ isAdmin: true });
|
||||
});
|
||||
|
||||
it('ignores updates to profileImagePath', async () => {
|
||||
|
||||
178
e2e/src/generate-date-tag-test-images.ts
Normal file
178
e2e/src/generate-date-tag-test-images.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script to generate test images with additional EXIF date tags
|
||||
* This creates actual JPEG images with embedded metadata for testing
|
||||
* Images are generated into e2e/test-assets/metadata/dates/
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import sharp from 'sharp';
|
||||
|
||||
interface TestImage {
|
||||
filename: string;
|
||||
description: string;
|
||||
exifTags: Record<string, string>;
|
||||
}
|
||||
|
||||
const testImages: TestImage[] = [
|
||||
{
|
||||
filename: 'time-created.jpg',
|
||||
description: 'Image with TimeCreated tag',
|
||||
exifTags: {
|
||||
TimeCreated: '2023:11:15 14:30:00',
|
||||
Make: 'Canon',
|
||||
Model: 'EOS R5',
|
||||
},
|
||||
},
|
||||
{
|
||||
filename: 'gps-datetime.jpg',
|
||||
description: 'Image with GPSDateTime and coordinates',
|
||||
exifTags: {
|
||||
GPSDateTime: '2023:11:15 12:30:00Z',
|
||||
GPSLatitude: '37.7749',
|
||||
GPSLongitude: '-122.4194',
|
||||
GPSLatitudeRef: 'N',
|
||||
GPSLongitudeRef: 'W',
|
||||
},
|
||||
},
|
||||
{
|
||||
filename: 'datetime-utc.jpg',
|
||||
description: 'Image with DateTimeUTC tag',
|
||||
exifTags: {
|
||||
DateTimeUTC: '2023:11:15 10:30:00',
|
||||
Make: 'Nikon',
|
||||
Model: 'D850',
|
||||
},
|
||||
},
|
||||
{
|
||||
filename: 'gps-datestamp.jpg',
|
||||
description: 'Image with GPSDateStamp and GPSTimeStamp',
|
||||
exifTags: {
|
||||
GPSDateStamp: '2023:11:15',
|
||||
GPSTimeStamp: '08:30:00',
|
||||
GPSLatitude: '51.5074',
|
||||
GPSLongitude: '-0.1278',
|
||||
GPSLatitudeRef: 'N',
|
||||
GPSLongitudeRef: 'W',
|
||||
},
|
||||
},
|
||||
{
|
||||
filename: 'sony-datetime2.jpg',
|
||||
description: 'Sony camera image with SonyDateTime2 tag',
|
||||
exifTags: {
|
||||
SonyDateTime2: '2023:11:15 06:30:00',
|
||||
Make: 'SONY',
|
||||
Model: 'ILCE-7RM5',
|
||||
},
|
||||
},
|
||||
{
|
||||
filename: 'date-priority-test.jpg',
|
||||
description: 'Image with multiple date tags to test priority',
|
||||
exifTags: {
|
||||
SubSecDateTimeOriginal: '2023:01:01 01:00:00',
|
||||
DateTimeOriginal: '2023:02:02 02:00:00',
|
||||
SubSecCreateDate: '2023:03:03 03:00:00',
|
||||
CreateDate: '2023:04:04 04:00:00',
|
||||
CreationDate: '2023:05:05 05:00:00',
|
||||
DateTimeCreated: '2023:06:06 06:00:00',
|
||||
TimeCreated: '2023:07:07 07:00:00',
|
||||
GPSDateTime: '2023:08:08 08:00:00',
|
||||
DateTimeUTC: '2023:09:09 09:00:00',
|
||||
GPSDateStamp: '2023:10:10',
|
||||
SonyDateTime2: '2023:11:11 11:00:00',
|
||||
},
|
||||
},
|
||||
{
|
||||
filename: 'new-tags-only.jpg',
|
||||
description: 'Image with only additional date tags (no standard tags)',
|
||||
exifTags: {
|
||||
TimeCreated: '2023:12:01 15:45:30',
|
||||
GPSDateTime: '2023:12:01 13:45:30Z',
|
||||
DateTimeUTC: '2023:12:01 13:45:30',
|
||||
GPSDateStamp: '2023:12:01',
|
||||
SonyDateTime2: '2023:12:01 08:45:30',
|
||||
GPSLatitude: '40.7128',
|
||||
GPSLongitude: '-74.0060',
|
||||
GPSLatitudeRef: 'N',
|
||||
GPSLongitudeRef: 'W',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const generateTestImages = async (): Promise<void> => {
|
||||
// Target directory: e2e/test-assets/metadata/dates/
|
||||
// Current file is in: e2e/src/
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const targetDir = join(__dirname, '..', 'test-assets', 'metadata', 'dates');
|
||||
|
||||
console.log('Generating test images with additional EXIF date tags...');
|
||||
console.log(`Target directory: ${targetDir}`);
|
||||
|
||||
for (const image of testImages) {
|
||||
try {
|
||||
const imagePath = join(targetDir, image.filename);
|
||||
|
||||
// Create unique JPEG file using Sharp
|
||||
const r = Math.floor(Math.random() * 256);
|
||||
const g = Math.floor(Math.random() * 256);
|
||||
const b = Math.floor(Math.random() * 256);
|
||||
|
||||
const jpegData = await sharp({
|
||||
create: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
channels: 3,
|
||||
background: { r, g, b },
|
||||
},
|
||||
})
|
||||
.jpeg({ quality: 90 })
|
||||
.toBuffer();
|
||||
|
||||
writeFileSync(imagePath, jpegData);
|
||||
|
||||
// Build exiftool command to add EXIF data
|
||||
const exifArgs = Object.entries(image.exifTags)
|
||||
.map(([tag, value]) => `-${tag}="${value}"`)
|
||||
.join(' ');
|
||||
|
||||
const command = `exiftool ${exifArgs} -overwrite_original "${imagePath}"`;
|
||||
|
||||
console.log(`Creating ${image.filename}: ${image.description}`);
|
||||
execSync(command, { stdio: 'pipe' });
|
||||
|
||||
// Verify the tags were written
|
||||
const verifyCommand = `exiftool -json "${imagePath}"`;
|
||||
const result = execSync(verifyCommand, { encoding: 'utf8' });
|
||||
const metadata = JSON.parse(result)[0];
|
||||
|
||||
console.log(` ✓ Created with ${Object.keys(image.exifTags).length} EXIF tags`);
|
||||
|
||||
// Log first date tag found for verification
|
||||
const firstDateTag = Object.keys(image.exifTags).find(
|
||||
(tag) => tag.includes('Date') || tag.includes('Time') || tag.includes('Created'),
|
||||
);
|
||||
if (firstDateTag && metadata[firstDateTag]) {
|
||||
console.log(` ✓ Verified ${firstDateTag}: ${metadata[firstDateTag]}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to create ${image.filename}:`, (error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\nTest image generation complete!');
|
||||
console.log('Files created in:', targetDir);
|
||||
console.log('\nTo test these images:');
|
||||
console.log(`cd ${targetDir} && exiftool -time:all -gps:all *.jpg`);
|
||||
};
|
||||
|
||||
export { generateTestImages };
|
||||
|
||||
// Run the generator if this file is executed directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
generateTestImages().catch(console.error);
|
||||
}
|
||||
@@ -7,6 +7,44 @@ describe(`immich-admin`, () => {
|
||||
await utils.adminSetup();
|
||||
});
|
||||
|
||||
describe('revoke-admin', () => {
|
||||
it('should revoke admin privileges from a user', async () => {
|
||||
const { child, promise } = immichAdmin(['revoke-admin']);
|
||||
|
||||
let data = '';
|
||||
child.stdout.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
if (data.includes('Please enter the user email:')) {
|
||||
child.stdin.end('admin@immich.cloud\n');
|
||||
}
|
||||
});
|
||||
|
||||
const { stdout, exitCode } = await promise;
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
expect(stdout).toContain('Admin access has been revoked from');
|
||||
});
|
||||
});
|
||||
|
||||
describe('grant-admin', () => {
|
||||
it('should grant admin privileges to a user', async () => {
|
||||
const { child, promise } = immichAdmin(['grant-admin']);
|
||||
|
||||
let data = '';
|
||||
child.stdout.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
if (data.includes('Please enter the user email:')) {
|
||||
child.stdin.end('admin@immich.cloud\n');
|
||||
}
|
||||
});
|
||||
|
||||
const { stdout, exitCode } = await promise;
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
expect(stdout).toContain('Admin access has been granted to');
|
||||
});
|
||||
});
|
||||
|
||||
describe('list-users', () => {
|
||||
it('should list the admin user', async () => {
|
||||
const { stdout, exitCode } = await immichAdmin(['list-users']).promise;
|
||||
|
||||
@@ -103,6 +103,7 @@ export const loginResponseDto = {
|
||||
accessToken: expect.any(String),
|
||||
name: 'Immich Admin',
|
||||
isAdmin: true,
|
||||
isOnboarded: false,
|
||||
profileImagePath: '',
|
||||
shouldChangePassword: true,
|
||||
userEmail: 'admin@immich.cloud',
|
||||
|
||||
@@ -33,7 +33,9 @@ test.describe('Registration', () => {
|
||||
// onboarding
|
||||
await expect(page).toHaveURL('/auth/onboarding');
|
||||
await page.getByRole('button', { name: 'Theme' }).click();
|
||||
await page.getByRole('button', { name: 'Privacy' }).click();
|
||||
await page.getByRole('button', { name: 'Language' }).click();
|
||||
await page.getByRole('button', { name: 'Server Privacy' }).click();
|
||||
await page.getByRole('button', { name: 'User Privacy' }).click();
|
||||
await page.getByRole('button', { name: 'Storage Template' }).click();
|
||||
await page.getByRole('button', { name: 'Done' }).click();
|
||||
|
||||
@@ -77,6 +79,13 @@ test.describe('Registration', () => {
|
||||
await page.getByLabel('Password').fill('new-password');
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
|
||||
// onboarding
|
||||
await expect(page).toHaveURL('/auth/onboarding');
|
||||
await page.getByRole('button', { name: 'Theme' }).click();
|
||||
await page.getByRole('button', { name: 'Language' }).click();
|
||||
await page.getByRole('button', { name: 'User Privacy' }).click();
|
||||
await page.getByRole('button', { name: 'Done' }).click();
|
||||
|
||||
// success
|
||||
await expect(page).toHaveURL(/\/photos/);
|
||||
});
|
||||
|
||||
89
e2e/src/web/specs/user-admin.e2e-spec.ts
Normal file
89
e2e/src/web/specs/user-admin.e2e-spec.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { getUserAdmin } from '@immich/sdk';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { asBearerAuth, utils } from 'src/utils';
|
||||
|
||||
test.describe('User Administration', () => {
|
||||
test.beforeAll(() => {
|
||||
utils.initSdk();
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await utils.resetDatabase();
|
||||
});
|
||||
|
||||
test('validate admin/users link', async ({ context, page }) => {
|
||||
const admin = await utils.adminSetup();
|
||||
await utils.setAuthCookies(context, admin.accessToken);
|
||||
|
||||
// Navigate to user management page and verify title and header
|
||||
await page.goto(`/admin/users`);
|
||||
await expect(page).toHaveTitle(/User Management/);
|
||||
await expect(page.getByText('User Management')).toBeVisible();
|
||||
});
|
||||
|
||||
test('create user', async ({ context, page }) => {
|
||||
const admin = await utils.adminSetup();
|
||||
await utils.setAuthCookies(context, admin.accessToken);
|
||||
|
||||
// Create a new user
|
||||
await page.goto('/admin/users');
|
||||
await page.getByRole('button', { name: 'Create user' }).click();
|
||||
await page.getByLabel('Email').fill('user@immich.cloud');
|
||||
await page.getByLabel('Password', { exact: true }).fill('password');
|
||||
await page.getByLabel('Confirm Password').fill('password');
|
||||
await page.getByLabel('Name').fill('Immich User');
|
||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
|
||||
// Verify the user exists in the user list
|
||||
await page.getByRole('row', { name: 'user@immich.cloud' });
|
||||
});
|
||||
|
||||
test('promote to admin', async ({ context, page }) => {
|
||||
const admin = await utils.adminSetup();
|
||||
await utils.setAuthCookies(context, admin.accessToken);
|
||||
|
||||
const user = await utils.userSetup(admin.accessToken, {
|
||||
name: 'Admin 2',
|
||||
email: 'admin2@immich.cloud',
|
||||
password: 'password',
|
||||
});
|
||||
|
||||
expect(user.isAdmin).toBe(false);
|
||||
|
||||
await page.goto(`/admin/users/${user.userId}`);
|
||||
|
||||
await page.getByRole('button', { name: 'Edit user' }).click();
|
||||
await expect(page.getByLabel('Admin User')).not.toBeChecked();
|
||||
await page.getByText('Admin User').click();
|
||||
await expect(page.getByLabel('Admin User')).toBeChecked();
|
||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||
|
||||
const updated = await getUserAdmin({ id: user.userId }, { headers: asBearerAuth(admin.accessToken) });
|
||||
expect(updated.isAdmin).toBe(true);
|
||||
});
|
||||
|
||||
test('revoke admin access', async ({ context, page }) => {
|
||||
const admin = await utils.adminSetup();
|
||||
await utils.setAuthCookies(context, admin.accessToken);
|
||||
|
||||
const user = await utils.userSetup(admin.accessToken, {
|
||||
name: 'Admin 2',
|
||||
email: 'admin2@immich.cloud',
|
||||
password: 'password',
|
||||
isAdmin: true,
|
||||
});
|
||||
|
||||
expect(user.isAdmin).toBe(true);
|
||||
|
||||
await page.goto(`/admin/users/${user.userId}`);
|
||||
|
||||
await page.getByRole('button', { name: 'Edit user' }).click();
|
||||
await expect(page.getByLabel('Admin User')).toBeChecked();
|
||||
await page.getByText('Admin User').click();
|
||||
await expect(page.getByLabel('Admin User')).not.toBeChecked();
|
||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||
|
||||
const updated = await getUserAdmin({ id: user.userId }, { headers: asBearerAuth(admin.accessToken) });
|
||||
expect(updated.isAdmin).toBe(false);
|
||||
});
|
||||
});
|
||||
Submodule e2e/test-assets updated: 8885d6d01c...18736fc27a
@@ -40,7 +40,6 @@
|
||||
"backup_keep_last_amount": "Aantal vorige rugsteune om te hou",
|
||||
"backup_settings": "Rugsteun instellings",
|
||||
"backup_settings_description": "Bestuur databasis rugsteun instellings",
|
||||
"check_all": "Kies Alles",
|
||||
"cleared_jobs": "Poste gevee vir: {job}",
|
||||
"config_set_by_file": "Config word tans deur 'n konfigurasielêer gestel",
|
||||
"confirm_delete_library": "Is jy seker jy wil {library}-biblioteek uitvee?",
|
||||
@@ -55,12 +54,10 @@
|
||||
"disable_login": "Deaktiveer aanmelding",
|
||||
"duplicate_detection_job_description": "Begin masjienleer op bates om soortgelyke beelde op te spoor. Maak staat op Smart Search",
|
||||
"exclusion_pattern_description": "Met uitsluitingspatrone kan jy lêers en vouers ignoreer wanneer jy jou biblioteek skandeer. Dit is nuttig as jy vouers het wat lêers bevat wat jy nie wil invoer nie, soos RAW-lêers.",
|
||||
"external_library_created_at": "Eksterne biblioteek (geskep op {date})",
|
||||
"external_library_management": "Eksterne Biblioteekbestuur",
|
||||
"face_detection": "Gesig deteksie",
|
||||
"failed_job_command": "Opdrag {command} het misluk vir werk: {job}",
|
||||
"force_delete_user_warning": "WAARSKUWING: Dit sal onmiddellik die gebruiker en alle bates verwyder. Dit kan nie ontdoen word nie en die lêers kan nie herstel word nie.",
|
||||
"forcing_refresh_library_files": "Forseer herlaai van alle biblioteeklêers",
|
||||
"image_format": "Formaat",
|
||||
"image_format_description": "WebP produseer kleiner lêers as JPEG, maar is stadiger om te enkodeer.",
|
||||
"image_prefer_embedded_preview": "Verkies ingebedde voorskou",
|
||||
|
||||
216
i18n/ar.json
216
i18n/ar.json
@@ -14,7 +14,6 @@
|
||||
"add_a_location": "إضافة موقع",
|
||||
"add_a_name": "إضافة إسم",
|
||||
"add_a_title": "إضافة عنوان",
|
||||
"add_endpoint": "Add endpoint",
|
||||
"add_exclusion_pattern": "إضافة نمط إستثناء",
|
||||
"add_import_path": "إضافة مسار الإستيراد",
|
||||
"add_location": "إضافة موقع",
|
||||
@@ -44,8 +43,6 @@
|
||||
"backup_keep_last_amount": "مقدار النسخ الاحتياطية السابقة للاحتفاظ بها",
|
||||
"backup_settings": "إعدادات النسخ الاحتياطي",
|
||||
"backup_settings_description": "إدارة إعدادات النسخ الاحتياطي لقاعدة البيانات",
|
||||
"check_all": "اختر الكل",
|
||||
"cleanup": "تنظيف",
|
||||
"cleared_jobs": "تم إخلاء مهام: {job}",
|
||||
"config_set_by_file": "الإعدادات حاليًا معينة عن طريق ملف الاعدادات",
|
||||
"confirm_delete_library": "هل أنت متأكد أنك تريد حذف مكتبة {library}؟",
|
||||
@@ -60,14 +57,12 @@
|
||||
"disable_login": "تعطيل تسجيل الدخول",
|
||||
"duplicate_detection_job_description": "بدء التعلم الآلي على المحتوى للعثور على الصور المتشابهة. يعتمد على البحث الذكي",
|
||||
"exclusion_pattern_description": "تتيح لك أنماط الاستبعاد تجاهل الملفات والمجلدات عند فحص مكتبتك. يعد هذا مفيدًا إذا كان لديك مجلدات تحتوي على ملفات لا تريد استيرادها، مثل ملفات RAW.",
|
||||
"external_library_created_at": "مكتبة خارجية (أُنشئت في {date})",
|
||||
"external_library_management": "إدارة المكتبة الخارجية",
|
||||
"face_detection": "إكتشاف الوجوه",
|
||||
"face_detection_description": "اكتشف الوجوه في الأصول باستخدام التعلم الآلي. بالنسبة لمقاطع الفيديو، يتم اعتبار الصورة المصغرة فقط. \"تحديث\" (إعادة) معالجة جميع الأصول. \"إعادة تعيين\" تمسح أيضًا جميع بيانات الوجوه الحالية. \"مفقود\" يضع الأصول التي لم تتم معالجتها بعد في قائمة الانتظار. سيتم وضع الوجوه المكتشفة في قائمة الانتظار للتعرف على الوجه بعد اكتمال اكتشاف الوجه، وتجميعها في أشخاص موجودين أو جدد.",
|
||||
"facial_recognition_job_description": "تجميع الوجوه المكتشفة كأشخاص. يتم تنفيذ هذه الخطوة بعد اكتمال اكتشاف الوجه. خيار \"إعادة التعيين\" يعيد تجميع جميع الوجوه. خيار \"المفقود\" يضع في قائمة الانتظار الوجوه التي لم يتم تعيين شخص لها.",
|
||||
"failed_job_command": "فشل الأمر {command} للمهمة: {job}",
|
||||
"force_delete_user_warning": "تحذير: سيؤدي ذلك إلى إزالة المستخدم وجميع محتوياته على الفور. لا يمكن التراجع عن هذا الإجراء ولا يمكن استرداد الملفات.",
|
||||
"forcing_refresh_library_files": "إجبار التحديث لجميع ملفات المكتبة",
|
||||
"image_format": "التنسيق",
|
||||
"image_format_description": "يُنتج WebP ملفات أصغر حجمًا من ملفات JPEG، ولكنه أبطأ في عملية الترميز.",
|
||||
"image_prefer_embedded_preview": "تفضيل المعاينة المدمجة",
|
||||
@@ -190,7 +185,7 @@
|
||||
"oauth_enable_description": "تسجيل الدخول باستخدام OAuth",
|
||||
"oauth_mobile_redirect_uri": "عنوان 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_settings": "OAuth",
|
||||
"oauth_settings_description": "إدارة إعدادات تسجيل الدخول OAuth",
|
||||
"oauth_settings_more_details": "لمزيد من التفاصيل حول هذه الميزة، يرجى الرجوع إلى <link>الوثائق</link>.",
|
||||
@@ -200,8 +195,6 @@
|
||||
"oauth_storage_quota_claim_description": "قم تلقائيًا بتعيين حصة التخزين للمستخدم على قيمة هذه المطالبة.",
|
||||
"oauth_storage_quota_default": "حصة التخزين الافتراضية (جيجابايت)",
|
||||
"oauth_storage_quota_default_description": "الحصة بالجيجابايت التي سيتم استخدامها عندما لا يتم توفير مطالبة (أدخل 0 لحصة غير محدودة).",
|
||||
"offline_paths": "مسارات غير متصلة",
|
||||
"offline_paths_description": "قد تكون هذه النتائج ناتجة عن حذف يدوي لملفات لا تتبع لمكتبة خارجية.",
|
||||
"password_enable_description": "تسجيل الدخول باستخدام البريد الكتروني وكلمة المرور",
|
||||
"password_settings": "تسجيل الدخول بكلمة المرور",
|
||||
"password_settings_description": "إدارة تسجيل الدخول بكلمة المرور",
|
||||
@@ -211,9 +204,6 @@
|
||||
"refreshing_all_libraries": "تحديث كافة المكتبات",
|
||||
"registration": "تسجيل المدير",
|
||||
"registration_description": "بما أنك أول مستخدم في النظام، سيتم تعيينك كمسؤول وستكون مسؤولًا عن المهام الإدارية، وسيتم إنشاء مستخدمين إضافيين بواسطتك.",
|
||||
"repair_all": "إصلاح الكل",
|
||||
"repair_matched_items": "تمت مطابقة {count, plural, one {# عنصر} other {# عناصر}}",
|
||||
"repaired_items": "تم إصلاح {count, plural, one {# عنصر} other {# عناصر}}",
|
||||
"require_password_change_on_login": "الطلب من المستخدم تغيير كلمة المرور عند تسجيل الدخول الأول",
|
||||
"reset_settings_to_default": "إعادة ضبط الإعدادات إلى الوضع الافتراضي",
|
||||
"reset_settings_to_recent_saved": "إعادة ضبط الإعدادات إلى الإعدادات المحفوظة مؤخرًا",
|
||||
@@ -242,7 +232,6 @@
|
||||
"storage_template_migration_info": "تغييرات القالب ستنطبق فقط على المحتويات الجديدة. لتطبيق القالب على المحتويات التي تم رفعها سابقًا، قم بتشغيل <link>{job}</link>.",
|
||||
"storage_template_migration_job": "وظيفة تهجير قالب التخزين",
|
||||
"storage_template_more_details": "لمزيد من التفاصيل حول هذه الميزة، يرجى الرجوع إلى <template-link>Storage Template</template-link> و<implications-link>implications</implications-link>",
|
||||
"storage_template_onboarding_description": "عند تفعيل هذه الميزة، سيقوم بتنظيم الملفات تلقائيًا بناءً على قالب محدد من قبل المستخدم. بسبب مشاكل الاستقرار، تم تعطيل الميزة افتراضيًا. للمزيد من المعلومات، يرجى الرجوع إلى <link>الوثائق</link>.",
|
||||
"storage_template_path_length": "الحد التقريبي لطول المسار: <b>{length, number}</b>/{limit, number}",
|
||||
"storage_template_settings": "قالب التخزين",
|
||||
"storage_template_settings_description": "إدارة هيكل المجلد واسم الملف للأصول المرفوعة",
|
||||
@@ -254,7 +243,6 @@
|
||||
"template_email_invite_album": "قالب دعوة الألبوم",
|
||||
"template_email_preview": "عرض مسبق",
|
||||
"template_email_settings": "نماذج البريد الالكتروني",
|
||||
"template_email_settings_description": "إدارة قوالب إشعارات البريد الإلكتروني المخصصة",
|
||||
"template_email_update_album": "تحديث قالب الألبوم",
|
||||
"template_email_welcome": "قالب البريد الإلكتروني الترحيبي",
|
||||
"template_settings": "قوالب الإشعارات",
|
||||
@@ -263,7 +251,6 @@
|
||||
"theme_custom_css_settings_description": "أوراق الأنماط المتتالية تسمح بتخصيص تصميم Immich.",
|
||||
"theme_settings": "إعدادات السمة",
|
||||
"theme_settings_description": "إدارة تخصيص واجهة ويب Immich",
|
||||
"these_files_matched_by_checksum": "تتم مطابقة هذه الملفات من خلال المجاميع الاختبارية الخاصة بهم",
|
||||
"thumbnail_generation_job": "إنشاء الصور المصغرة",
|
||||
"thumbnail_generation_job_description": "إنشاء صور مصغرة كبيرة وصغيرة وغير واضحة لكل أصل، بالإضافة إلى صور مصغرة لكل شخص",
|
||||
"transcoding_acceleration_api": "واجهة برمجة التطبيقات للتسريع",
|
||||
@@ -294,7 +281,6 @@
|
||||
"transcoding_hardware_acceleration_description": "تجريبي؛ أسرع بكثير، ولكن ستكون جودتها أقل عند نفس معدل البت",
|
||||
"transcoding_hardware_decoding": "فك تشفير الأجهزة",
|
||||
"transcoding_hardware_decoding_setting_description": "ينطبق ذلك فقط على NVENC، QSV، و RKMPP. يمكن التسريع من طرف لطرف بدلاً من تسريع الترميز فقط. قد لا يعمل على جميع مقاطع الفيديو.",
|
||||
"transcoding_hevc_codec": "كود HEVC",
|
||||
"transcoding_max_b_frames": "أقصى عدد من الإطارات B",
|
||||
"transcoding_max_b_frames_description": "القيم الأعلى تعزز كفاءة الضغط، ولكنها تبطئ عملية الترميز. قد لا تكون متوافقة مع التسريع العتادي على الأجهزة القديمة. قيمة 0 تعطل إطارات B، بينما تضبط القيمة -1 هذا القيمة تلقائيًا.",
|
||||
"transcoding_max_bitrate": "الحد الأقصى لمعدل البت",
|
||||
@@ -332,8 +318,6 @@
|
||||
"trash_number_of_days_description": "عدد أيام الاحتفاظ بالمحتويات في سلة المهملات قبل حذفها نهائيًا",
|
||||
"trash_settings": "إعدادات سلة المهملات",
|
||||
"trash_settings_description": "إدارة إعدادات سلة المهملات",
|
||||
"untracked_files": "الملفات التي لم يتم تعقبها",
|
||||
"untracked_files_description": "لا يتم تعقب هذه الملفات بواسطة التطبيق. يمكن أن تكون نتيجة لعمليات نقل فاشلة، أو عمليات تحميل متقطعة، أو يتم تركها في الخلف بسبب خطأ ما",
|
||||
"user_cleanup_job": "تنظيف المستخدم",
|
||||
"user_delete_delay": "سيتم جدولة حساب <b>{user}</b> ومحتوياته للحذف النهائي في غضون {delay, plural, one {# يوم} other {# أيام}}.",
|
||||
"user_delete_delay_settings": "فترة التأخير قبل الحذف",
|
||||
@@ -359,12 +343,8 @@
|
||||
"admin_password": "كلمة سر المشرف",
|
||||
"administration": "الإدارة",
|
||||
"advanced": "متقدم",
|
||||
"advanced_settings_log_level_title": "Log level: {}",
|
||||
"advanced_settings_prefer_remote_subtitle": "تكون بعض الأجهزة بطيئة للغاية في تحميل الصور المصغرة من الأصول الموجودة على الجهاز. قم بتنشيط هذا الإعداد لتحميل الصور البعيدة بدلاً من ذلك.",
|
||||
"advanced_settings_prefer_remote_title": "تفضل الصور البعيدة",
|
||||
"advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
|
||||
"advanced_settings_proxy_headers_title": "Proxy Headers",
|
||||
"advanced_settings_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_tile_subtitle": "إعدادات المستخدم المتقدمة",
|
||||
"advanced_settings_troubleshooting_subtitle": "تمكين الميزات الإضافية لاستكشاف الأخطاء وإصلاحها",
|
||||
@@ -387,10 +367,6 @@
|
||||
"album_remove_user": "هل ترغب في إزالة المستخدم؟",
|
||||
"album_remove_user_confirmation": "هل أنت متأكد أنك تريد إزالة {user}؟",
|
||||
"album_share_no_users": "يبدو أنك قمت بمشاركة هذا الألبوم مع جميع المستخدمين أو ليس لديك أي مستخدم للمشاركة معه.",
|
||||
"album_thumbnail_card_item": "عنصر واحد",
|
||||
"album_thumbnail_card_items": "{} items",
|
||||
"album_thumbnail_card_shared": " · . مشترك",
|
||||
"album_thumbnail_shared_by": "Shared by {}",
|
||||
"album_updated": "تم تحديث الألبوم",
|
||||
"album_updated_setting_description": "تلقي إشعارًا عبر البريد الإلكتروني عندما يحتوي الألبوم المشترك على محتويات جديدة",
|
||||
"album_user_left": "تم ترك {album}",
|
||||
@@ -428,10 +404,8 @@
|
||||
"archive": "الأرشيف",
|
||||
"archive_or_unarchive_photo": "أرشفة الصورة أو إلغاء أرشفتها",
|
||||
"archive_page_no_archived_assets": "لم يتم العثور على الأصول المؤرشفة",
|
||||
"archive_page_title": "Archive ({})",
|
||||
"archive_size": "حجم الأرشيف",
|
||||
"archive_size_description": "تكوين حجم الأرشيف للتنزيلات (بالجيجابايت)",
|
||||
"archived": "Archived",
|
||||
"archived_count": "{count, plural, other {الأرشيف #}}",
|
||||
"are_these_the_same_person": "هل هؤلاء هم نفس الشخص؟",
|
||||
"are_you_sure_to_do_this": "هل انت متأكد من أنك تريد أن تفعل هذا؟",
|
||||
@@ -453,39 +427,26 @@
|
||||
"asset_list_settings_title": "شبكة الصور",
|
||||
"asset_offline": "المحتوى غير اتصال",
|
||||
"asset_offline_description": "لم يعد هذا الأصل الخارجي موجودًا على القرص. يرجى الاتصال بمسؤول Immich للحصول على المساعدة.",
|
||||
"asset_restored_successfully": "Asset restored successfully",
|
||||
"asset_skipped": "تم تخطيه",
|
||||
"asset_skipped_in_trash": "في سلة المهملات",
|
||||
"asset_uploaded": "تم الرفع",
|
||||
"asset_uploading": "جارٍ الرفع…",
|
||||
"asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
|
||||
"asset_viewer_settings_title": "عارض الأصول",
|
||||
"assets": "المحتويات",
|
||||
"assets_added_count": "تمت إضافة {count, plural, one {# محتوى} other {# محتويات}}",
|
||||
"assets_added_to_album_count": "تمت إضافة {count, plural, one {# الأصل} other {# الأصول}} إلى الألبوم",
|
||||
"assets_added_to_name_count": "تم إضافة {count, plural, one {# محتوى} other {# محتويات }} إلى {hasName, select, true {<b>{name}</b>} other {ألبوم جديد}}",
|
||||
"assets_count": "{count, plural, one {# محتوى} other {# محتويات}}",
|
||||
"assets_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_permanently_deleted_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_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_from_server": "{} asset(s) trashed from the Immich server",
|
||||
"assets_were_part_of_album_count": "{count, plural, one {هذا المحتوى} other {هذه المحتويات}} في الألبوم بالفعل",
|
||||
"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_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_assets_scatter": "يمكن أن تنتشر الأصول عبر ألبومات متعددة. وبالتالي، يمكن تضمين الألبومات أو استبعادها أثناء عملية النسخ الاحتياطي.",
|
||||
"backup_album_selection_page_select_albums": "حدد الألبومات",
|
||||
@@ -494,11 +455,9 @@
|
||||
"backup_all": "الجميع",
|
||||
"backup_background_service_backup_failed_message": "فشل في النسخ الاحتياطي للأصول. جارٍ إعادة المحاولة...",
|
||||
"backup_background_service_connection_failed_message": "فشل في الاتصال بالخادم. جارٍ إعادة المحاولة...",
|
||||
"backup_background_service_current_upload_notification": "Uploading {}",
|
||||
"backup_background_service_default_notification": "التحقق من الأصول الجديدة ...",
|
||||
"backup_background_service_error_title": "خطأ في النسخ الاحتياطي",
|
||||
"backup_background_service_in_progress_notification": "النسخ الاحتياطي للأصول الخاصة بك...",
|
||||
"backup_background_service_upload_failure_notification": "Failed to upload {}",
|
||||
"backup_controller_page_albums": "ألبومات احتياطية",
|
||||
"backup_controller_page_background_app_refresh_disabled_content": "قم بتمكين تحديث تطبيق الخلفية في الإعدادات > عام > تحديث تطبيق الخلفية لاستخدام النسخ الاحتياطي في الخلفية.",
|
||||
"backup_controller_page_background_app_refresh_disabled_title": "تم تعطيل تحديث التطبيق في الخلفية",
|
||||
@@ -509,7 +468,6 @@
|
||||
"backup_controller_page_background_battery_info_title": "تحسين البطارية",
|
||||
"backup_controller_page_background_charging": "فقط أثناء الشحن",
|
||||
"backup_controller_page_background_configure_error": "فشل في تكوين خدمة الخلفية",
|
||||
"backup_controller_page_background_delay": "Delay new assets backup: {}",
|
||||
"backup_controller_page_background_description": "قم بتشغيل خدمة الخلفية لإجراء نسخ احتياطي لأي أصول جديدة تلقائيًا دون الحاجة إلى فتح التطبيق",
|
||||
"backup_controller_page_background_is_off": "تم إيقاف النسخ الاحتياطي التلقائي للخلفية",
|
||||
"backup_controller_page_background_is_on": "النسخ الاحتياطي التلقائي للخلفية قيد التشغيل",
|
||||
@@ -519,12 +477,8 @@
|
||||
"backup_controller_page_backup": "دعم",
|
||||
"backup_controller_page_backup_selected": "المحدد: ",
|
||||
"backup_controller_page_backup_sub": "النسخ الاحتياطي للصور ومقاطع الفيديو",
|
||||
"backup_controller_page_created": "Created on: {}",
|
||||
"backup_controller_page_desc_backup": "قم بتشغيل النسخ الاحتياطي الأمامي لتحميل الأصول الجديدة تلقائيًا إلى الخادم عند فتح التطبيق.",
|
||||
"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_none_selected": "لم يتم التحديد",
|
||||
"backup_controller_page_remainder": "بقية",
|
||||
@@ -533,7 +487,6 @@
|
||||
"backup_controller_page_start_backup": "بدء النسخ الاحتياطي",
|
||||
"backup_controller_page_status_off": "النسخة الاحتياطية التلقائية غير فعالة",
|
||||
"backup_controller_page_status_on": "النسخة الاحتياطية التلقائية فعالة",
|
||||
"backup_controller_page_storage_format": "{} of {} used",
|
||||
"backup_controller_page_to_backup": "الألبومات الاحتياطية",
|
||||
"backup_controller_page_total_sub": "جميع الصور ومقاطع الفيديو الفريدة من ألبومات مختارة",
|
||||
"backup_controller_page_turn_off": "قم بإيقاف تشغيل النسخ الاحتياطي المقدمة",
|
||||
@@ -546,7 +499,6 @@
|
||||
"backup_manual_success": "نجاح",
|
||||
"backup_manual_title": "حالة التحميل",
|
||||
"backup_options_page_title": "خيارات النسخ الاحتياطي",
|
||||
"backup_setting_subtitle": "Manage background and foreground upload settings",
|
||||
"backward": "الى الوراء",
|
||||
"birthdate_saved": "تم حفظ تاريخ الميلاد بنجاح",
|
||||
"birthdate_set_description": "يتم استخدام تاريخ الميلاد لحساب عمر هذا الشخص وقت التقاط الصورة.",
|
||||
@@ -558,21 +510,16 @@
|
||||
"bulk_keep_duplicates_confirmation": "هل أنت متأكد من أنك تريد الاحتفاظ بـ {count, plural, one {# محتوى مكرر} other {# محتويات مكررة}}؟ سيؤدي هذا إلى حل جميع مجموعات النسخ المكررة دون حذف أي شيء.",
|
||||
"bulk_trash_duplicates_confirmation": "هل أنت متأكد من أنك تريد إرسال {count, plural, one {# محتوى مكرر} other {# محتويات مكررة}} إلى سلة المهملات ؟ سيحتفظ هذا بأكبر محتوى من كل مجموعة ويرسل جميع النسخ المكررة الأخرى إلى سلة المهملات.",
|
||||
"buy": "شراء immich",
|
||||
"cache_settings_album_thumbnails": "Library page thumbnails ({} assets)",
|
||||
"cache_settings_clear_cache_button": "مسح ذاكرة التخزين المؤقت",
|
||||
"cache_settings_clear_cache_button_title": "يقوم بمسح ذاكرة التخزين المؤقت للتطبيق.سيؤثر هذا بشكل كبير على أداء التطبيق حتى إعادة بناء ذاكرة التخزين المؤقت.",
|
||||
"cache_settings_duplicated_assets_clear_button": "واضح",
|
||||
"cache_settings_duplicated_assets_subtitle": "الصور ومقاطع الفيديو اللتي تم تجاهلها المدرجة في التطبيق",
|
||||
"cache_settings_duplicated_assets_title": "Duplicated Assets ({})",
|
||||
"cache_settings_image_cache_size": "Image cache size ({} assets)",
|
||||
"cache_settings_statistics_album": "مكتبه الصور المصغره",
|
||||
"cache_settings_statistics_assets": "{} assets ({})",
|
||||
"cache_settings_statistics_full": "صور كاملة",
|
||||
"cache_settings_statistics_shared": "صورة ألبوم مشتركة",
|
||||
"cache_settings_statistics_thumbnail": "الصورة المصغرة",
|
||||
"cache_settings_statistics_title": "استخدام ذاكرة التخزين المؤقت",
|
||||
"cache_settings_subtitle": "تحكم في سلوك التخزين المؤقت لتطبيق الجوال.",
|
||||
"cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)",
|
||||
"cache_settings_tile_subtitle": "التحكم في سلوك التخزين المحلي",
|
||||
"cache_settings_tile_title": "التخزين المحلي",
|
||||
"cache_settings_title": "إعدادات التخزين المؤقت",
|
||||
@@ -581,12 +528,10 @@
|
||||
"camera_model": "طراز الكاميرا",
|
||||
"cancel": "إلغاء",
|
||||
"cancel_search": "الغي البحث",
|
||||
"canceled": "Canceled",
|
||||
"cannot_merge_people": "لا يمكن دمج الأشخاص",
|
||||
"cannot_undo_this_action": "لا يمكنك التراجع عن هذا الإجراء!",
|
||||
"cannot_update_the_description": "لا يمكن تحديث الوصف",
|
||||
"change_date": "غيّر التاريخ",
|
||||
"change_display_order": "Change display order",
|
||||
"change_expiration_time": "تغيير وقت انتهاء الصلاحية",
|
||||
"change_location": "غيّر الموقع",
|
||||
"change_name": "تغيير الإسم",
|
||||
@@ -601,10 +546,6 @@
|
||||
"change_pin_code": "تغيير الرقم السري",
|
||||
"change_your_password": "غير كلمة المرور الخاصة بك",
|
||||
"changed_visibility_successfully": "تم تغيير الرؤية بنجاح",
|
||||
"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": "تحقق من السجلات",
|
||||
"choose_matching_people_to_merge": "اختر الأشخاص المتطابقين لدمجهم",
|
||||
"city": "المدينة",
|
||||
@@ -613,14 +554,6 @@
|
||||
"clear_all_recent_searches": "مسح جميع عمليات البحث الأخيرة",
|
||||
"clear_message": "إخلاء الرسالة",
|
||||
"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": "باتجاه عقارب الساعة",
|
||||
"close": "إغلاق",
|
||||
"collapse": "طي",
|
||||
@@ -633,7 +566,6 @@
|
||||
"comments_are_disabled": "التعليقات معطلة",
|
||||
"common_create_new_album": "إنشاء ألبوم جديد",
|
||||
"common_server_error": "يرجى التحقق من اتصال الشبكة الخاص بك ، والتأكد من أن الجهاز قابل للوصول وإصدارات التطبيق/الجهاز متوافقة.",
|
||||
"completed": "Completed",
|
||||
"confirm": "تأكيد",
|
||||
"confirm_admin_password": "تأكيد كلمة مرور المسؤول",
|
||||
"confirm_delete_face": "هل أنت متأكد من حذف وجه {name} من الأصول؟",
|
||||
@@ -644,13 +576,11 @@
|
||||
"contain": "محتواة",
|
||||
"context": "السياق",
|
||||
"continue": "متابعة",
|
||||
"control_bottom_app_bar_album_info_shared": "{} items · Shared",
|
||||
"control_bottom_app_bar_create_new_album": "إنشاء ألبوم جديد",
|
||||
"control_bottom_app_bar_delete_from_immich": " حذف منال تطبيق",
|
||||
"control_bottom_app_bar_delete_from_local": "حذف من الجهاز",
|
||||
"control_bottom_app_bar_edit_location": "تحديد الوجهة",
|
||||
"control_bottom_app_bar_edit_time": "تحرير التاريخ والوقت",
|
||||
"control_bottom_app_bar_share_link": "Share Link",
|
||||
"control_bottom_app_bar_share_to": "مشاركة إلى",
|
||||
"control_bottom_app_bar_trash_from_immich": "حذفه ونقله في سله المهملات",
|
||||
"copied_image_to_clipboard": "تم نسخ الصورة إلى الحافظة.",
|
||||
@@ -672,7 +602,6 @@
|
||||
"create_link": "إنشاء رابط",
|
||||
"create_link_to_share": "إنشاء رابط للمشاركة",
|
||||
"create_link_to_share_description": "السماح لأي شخص لديه الرابط بمشاهدة الصورة (الصور) المحددة",
|
||||
"create_new": "CREATE NEW",
|
||||
"create_new_person": "إنشاء شخص جديد",
|
||||
"create_new_person_hint": "تعيين المحتويات المحددة لشخص جديد",
|
||||
"create_new_user": "إنشاء مستخدم جديد",
|
||||
@@ -682,11 +611,9 @@
|
||||
"create_tag_description": "أنشئ علامة جديدة. بالنسبة للعلامات المتداخلة، يرجى إدخال المسار الكامل للعلامة بما في ذلك الخطوط المائلة للأمام.",
|
||||
"create_user": "إنشاء مستخدم",
|
||||
"created": "تم الإنشاء",
|
||||
"crop": "Crop",
|
||||
"curated_object_page_title": "أشياء",
|
||||
"current_device": "الجهاز الحالي",
|
||||
"current_pin_code": "الرقم السري الحالي",
|
||||
"current_server_address": "Current server address",
|
||||
"custom_locale": "لغة مخصصة",
|
||||
"custom_locale_description": "تنسيق التواريخ والأرقام بناءً على اللغة والمنطقة",
|
||||
"daily_title_text_date": "E ، MMM DD",
|
||||
@@ -737,7 +664,6 @@
|
||||
"direction": "الإتجاه",
|
||||
"disabled": "معطل",
|
||||
"disallow_edits": "منع التعديلات",
|
||||
"discord": "Discord",
|
||||
"discover": "اكتشف",
|
||||
"dismiss_all_errors": "تجاهل كافة الأخطاء",
|
||||
"dismiss_error": "تجاهل الخطأ",
|
||||
@@ -749,26 +675,12 @@
|
||||
"documentation": "الوثائق",
|
||||
"done": "تم",
|
||||
"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_description": "تضمين مقاطع الفيديو المضمنة في الصور المتحركة كملف منفصل",
|
||||
"download_notfound": "Download not found",
|
||||
"download_paused": "Download paused",
|
||||
"download_settings": "التنزيلات",
|
||||
"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_asset_filename": "{filename} قيد التنزيل",
|
||||
"downloading_media": "Downloading media",
|
||||
"drop_files_to_upload": "قم بإسقاط الملفات في أي مكان لرفعها",
|
||||
"duplicates": "التكرارات",
|
||||
"duplicates_description": "قم بحل كل مجموعة من خلال الإشارة إلى التكرارات، إن وجدت",
|
||||
@@ -798,19 +710,15 @@
|
||||
"editor_crop_tool_h2_aspect_ratios": "نسب العرض إلى الارتفاع",
|
||||
"editor_crop_tool_h2_rotation": "التدوير",
|
||||
"email": "البريد الإلكتروني",
|
||||
"empty_folder": "This folder is empty",
|
||||
"empty_trash": "أفرغ سلة المهملات",
|
||||
"empty_trash_confirmation": "هل أنت متأكد أنك تريد إفراغ سلة المهملات؟ سيؤدي هذا إلى إزالة جميع المحتويات الموجودة في سلة المهملات بشكل نهائي من Immich.\nلا يمكنك التراجع عن هذا الإجراء!",
|
||||
"enable": "تفعيل",
|
||||
"enabled": "مفعل",
|
||||
"end_date": "تاريخ الإنتهاء",
|
||||
"enqueued": "Enqueued",
|
||||
"enter_wifi_name": "Enter WiFi name",
|
||||
"error": "خطأ",
|
||||
"error_change_sort_album": "Failed to change album sort order",
|
||||
"error_delete_face": "حدث خطأ في حذف الوجه من الأصول",
|
||||
"error_loading_image": "حدث خطأ أثناء تحميل الصورة",
|
||||
"error_saving_image": "Error: {}",
|
||||
"error_title": "خطأ - حدث خللٌ ما",
|
||||
"errors": {
|
||||
"cannot_navigate_next_asset": "لا يمكن الانتقال إلى المحتوى التالي",
|
||||
@@ -823,7 +731,6 @@
|
||||
"cant_get_number_of_comments": "لا يمكن الحصول على عدد التعليقات",
|
||||
"cant_search_people": "لا يمكن البحث عن الناس",
|
||||
"cant_search_places": "لا يمكن البحث عن الأماكن",
|
||||
"cleared_jobs": "اُخليت المهام لـ: {job}",
|
||||
"error_adding_assets_to_album": "حدث خطأٌ أثناء إضافة المحتويات إلى الألبوم",
|
||||
"error_adding_users_to_album": "حدث خطأٌ أثناء إضافة المستخدمين إلى الألبوم",
|
||||
"error_deleting_shared_user": "حدث خطأٌ أثناء حذف المستخدم المشترك",
|
||||
@@ -832,7 +739,6 @@
|
||||
"error_removing_assets_from_album": "خطأٌّ في إزالة المحتويات من الألبوم، تحقق من وحدة التحكم للحصول على مزيدٍ من التفاصيل",
|
||||
"error_selecting_all_assets": "خطأٌ في تحديد جميع المحتويات",
|
||||
"exclusion_pattern_already_exists": "نمط الاستبعاد هذا موجود مسبقًا.",
|
||||
"failed_job_command": "فشل الأمر {command} لوظيفة: {job}",
|
||||
"failed_to_create_album": "فشل إنشاء الألبوم",
|
||||
"failed_to_create_shared_link": "فشل إنشاء رابط مشترك",
|
||||
"failed_to_edit_shared_link": "فشل تعديل الرابط المشترك",
|
||||
@@ -849,7 +755,6 @@
|
||||
"paths_validation_failed": "فشل في التحقق من {paths, plural, one {# مسار} other {# مسارات}}",
|
||||
"profile_picture_transparent_pixels": "لا يمكن أن تحتوي صور الملف الشخصي على أجزاء/بكسلات شفافة. يرجى التكبير و/أو تحريك الصورة.",
|
||||
"quota_higher_than_disk_size": "لقد قمت بتعيين حصة نسبية أعلى من حجم القرص",
|
||||
"repair_unable_to_check_items": "تعذر التحقق من {count, select, one {عنصر} other {عناصر}}",
|
||||
"unable_to_add_album_users": "تعذر إضافة مستخدمين إلى الألبوم",
|
||||
"unable_to_add_assets_to_shared_link": "تعذر إضافة المحتويات إلى الرابط المشترك",
|
||||
"unable_to_add_comment": "تعذر إضافة التعليق",
|
||||
@@ -867,7 +772,6 @@
|
||||
"unable_to_change_visibility": "غير قادر على تغيير الظهور لـ {count, plural, one {# شخص} other {# أشخاص}}",
|
||||
"unable_to_complete_oauth_login": "غير قادر على إكمال تسجيل الدخول عبر OAuth",
|
||||
"unable_to_connect": "غير قادر على الإتصال",
|
||||
"unable_to_connect_to_server": "غير قادر على الإتصال بالسيرفر",
|
||||
"unable_to_copy_to_clipboard": "لا يمكن النسخ إلى الحافظة، تأكد من استخدامك للصفحة عبر https",
|
||||
"unable_to_create_admin_account": "غير قادر على إنشاء حساب المسؤول",
|
||||
"unable_to_create_api_key": "غير قادر على إنشاء مفتاح API جديد",
|
||||
@@ -891,10 +795,6 @@
|
||||
"unable_to_hide_person": "غير قادر على إخفاء الشخص",
|
||||
"unable_to_link_motion_video": "غير قادر على ربط فيديو الحركة",
|
||||
"unable_to_link_oauth_account": "غير قادر على ربط حساب OAuth",
|
||||
"unable_to_load_album": "غير قادر على تحميل الألبوم",
|
||||
"unable_to_load_asset_activity": "غير قادر على تحميل نشاط المحتويات",
|
||||
"unable_to_load_items": "غير قادر على تحميل العناصر",
|
||||
"unable_to_load_liked_status": "غير قادر على تحميل حالة الإعجاب",
|
||||
"unable_to_log_out_all_devices": "غير قادر على تسجيل الخروج من جميع الأجهزة",
|
||||
"unable_to_log_out_device": "غير قادر على تسجيل الخروج من الجهاز",
|
||||
"unable_to_login_with_oauth": "غير قادر على تسجيل الدخول باستخدام OAuth",
|
||||
@@ -905,11 +805,9 @@
|
||||
"unable_to_remove_album_users": "تعذر إزالة المستخدمين من الألبوم",
|
||||
"unable_to_remove_api_key": "تعذر إزالة مفتاح API",
|
||||
"unable_to_remove_assets_from_shared_link": "غير قادر على إزالة المحتويات من الرابط المشترك",
|
||||
"unable_to_remove_deleted_assets": "غير قادر على إزالة الملفات غير المتصلة",
|
||||
"unable_to_remove_library": "غير قادر على إزالة المكتبة",
|
||||
"unable_to_remove_partner": "غير قادر على إزالة الشريك",
|
||||
"unable_to_remove_reaction": "غير قادر على إزالة رد الفعل",
|
||||
"unable_to_repair_items": "غير قادر على إصلاح العناصر",
|
||||
"unable_to_reset_password": "غير قادر على إعادة تعيين كلمة المرور",
|
||||
"unable_to_resolve_duplicate": "غير قادر على حل التكرارات",
|
||||
"unable_to_restore_assets": "غير قادر على استعادة المحتويات",
|
||||
@@ -944,10 +842,6 @@
|
||||
"exif_bottom_sheet_location": "موقع",
|
||||
"exif_bottom_sheet_people": "الناس",
|
||||
"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": "خروج من العرض التقديمي",
|
||||
"expand_all": "توسيع الكل",
|
||||
"experimental_settings_new_asset_list_subtitle": "أعمال جارية",
|
||||
@@ -964,12 +858,9 @@
|
||||
"extension": "الإمتداد",
|
||||
"external": "خارجي",
|
||||
"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",
|
||||
"face_unassigned": "غير معين",
|
||||
"failed": "Failed",
|
||||
"failed_to_load_assets": "فشل تحميل الأصول",
|
||||
"failed_to_load_folder": "Failed to load folder",
|
||||
"favorite": "مفضل",
|
||||
"favorite_or_unfavorite_photo": "تفضيل أو إلغاء تفضيل الصورة",
|
||||
"favorites": "المفضلة",
|
||||
@@ -981,23 +872,18 @@
|
||||
"file_name_or_extension": "اسم الملف أو امتداده",
|
||||
"filename": "اسم الملف",
|
||||
"filetype": "نوع الملف",
|
||||
"filter": "Filter",
|
||||
"filter_people": "تصفية الاشخاص",
|
||||
"find_them_fast": "يمكنك العثور عليها بسرعة بالاسم من خلال البحث",
|
||||
"fix_incorrect_match": "إصلاح المطابقة غير الصحيحة",
|
||||
"folder": "Folder",
|
||||
"folder_not_found": "Folder not found",
|
||||
"folders": "المجلدات",
|
||||
"folders_feature_description": "تصفح عرض المجلد للصور ومقاطع الفيديو الموجودة على نظام الملفات",
|
||||
"forward": "إلى الأمام",
|
||||
"general": "عام",
|
||||
"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": "البدء",
|
||||
"go_back": "الرجوع للخلف",
|
||||
"go_to_folder": "اذهب إلى المجلد",
|
||||
"go_to_search": "اذهب إلى البحث",
|
||||
"grant_permission": "Grant permission",
|
||||
"group_albums_by": "تجميع الألبومات حسب...",
|
||||
"group_country": "مجموعة البلد",
|
||||
"group_no": "بدون تجميع",
|
||||
@@ -1007,12 +893,6 @@
|
||||
"haptic_feedback_switch": "تمكين ردود الفعل اللمسية",
|
||||
"haptic_feedback_title": "ردود فعل لمسية",
|
||||
"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})",
|
||||
"hide_all_people": "إخفاء جميع الأشخاص",
|
||||
"hide_gallery": "اخفاء المعرض",
|
||||
@@ -1036,8 +916,6 @@
|
||||
"home_page_upload_err_limit": "لا يمكن إلا تحميل 30 أحد الأصول في وقت واحد ، سوف يتخطى",
|
||||
"host": "المضيف",
|
||||
"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_alt_text_date": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {date}",
|
||||
"image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} تم التقاطها مع {person1} في {date}",
|
||||
@@ -1049,7 +927,6 @@
|
||||
"image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}، {country} مع {person1} و{person2} في {date}",
|
||||
"image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}، {country} مع {person1}، {person2}، و{person3} في {date}",
|
||||
"image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}, {country} with {person1}, {person2}, مع {additionalCount, number} آخرين في {date}",
|
||||
"image_saved_successfully": "Image saved",
|
||||
"image_viewer_page_state_provider_download_started": "بدأ التنزيل",
|
||||
"image_viewer_page_state_provider_download_success": "تم التنزيل بنجاح",
|
||||
"image_viewer_page_state_provider_share_error": "خطأ في المشاركة",
|
||||
@@ -1071,8 +948,6 @@
|
||||
"night_at_midnight": "كل ليلة عند منتصف الليل",
|
||||
"night_at_twoam": "كل ليلة الساعة 2 صباحا"
|
||||
},
|
||||
"invalid_date": "Invalid date",
|
||||
"invalid_date_format": "Invalid date format",
|
||||
"invite_people": "دعوة الأشخاص",
|
||||
"invite_to_album": "دعوة إلى الألبوم",
|
||||
"items_count": "{count, plural, one {# عنصر} other {# عناصر}}",
|
||||
@@ -1108,9 +983,6 @@
|
||||
"list": "قائمة",
|
||||
"loading": "تحميل",
|
||||
"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_picker_choose_on_map": "اختر على الخريطة",
|
||||
"location_picker_latitude_error": "أدخل خط عرض صالح",
|
||||
@@ -1126,7 +998,6 @@
|
||||
"login_form_api_exception": " استثناء برمجة التطبيقات. يرجى التحقق من عنوان الخادم والمحاولة مرة أخرى ",
|
||||
"login_form_back_button_text": "الرجوع للخلف",
|
||||
"login_form_email_hint": "yoursemail@email.com",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port",
|
||||
"login_form_endpoint_url": "url نقطة نهاية الخادم",
|
||||
"login_form_err_http": "يرجى تحديد http:// أو https://",
|
||||
"login_form_err_invalid_email": "بريد إلكتروني خاطئ",
|
||||
@@ -1160,8 +1031,6 @@
|
||||
"manage_your_devices": "إدارة الأجهزة التي تم تسجيل الدخول إليها",
|
||||
"manage_your_oauth_connection": "إدارة اتصال OAuth الخاص بك",
|
||||
"map": "الخريطة",
|
||||
"map_assets_in_bound": "{} photo",
|
||||
"map_assets_in_bounds": "{} photos",
|
||||
"map_cannot_get_user_location": "لا يمكن الحصول على موقع المستخدم",
|
||||
"map_location_dialog_yes": "نعم",
|
||||
"map_location_picker_page_use_location": "استخدم هذا الموقع",
|
||||
@@ -1175,9 +1044,7 @@
|
||||
"map_settings": "إعدادات الخريطة",
|
||||
"map_settings_dark_mode": "الوضع المظلم",
|
||||
"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_years": "Past {} years",
|
||||
"map_settings_dialog_title": "إعدادات الخريطة",
|
||||
"map_settings_include_show_archived": "تشمل الأرشفة",
|
||||
"map_settings_include_show_partners": "تضمين الشركاء",
|
||||
@@ -1192,8 +1059,6 @@
|
||||
"memories_setting_description": "إدارة ما تراه في ذكرياتك",
|
||||
"memories_start_over": "ابدأ من جديد",
|
||||
"memories_swipe_to_close": "اسحب لأعلى للإغلاق",
|
||||
"memories_year_ago": "A year ago",
|
||||
"memories_years_ago": "{} years ago",
|
||||
"memory": "ذكرى",
|
||||
"memory_lane_title": "ذكرياتٌ من {title}",
|
||||
"menu": "القائمة",
|
||||
@@ -1217,8 +1082,6 @@
|
||||
"my_albums": "ألبوماتي",
|
||||
"name": "الاسم",
|
||||
"name_or_nickname": "الاسم أو اللقب",
|
||||
"networking_settings": "Networking",
|
||||
"networking_subtitle": "Manage the server endpoint settings",
|
||||
"never": "أبداً",
|
||||
"new_album": "البوم جديد",
|
||||
"new_api_key": "مفتاح API جديد",
|
||||
@@ -1248,7 +1111,6 @@
|
||||
"no_results_description": "جرب كلمة رئيسية مرادفة أو أكثر عمومية",
|
||||
"no_shared_albums_message": "قم بإنشاء ألبوم لمشاركة الصور ومقاطع الفيديو مع الأشخاص في شبكتك",
|
||||
"not_in_any_album": "ليست في أي ألبوم",
|
||||
"not_selected": "Not selected",
|
||||
"note_apply_storage_label_to_previously_uploaded assets": "ملاحظة: لتطبيق تسمية التخزين على المحتويات التي تم رفعها مسبقًا، قم بتشغيل",
|
||||
"notes": "ملاحظات",
|
||||
"notification_permission_dialog_content": "لتمكين الإخطارات ، انتقل إلى الإعدادات و اختار السماح.",
|
||||
@@ -1258,18 +1120,13 @@
|
||||
"notification_toggle_setting_description": "تفعيل إشعارات البريد الإلكتروني",
|
||||
"notifications": "إشعارات",
|
||||
"notifications_setting_description": "إدارة الإشعارات",
|
||||
"oauth": "OAuth",
|
||||
"official_immich_resources": "الموارد الرسمية لشركة Immich",
|
||||
"offline": "غير متصل",
|
||||
"offline_paths": "مسارات غير متصلة",
|
||||
"offline_paths_description": "قد تكون هذه النتائج بسبب الحذف اليدوي للملفات التي لا تشكل جزءًا من مكتبة خارجية.",
|
||||
"ok": "نعم",
|
||||
"oldest_first": "الأقدم أولا",
|
||||
"on_this_device": "On this device",
|
||||
"onboarding": "الإعداد الأولي",
|
||||
"onboarding_privacy_description": "تعتمد الميزات التالية (اختياري) على خدمات خارجية، ويمكن تعطيلها في أي وقت في إعدادات الإدارة.",
|
||||
"onboarding_theme_description": "اختر نسق الألوان للنسخة الخاصة بك. يمكنك تغيير ذلك لاحقًا في إعداداتك.",
|
||||
"onboarding_welcome_description": "لنقم بإعداد نسختك باستخدام بعض الإعدادات الشائعة.",
|
||||
"onboarding_welcome_user": "مرحبا، {user}",
|
||||
"online": "متصل",
|
||||
"only_favorites": "المفضلة فقط",
|
||||
@@ -1289,14 +1146,12 @@
|
||||
"partner_can_access": "يستطيع {partner} الوصول",
|
||||
"partner_can_access_assets": "جميع الصور ومقاطع الفيديو الخاصة بك باستثناء تلك الموجودة في المؤرشفة والمحذوفة",
|
||||
"partner_can_access_location": "الموقع الذي تم التقاط صورك فيه",
|
||||
"partner_list_user_photos": "{user}'s photos",
|
||||
"partner_list_view_all": "عرض الكل",
|
||||
"partner_page_empty_message": "لم يتم مشاركة صورك بعد مع أي شريك.",
|
||||
"partner_page_no_more_users": "لا مزيد من المستخدمين لإضافة",
|
||||
"partner_page_partner_add_failed": "فشل في إضافة شريك",
|
||||
"partner_page_select_partner": "حدد شريكًا",
|
||||
"partner_page_shared_to_title": "مشترك ل",
|
||||
"partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
|
||||
"partner_sharing": "مشاركة الشركاء",
|
||||
"partners": "الشركاء",
|
||||
"password": "كلمة المرور",
|
||||
@@ -1353,7 +1208,6 @@
|
||||
"play_motion_photo": "تشغيل الصور المتحركة",
|
||||
"play_or_pause_video": "تشغيل الفيديو أو إيقافه مؤقتًا",
|
||||
"port": "المنفذ",
|
||||
"preferences_settings_subtitle": "Manage the app's preferences",
|
||||
"preferences_settings_title": "التفضيلات",
|
||||
"preset": "الإعداد المسبق",
|
||||
"preview": "معاينة",
|
||||
@@ -1418,7 +1272,6 @@
|
||||
"recent": "حديث",
|
||||
"recent-albums": "ألبومات الحديثة",
|
||||
"recent_searches": "عمليات البحث الأخيرة",
|
||||
"recently_added": "Recently added",
|
||||
"recently_added_page_title": "أضيف مؤخرا",
|
||||
"refresh": "تحديث",
|
||||
"refresh_encoded_videos": "تحديث مقاطع الفيديو المشفرة",
|
||||
@@ -1476,7 +1329,6 @@
|
||||
"role_editor": "المحرر",
|
||||
"role_viewer": "العارض",
|
||||
"save": "حفظ",
|
||||
"save_to_gallery": "Save to gallery",
|
||||
"saved_api_key": "تم حفظ مفتاح الـ API",
|
||||
"saved_profile": "تم حفظ الملف",
|
||||
"saved_settings": "تم حفظ الإعدادات",
|
||||
@@ -1498,31 +1350,17 @@
|
||||
"search_city": "البحث حسب المدينة...",
|
||||
"search_country": "البحث حسب الدولة...",
|
||||
"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_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_existing_person": "البحث عن شخص موجود",
|
||||
"search_no_more_result": "No more results",
|
||||
"search_no_people": "لا يوجد أشخاص",
|
||||
"search_no_people_named": "لا يوجد أشخاص بالاسم \"{name}\"",
|
||||
"search_no_result": "No results found, try a different search term or combination",
|
||||
"search_options": "خيارات البحث",
|
||||
"search_page_categories": "فئات",
|
||||
"search_page_motion_photos": "الصور المتحركه",
|
||||
"search_page_no_objects": "لا توجد معلومات عن أشياء متاحة",
|
||||
"search_page_no_places": "لا توجد معلومات متوفرة للأماكن",
|
||||
"search_page_screenshots": "لقطات الشاشة",
|
||||
"search_page_search_photos_videos": "Search for your photos and videos",
|
||||
"search_page_selfies": " صور ذاتيه",
|
||||
"search_page_things": "أشياء",
|
||||
"search_page_view_all_button": "عرض الكل",
|
||||
@@ -1561,7 +1399,6 @@
|
||||
"selected_count": "{count, plural, other {# محددة }}",
|
||||
"send_message": "إرسال رسالة",
|
||||
"send_welcome_email": "إرسال بريدًا إلكترونيًا ترحيبيًا",
|
||||
"server_endpoint": "Server Endpoint",
|
||||
"server_info_box_app_version": "نسخة التطبيق",
|
||||
"server_info_box_server_url": "عنوان URL الخادم",
|
||||
"server_offline": "الخادم غير متصل",
|
||||
@@ -1582,29 +1419,20 @@
|
||||
"setting_image_viewer_preview_title": "تحميل صورة معاينة",
|
||||
"setting_image_viewer_title": "الصور",
|
||||
"setting_languages_apply": "تغيير الإعدادات",
|
||||
"setting_languages_subtitle": "Change the app's language",
|
||||
"setting_languages_title": "اللغات",
|
||||
"setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
|
||||
"setting_notifications_notify_hours": "{} hours",
|
||||
"setting_notifications_notify_immediately": "في الحال",
|
||||
"setting_notifications_notify_minutes": "{} minutes",
|
||||
"setting_notifications_notify_never": "أبداً",
|
||||
"setting_notifications_notify_seconds": "{} seconds",
|
||||
"setting_notifications_single_progress_subtitle": "معلومات التقدم التفصيلية تحميل لكل أصل",
|
||||
"setting_notifications_single_progress_title": "إظهار تقدم التفاصيل الاحتياطية الخلفية",
|
||||
"setting_notifications_subtitle": "اضبط تفضيلات الإخطار",
|
||||
"setting_notifications_total_progress_subtitle": "التقدم التحميل العام (تم القيام به/إجمالي الأصول)",
|
||||
"setting_notifications_total_progress_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_require_restart": "يرجى إعادة تشغيل لتطبيق هذا الإعداد",
|
||||
"settings_saved": "تم حفظ الإعدادات",
|
||||
"setup_pin_code": "تحديد رقم سري",
|
||||
"share": "مشاركة",
|
||||
"share_add_photos": "إضافة الصور",
|
||||
"share_assets_selected": "{} selected",
|
||||
"share_dialog_preparing": "تحضير...",
|
||||
"shared": "مُشتَرك",
|
||||
"shared_album_activities_input_disable": "التعليق معطل",
|
||||
@@ -1618,40 +1446,22 @@
|
||||
"shared_by_user": "تمت المشاركة بواسطة {user}",
|
||||
"shared_by_you": "تمت مشاركته من قِبلك",
|
||||
"shared_from_partner": "صور من {partner}",
|
||||
"shared_intent_upload_button_progress_text": "{} / {} Uploaded",
|
||||
"shared_link_app_bar_title": "روابط مشتركة",
|
||||
"shared_link_clipboard_copied_massage": "نسخ إلى الحافظة",
|
||||
"shared_link_clipboard_text": "Link: {}\nPassword: {}",
|
||||
"shared_link_create_error": "خطأ أثناء إنشاء رابط مشترك",
|
||||
"shared_link_edit_description_hint": "أدخل وصف المشاركة",
|
||||
"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_hours": "{} hours",
|
||||
"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_submit_button": "تحديث الرابط",
|
||||
"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_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_options": "خيارات الرابط المشترك",
|
||||
"shared_links": "روابط مشتركة",
|
||||
"shared_links_description": "وصف الروابط المشتركة",
|
||||
"shared_photos_and_videos_count": "{assetCount, plural, other {# الصور ومقاطع الفيديو المُشارَكة.}}",
|
||||
"shared_with_me": "Shared with me",
|
||||
"shared_with_partner": "تمت المشاركة مع {partner}",
|
||||
"sharing": "مشاركة",
|
||||
"sharing_enter_password": "الرجاء إدخال كلمة المرور لعرض هذه الصفحة.",
|
||||
@@ -1727,9 +1537,6 @@
|
||||
"support_third_party_description": "تم حزم تثبيت immich الخاص بك بواسطة جهة خارجية. قد تكون المشكلات التي تواجهها ناجمة عن هذه الحزمة، لذا يرجى طرح المشكلات معهم في المقام الأول باستخدام الروابط أدناه.",
|
||||
"swap_merge_direction": "تبديل اتجاه الدمج",
|
||||
"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_assets": "أصول العلامة",
|
||||
"tag_created": "تم إنشاء العلامة: {tag}",
|
||||
@@ -1744,14 +1551,8 @@
|
||||
"theme_selection": "اختيار السمة",
|
||||
"theme_selection_description": "قم بتعيين السمة تلقائيًا على اللون الفاتح أو الداكن بناءً على تفضيلات نظام المتصفح الخاص بك",
|
||||
"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_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_theme_subtitle": "اختر إعدادات مظهر التطبيق",
|
||||
"theme_setting_three_stage_loading_subtitle": "قد يزيد التحميل من ثلاث مراحل من أداء التحميل ولكنه يسبب تحميل شبكة أعلى بكثير",
|
||||
@@ -1768,22 +1569,18 @@
|
||||
"to_parent": "انتقل إلى الوالد",
|
||||
"to_trash": "حذف",
|
||||
"toggle_settings": "الإعدادات",
|
||||
"toggle_theme": "تبديل المظهر الداكن",
|
||||
"total": "الإجمالي",
|
||||
"total_usage": "الاستخدام الإجمالي",
|
||||
"trash": "المهملات",
|
||||
"trash_all": "نقل الكل إلى سلة المهملات",
|
||||
"trash_count": "سلة المحملات {count, number}",
|
||||
"trash_delete_asset": "حذف/نقل المحتوى إلى سلة المهملات",
|
||||
"trash_emptied": "Emptied trash",
|
||||
"trash_no_results_message": "ستظهر هنا الصور ومقاطع الفيديو المحذوفة.",
|
||||
"trash_page_delete_all": "حذف الكل",
|
||||
"trash_page_empty_trash_dialog_content": "هل تريد تفريغ أصولك المهملة؟ ستتم إزالة هذه العناصر نهائيًا من التطبيق",
|
||||
"trash_page_info": "Trashed items will be permanently deleted after {} days",
|
||||
"trash_page_no_assets": "لا توجد اصول في سله المهملات",
|
||||
"trash_page_restore_all": "استعادة الكل",
|
||||
"trash_page_select_assets_btn": "اختر الأصول ",
|
||||
"trash_page_title": "Trash ({})",
|
||||
"trashed_items_will_be_permanently_deleted_after": "سيتم حذفُ العناصر المحذوفة نِهائيًا بعد {days, plural, one {# يوم} other {# أيام }}.",
|
||||
"type": "النوع",
|
||||
"unable_to_change_pin_code": "تفيير الرقم السري غير ممكن",
|
||||
@@ -1808,8 +1605,6 @@
|
||||
"unselect_all_duplicates": "إلغاء تحديد كافة النسخ المكررة",
|
||||
"unstack": "فك الكومه",
|
||||
"unstacked_assets_count": "تم إخراج {count, plural, one {# الأصل} other {# الأصول}} من التكديس",
|
||||
"untracked_files": "الملفات التي لم يتم تعقبها",
|
||||
"untracked_files_decription": "لا يتم تعقب هذه الملفات بواسطة التطبيق. يمكن أن تكون نتيجةً لعمليات نقل فاشلة، أو عمليات رفع متقطعة، أو يتم تركها في الخلف بسبب خللاً ما",
|
||||
"up_next": "التالي",
|
||||
"updated_password": "تم تحديث كلمة المرور",
|
||||
"upload": "رفع",
|
||||
@@ -1823,11 +1618,8 @@
|
||||
"upload_status_errors": "الأخطاء",
|
||||
"upload_status_uploaded": "تم الرفع",
|
||||
"upload_success": "تم الرفع بنجاح، قم بتحديث الصفحة لرؤية المحتويات المرفوعة الجديدة.",
|
||||
"upload_to_immich": "Upload to Immich ({})",
|
||||
"uploading": "Uploading",
|
||||
"url": "عنوان URL",
|
||||
"usage": "الاستخدام",
|
||||
"use_current_connection": "use current connection",
|
||||
"use_custom_date_range": "استخدم النطاق الزمني المخصص بدلاً من ذلك",
|
||||
"user": "مستخدم",
|
||||
"user_id": "معرف المستخدم",
|
||||
@@ -1844,16 +1636,10 @@
|
||||
"users": "المستخدمين",
|
||||
"utilities": "أدوات",
|
||||
"validate": "تحقْق",
|
||||
"validate_endpoint_error": "Please enter a valid URL",
|
||||
"variables": "المتغيرات",
|
||||
"version": "الإصدار",
|
||||
"version_announcement_closing": "صديقك، أليكس",
|
||||
"version_announcement_message": "مرحبًا! يتوفر إصدار جديد من Immich. يُرجى تخصيص بعض الوقت لقراءة <link>ملاحظات الإصدار</link> للتأكد من تحديث إعداداتك لمنع أي أخطاء في التكوين، خاصة إذا كنت تستخدم WatchTower أو أي آلية تتولى تحديث مثيل Immich الخاص بك تلقائيًا.",
|
||||
"version_announcement_overlay_release_notes": "ملاحظات الإصدار",
|
||||
"version_announcement_overlay_text_1": "مرحبًا يا صديقي ، هناك إصدار جديد",
|
||||
"version_announcement_overlay_text_2": "من فضلك خذ وقتك لزيارة",
|
||||
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
|
||||
"version_announcement_overlay_title": "نسخه جديده متاحه للخادم ",
|
||||
"version_history": "تاريخ الإصدار",
|
||||
"version_history_item": "تم تثبيت {version} في {date}",
|
||||
"video": "فيديو",
|
||||
|
||||
@@ -34,18 +34,15 @@
|
||||
"backup_database_enable_description": "Verilənlər bazasının ehtiyat nüsxələrini aktiv et",
|
||||
"backup_settings": "Ehtiyat Nüsxə Parametrləri",
|
||||
"backup_settings_description": "Verilənlər bazasının ehtiyat nüsxə parametrlərini idarə et",
|
||||
"check_all": "Hamısını yoxla",
|
||||
"config_set_by_file": "Konfiqurasiya hal-hazırda konfiqurasiya faylı ilə təyin olunub",
|
||||
"confirm_delete_library": "{library} kitabxanasını silmək istədiyinizdən əminmisiniz?",
|
||||
"confirm_email_below": "Təsdiqləmək üçün aşağıya {email} yazın",
|
||||
"confirm_user_password_reset": "{user} adlı istifadəçinin şifrəsini sıfırlamaq istədiyinizdən əminmisiniz?",
|
||||
"disable_login": "Giriş etməni söndür",
|
||||
"duplicate_detection_job_description": "Bənzər şəkilləri tapmaq üçün maşın öyrənməsini işə salın. Bu prosses Smart Search funksiyasına əsaslanır",
|
||||
"external_library_created_at": "Xarici kitabxana ({date} (tarixində yaradıldı)",
|
||||
"external_library_management": "Xarici kitabxana idarəetməsi",
|
||||
"face_detection": "Üz tanıma",
|
||||
"force_delete_user_warning": "XƏBƏRDARLIQ: Bu əməliyyat istifadəçi və bütün məlumatları siləcəkdir. Bu prossesi və silinən faylları geri qaytarmaq olmaz.",
|
||||
"forcing_refresh_library_files": "Bütün kitabxana fayllarını məcburi yeniləmə",
|
||||
"image_format_description": "WebP, JPEG faylına görə daha kiçik həcmə sahibdir, lakin onu kodlaşdırmaq daha çox vaxt alır.",
|
||||
"image_preview_title": "Önizləmə parametrləri",
|
||||
"image_quality": "Keyfiyyət",
|
||||
@@ -76,7 +73,6 @@
|
||||
"library_watching_settings_description": "Dəyişdirilən faylları avtomatik olaraq yoxla",
|
||||
"logging_enable_description": "Jurnalı aktivləşdir",
|
||||
"logging_level_description": "Aktiv edildikdə hansı jurnal səviyyəsi istifadə olunur.",
|
||||
"logging_settings": "",
|
||||
"machine_learning_clip_model": "CLIP modeli",
|
||||
"machine_learning_clip_model_description": "<link>Burada</link>qeyd olunan CLIP modelinin adı. Modeli dəyişdirdikdən sonra bütün şəkillər üçün 'Ağıllı Axtarış' funksiyasını yenidən işə salmalısınız.",
|
||||
"machine_learning_duplicate_detection": "Dublikat Aşkarlama",
|
||||
|
||||
@@ -44,8 +44,6 @@
|
||||
"backup_keep_last_amount": "Колькасць папярэдніх рэзервовых копій для захавання",
|
||||
"backup_settings": "Налады рэзервовага капіявання",
|
||||
"backup_settings_description": "Кіраванне наладамі дампа базы дадзеных. Заўвага: гэтыя задачы не кантралююцца, і ў выпадку няўдачы паведамленне адпраўлена не будзе.",
|
||||
"check_all": "Праверыць усе",
|
||||
"cleanup": "Ачыстка",
|
||||
"cleared_jobs": "Ачышчаны заданні для: {job}",
|
||||
"config_set_by_file": "Канфігурацыя ў зараз усталявана праз файл канфігурацыі",
|
||||
"confirm_delete_library": "Вы ўпэўнены што жадаеце выдаліць {library} бібліятэку?",
|
||||
@@ -60,14 +58,12 @@
|
||||
"disable_login": "Адключыць уваход",
|
||||
"duplicate_detection_job_description": "Запусціць машыннае навучанне на актывах для выяўлення падобных выяў. Залежыць ад Smart Search",
|
||||
"exclusion_pattern_description": "Шаблоны выключэння дазваляюць ігнараваць файлы і папкі пры сканаванні вашай бібліятэкі. Гэта карысна, калі ў вас ёсць папкі, якія змяшчаюць файлы, якія вы не хочаце імпартаваць, напрыклад, файлы RAW.",
|
||||
"external_library_created_at": "Знешняя бібліятэка (створана {date})",
|
||||
"external_library_management": "Кіраванне знешняй бібліятэкай",
|
||||
"face_detection": "Выяўленне твараў",
|
||||
"face_detection_description": "Выяўляць твары на фотаздымках і відэа з дапамогай машыннага навучання. Для відэа ўлічваецца толькі мініяцюра. \"Абнавіць\" (пера)апрацоўвае ўсе медыя. \"Скінуць\" дадаткова ачышчае ўсе бягучыя дадзеныя пра твары. \"Адсутнічае\" ставіць у чаргу медыя, якія яшчэ не былі апрацаваныя. Выяўленыя твары будуць пастаўлены ў чаргу для распазнавання асоб пасля завяршэння выяўлення твараў, з групаваннем іх па існуючых або новых людзях.",
|
||||
"facial_recognition_job_description": "Групаваць выяўленыя твары па асобах. Гэты этап выконваецца пасля завяршэння выяўлення твараў. \"Скінуць\" (паўторна) перагрупоўвае ўсе твары. \"Адсутнічае\" ставіць у чаргу твары, якія яшчэ не прыпісаныя да якой-небудзь асобы.",
|
||||
"failed_job_command": "Каманда {command} не выканалася для задання: {job}",
|
||||
"force_delete_user_warning": "ПАПЯРЭДЖАННЕ: Гэта дзеянне неадкладна выдаліць карыстальніка і ўсе аб'екты. Гэта дзеянне не можа быць адроблена і файлы немагчыма будзе аднавіць.",
|
||||
"forcing_refresh_library_files": "Прымусовае абнаўленне ўсіх файлаў бібліятэкі",
|
||||
"image_format": "Фармат",
|
||||
"image_format_description": "WebP стварае меншыя файлы, чым JPEG, але павольней кадуе.",
|
||||
"image_fullsize_description": "Выява ў поўным памеры без метаданых, выкарыстоўваецца пры павелічэнні",
|
||||
|
||||
719
i18n/bg.json
719
i18n/bg.json
File diff suppressed because it is too large
Load Diff
844
i18n/bi.json
844
i18n/bi.json
@@ -3,8 +3,6 @@
|
||||
"account": "Akaont",
|
||||
"account_settings": "Seting blo Akaont",
|
||||
"acknowledge": "Akcept",
|
||||
"action": "",
|
||||
"actions": "",
|
||||
"active": "Stap Mekem",
|
||||
"activity": "Wanem hemi Mekem",
|
||||
"activity_changed": "WAnem hemi Mekem hemi",
|
||||
@@ -16,845 +14,5 @@
|
||||
"add_exclusion_pattern": "Putem wan paten wae hemi karem aot",
|
||||
"add_import_path": "Putem wan pat blo import",
|
||||
"add_location": "Putem wan place blo hem",
|
||||
"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_enable_description": "",
|
||||
"oauth_mobile_redirect_uri": "",
|
||||
"oauth_mobile_redirect_uri_override": "",
|
||||
"oauth_mobile_redirect_uri_override_description": "",
|
||||
"oauth_settings": "",
|
||||
"oauth_settings_description": "",
|
||||
"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": ""
|
||||
"add_more_users": "Putem mor man"
|
||||
}
|
||||
|
||||
@@ -13,5 +13,7 @@
|
||||
"add_a_location": "একটি অবস্থান যোগ করুন",
|
||||
"add_a_name": "একটি নাম যোগ করুন",
|
||||
"add_a_title": "একটি শিরোনাম যোগ করুন",
|
||||
"add_endpoint": "এন্ডপয়েন্ট যোগ করুন"
|
||||
"add_endpoint": "এন্ডপয়েন্ট যোগ করুন",
|
||||
"add_exclusion_pattern": "বহির্ভূতকরণ নমুনা",
|
||||
"add_url": "লিঙ্ক যোগ করুন"
|
||||
}
|
||||
|
||||
181
i18n/ca.json
181
i18n/ca.json
@@ -22,6 +22,7 @@
|
||||
"add_partner": "Afegir company/a",
|
||||
"add_path": "Afegir una ruta",
|
||||
"add_photos": "Afegir fotografies",
|
||||
"add_tag": "Afegir una etiqueta",
|
||||
"add_to": "Afegir a…",
|
||||
"add_to_album": "Afegir a un l'àlbum",
|
||||
"add_to_album_bottom_sheet_added": "Afegit a {album}",
|
||||
@@ -33,6 +34,7 @@
|
||||
"added_to_favorites_count": "{count, number} afegits als preferits",
|
||||
"admin": {
|
||||
"add_exclusion_pattern_description": "Afegeix patrons d'exclusió. Es permet englobar fent ús de *, **, i ?. Per a ignorar els fitxers de qualsevol directori anomenat \"Raw\" introduïu \"**/Raw/**\". Per a ignorar els fitxers acabats en \".tif\" introduïu \"**/*.tif\". Per a ignorar una ruta absoluta, utilitzeu \"/ruta/a/ignorar/**\".",
|
||||
"admin_user": "Administrador",
|
||||
"asset_offline_description": "Aquest recurs de la biblioteca externa ja no es troba al disc i s'ha mogut a la paperera. Si el fitxer s'ha mogut dins de la biblioteca, comproveu la vostra línia de temps per trobar el nou recurs corresponent. Per restaurar aquest recurs, assegureu-vos que Immich pugui accedir a la ruta del fitxer següent i escanegeu la biblioteca.",
|
||||
"authentication_settings": "Configuració de l'autenticació",
|
||||
"authentication_settings_description": "Gestiona la contrasenya, OAuth i altres configuracions de l'autenticació",
|
||||
@@ -43,9 +45,7 @@
|
||||
"backup_database_enable_description": "Habilitar bolcat de la base de dades",
|
||||
"backup_keep_last_amount": "Quantitat de bolcats anteriors per conservar",
|
||||
"backup_settings": "Configuració dels bolcats",
|
||||
"backup_settings_description": "Gestionar la configuració bolcats de la base de dades. Nota: els treballs no es monitoritzen ni es notifiquen les fallades.",
|
||||
"check_all": "Marca-ho tot",
|
||||
"cleanup": "Neteja",
|
||||
"backup_settings_description": "Gestionar la configuració de bolcats de la base de dades. Nota: els treballs no es monitoritzen ni es notifiquen els errors.",
|
||||
"cleared_jobs": "Tasques esborrades per a: {job}",
|
||||
"config_set_by_file": "La configuració està definida per un fitxer de configuració",
|
||||
"confirm_delete_library": "Esteu segurs que voleu eliminar la llibreria {library}?",
|
||||
@@ -59,16 +59,14 @@
|
||||
"cron_expression_description": "Estableix l'interval d'escaneig amb el format cron. Per obtenir més informació, consulteu, p.e <link>Crontab Guru</link>",
|
||||
"cron_expression_presets": "Ajustos predefinits d'expressions Cron",
|
||||
"disable_login": "Deshabiliteu l'inici de sessió",
|
||||
"duplicate_detection_job_description": "Executa l'aprenentatge automàtic en els elements per a detectar imatges semblants. Fa servir l'Smart Search",
|
||||
"duplicate_detection_job_description": "Executa l'aprenentatge automàtic en els elements per a detectar imatges semblants. Fa servir la cerca intel·ligent",
|
||||
"exclusion_pattern_description": "Els patrons d'exclusió permeten ignorar fitxers i carpetes quan escanegeu una llibreria. Això és útil si teniu carpetes que contenen fitxer que no voleu importar, com els fitxers RAW.",
|
||||
"external_library_created_at": "Llibreria externa (creada el {date})",
|
||||
"external_library_management": "Gestió de llibreries externes",
|
||||
"face_detection": "Detecció de cares",
|
||||
"face_detection_description": "Detecta les cares fent servir aprenentatge automàtic. Per a videos només és té en compte la miniatura. \"Actualitzar\" reprocessa tots els elements. \"Resetejar\" esborra tota la informació de cares actuals. \"Pendent\" afegeix a la cua els elements que encara no han estat processats. Les cares detectades s'afegiran a la cua per al Reconeixement Facial després de completar la Detecció Facial, tot agrupant-les entre noves persones o les ja existents.",
|
||||
"facial_recognition_job_description": "Agrupa les cares detectades per persona. Aquest pas s'executa després de completar la detecció de cares. \"Resetejar\" reagrupa totes les cares. \"Pendent\" afegeix a la cua les cares que no tenen cap persona assignada.",
|
||||
"failed_job_command": "La comanda {command} ha fallat per la tasca: {job}",
|
||||
"force_delete_user_warning": "COMPTE: Aquesta acció eliminara immediatament l'usuari i els seus elements. Aquesta acció és irreversible i els fitxers no es poden recuperar.",
|
||||
"forcing_refresh_library_files": "Força l'actualització de tots els fitxers de les biblioteques",
|
||||
"image_format": "Format",
|
||||
"image_format_description": "WebP genera fitxers més petits que JPEG, però codifica més lentament.",
|
||||
"image_fullsize_description": "Imatges a tamany complet sense metadades, utilitzades quan es fa zoom",
|
||||
@@ -91,7 +89,7 @@
|
||||
"image_thumbnail_description": "Miniatura petita amb metadades eliminades, que s'utilitza quan es visualitzen grups de fotos com la línia de temps principal",
|
||||
"image_thumbnail_quality_description": "Qualitat de miniatura d'1 a 100. Més alt és millor, però produeix fitxers més grans i pot reduir la capacitat de resposta de l'aplicació.",
|
||||
"image_thumbnail_title": "Configuració de miniatures",
|
||||
"job_concurrency": "{job} concurrència",
|
||||
"job_concurrency": "{job} simultàniament",
|
||||
"job_created": "Tasca creada",
|
||||
"job_not_concurrency_safe": "Aquesta tasca no és segura per a la conconcurrència.",
|
||||
"job_settings": "Configuració de les tasques",
|
||||
@@ -107,7 +105,7 @@
|
||||
"library_scanning_enable_description": "Habilita l'escaneig periòdic de biblioteques",
|
||||
"library_settings": "Llibreria externes",
|
||||
"library_settings_description": "Gestiona la configuració de les llibreries externes",
|
||||
"library_tasks_description": "Escaneja biblioteques externes per arxius nous o canviats",
|
||||
"library_tasks_description": "Escaneja les biblioteques externes per trobar arxius nous o canviats",
|
||||
"library_watching_enable_description": "Consultar llibreries externes per detectar canvis en fitxers",
|
||||
"library_watching_settings": "Monitoratge de la llibreria (EXPERIMENTAL)",
|
||||
"library_watching_settings_description": "Monitorització automàtica de fitxers modificats",
|
||||
@@ -115,19 +113,19 @@
|
||||
"logging_level_description": "Quan està habilitat, quin nivell de registre es vol emprar.",
|
||||
"logging_settings": "Registre",
|
||||
"machine_learning_clip_model": "Model CLIP",
|
||||
"machine_learning_clip_model_description": "El nom d'un model CLIP que apareix a <link>aquí</link>. Tingues en compte que has de tornar a executar l'Smart Search' per a totes les imatges quan es canvia de model.",
|
||||
"machine_learning_clip_model_description": "El nom d'un model CLIP que apareix a <link>aquí</link>. Tingues en compte que has de tornar a executar la cerca intel·ligent per a totes les imatges quan es canvia de model.",
|
||||
"machine_learning_duplicate_detection": "Detecció de duplicats",
|
||||
"machine_learning_duplicate_detection_enabled": "Activa detecció de duplicats",
|
||||
"machine_learning_duplicate_detection_enabled_description": "Si es deshabilitat, els elements exactament idèntics encara es desduplicaran.",
|
||||
"machine_learning_duplicate_detection_enabled": "Activa la detecció de duplicats",
|
||||
"machine_learning_duplicate_detection_enabled_description": "Si està desactivada, els elements idèntics encara es desduplicaran.",
|
||||
"machine_learning_duplicate_detection_setting_description": "Usa incrustacions CLIP per a trobar prossibles duplicats",
|
||||
"machine_learning_enabled": "Activa l'aprenentatge automàtic",
|
||||
"machine_learning_enabled_description": "Si està deshabilitat, totes les funcions ML es deshabilitaran sense tenir en compte la configuració següent.",
|
||||
"machine_learning_enabled_description": "Si està desactivat, totes les funcions ML es deshabilitaran sense tenir en compte la configuració següent.",
|
||||
"machine_learning_facial_recognition": "Reconeixement facial",
|
||||
"machine_learning_facial_recognition_description": "Detecta, reconeix i agrupa cares a les imatges",
|
||||
"machine_learning_facial_recognition_model": "Model de reconeixement facial",
|
||||
"machine_learning_facial_recognition_model_description": "Els models es llisten en ordre descent segons la mida. Els models més grans són més lents i usen més memòria però produeixen millors resultats. Tingueu en compte que després de canviar un model haureu de tornar a executar la tasca de detecció de cares per a totes les imatges.",
|
||||
"machine_learning_facial_recognition_setting": "Activa reconeixement facial",
|
||||
"machine_learning_facial_recognition_setting_description": "Si està deshabilitat, les imatges no es codificaran pel reconeixement facial i no s'afegiran a la secció Persones de la pàgina Explorar.",
|
||||
"machine_learning_facial_recognition_setting": "Activa el reconeixement facial",
|
||||
"machine_learning_facial_recognition_setting_description": "Si està desactivat, les imatges no es codificaran pel reconeixement facial i no s'afegiran a la secció Persones de la pàgina Explorar.",
|
||||
"machine_learning_max_detection_distance": "Distància màxima de detecció",
|
||||
"machine_learning_max_detection_distance_description": "Diferència màxima entre dues imatges per a considerar-les duplicades, en un rang d'entre 0.001-0.1. Com més elevat el valor més detecció de duplicats, però pot resultar en falsos positius.",
|
||||
"machine_learning_max_recognition_distance": "Màxima diferència de reconeixement",
|
||||
@@ -138,17 +136,17 @@
|
||||
"machine_learning_min_recognized_faces_description": "El nombre mínim de cares reconegudes per crear una persona. Augmentar aquest valor fa que el reconeixement facial sigui més precís, però augmenta la possibilitat que una cara no sigui assignada a una persona.",
|
||||
"machine_learning_settings": "Configuració d'aprenentatge automàtic",
|
||||
"machine_learning_settings_description": "Gestiona funcions i configuració d'aprenentatge automàtic",
|
||||
"machine_learning_smart_search": "Cerca Intel·ligent",
|
||||
"machine_learning_smart_search": "Cerca intel·ligent",
|
||||
"machine_learning_smart_search_description": "Cerca imatges semànticament emprant incrustacions CLIP",
|
||||
"machine_learning_smart_search_enabled": "Activa la cerca intel·ligent",
|
||||
"machine_learning_smart_search_enabled_description": "Si està deshabilitat les imatges no es codificaran per la cerca intel·ligent.",
|
||||
"machine_learning_smart_search_enabled_description": "Si està desactivada, les imatges no es codificaran per la cerca intel·ligent.",
|
||||
"machine_learning_url_description": "L'URL del servidor d'aprenentatge automàtic. Si es proporciona més d'un URL, s'intentarà accedir a cada servidor en ordre fins que un d'ells respongui correctament.",
|
||||
"manage_concurrency": "Gestiona la concurrència",
|
||||
"manage_log_settings": "Gestiona la configuració del registre",
|
||||
"map_dark_style": "Tema fosc",
|
||||
"map_enable_description": "Habilita característiques del mapa",
|
||||
"map_gps_settings": "Configuració de mapa i GPS",
|
||||
"map_gps_settings_description": "Gestiona la configuració de mapa i GPS (Geocodificació inversa)",
|
||||
"map_gps_settings": "Configuració del mapa i GPS",
|
||||
"map_gps_settings_description": "Gestiona la configuració del mapa i GPS (Geocodificació inversa)",
|
||||
"map_implications": "La funció mapa depèn del servei extern de tesel·les (tiles.immich.cloud)",
|
||||
"map_light_style": "Tema clar",
|
||||
"map_manage_reverse_geocoding_settings": "Gestiona els paràmetres de <link>geocodificació inversa</link>",
|
||||
@@ -173,7 +171,7 @@
|
||||
"note_apply_storage_label_previous_assets": "Nota: Per aplicar l'etiquetatge d'emmagatzematge a elements pujats prèviament, executeu la",
|
||||
"note_cannot_be_changed_later": "NOTA: Això és irreversible!",
|
||||
"notification_email_from_address": "Des de l'adreça",
|
||||
"notification_email_from_address_description": "Adreça de correu electrònic del remitent, per exemple: \"Immich Photo Server <noreply@example.com>\"",
|
||||
"notification_email_from_address_description": "Adreça de correu electrònic del remitent, per exemple: \"Immich Photo Server <noreply@example.com>\". Assegureu-vos d'utilitzar una adreça des de la qual tingueu permís per enviar correus electrònics.",
|
||||
"notification_email_host_description": "Amfitrió del servidor de correu electrònic (p.ex. smtp.immich.app)",
|
||||
"notification_email_ignore_certificate_errors": "Ignora els errors de certificat",
|
||||
"notification_email_ignore_certificate_errors_description": "Ignora els errors de validació de certificat TLS (no recomanat)",
|
||||
@@ -197,7 +195,7 @@
|
||||
"oauth_enable_description": "Iniciar sessió amb OAuth",
|
||||
"oauth_mobile_redirect_uri": "URI de redirecció mòbil",
|
||||
"oauth_mobile_redirect_uri_override": "Sobreescriu l'URI de redirecció mòbil",
|
||||
"oauth_mobile_redirect_uri_override_description": "Habilita quan 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_settings": "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>.",
|
||||
@@ -209,8 +207,6 @@
|
||||
"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_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_settings": "Inici de sessió amb contrasenya",
|
||||
"password_settings_description": "Gestiona la configuració de l'inici de sessió amb contrasenya",
|
||||
@@ -220,9 +216,6 @@
|
||||
"refreshing_all_libraries": "Actualitzant totes les biblioteques",
|
||||
"registration": "Registre d'administrador",
|
||||
"registration_description": "Com que ets el primer usuari del sistema, seràs designat com a administrador i seràs responsable de les tasques administratives. També seràs l'encarregat de crear usuaris addicionals.",
|
||||
"repair_all": "Reparar tot",
|
||||
"repair_matched_items": "Coincidència {count, plural, one {# element} other {# elements}}",
|
||||
"repaired_items": "Corregit {count, plural, one {# element} other {# elements}}",
|
||||
"require_password_change_on_login": "Requerir que l'usuari canviï la contrasenya en el primer inici de sessió",
|
||||
"reset_settings_to_default": "Restablir configuracions per defecte",
|
||||
"reset_settings_to_recent_saved": "Restablir la configuració guardada més recent",
|
||||
@@ -251,7 +244,6 @@
|
||||
"storage_template_migration_info": "Les extensions es convertiran a minúscules. Els canvis de plantilla només s'aplicaran a nous elements. Per aplicar la plantilla rectroactivament a elements pujats prèviament, executeu la <link>{job}</link>.",
|
||||
"storage_template_migration_job": "Tasca de migració de la plantilla d'emmagatzematge",
|
||||
"storage_template_more_details": "Per obtenir més detalls sobre aquesta funció, consulteu la <template-link>Storage Template</template-link> i les seves <implications-link>implications</implications-link>",
|
||||
"storage_template_onboarding_description": "Quan està activada, aquesta funció organitzarà automàticament els fitxers en funció d'una plantilla definida per l'usuari. A causa de problemes d'estabilitat, la funció s'ha desactivat de manera predeterminada. Per obtenir més informació, consulteu la <link>documentation</link>.",
|
||||
"storage_template_path_length": "Límit aproximat de longitud de la ruta: <b>{length, number}</b>/{limit, number}",
|
||||
"storage_template_settings": "Plantilla d'emmagatzematge",
|
||||
"storage_template_settings_description": "Gestiona l'estructura de les carpetes i el nom del fitxers dels elements pujats",
|
||||
@@ -263,16 +255,14 @@
|
||||
"template_email_invite_album": "Plantilla per l'àlbum d'invitacions",
|
||||
"template_email_preview": "Vista prèvia",
|
||||
"template_email_settings": "Plantilles de correu electrònic",
|
||||
"template_email_settings_description": "Gestionar les plantilles de notificació per correu electrònic personalitzades",
|
||||
"template_email_update_album": "Actualitzar la plantilla de l'àlbum",
|
||||
"template_email_welcome": "Plantilla del correu de benvinguda",
|
||||
"template_settings": "Plantilles de notificació",
|
||||
"template_settings_description": "Gestiona les plantilles personalitzades per les notificacions.",
|
||||
"template_settings_description": "Gestiona les plantilles personalitzades per les notificacions",
|
||||
"theme_custom_css_settings": "CSS personalitzat",
|
||||
"theme_custom_css_settings_description": "Els Fulls d'Estil en Cascada permeten personalitzar el disseny d'Immich.",
|
||||
"theme_custom_css_settings_description": "Els fulls d'estil en cascada permeten personalitzar el disseny d'Immich.",
|
||||
"theme_settings": "Configuració del tema",
|
||||
"theme_settings_description": "Gestiona la personalització de la interfície web Immich",
|
||||
"these_files_matched_by_checksum": "Aquests fitxers coincideixen amb els seus checksums",
|
||||
"thumbnail_generation_job": "Generar miniatures",
|
||||
"thumbnail_generation_job_description": "Genera miniatures grans, petites i borroses per a cada element, així com miniatures per a cada persona",
|
||||
"transcoding_acceleration_api": "API d'acceleració",
|
||||
@@ -300,10 +290,9 @@
|
||||
"transcoding_encoding_options": "Opcions de codificació",
|
||||
"transcoding_encoding_options_description": "Establiu còdecs, resolució, qualitat i altres opcions per als vídeos codificats",
|
||||
"transcoding_hardware_acceleration": "Acceleració de maquinari",
|
||||
"transcoding_hardware_acceleration_description": "Experimental. Molt més ràpid, però tindrà una qualitat més baixa amb la mateixa taxa de bits",
|
||||
"transcoding_hardware_acceleration_description": "Experimental: transcodificació més ràpida però pot perdre qualitat amb la mateixa tasa de bits",
|
||||
"transcoding_hardware_decoding": "Descodificació de maquinari",
|
||||
"transcoding_hardware_decoding_setting_description": "Habilita l'acceleració d'extrem a extrem en lloc d'accelerar només la codificació. És possible que no funcioni en tots els vídeos.",
|
||||
"transcoding_hevc_codec": "Còdec HEVC",
|
||||
"transcoding_max_b_frames": "Nombre màxim de B-frames",
|
||||
"transcoding_max_b_frames_description": "Els valors més alts milloren l'eficiència de la compressió, però alenteixen la codificació. És possible que no sigui compatible amb l'acceleració de maquinari en dispositius antics. 0 desactiva els B-frames, mentre que -1 estableix aquest valor automàticament.",
|
||||
"transcoding_max_bitrate": "Taxa de bits màxima",
|
||||
@@ -341,8 +330,6 @@
|
||||
"trash_number_of_days_description": "Nombre de dies per mantenir els recursos a la paperera abans de suprimir-los permanentment",
|
||||
"trash_settings": "Configuració de la paperera",
|
||||
"trash_settings_description": "Gestiona la configuració de la paperera",
|
||||
"untracked_files": "Fitxers sense seguiment",
|
||||
"untracked_files_description": "L'aplicació no fa un seguiment d'aquests fitxers. Poden ser el resultat de moviments fallits, càrregues interrompudes o deixades enrere a causa d'un error",
|
||||
"user_cleanup_job": "Neteja d'usuari",
|
||||
"user_delete_delay": "El compte i els recursos de <b>{user}</b> es programaran per a la supressió permanent en {delay, plural, one {# dia} other {# dies}}.",
|
||||
"user_delete_delay_settings": "Retard de la supressió",
|
||||
@@ -367,7 +354,7 @@
|
||||
},
|
||||
"admin_email": "Correu de l'administrador",
|
||||
"admin_password": "Contrasenya de l'administrador",
|
||||
"administration": "Administrador",
|
||||
"administration": "Administració",
|
||||
"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_enable_alternate_media_filter_title": "Utilitza el filtre de sincronització d'àlbums de dispositius alternatius",
|
||||
@@ -401,10 +388,6 @@
|
||||
"album_remove_user": "Eliminar l'usuari?",
|
||||
"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_thumbnail_card_item": "1 element",
|
||||
"album_thumbnail_card_items": "{count} elements",
|
||||
"album_thumbnail_card_shared": " · Compartit",
|
||||
"album_thumbnail_shared_by": "Compartit per {user}",
|
||||
"album_updated": "Àlbum actualitzat",
|
||||
"album_updated_setting_description": "Rep una notificació per correu electrònic quan un àlbum compartit tingui recursos nous",
|
||||
"album_user_left": "Surt de {album}",
|
||||
@@ -420,6 +403,9 @@
|
||||
"album_with_link_access": "Permet que qualsevol persona que tingui l'enllaç vegi fotos i persones d'aquest àlbum.",
|
||||
"albums": "Àlbums",
|
||||
"albums_count": "{count, plural, one {{count, number} Àlbum} other {{count, number} Àlbums}}",
|
||||
"albums_default_sort_order": "Ordre per defecte de l'àlbum",
|
||||
"albums_default_sort_order_description": "Ordre de classificació inicial dels recursos al crear àlbums nous.",
|
||||
"albums_feature_description": "Col·leccions d'actius que es poden compartir amb altres usuaris.",
|
||||
"all": "Tots",
|
||||
"all_albums": "Tots els àlbum",
|
||||
"all_people": "Tota la gent",
|
||||
@@ -478,9 +464,12 @@
|
||||
"assets_added_count": "{count, plural, one {Afegit un element} other {Afegits # elements}}",
|
||||
"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_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} no es pot afegir a l'àlbum",
|
||||
"assets_count": "{count, plural, one {# recurs} other {# recursos}}",
|
||||
"assets_deleted_permanently": "{count} element(s) esborrats permanentment",
|
||||
"assets_deleted_permanently_from_server": "{count} element(s) esborrats permanentment del servidor d'Immich",
|
||||
"assets_downloaded_failed": "{count, plural, one {S'ha baixat un arxiu - {error} l'arxiu ha fallat} other {S'han baixat # arxius - {error} els arxius han fallat}}",
|
||||
"assets_downloaded_successfully": "{count, plural, one {S'ha baixat un arxiu amb èxit} other {S'han baixat # arxius amb èxit}}",
|
||||
"assets_moved_to_trash_count": "{count, plural, one {# recurs mogut} other {# recursos moguts}} a la paperera",
|
||||
"assets_permanently_deleted_count": "{count, plural, one {# recurs esborrat} other {# recursos esborrats}} permanentment",
|
||||
"assets_removed_count": "{count, plural, one {# element eliminat} other {# elements eliminats}}",
|
||||
@@ -495,6 +484,7 @@
|
||||
"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_title": "Canvi automàtic d'URL",
|
||||
"autoplay_slideshow": "Reprodueix automàticament les diapositives",
|
||||
"back": "Enrere",
|
||||
"back_close_deselect": "Tornar, tancar o anul·lar la selecció",
|
||||
"background_location_permission": "Permís d'ubicació en segon pla",
|
||||
@@ -562,6 +552,10 @@
|
||||
"backup_options_page_title": "Opcions de còpia de seguretat",
|
||||
"backup_setting_subtitle": "Gestiona la configuració de càrrega en segon pla i en primer pla",
|
||||
"backward": "Enrere",
|
||||
"biometric_auth_enabled": "Autentificació biomètrica activada",
|
||||
"biometric_locked_out": "Esteu bloquejats fora de l'autenticació biomètrica",
|
||||
"biometric_no_options": "No hi ha opcions biomètriques disponibles",
|
||||
"biometric_not_available": "L'autenticació biomètrica no està disponible en aquest dispositiu",
|
||||
"birthdate_saved": "Data de naixement guardada amb èxit",
|
||||
"birthdate_set_description": "La data de naixement s'utilitza per calcular l'edat d'aquesta persona en el moment d'una foto.",
|
||||
"blurred_background": "Fons difuminat",
|
||||
@@ -572,21 +566,17 @@
|
||||
"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.",
|
||||
"buy": "Comprar Immich",
|
||||
"cache_settings_album_thumbnails": "Miniatures de la pàgina de la biblioteca ({count} elements)",
|
||||
"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_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_title": "Elements duplicats ({count})",
|
||||
"cache_settings_image_cache_size": "Mida de la memòria cau d'imatges ({count} elements)",
|
||||
"cache_settings_statistics_album": "Miniatures de la biblioteca",
|
||||
"cache_settings_statistics_assets": "{count} elements ({size})",
|
||||
"cache_settings_statistics_full": "Imatges completes",
|
||||
"cache_settings_statistics_shared": "Miniatures d'àlbums compartits",
|
||||
"cache_settings_statistics_thumbnail": "Miniatures",
|
||||
"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_thumbnail_size": "Mida de la memòria cau de les miniatures ({count} elements)",
|
||||
"cache_settings_tile_subtitle": "Controla el comportament de l'emmagatzematge local",
|
||||
"cache_settings_tile_title": "Emmagatzematge local",
|
||||
"cache_settings_title": "Configuració de la memòria cau",
|
||||
@@ -599,7 +589,10 @@
|
||||
"cannot_merge_people": "No es pot fusionar gent",
|
||||
"cannot_undo_this_action": "Aquesta acció no es pot desfer!",
|
||||
"cannot_update_the_description": "No es pot actualitzar la descripció",
|
||||
"cast": "Emet",
|
||||
"cast_description": "Configurar les destinacions de transmissió disponibles",
|
||||
"change_date": "Canvia la data",
|
||||
"change_description": "Canvia la descripció",
|
||||
"change_display_order": "Canvia l'ordre de visualització",
|
||||
"change_expiration_time": "Canvia la data d'expiració",
|
||||
"change_location": "Canvia la ubicació",
|
||||
@@ -615,7 +608,6 @@
|
||||
"change_pin_code": "Canviar el codi PIN",
|
||||
"change_your_password": "Canvia la teva contrasenya",
|
||||
"changed_visibility_successfully": "Visibilitat canviada amb èxit",
|
||||
"check_all": "Marqueu-ho tot",
|
||||
"check_corrupt_asset_backup": "Comprovar les còpies de seguretat corruptes",
|
||||
"check_corrupt_asset_backup_button": "Realitzar comprovació",
|
||||
"check_corrupt_asset_backup_description": "Executeu aquesta comprovació només mitjançant Wi-Fi i un cop s'hagi fet una còpia de seguretat de tots els actius. El procediment pot trigar uns minuts.",
|
||||
@@ -655,10 +647,13 @@
|
||||
"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_tag_face": "Vols etiquetar aquesta cara com a {name}?",
|
||||
"confirm_tag_face_unnamed": "Com vols etiquetar aquesta cara?",
|
||||
"connected_device": "Dispositiu connectat",
|
||||
"connected_to": "Connectat a",
|
||||
"contain": "Contingut",
|
||||
"context": "Context",
|
||||
"continue": "Continuar",
|
||||
"control_bottom_app_bar_album_info_shared": "{count} elements - Compartits",
|
||||
"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_local": "Suprimeix del dispositiu",
|
||||
@@ -707,6 +702,7 @@
|
||||
"daily_title_text_date": "E, dd MMM",
|
||||
"daily_title_text_date_year": "E, dd MMM, yyyy",
|
||||
"dark": "Fosc",
|
||||
"darkTheme": "Activa/desactiva el tema fosc",
|
||||
"date_after": "Data posterior a",
|
||||
"date_and_time": "Data i hora",
|
||||
"date_before": "Data anterior a",
|
||||
@@ -754,6 +750,7 @@
|
||||
"disallow_edits": "No permetre les edicions",
|
||||
"discord": "Discord",
|
||||
"discover": "Descobreix",
|
||||
"discovered_devices": "Dispositius descoberts",
|
||||
"dismiss_all_errors": "Descarta tots els errors",
|
||||
"dismiss_error": "Descarta l'error",
|
||||
"display_options": "Opcions de visualització",
|
||||
@@ -769,7 +766,6 @@
|
||||
"download_enqueue": "Descàrrega en cua",
|
||||
"download_error": "Error de descàrrega",
|
||||
"download_failed": "Descàrrega ha fallat",
|
||||
"download_filename": "arxiu: {filename}",
|
||||
"download_finished": "Descàrrega acabada",
|
||||
"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",
|
||||
@@ -793,6 +789,8 @@
|
||||
"edit_avatar": "Edita l'avatar",
|
||||
"edit_date": "Edita la data",
|
||||
"edit_date_and_time": "Edita data i hora",
|
||||
"edit_description": "Edita la descripció",
|
||||
"edit_description_prompt": "Si us plau, selecciona una nova descripció:",
|
||||
"edit_exclusion_pattern": "Edita patró d'exclusió",
|
||||
"edit_faces": "Edita les cares",
|
||||
"edit_import_path": "Edita la ruta d'importació",
|
||||
@@ -818,15 +816,19 @@
|
||||
"empty_trash": "Buidar la paperera",
|
||||
"empty_trash_confirmation": "Esteu segur que voleu buidar la paperera? Això eliminarà tots els recursos a la paperera permanentment d'Immich.\nNo podeu desfer aquesta acció!",
|
||||
"enable": "Activar",
|
||||
"enable_biometric_auth_description": "Introduïu el codi PIN per a habilitar l'autenticació biomètrica",
|
||||
"enabled": "Activat",
|
||||
"end_date": "Data final",
|
||||
"enqueued": "En cua",
|
||||
"enter_wifi_name": "Introdueix el nom de Wi-Fi",
|
||||
"enter_your_pin_code": "Introduïu el codi PIN",
|
||||
"enter_your_pin_code_subtitle": "Introduïu el codi PIN per a accedir a la carpeta protegida",
|
||||
"error": "Error",
|
||||
"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_loading_image": "Error carregant la imatge",
|
||||
"error_saving_image": "Error: {error}",
|
||||
"error_tag_face_bounding_box": "Error a l'etiquetar la cara - no s'han pogut obtenir les coordenades de l'àrea",
|
||||
"error_title": "Error - Quelcom ha anat malament",
|
||||
"errors": {
|
||||
"cannot_navigate_next_asset": "No es pot navegar a l'element següent",
|
||||
@@ -839,7 +841,6 @@
|
||||
"cant_get_number_of_comments": "No es pot obtenir el nombre de comentaris",
|
||||
"cant_search_people": "No es poden cercar persones",
|
||||
"cant_search_places": "No es poden cercar llocs",
|
||||
"cleared_jobs": "Tasques buides per a: {job}",
|
||||
"error_adding_assets_to_album": "Error afegint elements a l'àlbum",
|
||||
"error_adding_users_to_album": "Error afegint usuaris a l'àlbum",
|
||||
"error_deleting_shared_user": "S'ha produït un error en suprimir l'usuari compartit",
|
||||
@@ -848,7 +849,6 @@
|
||||
"error_removing_assets_from_album": "Error eliminant els elements de l'àlbum, consulteu la consola per obtenir més detalls",
|
||||
"error_selecting_all_assets": "Error seleccionant tots els elements",
|
||||
"exclusion_pattern_already_exists": "Aquest patró d’exclusió ja existeix.",
|
||||
"failed_job_command": "L'ordre {command} ha fallat per a la tasca: {job}",
|
||||
"failed_to_create_album": "No s'ha pogut crear l'àlbum",
|
||||
"failed_to_create_shared_link": "No s'ha pogut crear l'enllaç compartit",
|
||||
"failed_to_edit_shared_link": "No s'ha pogut editar l'enllaç compartit",
|
||||
@@ -867,7 +867,6 @@
|
||||
"paths_validation_failed": "{paths, plural, one {# ruta} other {# rutes}} no ha pogut validar",
|
||||
"profile_picture_transparent_pixels": "Les fotos de perfil no poden tenir píxels transparents. Per favor, feu zoom in, mogueu la imatge o ambdues.",
|
||||
"quota_higher_than_disk_size": "Heu establert una quota més gran que la mida de disc",
|
||||
"repair_unable_to_check_items": "No es pot comprovar {count, select, one {l'element} other {els elements}}",
|
||||
"unable_to_add_album_users": "No es poden afegir usuaris a l'àlbum",
|
||||
"unable_to_add_assets_to_shared_link": "No s'han pogut afegir els elements a l'enllaç compartit",
|
||||
"unable_to_add_comment": "No es pot afegir el comentari",
|
||||
@@ -879,13 +878,13 @@
|
||||
"unable_to_archive_unarchive": "No es pot {archived, select, true {arxivar} other {desarxivar}}",
|
||||
"unable_to_change_album_user_role": "No es pot canviar el rol d'usuari de l'àlbum",
|
||||
"unable_to_change_date": "No es pot canviar la data",
|
||||
"unable_to_change_description": "No s'ha pogut canviar la descripció",
|
||||
"unable_to_change_favorite": "No es pot canviar el favorit per a aquest recurs",
|
||||
"unable_to_change_location": "No es pot canviar la ubicació",
|
||||
"unable_to_change_password": "No es pot canviar la contrasenya",
|
||||
"unable_to_change_visibility": "No es pot canviar la visibilitat de {count, plural, one {# persona} other {# persones}}",
|
||||
"unable_to_complete_oauth_login": "No es pot completar l'inici de sessió OAuth",
|
||||
"unable_to_connect": "No pot connectar",
|
||||
"unable_to_connect_to_server": "No es pot connectar al servidor",
|
||||
"unable_to_copy_to_clipboard": "No es pot copiar al porta-retalls, assegureu-vos que esteu accedint a la pàgina mitjançant https",
|
||||
"unable_to_create_admin_account": "No es pot crear un compte d'administrador",
|
||||
"unable_to_create_api_key": "No es pot crear una clau d'API nova",
|
||||
@@ -909,10 +908,6 @@
|
||||
"unable_to_hide_person": "No es pot amagar la persona",
|
||||
"unable_to_link_motion_video": "No es pot enllaçar el vídeo en moviment",
|
||||
"unable_to_link_oauth_account": "No es pot enllaçar el compte OAuth",
|
||||
"unable_to_load_album": "No es pot carregar l'àlbum",
|
||||
"unable_to_load_asset_activity": "No es pot carregar l'activitat dels recursos",
|
||||
"unable_to_load_items": "No es poden carregar els elements",
|
||||
"unable_to_load_liked_status": "No es pot carregar l'estat de m'agrada",
|
||||
"unable_to_log_out_all_devices": "No es poden tancar la sessió de tots els dispositius",
|
||||
"unable_to_log_out_device": "No es pot tancar la sessió del dispositiu",
|
||||
"unable_to_login_with_oauth": "No es pot iniciar sessió amb OAuth",
|
||||
@@ -923,11 +918,9 @@
|
||||
"unable_to_remove_album_users": "No es poden eliminar usuaris de l'àlbum",
|
||||
"unable_to_remove_api_key": "No es pot eliminar la clau de l'API",
|
||||
"unable_to_remove_assets_from_shared_link": "No es poden eliminar recursos de l'enllaç compartit",
|
||||
"unable_to_remove_deleted_assets": "No es poden eliminar els fitxers fora de línia",
|
||||
"unable_to_remove_library": "No es pot eliminar la biblioteca",
|
||||
"unable_to_remove_partner": "No es pot eliminar company/a",
|
||||
"unable_to_remove_reaction": "No es pot eliminar la reacció",
|
||||
"unable_to_repair_items": "No es poden reparar els elements",
|
||||
"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",
|
||||
@@ -957,13 +950,12 @@
|
||||
"unable_to_update_user": "No es pot actualitzar l'usuari",
|
||||
"unable_to_upload_file": "No es pot carregar el fitxer"
|
||||
},
|
||||
"exif": "Exif",
|
||||
"exif": "EXIF",
|
||||
"exif_bottom_sheet_description": "Afegeix descripció...",
|
||||
"exif_bottom_sheet_details": "DETALLS",
|
||||
"exif_bottom_sheet_location": "UBICACIÓ",
|
||||
"exif_bottom_sheet_people": "PERSONES",
|
||||
"exif_bottom_sheet_person_add_person": "Afegir nom",
|
||||
"exif_bottom_sheet_person_age": "Edat {age}",
|
||||
"exif_bottom_sheet_person_age_months": "Edat {months} mesos",
|
||||
"exif_bottom_sheet_person_age_year_months": "Edat 1 any, {months} mesos",
|
||||
"exif_bottom_sheet_person_age_years": "Edat {years}",
|
||||
@@ -987,6 +979,7 @@
|
||||
"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",
|
||||
"face_unassigned": "Sense assignar",
|
||||
"failed": "Fallat",
|
||||
"failed_to_authenticate": "No s'ha pogut autenticar",
|
||||
"failed_to_load_assets": "Error carregant recursos",
|
||||
"failed_to_load_folder": "No s'ha pogut carregar la carpeta",
|
||||
"favorite": "Preferit",
|
||||
@@ -1010,6 +1003,8 @@
|
||||
"folders": "Carpetes",
|
||||
"folders_feature_description": "Explorar la vista de carpetes per les fotos i vídeos del sistema d'arxius",
|
||||
"forward": "Endavant",
|
||||
"gcast_enabled": "Google Cast",
|
||||
"gcast_enabled_description": "Aquesta funció carrega recursos externs de Google per funcionar.",
|
||||
"general": "General",
|
||||
"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",
|
||||
@@ -1052,6 +1047,8 @@
|
||||
"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_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_locked_error_local": "No s'han pogut moure els recursos locals a la carpeta bloquejada, saltant",
|
||||
"home_page_locked_error_partner": "No s'han pogut moure els recursos de la parella a la carpeta bloquejada, saltant",
|
||||
"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",
|
||||
"host": "Amfitrió",
|
||||
@@ -1096,6 +1093,12 @@
|
||||
"invalid_date_format": "Format de data invàlid",
|
||||
"invite_people": "Convida gent",
|
||||
"invite_to_album": "Convida a l'àlbum",
|
||||
"ios_debug_info_fetch_ran_at": "La recuperació s'ha executat {dateTime}",
|
||||
"ios_debug_info_last_sync_at": "Darrera sincronització {dateTime}",
|
||||
"ios_debug_info_no_processes_queued": "No hi ha processos en segon pla en cua",
|
||||
"ios_debug_info_no_sync_yet": "Encara no s'ha executat cap tasca de sincronització en segon pla",
|
||||
"ios_debug_info_processes_queued": "{count, plural, one {Un procés en segon pla a la cua} other {{count} processos en segon pla a la cua}}",
|
||||
"ios_debug_info_processing_ran_at": "El processament s'ha executat {dateTime}",
|
||||
"items_count": "{count, plural, one {# element} other {# elements}}",
|
||||
"jobs": "Tasques",
|
||||
"keep": "Mantenir",
|
||||
@@ -1104,6 +1107,9 @@
|
||||
"kept_this_deleted_others": "S'ha conservat aquest element i s'han suprimit {count, plural, one {# asset} other {# assets}}",
|
||||
"keyboard_shortcuts": "Dreceres de teclat",
|
||||
"language": "Idioma",
|
||||
"language_no_results_subtitle": "Prova d'ajustar el terme de cerca",
|
||||
"language_no_results_title": "No s'han trobat idiomes",
|
||||
"language_search_hint": "Cerca idiomes...",
|
||||
"language_setting_description": "Seleccioneu el vostre idioma",
|
||||
"last_seen": "Vist per últim cop",
|
||||
"latest_version": "Última versió",
|
||||
@@ -1129,6 +1135,7 @@
|
||||
"list": "Llista",
|
||||
"loading": "Carregant",
|
||||
"loading_search_results_failed": "No s'han pogut carregar els resultats de la cerca",
|
||||
"local_asset_cast_failed": "No es pot convertir un actiu que no s'ha penjat al servidor",
|
||||
"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",
|
||||
"location_permission": "Permís d'ubicació",
|
||||
@@ -1138,6 +1145,8 @@
|
||||
"location_picker_latitude_hint": "Introdueix aquí la latitud",
|
||||
"location_picker_longitude_error": "Introdueix una longitud vàlida",
|
||||
"location_picker_longitude_hint": "Introdueix aquí la longitud",
|
||||
"lock": "Bloqueja",
|
||||
"locked_folder": "Carpeta bloquejada",
|
||||
"log_out": "Tanca la sessió",
|
||||
"log_out_all_devices": "Tanqueu la sessió de tots els dispositius",
|
||||
"logged_out_all_devices": "S'ha tancat la sessió de tots els dispositius",
|
||||
@@ -1171,7 +1180,7 @@
|
||||
"look": "Aspecte",
|
||||
"loop_videos": "Vídeos en bucle",
|
||||
"loop_videos_description": "Habilita la reproducció en bucle del vídeo en els detalls.",
|
||||
"main_branch_warning": "Esteu usant una versió de desenvolupaent. Recomanem fer servir una versió publicada!",
|
||||
"main_branch_warning": "Esteu utilitzant una versió en desenvolupament; Recomanem fer servir una versió publicada!",
|
||||
"main_menu": "Menú principal",
|
||||
"make": "Fabricant",
|
||||
"manage_shared_links": "Administrar enllaços compartits",
|
||||
@@ -1217,8 +1226,6 @@
|
||||
"memories_setting_description": "Gestiona el que veus als teus records",
|
||||
"memories_start_over": "Torna a començar",
|
||||
"memories_swipe_to_close": "Llisca per tancar",
|
||||
"memories_year_ago": "Fa un any",
|
||||
"memories_years_ago": "Fa {years, plural, other {# years}} anys",
|
||||
"memory": "Record",
|
||||
"memory_lane_title": "Línia de records {title}",
|
||||
"menu": "Menú",
|
||||
@@ -1235,6 +1242,10 @@
|
||||
"month": "Mes",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"more": "Més",
|
||||
"move": "Moure",
|
||||
"move_off_locked_folder": "Moure fora de la carpeta bloquejada",
|
||||
"move_to_locked_folder": "Moure a la carpeta bloquejada",
|
||||
"move_to_locked_folder_confirmation": "Aquestes fotos i vídeos seran eliminades de tots els àlbums, i només podran ser vistes des de la carpeta bloquejada",
|
||||
"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",
|
||||
@@ -1252,6 +1263,7 @@
|
||||
"new_password": "Nova contrasenya",
|
||||
"new_person": "Persona nova",
|
||||
"new_pin_code": "Nou codi PIN",
|
||||
"new_pin_code_subtitle": "Aquesta és la primera vegada que accedeixes a la carpeta bloquejada. Crea una codi PIN i accedeix de manera segura a aquesta pàgina",
|
||||
"new_user_created": "Nou usuari creat",
|
||||
"new_version_available": "NOVA VERSIÓ DISPONIBLE",
|
||||
"newest_first": "El més nou primer",
|
||||
@@ -1264,11 +1276,13 @@
|
||||
"no_archived_assets_message": "Arxiveu fotos i vídeos per ocultar-los de Fotos",
|
||||
"no_assets_message": "FEU CLIC PER PUJAR LA VOSTRA PRIMERA FOTO",
|
||||
"no_assets_to_show": "No hi ha elements per mostrar",
|
||||
"no_cast_devices_found": "No s'han trobat dispositius per transmetre",
|
||||
"no_duplicates_found": "No s'han trobat duplicats.",
|
||||
"no_exif_info_available": "No hi ha informació d'exif disponible",
|
||||
"no_explore_results_message": "Penja més fotos per explorar la teva col·lecció.",
|
||||
"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_locked_photos_message": "Les fotos i vídeos d'aquesta carpeta estan ocultes, i no es mostraran a mesura que navegues o cerques a la teva biblioteca.",
|
||||
"no_name": "Sense nom",
|
||||
"no_notifications": "No hi ha notificacions",
|
||||
"no_people_found": "No s'han trobat coincidències de persones",
|
||||
@@ -1280,6 +1294,7 @@
|
||||
"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",
|
||||
"notes": "Notes",
|
||||
"nothing_here_yet": "No hi ha res encara",
|
||||
"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_enable_button": "Activa les notificacions",
|
||||
@@ -1290,15 +1305,15 @@
|
||||
"oauth": "OAuth",
|
||||
"official_immich_resources": "Recursos oficials d'Immich",
|
||||
"offline": "Fora de línia",
|
||||
"offline_paths": "Rutes fora de línia",
|
||||
"offline_paths_description": "Aquests resultats poden ser deguts a la supressió manual de fitxers que no formen part d'una biblioteca externa.",
|
||||
"ok": "D'acord",
|
||||
"oldest_first": "El més vell primer",
|
||||
"on_this_device": "En aquest dispositiu",
|
||||
"onboarding": "Incorporació",
|
||||
"onboarding_privacy_description": "Les següents funcions (opcionals) depenen de serveis externs i poden desactivarse en qualsevol moment de dels ajustos.",
|
||||
"onboarding_locale_description": "Tria el teu llenguatge preferit. Pots canviar aquesta opció mes tard a la configuració.",
|
||||
"onboarding_privacy_description": "Les següents funcions (opcionals) depenen de serveis externs i poden desactivarse en qualsevol moment des de la configuració.",
|
||||
"onboarding_server_welcome_description": "Configurem la instància amb alguns paràmetres comuns.",
|
||||
"onboarding_theme_description": "Trieu un tema de color per a la vostra instància. Podeu canviar-ho més endavant a la vostra configuració.",
|
||||
"onboarding_welcome_description": "Configurem la vostra instància amb alguns paràmetres habituals.",
|
||||
"onboarding_user_welcome_description": "Comencem!",
|
||||
"onboarding_welcome_user": "Benvingut, {user}",
|
||||
"online": "En línia",
|
||||
"only_favorites": "Només preferits",
|
||||
@@ -1355,6 +1370,8 @@
|
||||
"permanently_delete_assets_prompt": "Esteu segur que voleu suprimir permanentment {count, plural, one {aquest recurs?} other {aquests <b>#</b> recursos?}} Això també {count, plural, one {el} other {els}} suprimirà del seu àlbum.",
|
||||
"permanently_deleted_asset": "Element eliminat permanentment",
|
||||
"permanently_deleted_assets_count": "{count, plural, one {S'ha eliminat # element} other {S'han eliminat # elements}} permanentment",
|
||||
"permission": "Permís",
|
||||
"permission_empty": "El seu permís no hauria d'estar buit",
|
||||
"permission_onboarding_back": "Torna",
|
||||
"permission_onboarding_continue_anyway": "Continua de totes maneres",
|
||||
"permission_onboarding_get_started": "Comença",
|
||||
@@ -1375,6 +1392,7 @@
|
||||
"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",
|
||||
"pin_verification": "Verificació de codi PIN",
|
||||
"place": "Lloc",
|
||||
"places": "Llocs",
|
||||
"places_count": "{count, plural, one {{count, number} Lloc} other {{count, number} Llocs}}",
|
||||
@@ -1382,6 +1400,7 @@
|
||||
"play_memories": "Reproduir records",
|
||||
"play_motion_photo": "Reproduir Fotos en Moviment",
|
||||
"play_or_pause_video": "Reproduir o posar en pausa el vídeo",
|
||||
"please_auth_to_access": "Per favor, autentica't per accedir",
|
||||
"port": "Port",
|
||||
"preferences_settings_subtitle": "Gestiona les preferències de l'aplicació",
|
||||
"preferences_settings_title": "Preferències",
|
||||
@@ -1389,7 +1408,10 @@
|
||||
"preview": "Previsualització",
|
||||
"previous": "Anterior",
|
||||
"previous_memory": "Memòria anterior",
|
||||
"previous_or_next_photo": "Foto anterior o següent",
|
||||
"previous_or_next_day": "Dia endavant/enrere",
|
||||
"previous_or_next_month": "Mes endavant/enrere",
|
||||
"previous_or_next_photo": "Foto endavant/enrere",
|
||||
"previous_or_next_year": "Any endavant/enrere",
|
||||
"primary": "Primària",
|
||||
"privacy": "Privacitat",
|
||||
"profile": "Perfil",
|
||||
@@ -1398,8 +1420,8 @@
|
||||
"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_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_minor": "L'aplicació mòbil està desactualitzada. Si us plau, actualitzeu a l'última versió menor.",
|
||||
"profile_drawer_server_out_of_date_major": "El servidor està desactualitzat. Si us plau, actualitzeu a l'última versió major.",
|
||||
"profile_drawer_server_out_of_date_minor": "El servidor està desactualitzat. Si us plau, actualitzeu a l'última versió menor.",
|
||||
"profile_image_of_user": "Imatge de perfil de {user}",
|
||||
"profile_picture_set": "Imatge de perfil configurada.",
|
||||
"public_album": "Àlbum públic",
|
||||
@@ -1424,7 +1446,7 @@
|
||||
"purchase_lifetime_description": "Compra de per vida",
|
||||
"purchase_option_title": "OPCIONS DE COMPRA",
|
||||
"purchase_panel_info_1": "Crear Immich requereix molt de temps i esforç, tenim enginyers a temps complet treballant-hi per fer-ho tan bo com sigui possible. La nostra missió és que el programari de codi obert i les pràctiques empresarials ètiques es converteixin en una font d'ingressos sostenible per als desenvolupadors i creïn un ecosistema que respecti la privacitat amb alternatives reals als serveis cloud explotadors.",
|
||||
"purchase_panel_info_2": "Com que estem compromesos a no afegir murs de pagament, aquesta compra no us atorgarà cap funció addicional a Immich. Confiem en usuaris com tu per donar suport al desenvolupament continu d'Immich.",
|
||||
"purchase_panel_info_2": "Estem compromesos a no afegir murs de pagament, aquesta compra no us otorgarà cap funció addicional a Immich. Confiem en usuaris com tu per donar suport al desenvolupament continuat d'Immich.",
|
||||
"purchase_panel_title": "Donar suport al projecte",
|
||||
"purchase_per_server": "Per servidor",
|
||||
"purchase_per_user": "Per usuari",
|
||||
@@ -1472,9 +1494,12 @@
|
||||
"remove_deleted_assets": "Suprimeix fitxers fora de línia",
|
||||
"remove_from_album": "Treu de l'àlbum",
|
||||
"remove_from_favorites": "Eliminar dels preferits",
|
||||
"remove_from_locked_folder": "Elimina de la carpeta bloquejada",
|
||||
"remove_from_locked_folder_confirmation": "Segur que vols moure aquestes fotos i vídeos fora de la carpeta bloquejada? Seran visibles a la teva biblioteca.",
|
||||
"remove_from_shared_link": "Eliminar de l'enllaç compartit",
|
||||
"remove_memory": "Eliminar memòria",
|
||||
"remove_photo_from_memory": "Traieu la foto d'aquesta memòria",
|
||||
"remove_tag": "Elimina l'etiqueta",
|
||||
"remove_url": "Eliminar URL",
|
||||
"remove_user": "Eliminar l'usuari",
|
||||
"removed_api_key": "Eliminada la clau d'API: {name}",
|
||||
@@ -1601,6 +1626,7 @@
|
||||
"server_info_box_server_url": "URL del servidor",
|
||||
"server_offline": "Servidor fora de línia",
|
||||
"server_online": "Servidor en línia",
|
||||
"server_privacy": "Privadesa del servidor",
|
||||
"server_stats": "Estadístiques del servidor",
|
||||
"server_version": "Versió del servidor",
|
||||
"set": "Establir",
|
||||
@@ -1610,6 +1636,7 @@
|
||||
"set_date_of_birth": "Establir data de naixement",
|
||||
"set_profile_picture": "Establir imatge de perfil",
|
||||
"set_slideshow_to_fullscreen": "Mostra Diapositives en pantalla completa",
|
||||
"set_stack_primary_asset": "Estableix com a actiu principal",
|
||||
"setting_image_viewer_help": "El visor de detalls carrega primer la miniatura petita, després carrega la vista prèvia de mida mitjana (si està habilitada), finalment carrega l'original (si està habilitada).",
|
||||
"setting_image_viewer_original_subtitle": "Activa per carregar la imatge en resolució original (molt gran!). Desactiva per reduir el consum de dades (tant de xarxa com de memòria cau).",
|
||||
"setting_image_viewer_original_title": "Carrega la imatge original",
|
||||
@@ -1618,7 +1645,6 @@
|
||||
"setting_image_viewer_title": "Imatges",
|
||||
"setting_languages_apply": "Aplicar",
|
||||
"setting_languages_subtitle": "Canvia el llenguatge de l'aplicació",
|
||||
"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_hours": "{count} hores",
|
||||
"setting_notifications_notify_immediately": "immediatament",
|
||||
@@ -1641,6 +1667,7 @@
|
||||
"share_add_photos": "Afegeix fotografies",
|
||||
"share_assets_selected": "{count} seleccionats",
|
||||
"share_dialog_preparing": "S'està preparant...",
|
||||
"share_link": "Compartir Enllaç",
|
||||
"shared": "Compartit",
|
||||
"shared_album_activities_input_disable": "Els comentaris estan desactivats",
|
||||
"shared_album_activity_remove_content": "Voleu eliminar aquesta activitat?",
|
||||
@@ -1747,6 +1774,7 @@
|
||||
"start_date": "Data inicial",
|
||||
"state": "Regió",
|
||||
"status": "Estat",
|
||||
"stop_casting": "Atura la transmisió",
|
||||
"stop_motion_photo": "Atura foto en moviment",
|
||||
"stop_photo_sharing": "Deixar de compartir les teves fotos?",
|
||||
"stop_photo_sharing_description": "{partner} no podrà tornar a accedir a les vostres fotos.",
|
||||
@@ -1804,7 +1832,6 @@
|
||||
"to_parent": "Anar als pares",
|
||||
"to_trash": "Paperera",
|
||||
"toggle_settings": "Canvia configuració",
|
||||
"toggle_theme": "Alternar tema",
|
||||
"total": "Total",
|
||||
"total_usage": "Ús total",
|
||||
"trash": "Paperera",
|
||||
@@ -1826,6 +1853,7 @@
|
||||
"unable_to_setup_pin_code": "No s'ha pogut configurar el codi PIN",
|
||||
"unarchive": "Desarxivar",
|
||||
"unarchived_count": "{count, plural, other {# elements desarxivats}}",
|
||||
"undo": "Desfer",
|
||||
"unfavorite": "Reverteix preferit",
|
||||
"unhide_person": "Mostra persona",
|
||||
"unknown": "Desconegut",
|
||||
@@ -1844,8 +1872,6 @@
|
||||
"unselect_all_duplicates": "Desmarqueu tots els duplicats",
|
||||
"unstack": "Desapila",
|
||||
"unstacked_assets_count": "No apilat {count, plural, one {# recurs} other {# recursos}}",
|
||||
"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",
|
||||
"up_next": "Pròxim",
|
||||
"updated_at": "Actualitzat",
|
||||
"updated_password": "Contrasenya actualitzada",
|
||||
@@ -1864,6 +1890,7 @@
|
||||
"uploading": "Pujant",
|
||||
"url": "URL",
|
||||
"usage": "Ús",
|
||||
"use_biometric": "Empra biometria",
|
||||
"use_current_connection": "utilitzar la connexió actual",
|
||||
"use_custom_date_range": "Fes servir un rang de dates personalitzat",
|
||||
"user": "Usuari",
|
||||
@@ -1872,6 +1899,7 @@
|
||||
"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_privacy": "Privadesa d'Usuari",
|
||||
"user_purchase_settings": "Compra",
|
||||
"user_purchase_settings_description": "Gestiona la teva compra",
|
||||
"user_role_set": "Establir {user} com a {role}",
|
||||
@@ -1887,11 +1915,6 @@
|
||||
"version": "Versió",
|
||||
"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_overlay_release_notes": "notes de llançament",
|
||||
"version_announcement_overlay_text_1": "Hola amic, hi ha una nova versió d'",
|
||||
"version_announcement_overlay_text_2": "si us plau, pren-te una estona per visitar les ",
|
||||
"version_announcement_overlay_text_3": " i assegura't que la teva configuració de docker-compose i .env estiguin actualitzades per evitar qualsevol error de configuració, especialment si utilitzes WatchTower o qualsevol mecanisme que gestioni l'actualització automàtica de l'aplicació del servidor.",
|
||||
"version_announcement_overlay_title": "Nova versió del servidor disponible 🎉",
|
||||
"version_history": "Historial de versions",
|
||||
"version_history_item": "Instal·lat {version} el {date}",
|
||||
"video": "Vídeo",
|
||||
@@ -1911,6 +1934,7 @@
|
||||
"view_previous_asset": "Mostra l'element anterior",
|
||||
"view_qr_code": "Veure codi QR",
|
||||
"view_stack": "Veure la pila",
|
||||
"view_user": "Veure Usuari",
|
||||
"viewer_remove_from_stack": "Elimina de la pila",
|
||||
"viewer_stack_use_as_main_asset": "Fes servir com a element principal",
|
||||
"viewer_unstack": "Desapila",
|
||||
@@ -1921,6 +1945,7 @@
|
||||
"welcome": "Benvingut",
|
||||
"welcome_to_immich": "Benvingut a immich",
|
||||
"wifi_name": "Nom Wi-Fi",
|
||||
"wrong_pin_code": "Codi PIN incorrecte",
|
||||
"year": "Any",
|
||||
"years_ago": "Fa {years, plural, one {# any} other {# anys}}",
|
||||
"yes": "Sí",
|
||||
|
||||
203
i18n/cs.json
203
i18n/cs.json
@@ -22,6 +22,7 @@
|
||||
"add_partner": "Přidat partnera",
|
||||
"add_path": "Přidat cestu",
|
||||
"add_photos": "Přidat fotky",
|
||||
"add_tag": "Přidat značku",
|
||||
"add_to": "Přidat do…",
|
||||
"add_to_album": "Přidat do alba",
|
||||
"add_to_album_bottom_sheet_added": "Přidáno do {album}",
|
||||
@@ -33,6 +34,7 @@
|
||||
"added_to_favorites_count": "Přidáno {count, number} do oblíbených",
|
||||
"admin": {
|
||||
"add_exclusion_pattern_description": "Přidání vzorů vyloučení. Podporováno je globování pomocí *, ** a ?. Chcete-li ignorovat všechny soubory v jakémkoli adresáři s názvem \"Raw\", použijte \"**/Raw/**\". Chcete-li ignorovat všechny soubory končící na \".tif\", použijte \"**/*.tif\". Chcete-li ignorovat absolutní cestu, použijte příkaz \"/path/to/ignore/**\".",
|
||||
"admin_user": "Administrátor",
|
||||
"asset_offline_description": "Tato položka externí knihovny se již na disku nenachází a byla přesunuta do koše. Pokud byl soubor přesunut v rámci knihovny, zkontrolujte časovou osu a vyhledejte nové odpovídající položku. Chcete-li tuto položku obnovit, ujistěte se, že je cesta k níže uvedenému souboru přístupná pomocí aplikace Immich a prohledejte knihovnu.",
|
||||
"authentication_settings": "Přihlašování",
|
||||
"authentication_settings_description": "Správa hesel, OAuth a dalších nastavení ověření",
|
||||
@@ -43,9 +45,7 @@
|
||||
"backup_database_enable_description": "Povolit výpisy z databáze",
|
||||
"backup_keep_last_amount": "Počet předchozích výpisů, které se mají ponechat",
|
||||
"backup_settings": "Nastavení výpisu databáze",
|
||||
"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í.",
|
||||
"check_all": "Vše zkontrolovat",
|
||||
"cleanup": "Vyčištění",
|
||||
"backup_settings_description": "Správa nastavení výpisu databáze.",
|
||||
"cleared_jobs": "Hotové úlohy pro: {job}",
|
||||
"config_set_by_file": "Konfigurace je aktuálně prováděna konfiguračním souborem",
|
||||
"confirm_delete_library": "Opravdu chcete odstranit knihovnu {library}?",
|
||||
@@ -61,14 +61,12 @@
|
||||
"disable_login": "Zakázat přihlášení",
|
||||
"duplicate_detection_job_description": "Spuštění strojového učení na položkách za účelem detekce podobných obrázků. Spoléhá na Chytré vyhledávání",
|
||||
"exclusion_pattern_description": "Vzory vyloučení umožňují při prohledávání knihovny ignorovat soubory a složky. To je užitečné, pokud máte složky obsahující soubory, které nechcete importovat, například RAW soubory.",
|
||||
"external_library_created_at": "Externí knihovna (vytvořena {date})",
|
||||
"external_library_management": "Správa externích knihoven",
|
||||
"face_detection": "Detekce obličejů",
|
||||
"face_detection_description": "Detekce obličejů v obrázcích pomocí strojového učení. U videí se bere v úvahu pouze miniatura. „Obnovit“ znovu zpracuje všechny položky. „Resetovat“ navíc vymaže všechna aktuální data obličejů. „Chybějící“ zařadí do fronty položky, které ještě nebyly zpracovány. Zjištěné obličeje budou po dokončení funkce Rozpoznávání obličejů zařazeny do fronty a seskupeny do stávajících nebo nových osob.",
|
||||
"facial_recognition_job_description": "Seskupí nalezené obličeje do osob. Tento krok se spustí po dokončení detekce obličejů. „Resetovat“ znovu seskupí všechny obličeje. „Chybějící“ zpracuje obličeje, které nemají přiřazenou osobu.",
|
||||
"failed_job_command": "Příkaz {command} se nezdařil pro úlohu: {job}",
|
||||
"force_delete_user_warning": "UPOZORNĚNÍ: Tímto okamžitě odstraníte uživatele a všechny jeho položky. Tento krok nelze vrátit zpět a soubory nelze obnovit.",
|
||||
"forcing_refresh_library_files": "Vynucení obnovy všech souborů knihovny",
|
||||
"image_format": "Formát",
|
||||
"image_format_description": "WebP vytváří menší soubory než JPEG, ale je pomalejší při kódování.",
|
||||
"image_fullsize_description": "Obrázek v plné velikosti s odstraněnými metadaty, použito při přiblížení",
|
||||
@@ -173,7 +171,7 @@
|
||||
"note_apply_storage_label_previous_assets": "Upozornění: Pro uplatnění Štítku úložiště na dříve nahrané položky spusťte",
|
||||
"note_cannot_be_changed_later": "UPOZORNĚNÍ: Toto nelze později změnit!",
|
||||
"notification_email_from_address": "Adresa Od",
|
||||
"notification_email_from_address_description": "E-mailová adresa odesílatele, např.: „Immich Photo Server <noreply@example.com>“",
|
||||
"notification_email_from_address_description": "E-mailová adresa odesílatele, např.: „Immich Photo Server <noreply@example.com>“. Ujistěte se, že používáte adresu, ze které smíte odesílat e-maily.",
|
||||
"notification_email_host_description": "Adresa e-mailového serveru (např. smtp.immich.app)",
|
||||
"notification_email_ignore_certificate_errors": "Ignorovat chyby certifikátů",
|
||||
"notification_email_ignore_certificate_errors_description": "Ignorovat chyby ověření certifikátu TLS (nedoporučuje se)",
|
||||
@@ -197,7 +195,7 @@
|
||||
"oauth_enable_description": "Přihlásit pomocí OAuth",
|
||||
"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_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_settings": "OAuth",
|
||||
"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>.",
|
||||
@@ -206,11 +204,9 @@
|
||||
"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_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.",
|
||||
"oauth_timeout": "Časový limit požadavku",
|
||||
"oauth_timeout_description": "Časový limit pro požadavky v milisekundách",
|
||||
"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.",
|
||||
"password_enable_description": "Přihlášení pomocí e-mailu a hesla",
|
||||
"password_settings": "Přihlášení heslem",
|
||||
"password_settings_description": "Správa nastavení přihlašování pomocí hesla",
|
||||
@@ -220,9 +216,6 @@
|
||||
"refreshing_all_libraries": "Obnovení všech knihoven",
|
||||
"registration": "Registrace správce",
|
||||
"registration_description": "Vzhledem k tomu, že jste prvním uživatelem v systému, budete přiřazen jako správce a budete zodpovědný za úkoly správy a další uživatelé budou vytvořeni vámi.",
|
||||
"repair_all": "Opravit vše",
|
||||
"repair_matched_items": "Shoda {count, plural, one {# položky} other {# položek}}",
|
||||
"repaired_items": "{count, plural, one {Opravena # položka} few {Opraveny # položky} other {Opraveno # položek}}",
|
||||
"require_password_change_on_login": "Požadovat, aby si uživatel při prvním přihlášení změnil heslo",
|
||||
"reset_settings_to_default": "Obnovení výchozího nastavení",
|
||||
"reset_settings_to_recent_saved": "Obnovit poslední uložené nastavení",
|
||||
@@ -251,7 +244,7 @@
|
||||
"storage_template_migration_info": "Šablona úložiště převede všechny přípony na malá písmena. Změny šablon se uplatní pouze u nových položek. Chcete-li šablonu zpětně použít na dříve nahrané položky, spusťte <link>{job}</link>.",
|
||||
"storage_template_migration_job": "Úloha migrace šablony úložiště",
|
||||
"storage_template_more_details": "Další podrobnosti o této funkci naleznete v sekci <template-link>Šablona úložiště</template-link> včetně jejích <implications-link>důsledků</implications-link>",
|
||||
"storage_template_onboarding_description": "Je-li tato funkce povolena, automaticky uspořádá soubory na základě uživatelem definované šablony. Z důvodu problémů se stabilitou byla tato funkce ve výchozím nastavení vypnuta. Další informace naleznete v <link>dokumentaci</link>.",
|
||||
"storage_template_onboarding_description_v2": "Pokud je tato funkce povolena, automaticky uspořádá soubory na základě uživatelem definované šablony. Další informace naleznete v <link>dokumentaci</link>.",
|
||||
"storage_template_path_length": "Přibližný limit délky cesty: <b>{length, number}</b>/{limit, number}",
|
||||
"storage_template_settings": "Šablona úložiště",
|
||||
"storage_template_settings_description": "Správa struktury složek a názvů nahraných souborů",
|
||||
@@ -263,16 +256,14 @@
|
||||
"template_email_invite_album": "Šablona pozvánky do alba",
|
||||
"template_email_preview": "Náhled",
|
||||
"template_email_settings": "Šablony e-mailů",
|
||||
"template_email_settings_description": "Správa vlastních šablon e-mailových oznámení",
|
||||
"template_email_update_album": "Šablona aktualizace alba",
|
||||
"template_email_welcome": "Šablona uvítacího e-mailu",
|
||||
"template_settings": "Šablony oznámení",
|
||||
"template_settings_description": "Správa vlastních šablon oznámení.",
|
||||
"template_settings_description": "Správa vlastních šablon oznámení",
|
||||
"theme_custom_css_settings": "Vlastní CSS",
|
||||
"theme_custom_css_settings_description": "Kaskádové styly umožňují přizpůsobit design aplikace Immich.",
|
||||
"theme_settings": "Motivy",
|
||||
"theme_settings_description": "Správa přizpůsobení webového rozhraní Immich",
|
||||
"these_files_matched_by_checksum": "Tyto soubory jsou porovnávány podle jejich kontrolních součtů",
|
||||
"thumbnail_generation_job": "Generování miniatur",
|
||||
"thumbnail_generation_job_description": "Generování velkých, malých a rozmazaných miniatur pro každý obrázek a miniatur pro každou osobu",
|
||||
"transcoding_acceleration_api": "API pro akceleraci",
|
||||
@@ -300,10 +291,9 @@
|
||||
"transcoding_encoding_options": "Možnosti kódování",
|
||||
"transcoding_encoding_options_description": "Nastavte kodeky, rozlišení, kvalitu a další možnosti pro kódovaná videa",
|
||||
"transcoding_hardware_acceleration": "Hardwarová akcelerace",
|
||||
"transcoding_hardware_acceleration_description": "Experimentální; mnohem rychlejší, ale při stejném datovém toku bude mít nižší kvalitu",
|
||||
"transcoding_hardware_acceleration_description": "Experimentální: rychlejší kódování, ale při stejném datovém toku může mít nižší kvalitu",
|
||||
"transcoding_hardware_decoding": "Hardwarové dekódování",
|
||||
"transcoding_hardware_decoding_setting_description": "Povoluje kompletní akceleraci namísto akcelerace pouze kódování. Nemusí fungovat u všech videí.",
|
||||
"transcoding_hevc_codec": "Kodek HEVC",
|
||||
"transcoding_max_b_frames": "Maximální počet B-snímků",
|
||||
"transcoding_max_b_frames_description": "Vyšší hodnoty zvyšují účinnost komprese, ale zpomalují kódování. Nemusí být kompatibilní s hardwarovou akcelerací na starších zařízeních. Hodnota 0 zakáže B-snímky, zatímco -1 tuto hodnotu nastaví automaticky.",
|
||||
"transcoding_max_bitrate": "Maximální datový tok",
|
||||
@@ -341,8 +331,6 @@
|
||||
"trash_number_of_days_description": "Počet dní, po které je třeba položku ponechat v koši, než bude trvale odstraněna",
|
||||
"trash_settings": "Koš",
|
||||
"trash_settings_description": "Správa nastavení koše",
|
||||
"untracked_files": "Neznámé soubory",
|
||||
"untracked_files_description": "Tyto soubory nejsou aplikaci známy. Mohou být výsledkem neúspěšných přesunů, přerušeného nahrávání nebo mohou zůstat pozadu kvůli chybě",
|
||||
"user_cleanup_job": "Promazání uživatelů",
|
||||
"user_delete_delay": "Účet a položky uživatele <b>{user}</b> budou trvale smazány za {delay, plural, one {# den} few {# dny} other {# dní}}.",
|
||||
"user_delete_delay_settings": "Odložení odstranění",
|
||||
@@ -383,8 +371,8 @@
|
||||
"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_title": "Řešení problémů",
|
||||
"age_months": "Věk {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_months": "{months, plural, one {# měsíc} few {# měsíce} other {# měsíců}}",
|
||||
"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}}",
|
||||
"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",
|
||||
@@ -401,10 +389,6 @@
|
||||
"album_remove_user": "Odebrat uživatele?",
|
||||
"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_thumbnail_card_item": "1 položka",
|
||||
"album_thumbnail_card_items": "{count} položek",
|
||||
"album_thumbnail_card_shared": " · Sdíleno",
|
||||
"album_thumbnail_shared_by": "Sdílel(a) {user}",
|
||||
"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_user_left": "Opustil {album}",
|
||||
@@ -420,6 +404,9 @@
|
||||
"album_with_link_access": "Nechte kohokoli s odkazem zobrazit fotografie a lidi v tomto albu.",
|
||||
"albums": "Alba",
|
||||
"albums_count": "{count, plural, one {{count, number} album} few {{count, number} alba} other {{count, number} alb}}",
|
||||
"albums_default_sort_order": "Výchozí řazení alb",
|
||||
"albums_default_sort_order_description": "Výchozí řazení položek při vytváření nových alb.",
|
||||
"albums_feature_description": "Sbírky položek, které lze sdílet s ostatními uživateli.",
|
||||
"all": "Vše",
|
||||
"all_albums": "Všechna alba",
|
||||
"all_people": "Všichni lidé",
|
||||
@@ -478,9 +465,12 @@
|
||||
"assets_added_count": "{count, plural, one {Přidána # položka} few {Přidány # položky} other {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_cannot_be_added_to_album_count": "{count, plural, one {Položku} other {Položky}} nelze přidat do alba",
|
||||
"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_from_server": "{count} položek trvale odstraněno z Immich serveru",
|
||||
"assets_downloaded_failed": "{count, plural, one {Stažen # soubor - {error} souborů selhalo} few {Staženy # soubory - {error} souborů selhalo} other {Staženo # souborů - {error} souborů selhalo}}",
|
||||
"assets_downloaded_successfully": "{count, plural, one {Úspěšně stažen # soubor} few {Úspěšně staženy # soubory} other {Úspěšně staženo # souborů}}",
|
||||
"assets_moved_to_trash_count": "Do koše {count, plural, one {přesunuta # položka} few {přesunuty # položky} other {přesunuto # položek}}",
|
||||
"assets_permanently_deleted_count": "Trvale {count, plural, one {smazána # položka} few {smazány # položky} other {smazáno # položek}}",
|
||||
"assets_removed_count": "{count, plural, one {Odstraněna # položka} few {Odstraněny # položky} other {Odstraněno # položek}}",
|
||||
@@ -495,6 +485,7 @@
|
||||
"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_title": "Automatické přepínání URL",
|
||||
"autoplay_slideshow": "Automatické přehrávání prezentace",
|
||||
"back": "Zpět",
|
||||
"back_close_deselect": "Zpět, zavřít nebo zrušit výběr",
|
||||
"background_location_permission": "Povolení polohy na pozadí",
|
||||
@@ -562,6 +553,10 @@
|
||||
"backup_options_page_title": "Nastavení záloh",
|
||||
"backup_setting_subtitle": "Správa nastavení zálohování na pozadí a na popředí",
|
||||
"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_set_description": "Datum narození se používá k výpočtu věku osoby v době pořízení fotografie.",
|
||||
"blurred_background": "Rozmazané pozadí",
|
||||
@@ -572,21 +567,17 @@
|
||||
"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í.",
|
||||
"buy": "Zakoupit Immich",
|
||||
"cache_settings_album_thumbnails": "Náhledy stránek knihovny ({count} položek)",
|
||||
"cache_settings_clear_cache_button": "Vymazat vyrovnávací paměť",
|
||||
"cache_settings_clear_cache_button_title": "Vymaže vyrovnávací paměť aplikace. To výrazně ovlivní výkon aplikace, dokud se vyrovnávací paměť neobnoví.",
|
||||
"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_title": "Duplicitní položky ({count})",
|
||||
"cache_settings_image_cache_size": "Velikost vyrovnávací paměti ({count} položek)",
|
||||
"cache_settings_statistics_album": "Knihovna náhledů",
|
||||
"cache_settings_statistics_assets": "{count, plural, one {# položka} few {# položky} other {# položek}} ({size})",
|
||||
"cache_settings_statistics_full": "Kompletní fotografie",
|
||||
"cache_settings_statistics_shared": "Sdílené náhledy alb",
|
||||
"cache_settings_statistics_thumbnail": "Náhledy",
|
||||
"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_thumbnail_size": "Velikost vyrovnávací paměti náhledů ({count, plural, one {# položka} few {# položky} other {# položek}})",
|
||||
"cache_settings_tile_subtitle": "Ovládání chování místního úložiště",
|
||||
"cache_settings_tile_title": "Místní úložiště",
|
||||
"cache_settings_title": "Nastavení vyrovnávací paměti",
|
||||
@@ -599,12 +590,15 @@
|
||||
"cannot_merge_people": "Nelze sloučit osoby",
|
||||
"cannot_undo_this_action": "Tuto akci nelze vrátit zpět!",
|
||||
"cannot_update_the_description": "Nelze aktualizovat popis",
|
||||
"cast": "Odeslat do zařízení",
|
||||
"cast_description": "Nastavení dostupných cílů přenosu",
|
||||
"change_date": "Změnit datum",
|
||||
"change_description": "Změnit popis",
|
||||
"change_display_order": "Změnit pořadí zobrazení",
|
||||
"change_expiration_time": "Změna konce platnosti",
|
||||
"change_location": "Změna polohy",
|
||||
"change_name": "Změnit jméno",
|
||||
"change_name_successfully": "Změna jména proběhla úspěšně",
|
||||
"change_name_successfully": "Jméno bylo úspěšně změněno",
|
||||
"change_password": "Změna hesla",
|
||||
"change_password_description": "Buď se do systému přihlašujete poprvé, nebo jste byli požádáni o změnu hesla. Zadejte prosím nové heslo níže.",
|
||||
"change_password_form_confirm_password": "Potvrďte heslo",
|
||||
@@ -615,7 +609,6 @@
|
||||
"change_pin_code": "Změnit PIN kód",
|
||||
"change_your_password": "Změna vašeho hesla",
|
||||
"changed_visibility_successfully": "Změna viditelnosti proběhla úspěšně",
|
||||
"check_all": "Zkontrolovat vše",
|
||||
"check_corrupt_asset_backup": "Kontrola poškozených záloh položek",
|
||||
"check_corrupt_asset_backup_button": "Provést kontrolu",
|
||||
"check_corrupt_asset_backup_description": "Tuto kontrolu provádějte pouze přes Wi-Fi a po zálohování všech prostředků. Takto operace může trvat několik minut.",
|
||||
@@ -655,10 +648,13 @@
|
||||
"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_tag_face": "Opravdu chcete označit tento obličej jako {name}?",
|
||||
"confirm_tag_face_unnamed": "Opravdu chcete označit tento obličej?",
|
||||
"connected_device": "Připojené zařízení",
|
||||
"connected_to": "Připojeno k",
|
||||
"contain": "Obsah",
|
||||
"context": "Kontext",
|
||||
"continue": "Pokračovat",
|
||||
"control_bottom_app_bar_album_info_shared": "{count, plural, one {# položka – sdílená} few {# položky – sdílené} other {# položek – sdílených}}",
|
||||
"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_local": "Smazat ze zařízení",
|
||||
@@ -707,6 +703,7 @@
|
||||
"daily_title_text_date": "EEEE, d. MMMM",
|
||||
"daily_title_text_date_year": "EEEE, d. MMMM y",
|
||||
"dark": "Tmavý",
|
||||
"darkTheme": "Přepnout tmavý motiv",
|
||||
"date_after": "Datum po",
|
||||
"date_and_time": "Datum a čas",
|
||||
"date_before": "Datum před",
|
||||
@@ -754,6 +751,7 @@
|
||||
"disallow_edits": "Zakázat úpravy",
|
||||
"discord": "Discord",
|
||||
"discover": "Objevit",
|
||||
"discovered_devices": "Nalezená zařízení",
|
||||
"dismiss_all_errors": "Zrušit všechny chyby",
|
||||
"dismiss_error": "Zrušit chybu",
|
||||
"display_options": "Možnosti zobrazení",
|
||||
@@ -769,7 +767,6 @@
|
||||
"download_enqueue": "Stahování ve frontě",
|
||||
"download_error": "Chyba při stahování",
|
||||
"download_failed": "Stahování selhalo",
|
||||
"download_filename": "soubor: {filename}",
|
||||
"download_finished": "Stahování dokončeno",
|
||||
"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",
|
||||
@@ -793,6 +790,8 @@
|
||||
"edit_avatar": "Upravit avatar",
|
||||
"edit_date": "Upravit datum",
|
||||
"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_faces": "Upravit obličeje",
|
||||
"edit_import_path": "Upravit cestu importu",
|
||||
@@ -818,15 +817,19 @@
|
||||
"empty_trash": "Vyprázdnit koš",
|
||||
"empty_trash_confirmation": "Opravdu chcete vysypat koš? Tím se z Immiche trvale odstraní všechny položky v koši.\nTuto akci nelze vrátit zpět!",
|
||||
"enable": "Povolit",
|
||||
"enable_biometric_auth_description": "Zadejte váš PIN kód pro povolení biometrického ověřování",
|
||||
"enabled": "Povoleno",
|
||||
"end_date": "Konečné datum",
|
||||
"enqueued": "Ve frontě",
|
||||
"enter_wifi_name": "Zadejte název Wi-Fi",
|
||||
"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_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_loading_image": "Chyba při načítání obrázku",
|
||||
"error_saving_image": "Chyba: {error}",
|
||||
"error_tag_face_bounding_box": "Chyba při označování obličeje - nelze získat souřadnice ohraničujícího rámečku",
|
||||
"error_title": "Chyba - Něco se pokazilo",
|
||||
"errors": {
|
||||
"cannot_navigate_next_asset": "Nelze přejít na další položku",
|
||||
@@ -839,7 +842,6 @@
|
||||
"cant_get_number_of_comments": "Nelze načíst počet komentářů",
|
||||
"cant_search_people": "Nelze vyhledávat lidi",
|
||||
"cant_search_places": "Nelze vyhledávat místa",
|
||||
"cleared_jobs": "Vyřízené úlohy pro: {job}",
|
||||
"error_adding_assets_to_album": "Chyba při přidávání položek do alba",
|
||||
"error_adding_users_to_album": "Chyba při přidávání uživatelů do alba",
|
||||
"error_deleting_shared_user": "Chyba při odstraňování sdíleného uživatele",
|
||||
@@ -848,7 +850,6 @@
|
||||
"error_removing_assets_from_album": "Chyba při odstraňování položek z alba, další podrobnosti najdete v konzoli",
|
||||
"error_selecting_all_assets": "Chyba při výběru všech položek",
|
||||
"exclusion_pattern_already_exists": "Tento vzor vyloučení již existuje.",
|
||||
"failed_job_command": "Příkaz {command} se nezdařil pro úlohu: {job}",
|
||||
"failed_to_create_album": "Nepodařilo se vytvořit album",
|
||||
"failed_to_create_shared_link": "Nepodařilo se vytvořit sdílený odkaz",
|
||||
"failed_to_edit_shared_link": "Nepodařilo se upravit sdílený odkaz",
|
||||
@@ -867,7 +868,6 @@
|
||||
"paths_validation_failed": "{paths, plural, one {# cesta neprošla} few {# cesty neprošly} other {# cest neprošlo}} kontrolou",
|
||||
"profile_picture_transparent_pixels": "Profilové obrázky nemohou mít průhledné pixely. Obrázek si prosím zvětšete nebo posuňte.",
|
||||
"quota_higher_than_disk_size": "Nastavili jste kvótu vyšší, než je velikost disku",
|
||||
"repair_unable_to_check_items": "Nelze zkontrolovat {count, select, one {položku} other {položky}}",
|
||||
"unable_to_add_album_users": "Nelze přidat uživatele do alba",
|
||||
"unable_to_add_assets_to_shared_link": "Nelze přidat položky do sdíleného odkazu",
|
||||
"unable_to_add_comment": "Nelze přidat komentář",
|
||||
@@ -879,13 +879,13 @@
|
||||
"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_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_location": "Nelze změnit polohu",
|
||||
"unable_to_change_password": "Nelze změnit heslo",
|
||||
"unable_to_change_visibility": "Nelze změnit viditelnost u {count, plural, one {# osoby} few {# osob} other {# lidí}}",
|
||||
"unable_to_complete_oauth_login": "Nelze dokončit OAuth přihlášení",
|
||||
"unable_to_connect": "Nelze se připojit",
|
||||
"unable_to_connect_to_server": "Nepodařilo se připojit k serveru",
|
||||
"unable_to_copy_to_clipboard": "Nelze zkopírovat do schránky, ujistěte se, že na stránku přistupujete přes https",
|
||||
"unable_to_create_admin_account": "Nelze vytvořit účet správce",
|
||||
"unable_to_create_api_key": "Nelze vytvořit nový API klíč",
|
||||
@@ -909,10 +909,6 @@
|
||||
"unable_to_hide_person": "Nelze skrýt osobu",
|
||||
"unable_to_link_motion_video": "Nelze připojit pohyblivé video",
|
||||
"unable_to_link_oauth_account": "Nelze propojit OAuth účet",
|
||||
"unable_to_load_album": "Nelze načíst album",
|
||||
"unable_to_load_asset_activity": "Nelze načíst aktivitu položky",
|
||||
"unable_to_load_items": "Nelze načíst položky",
|
||||
"unable_to_load_liked_status": "Nelze načíst stav oblíbených",
|
||||
"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_login_with_oauth": "Nelze se přihlásit pomocí OAuth",
|
||||
@@ -923,11 +919,9 @@
|
||||
"unable_to_remove_album_users": "Nelze odebrat uživatele z alba",
|
||||
"unable_to_remove_api_key": "Nelze odstranit API klíč",
|
||||
"unable_to_remove_assets_from_shared_link": "Nelze odstranit položky ze sdíleného odkazu",
|
||||
"unable_to_remove_deleted_assets": "Nelze odstranit offline soubory",
|
||||
"unable_to_remove_library": "Nelze odstranit knihovnu",
|
||||
"unable_to_remove_partner": "Nelze odebrat partnera",
|
||||
"unable_to_remove_reaction": "Nelze odstranit reakci",
|
||||
"unable_to_repair_items": "Nelze opravit položky",
|
||||
"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",
|
||||
@@ -963,10 +957,9 @@
|
||||
"exif_bottom_sheet_location": "POLOHA",
|
||||
"exif_bottom_sheet_people": "LIDÉ",
|
||||
"exif_bottom_sheet_person_add_person": "Přidat jméno",
|
||||
"exif_bottom_sheet_person_age": "Věk {age, plural, one {# rok} few {# roky} other {# let}}",
|
||||
"exif_bottom_sheet_person_age_months": "Věk {months, plural, one {# měsíc} few {# měsíce} other {# měsíců}}",
|
||||
"exif_bottom_sheet_person_age_year_months": "Věk 1 rok, {months, plural, one {# měsíc} other {# měsíce}}",
|
||||
"exif_bottom_sheet_person_age_years": "Věk {years, plural, one {# rok} few {# roky} other {# let}}",
|
||||
"exif_bottom_sheet_person_age_months": "{months} měsíců",
|
||||
"exif_bottom_sheet_person_age_year_months": "1 rok a {months} měsíců",
|
||||
"exif_bottom_sheet_person_age_years": "{years} let",
|
||||
"exit_slideshow": "Ukončit prezentaci",
|
||||
"expand_all": "Rozbalit vše",
|
||||
"experimental_settings_new_asset_list_subtitle": "Zpracovávám",
|
||||
@@ -987,6 +980,7 @@
|
||||
"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ů",
|
||||
"face_unassigned": "Nepřiřazena",
|
||||
"failed": "Selhalo",
|
||||
"failed_to_authenticate": "Ověření se nezdařilo",
|
||||
"failed_to_load_assets": "Nepodařilo se načíst položky",
|
||||
"failed_to_load_folder": "Nepodařilo se načíst složku",
|
||||
"favorite": "Oblíbit",
|
||||
@@ -1010,6 +1004,8 @@
|
||||
"folders": "Složky",
|
||||
"folders_feature_description": "Procházení zobrazení složek s fotografiemi a videi v souborovém systému",
|
||||
"forward": "Dopředu",
|
||||
"gcast_enabled": "Google Cast",
|
||||
"gcast_enabled_description": "Tato funkce načítá externí zdroje z Googlu, aby mohla fungovat.",
|
||||
"general": "Obecné",
|
||||
"get_help": "Získat pomoc",
|
||||
"get_wifiname_error": "Nepodařilo se získat název Wi-Fi. Zkontrolujte, zda jste udělili potřebná oprávnění a zda jste připojeni k Wi-Fi síti",
|
||||
@@ -1052,6 +1048,8 @@
|
||||
"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_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_upload_err_limit": "Lze nahrát nejvýše 30 položek najednou, přeskakuji",
|
||||
"host": "Hostitel",
|
||||
@@ -1096,6 +1094,12 @@
|
||||
"invalid_date_format": "Chybný formát data",
|
||||
"invite_people": "Pozvat lidi",
|
||||
"invite_to_album": "Pozvat do alba",
|
||||
"ios_debug_info_fetch_ran_at": "Data načtena {dateTime}",
|
||||
"ios_debug_info_last_sync_at": "Poslední synchronizace {dateTime}",
|
||||
"ios_debug_info_no_processes_queued": "Žádné procesy na pozadí ve frontě",
|
||||
"ios_debug_info_no_sync_yet": "Dosud nebyla spuštěna žádná úloha synchronizace na pozadí",
|
||||
"ios_debug_info_processes_queued": "{count, plural, one {{count} proces na pozadí ve frontě} few {{count} procesy na pozadí ve frontě} other {{count} procesů na pozadí ve frontě}}",
|
||||
"ios_debug_info_processing_ran_at": "Zpracování spuštěno {dateTime}",
|
||||
"items_count": "{count, plural, one {# položka} few {# položky} other {# položek}}",
|
||||
"jobs": "Úlohy",
|
||||
"keep": "Ponechat",
|
||||
@@ -1104,6 +1108,9 @@
|
||||
"kept_this_deleted_others": "Ponechána tato položka a {count, plural, one {odstraněna # položka} few {odstraněny # položky} other {odstraněno # položek}}",
|
||||
"keyboard_shortcuts": "Klávesové zkratky",
|
||||
"language": "Jazyk",
|
||||
"language_no_results_subtitle": "Zkuste upravit hledaný výraz",
|
||||
"language_no_results_title": "Nebyly nalezeny žádné jazyky",
|
||||
"language_search_hint": "Vyhledat jazyk...",
|
||||
"language_setting_description": "Vyberte upřednostňovaný jazyk",
|
||||
"last_seen": "Naposledy viděno",
|
||||
"latest_version": "Nejnovější verze",
|
||||
@@ -1129,6 +1136,7 @@
|
||||
"list": "Seznam",
|
||||
"loading": "Načítání",
|
||||
"loading_search_results_failed": "Načítání výsledků vyhledávání se nezdařilo",
|
||||
"local_asset_cast_failed": "Nelze odeslat položku, která není nahraná na serveru",
|
||||
"local_network": "Místní síť",
|
||||
"local_network_sheet_info": "Aplikace se při použití zadané sítě Wi-Fi připojí k serveru prostřednictvím tohoto URL",
|
||||
"location_permission": "Oprávnění polohy",
|
||||
@@ -1138,8 +1146,11 @@
|
||||
"location_picker_latitude_hint": "Zadejte vlastní zeměpisnou šířku",
|
||||
"location_picker_longitude_error": "Zadejte platnou 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_all_devices": "Odhlásit všechna zařízení",
|
||||
"logged_in_as": "Přihlášen jako {user}",
|
||||
"logged_out_all_devices": "Všechna zařízení odhlášena",
|
||||
"logged_out_device": "Zařízení odhlášeno",
|
||||
"login": "Přihlášení",
|
||||
@@ -1182,8 +1193,8 @@
|
||||
"manage_your_devices": "Správa přihlášených zařízení",
|
||||
"manage_your_oauth_connection": "Správa OAuth propojení",
|
||||
"map": "Mapa",
|
||||
"map_assets_in_bound": "{count, plural, one {# fotka} few {# fotky} other {# fotek}}",
|
||||
"map_assets_in_bounds": "{count, plural, one {# fotka} few {# fotky} other {# fotek}}",
|
||||
"map_assets_in_bound": "{count} fotka",
|
||||
"map_assets_in_bounds": "{count} fotek",
|
||||
"map_cannot_get_user_location": "Nelze zjistit polohu uživatele",
|
||||
"map_location_dialog_yes": "Ano",
|
||||
"map_location_picker_page_use_location": "Použít tuto polohu",
|
||||
@@ -1197,9 +1208,9 @@
|
||||
"map_settings": "Nastavení mapy",
|
||||
"map_settings_dark_mode": "Tmavý režim",
|
||||
"map_settings_date_range_option_day": "Posledních 24 hodin",
|
||||
"map_settings_date_range_option_days": "Posledních {days, plural, one {# den} few {# dny} other {# dní}}",
|
||||
"map_settings_date_range_option_days": "Posledních {days} dní",
|
||||
"map_settings_date_range_option_year": "Poslední rok",
|
||||
"map_settings_date_range_option_years": "Poslední {years, plural, one {# rok} few {# roky} other {# roky}}",
|
||||
"map_settings_date_range_option_years": "Poslední {years} roky",
|
||||
"map_settings_dialog_title": "Nastavení map",
|
||||
"map_settings_include_show_archived": "Zahrnout archivované",
|
||||
"map_settings_include_show_partners": "Včetně partnerů",
|
||||
@@ -1217,8 +1228,6 @@
|
||||
"memories_setting_description": "Správa toho, co vidíte ve svých vzpomínkách",
|
||||
"memories_start_over": "Začít znovu",
|
||||
"memories_swipe_to_close": "Přejetím nahoru zavřete",
|
||||
"memories_year_ago": "Před rokem",
|
||||
"memories_years_ago": "Před {years, plural, one {# rokem} few {# roky} other {# lety}}",
|
||||
"memory": "Vzpomínka",
|
||||
"memory_lane_title": "Řada vzpomínek {title}",
|
||||
"menu": "Nabídka",
|
||||
@@ -1235,6 +1244,10 @@
|
||||
"month": "Měsíc",
|
||||
"monthly_title_text_date_format": "LLLL y",
|
||||
"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",
|
||||
@@ -1252,6 +1265,7 @@
|
||||
"new_password": "Nové heslo",
|
||||
"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_version_available": "NOVÁ VERZE K DISPOZICI",
|
||||
"newest_first": "Nejnovější první",
|
||||
@@ -1264,11 +1278,13 @@
|
||||
"no_archived_assets_message": "Archivujte fotografie a videa a skryjte je ze zobrazení v sekci Fotky",
|
||||
"no_assets_message": "KLIKNĚTE PRO NAHRÁNÍ PRVNÍ FOTOGRAFIE",
|
||||
"no_assets_to_show": "Žádné položky k zobrazení",
|
||||
"no_cast_devices_found": "Nebyla nalezena žádná zařízení",
|
||||
"no_duplicates_found": "Nebyly nalezeny žádné duplicity.",
|
||||
"no_exif_info_available": "Exif není k dispozici",
|
||||
"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_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í nebo vyhledávání v knihovně se nezobrazují.",
|
||||
"no_name": "Bez jména",
|
||||
"no_notifications": "Žádná oznámení",
|
||||
"no_people_found": "Nebyli nalezeni žádní odpovídající lidé",
|
||||
@@ -1280,6 +1296,7 @@
|
||||
"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",
|
||||
"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_list_tile_content": "Udělte oprávnění k aktivaci oznámení.",
|
||||
"notification_permission_list_tile_enable_button": "Povolit oznámení",
|
||||
@@ -1290,15 +1307,15 @@
|
||||
"oauth": "OAuth",
|
||||
"official_immich_resources": "Oficiální zdroje Immich",
|
||||
"offline": "Offline",
|
||||
"offline_paths": "Offline cesty",
|
||||
"offline_paths_description": "Tyto výsledky mohou být způsobeny ručním odstraněním souborů, které nejsou součástí externí knihovny.",
|
||||
"ok": "Ok",
|
||||
"oldest_first": "Nejstarší první",
|
||||
"on_this_device": "V tomto zařízení",
|
||||
"onboarding": "Zahájení",
|
||||
"onboarding_privacy_description": "Následující (volitelné) funkce jsou závislé na externích službách a lze je kdykoli zakázat v nastavení správy.",
|
||||
"onboarding_locale_description": "Vyberte preferovaný jazyk. Tento výběr můžete později změnit v nastavení.",
|
||||
"onboarding_privacy_description": "Následující (volitelné) funkce jsou závislé na externích službách a lze je kdykoli zakázat v nastavení.",
|
||||
"onboarding_server_welcome_description": "Pojďme nastavit vaši instanci pomocí několika běžných 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_user_welcome_description": "Pojďme na to!",
|
||||
"onboarding_welcome_user": "Vítej, {user}",
|
||||
"online": "Online",
|
||||
"only_favorites": "Pouze oblíbené",
|
||||
@@ -1355,6 +1372,8 @@
|
||||
"permanently_delete_assets_prompt": "Opravdu chcete trvale smazat {count, plural, one {tuto položku} few {tyto <b>#</b> položky} other {těchto <b>#</b> položek}}? Tím {count, plural, one {ji také odstraníte z jejích} other {je také odstraníte z jejich}} alb.",
|
||||
"permanently_deleted_asset": "Položka trvale odstraněna",
|
||||
"permanently_deleted_assets_count": "{count, plural, one {Položka trvale vymazána} other {Položky trvale vymazány}}",
|
||||
"permission": "Oprávnění",
|
||||
"permission_empty": "Vaše oprávnění by nemělo být prázdné",
|
||||
"permission_onboarding_back": "Zpět",
|
||||
"permission_onboarding_continue_anyway": "Přesto pokračovat",
|
||||
"permission_onboarding_get_started": "Začít",
|
||||
@@ -1364,7 +1383,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_request": "Immich potřebuje přístup k zobrazení vašich fotek a videí.",
|
||||
"person": "Osoba",
|
||||
"person_birthdate": "Narozen/a {date}",
|
||||
"person_birthdate": "Narozen(a) {date}",
|
||||
"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.",
|
||||
"photos": "Fotky",
|
||||
@@ -1375,6 +1394,7 @@
|
||||
"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",
|
||||
"places": "Místa",
|
||||
"places_count": "{count, plural, one {{count, number} místo} few {{count, number} místa} other {{count, number} míst}}",
|
||||
@@ -1382,6 +1402,7 @@
|
||||
"play_memories": "Přehrát vzpomníky",
|
||||
"play_motion_photo": "Přehrát pohybovou fotografii",
|
||||
"play_or_pause_video": "Přehrát nebo pozastavit video",
|
||||
"please_auth_to_access": "Pro přístup se prosím ověřte",
|
||||
"port": "Port",
|
||||
"preferences_settings_subtitle": "Správa předvoleb aplikace",
|
||||
"preferences_settings_title": "Předvolby",
|
||||
@@ -1389,7 +1410,10 @@
|
||||
"preview": "Náhled",
|
||||
"previous": "Předchozí",
|
||||
"previous_memory": "Předchozí vzpomínka",
|
||||
"previous_or_next_photo": "Předchozí nebo další fotka",
|
||||
"previous_or_next_day": "Následující/předchozí den",
|
||||
"previous_or_next_month": "Následující/předchozí měsíc",
|
||||
"previous_or_next_photo": "Následující/předchozí fotografie",
|
||||
"previous_or_next_year": "Následující/předchozí rok",
|
||||
"primary": "Primární",
|
||||
"privacy": "Soukromí",
|
||||
"profile": "Profil",
|
||||
@@ -1472,9 +1496,12 @@
|
||||
"remove_deleted_assets": "Odstranit offline soubory",
|
||||
"remove_from_album": "Odstranit z alba",
|
||||
"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_memory": "Odstranit vzpomínku",
|
||||
"remove_photo_from_memory": "Odstranit fotografii z této vzpomínky",
|
||||
"remove_tag": "Odstranit značku",
|
||||
"remove_url": "Odstranit URL",
|
||||
"remove_user": "Odebrat uživatele",
|
||||
"removed_api_key": "Odstraněn API klíč: {name}",
|
||||
@@ -1581,6 +1608,7 @@
|
||||
"select_album_cover": "Vybrat obal alba",
|
||||
"select_all": "Vybrat vše",
|
||||
"select_all_duplicates": "Vybrat všechny duplicity",
|
||||
"select_all_in": "Vybrat vše ve skupině {group}",
|
||||
"select_avatar_color": "Vyberte barvu avatara",
|
||||
"select_face": "Vybrat obličej",
|
||||
"select_featured_photo": "Vybrat hlavní fotografii",
|
||||
@@ -1601,6 +1629,7 @@
|
||||
"server_info_box_server_url": "URL serveru",
|
||||
"server_offline": "Server offline",
|
||||
"server_online": "Server online",
|
||||
"server_privacy": "Ochrana soukromí serveru",
|
||||
"server_stats": "Statistiky serveru",
|
||||
"server_version": "Verze serveru",
|
||||
"set": "Nastavit",
|
||||
@@ -1610,6 +1639,7 @@
|
||||
"set_date_of_birth": "Nastavit datum narození",
|
||||
"set_profile_picture": "Nastavit profilový obrázek",
|
||||
"set_slideshow_to_fullscreen": "Nastavit prezentaci na celou obrazovku",
|
||||
"set_stack_primary_asset": "Nastavit jako hlavní položku",
|
||||
"setting_image_viewer_help": "V prohlížeči detailů se nejprve načte malá miniatura, poté se načte náhled střední velikosti (je-li povolen) a nakonec se načte originál (je-li povolen).",
|
||||
"setting_image_viewer_original_subtitle": "Umožňuje načíst původní obrázek v plném rozlišení (velký!). Zakažte pro snížení využití dat (v síti i v mezipaměti zařízení).",
|
||||
"setting_image_viewer_original_title": "Načíst původní obrázek",
|
||||
@@ -1618,13 +1648,12 @@
|
||||
"setting_image_viewer_title": "Obrázky",
|
||||
"setting_languages_apply": "Použít",
|
||||
"setting_languages_subtitle": "Změna jazyka aplikace",
|
||||
"setting_languages_title": "Jazyk",
|
||||
"setting_notifications_notify_failures_grace_period": "Oznámení o selhání zálohování na pozadí: {duration}",
|
||||
"setting_notifications_notify_hours": "{count, plural, one {# hodina} few {# hodiny} other {# hodin}}",
|
||||
"setting_notifications_notify_hours": "{count} hodin",
|
||||
"setting_notifications_notify_immediately": "okamžitě",
|
||||
"setting_notifications_notify_minutes": "{count, plural, one {# minuta} few {# minuty} other {# minut}}",
|
||||
"setting_notifications_notify_minutes": "{count} minut",
|
||||
"setting_notifications_notify_never": "nikdy",
|
||||
"setting_notifications_notify_seconds": "{count, plural, one {# sekunda} few {# sekundy} other {# sekund}}",
|
||||
"setting_notifications_notify_seconds": "{count} sekund",
|
||||
"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_subtitle": "Přizpůsobení předvoleb oznámení",
|
||||
@@ -1641,6 +1670,7 @@
|
||||
"share_add_photos": "Přidat fotografie",
|
||||
"share_assets_selected": "{count} vybráno",
|
||||
"share_dialog_preparing": "Připravuji...",
|
||||
"share_link": "Sdílet odkaz",
|
||||
"shared": "Sdílené",
|
||||
"shared_album_activities_input_disable": "Komentář je vypnutý",
|
||||
"shared_album_activity_remove_content": "Chcete odstranit tuto aktivitu?",
|
||||
@@ -1660,25 +1690,25 @@
|
||||
"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_expire_after_option_day": "1 den",
|
||||
"shared_link_edit_expire_after_option_days": "{count, plural, one {# den} few {# dny} other {# dní}}",
|
||||
"shared_link_edit_expire_after_option_days": "{count} dní",
|
||||
"shared_link_edit_expire_after_option_hour": "1 hodina",
|
||||
"shared_link_edit_expire_after_option_hours": "{count, plural, one {# hodina} few {# hodiny} other {# hodin}}",
|
||||
"shared_link_edit_expire_after_option_hours": "{count} hodin",
|
||||
"shared_link_edit_expire_after_option_minute": "1 minuta",
|
||||
"shared_link_edit_expire_after_option_minutes": "{count, plural, one {# minuta} few {# minuty} other {# minut}}",
|
||||
"shared_link_edit_expire_after_option_months": "{count, plural, one {# měsíc} few {# měsíce} other {# měsíců}}",
|
||||
"shared_link_edit_expire_after_option_year": "{count, plural, one {# rok} few {# roky} other {# let}}",
|
||||
"shared_link_edit_expire_after_option_minutes": "{count} minut",
|
||||
"shared_link_edit_expire_after_option_months": "{count} měsíce",
|
||||
"shared_link_edit_expire_after_option_year": "{count} rok",
|
||||
"shared_link_edit_password_hint": "Zadejte heslo pro sdílení",
|
||||
"shared_link_edit_submit_button": "Aktualizovat odkaz",
|
||||
"shared_link_error_server_url_fetch": "Nelze načíst url serveru",
|
||||
"shared_link_expires_day": "Vyprší za {count, plural, one {# den} few {# dny} other {# dní}}",
|
||||
"shared_link_expires_days": "Vyprší za {count, plural, one {# den} few {# dny} other {# dní}}",
|
||||
"shared_link_expires_hour": "Vyprší za {count, plural, one {# hodina} few {# hodiny} other {# hodin}}",
|
||||
"shared_link_expires_hours": "Vyprší za {count, plural, one {# hodina} few {# hodiny} other {# hodin}}",
|
||||
"shared_link_expires_minute": "Vyprší za {count, plural, one {# minuta} few {# minuty} other {# minut}}",
|
||||
"shared_link_expires_minutes": "Vyprší za {count, plural, one {# minuta} few {# minuty} other {# minut}}",
|
||||
"shared_link_expires_day": "Vyprší za {count} den",
|
||||
"shared_link_expires_days": "Vyprší za {count} dní",
|
||||
"shared_link_expires_hour": "Vyprší za {count} hodinu",
|
||||
"shared_link_expires_hours": "Vyprší za {count} hodin",
|
||||
"shared_link_expires_minute": "Vyprší za {count} minutu",
|
||||
"shared_link_expires_minutes": "Vyprší za {count} minut",
|
||||
"shared_link_expires_never": "Platnost ∞",
|
||||
"shared_link_expires_second": "Vyprší za {count, plural, one {# sekunda} few {# sekundy} other {# sekund}}",
|
||||
"shared_link_expires_seconds": "Vyprší za {count, plural, one {# sekunda} few {# sekundy} other {# sekund}}",
|
||||
"shared_link_expires_second": "Vyprší za {count} sekundu",
|
||||
"shared_link_expires_seconds": "Vyprší za {count} sekund",
|
||||
"shared_link_individual_shared": "Individuální sdílení",
|
||||
"shared_link_info_chip_metadata": "EXIF",
|
||||
"shared_link_manage_links": "Spravovat sdílené odkazy",
|
||||
@@ -1747,6 +1777,7 @@
|
||||
"start_date": "Počáteční datum",
|
||||
"state": "Stát",
|
||||
"status": "Stav",
|
||||
"stop_casting": "Zastavit odesílání",
|
||||
"stop_motion_photo": "Zastavit pohyblivou fotografii",
|
||||
"stop_photo_sharing": "Přestat sdílet své fotografie?",
|
||||
"stop_photo_sharing_description": "{partner} již nebude mít přístup k vašim fotkám.",
|
||||
@@ -1804,7 +1835,6 @@
|
||||
"to_parent": "Přejít k rodiči",
|
||||
"to_trash": "Vyhodit",
|
||||
"toggle_settings": "Přepnout nastavení",
|
||||
"toggle_theme": "Přepnout tmavý motiv",
|
||||
"total": "Celkem",
|
||||
"total_usage": "Celkové využití",
|
||||
"trash": "Koš",
|
||||
@@ -1815,7 +1845,7 @@
|
||||
"trash_no_results_message": "Zde se zobrazí odstraněné fotky a videa.",
|
||||
"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_info": "Vyhozené položky budou trvale smazány po {count, plural, one {# dni} other {# dnech}}",
|
||||
"trash_page_info": "Vyhozené položky budou trvale smazány po {days} dnech",
|
||||
"trash_page_no_assets": "Žádné vyhozené položky",
|
||||
"trash_page_restore_all": "Obnovit všechny",
|
||||
"trash_page_select_assets_btn": "Vybrat položky",
|
||||
@@ -1826,6 +1856,7 @@
|
||||
"unable_to_setup_pin_code": "Nelze nastavit PIN kód",
|
||||
"unarchive": "Odarchivovat",
|
||||
"unarchived_count": "{count, plural, one {Odarchivována #} few {Odarchivovány #} other {Odarchivováno #}}",
|
||||
"undo": "Vrátit zpět",
|
||||
"unfavorite": "Zrušit oblíbení",
|
||||
"unhide_person": "Zrušit skrytí osoby",
|
||||
"unknown": "Neznámý",
|
||||
@@ -1842,10 +1873,9 @@
|
||||
"unsaved_change": "Neuložená změna",
|
||||
"unselect_all": "Zrušit výběr všech",
|
||||
"unselect_all_duplicates": "Zrušit výběr všech duplicit",
|
||||
"unselect_all_in": "Zrušit výběr ve skupině {group}",
|
||||
"unstack": "Zrušit seskupení",
|
||||
"unstacked_assets_count": "{count, plural, one {Rozložená # položka} few {Rozložené # položky} other {Rozložených # položiek}}",
|
||||
"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ě",
|
||||
"up_next": "To je prozatím vše",
|
||||
"updated_at": "Aktualizováno",
|
||||
"updated_password": "Heslo aktualizováno",
|
||||
@@ -1864,6 +1894,7 @@
|
||||
"uploading": "Nahrávání",
|
||||
"url": "URL",
|
||||
"usage": "Využití",
|
||||
"use_biometric": "Použít biometrické údaje",
|
||||
"use_current_connection": "použít aktuální připojení",
|
||||
"use_custom_date_range": "Použít vlastní rozsah dat",
|
||||
"user": "Uživatel",
|
||||
@@ -1872,6 +1903,7 @@
|
||||
"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_privacy": "Ochrana soukromí uživatelů",
|
||||
"user_purchase_settings": "Nákup",
|
||||
"user_purchase_settings_description": "Správa vašeho nákupu",
|
||||
"user_role_set": "Uživatel {user} nastaven jako {role}",
|
||||
@@ -1887,11 +1919,6 @@
|
||||
"version": "Verze",
|
||||
"version_announcement_closing": "Váš přítel Alex",
|
||||
"version_announcement_message": "Ahoj! K dispozici je nová verze aplikace Immich. Věnujte prosím chvíli přečtení <link>poznámek k vydání</link> a ujistěte se, že je vaše nastavení aktuální, abyste předešli případným chybným konfiguracím, zejména pokud používáte WatchTower nebo jiný mechanismus, který se stará o automatickou aktualizaci instance aplikace Immich.",
|
||||
"version_announcement_overlay_release_notes": "poznámky k vydání",
|
||||
"version_announcement_overlay_text_1": "Ahoj, k dispozici je nová verze",
|
||||
"version_announcement_overlay_text_2": "najděte si čas na návštěvu ",
|
||||
"version_announcement_overlay_text_3": " a ujistěte se, že vaše konfigurace docker-compose a .env je aktuální, abyste předešli nesprávné konfiguraci, zvláště pokud používáte WatchTower nebo jakýkoli mechanismus, který podporuje automatické aktualizace serverových aplikací.",
|
||||
"version_announcement_overlay_title": "K dispozici je nová verze serveru 🎉",
|
||||
"version_history": "Historie verzí",
|
||||
"version_history_item": "Nainstalováno {version} dne {date}",
|
||||
"video": "Video",
|
||||
@@ -1911,6 +1938,7 @@
|
||||
"view_previous_asset": "Zobrazit předchozí položku",
|
||||
"view_qr_code": "Zobrazit QR kód",
|
||||
"view_stack": "Zobrazit seskupení",
|
||||
"view_user": "Zobrazit uživatele",
|
||||
"viewer_remove_from_stack": "Odstranit ze zásobníku",
|
||||
"viewer_stack_use_as_main_asset": "Použít jako hlavní položku",
|
||||
"viewer_unstack": "Rozbalit zásobník",
|
||||
@@ -1921,6 +1949,7 @@
|
||||
"welcome": "Vítejte",
|
||||
"welcome_to_immich": "Vítejte v Immichi",
|
||||
"wifi_name": "Název Wi-Fi",
|
||||
"wrong_pin_code": "Chybný PIN kód",
|
||||
"year": "Rok",
|
||||
"years_ago": "Před {years, plural, one {rokem} other {# lety}}",
|
||||
"yes": "Ano",
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
"asset_offline_description": "Библиотекӑн ҫак тулаш файлне дискра урӑх тупайман, карҫинккана куҫарнӑ. Енчен те файла вулавӑш ӑшне куҫарнӑ пулсан, тивӗҫлӗ ҫӗнӗ ресурс тупас тесен хӑвӑрӑн вӑхӑтлӑх шкалӑна тӗрӗслӗр. Ҫак файла ҫӗнӗрен чӗртес тесен файл патне каймалли ҫула Immich валли аяларах ҫитернине курса ӗненӗр, библиотекӑна сканерланине пурнӑҫлӑр.",
|
||||
"authentication_settings_disable_all": "Эсир кӗмелли пур меслетсене те чарса лартасшӑн тесе шутлатӑр-и? Кӗмелли шӑтӑка пӗтӗмпех уҫаҫҫӗ.",
|
||||
"background_task_job": "Курăнман ӗҫсем",
|
||||
"check_all": "Пурне те тӗрӗслӗр",
|
||||
"cleared_jobs": "Ӗҫсене тасатнӑ:{job}",
|
||||
"confirm_email_below": "Ҫирӗплетес тесен, аяларах «{email}» кӗртӗр",
|
||||
"confirm_reprocess_all_faces": "Пӗтӗм сӑнӗсене тепӗр хут палӑртас килет тесе шанатӑр-и? Ҫавӑн пекех ятсене пур ҫынран та хуратӗҫ.",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user