Compare commits

...

246 Commits

Author SHA1 Message Date
mertalev
930961825e queue assets without detected faces 2025-05-14 20:36:32 -04:00
mertalev
fdc8f91b18 revert image size change 2025-05-13 23:45:59 -04:00
mertalev
016a760dda use original image for ml 2025-05-13 23:41:57 -04:00
mertalev
c15507baad remove nesting 2025-05-13 13:20:41 -04:00
mertalev
1691706666 avoid always printing "vector reindexing complete" 2025-05-13 12:56:03 -04:00
mertalev
a96026c821 tighten range 2025-05-13 12:48:38 -04:00
Mert
5740928843 Update docs/docs/administration/postgres-standalone.md
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
2025-05-13 12:26:56 -04:00
mertalev
6126ac77b5 update docker compose files 2025-05-12 20:57:09 -04:00
mertalev
8c166b9381 outdated message 2025-05-12 20:57:09 -04:00
mertalev
d656cc2198 redundant switch 2025-05-12 20:57:09 -04:00
mertalev
32f25580ec revert refreshfaces sql change 2025-05-12 20:57:09 -04:00
mertalev
34f72a8251 maybe fix sql generation 2025-05-12 20:57:09 -04:00
mertalev
e851884f88 handle different db name 2025-05-12 20:57:09 -04:00
mertalev
db2493d003 preexisiting pg docs 2025-05-12 20:57:09 -04:00
mertalev
595f4c6d2e simplify dummy 2025-05-12 20:57:09 -04:00
mertalev
36481d037f accurate dummy vector 2025-05-12 20:57:09 -04:00
mertalev
217f6fe4fa fix new instance 2025-05-12 20:57:09 -04:00
mertalev
e90f28985a cascade 2025-05-12 20:57:09 -04:00
mertalev
0c9890b70f update image for sql checker
update images for gha
2025-05-12 20:57:09 -04:00
mertalev
b750440f90 set probes 2025-05-12 20:57:09 -04:00
mertalev
c80b16d24e wip
auto-detect available extensions

auto-recovery, fix reindexing check

use original image for ml
2025-05-12 20:57:08 -04:00
Jason Rasmussen
81d959a27e refactor: remove unused props (#18240) 2025-05-12 22:31:37 +00:00
Jason Rasmussen
bb775110ef refactor: password reset success modal (#18239) 2025-05-12 18:18:13 -04:00
Jason Rasmussen
7280331b76 refactor: confirm modal (#18238) 2025-05-12 22:02:49 +00:00
Jason Rasmussen
93ee6ee0a5 refactor: dialog controller (#18235) 2025-05-12 17:48:05 -04:00
Daniel Dietzler
7544a678ec refactor: remove unnecessary bg-color attributes and move to ui lib vars (#18234) 2025-05-12 17:17:01 -04:00
Jason Rasmussen
3066c8198c feat(web): user detail page (#18230)
feat: user detail page
2025-05-12 16:50:26 -04:00
Jason Rasmussen
eb8dfa283e fix(web): no rounded map on /map page (#18232) 2025-05-12 14:15:15 -04:00
Daniel Dietzler
41a127e2ab refactor: avatar selector modal (#18228) 2025-05-12 10:56:36 -04:00
Daniel Dietzler
feb475561e fix: missing translation in pin settings (#18203) 2025-05-10 15:27:42 -04:00
Alex
4c4c67f0d2 chore(web): color tuning (#18193) 2025-05-10 20:55:06 +02:00
Daimolean
381b66bf70 fix(web): IconButton size in user restore (#18194) 2025-05-10 07:28:37 -05:00
renovate[bot]
a89f3ad97c fix(deps): update typescript-projects (#18133)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-10 13:07:20 +02:00
Jason Rasmussen
c473511133 feat(web): stat card tweaks (#18189)
feat: stat card tweaks
2025-05-09 17:56:41 -05:00
Ben McCann
0d66a6b51f chore(web): upgrade enhanced-img (#18186) 2025-05-09 16:05:07 -05:00
Jason Rasmussen
66400b2e8e fix(web): user restore (#18188) 2025-05-09 21:05:01 +00:00
Alex
87cdf0ebd9 chore: use correct font on buy button (#18187) 2025-05-09 17:04:03 -04:00
Alex
3f719bd8d7 feat: user pin-code (#18138)
* feat: user pincode

* pr feedback

* chore: cleanup

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2025-05-09 17:00:58 -04:00
Daniel Dietzler
55af925ab3 refactor: shared link url (#18185) 2025-05-09 15:23:00 -05:00
Alex
ff63b0fa8f docs: face lift, botox x3 (#18184)
* chore: docs face lift

* logo and fonts

* docs: face lift, botox x3

* docs: face lift, botox x3
2025-05-09 13:27:21 -05:00
Daniel Dietzler
f21fe8716c refactor: shortcuts modal (#18175) 2025-05-09 13:24:36 -04:00
Daniel Dietzler
6a69dafd31 refactor: share modals (#18183) 2025-05-09 16:59:29 +00:00
Daniel Dietzler
47b1938f17 fix: search filter modal close (#18180) 2025-05-09 10:10:10 -05:00
Martin Schmidt
2ffcfe06f3 fix: properly work with languages with multiple scripts (#18167)
Co-authored-by: Ewe Zu Lin <zlewe1997@gmail.com>
2025-05-09 10:09:24 -05:00
Daniel Dietzler
89551edee5 fix: z-index war in the asset viewer (#18091) 2025-05-09 10:17:26 -04:00
Zack Pollard
cb6c541ae1 fix: constraint migration to handle any existing pkey name (#18178) 2025-05-09 13:45:44 +00:00
luzpaz
b1e1362246 fix: various typos (grouped in to separate commits) (#18177) 2025-05-09 13:10:34 +00:00
Alex
ccc2b191dd fix: notification text's color (#18170) 2025-05-08 19:07:12 -04:00
Alex
bb7010b2bb chore: rounded map corner when needed (#18163) 2025-05-09 00:49:16 +02:00
Daniel Dietzler
8db666bc38 refactor: search filter modal (#18159) 2025-05-08 15:36:05 -05:00
Daimolean
eace0f716d fix(web): add photos to album (#18166) 2025-05-08 20:24:51 +00:00
bo0tzz
96743b6c33 fix: properly set cache key suffix in image build (#18169) 2025-05-08 15:24:29 -05:00
bo0tzz
ff181cf346 fix: always set cache-key-base during image build (#18168) 2025-05-08 15:14:33 -05:00
Daimolean
0cd5960007 fix(web): ui (#18160)
* fix(web): ui

* fix(web): ui

* lint

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-05-08 19:32:45 +00:00
Dan Pizappi
698592c1b0 chore: update truenas install guide (#18142)
* Update truenas.md

* Update truenas.md

fix link

* Update truenas.md

* Update docs/docs/install/truenas.md

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-05-08 13:51:04 +00:00
Robert Vollmer
f75d853e9a fix(mobile): Remote video playback and asset download on Android with mTLS (#16403)
* Add class to apply SSL options

* Apply client certificate for native Android code

* Refactor self-signed check

* Allow self-signed certificates

* Fix Dart analysis

* Add HostnameVerifier

Android explicitly does NOT check the Common Name of a certificate,
only the Subject Alt Names. Chances are that someone who self-signs a
certificate doesn't go through the extra steps to add a SAN, and in
that case the connection would be prevented by the HostnameVerifier
even thought the TrustManager was fine with the certificate itself.

* Rename parameter like in Dart

* Fix NPE

* Catch all native errors in HttpSSLOptionsPlugin

* Workaround for too early onChanged() callback

* Fix formatting

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-05-08 13:45:11 +00:00
Alex
3a1e3e82e7 fix: notification text's color (#18151) 2025-05-08 12:15:11 +02:00
bo0tzz
0beb3ac4c1 feat: extract multi-arch image building to shared logic (#17877) 2025-05-08 12:00:05 +02:00
Daniel Dietzler
894545aeed refactor: modal manager types (#18150) 2025-05-07 22:08:19 +00:00
Daniel Dietzler
5250269fa4 refactor: user page modals (#18147) 2025-05-07 17:58:46 -04:00
Daniel Dietzler
a169fb6a79 refactor: map (#18143) 2025-05-07 17:39:50 -04:00
Daniel Dietzler
09ced9a171 refactor: help modal (#18145) 2025-05-07 17:31:38 -04:00
Jason Rasmussen
a6e5e4f625 fix: schema ci checks (#18146) 2025-05-07 21:14:20 +00:00
Daniel Dietzler
bbd8de177b refactor: side bar modals (#18134) 2025-05-07 09:01:51 -05:00
bo0tzz
867f6e64f9 chore: run all e2e tests on github runners (#17987)
* chore: run all e2e tests on github runners

* fix: use it.each for multi-case test
2025-05-07 01:42:48 -04:00
SGT
ec6379b0b2 chore: remove usage of deprecated Kysely method (#18127)
* minor update. fix usage of deprecated method'

* restore original formatting
2025-05-06 17:01:02 -04:00
Mert
2a80251dc3 fix(server): more robust person thumbnail generation (#17974)
* more robust person thumbnail generation

* clamp bounding boxes

* update sql

* no need to process invalid images after decoding

* cursed knowledge

* new line
2025-05-06 14:18:22 -04:00
Alex
d33ce13561 feat(server): visibility column (#17939)
* feat: private view

* pr feedback

* sql generation

* feat: visibility column

* fix: set visibility value as the same as the still part after unlinked live photos

* fix: test

* pr feedback
2025-05-06 12:12:48 -05:00
Nicholas Flamy
016d7a6ceb fix(docs): remove old patch versions from version switcher (#18130) 2025-05-06 17:53:17 +01:00
renovate[bot]
8ff25a4f7a fix(deps): update dependency @react-email/components to ^0.0.37 (#18126)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-06 18:23:45 +02:00
renovate[bot]
61a3eba1bd fix(deps): update machine-learning (#18118) 2025-05-06 15:27:34 +00:00
David Cruz
7072e48cbe feat: Add DB_SSL_MODE environment variable for Postgres sslmode (#18025)
* feat: Add DB_SSL_MODE environment variable for Postgres sslmode

* chore: clean up

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2025-05-06 09:25:37 -04:00
shenlong
ece977d9ca fix(mobile): empty translation placeholders (#18063)
* fix: empty placeholders

* fix: use namedArgs

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-05-06 09:21:05 -04:00
Jason Rasmussen
2af8095880 fix(web): e2e download tests (#18125) 2025-05-06 15:07:04 +02:00
renovate[bot]
30822fcd10 fix(deps): update typescript-projects (#18124)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-06 14:50:22 +02:00
Alex
c578273e7a chore: modal shenanigan (#18116) 2025-05-06 08:47:58 -04:00
Jovan Gerodetti
118a3fc9db fix: update assets when duplicateId is provided as null (#18071)
Update assets when duplicateId is provided as null
2025-05-06 08:47:19 -04:00
Daniel Dietzler
1138f6dcce refactor: job create modal (#18106)
* refactor: job create modal

* chore: better modal manager types (#18107)
2025-05-06 08:44:44 -04:00
renovate[bot]
33f3751b72 chore(deps): update github-actions (#18114)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-06 08:39:14 -04:00
renovate[bot]
b8509e6411 chore(deps): update docker.io/valkey/valkey:8-bookworm docker digest to 4a9f847 (#18113)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-06 08:35:24 -04:00
renovate[bot]
bd43edbcd7 chore(deps): update prom/prometheus docker digest to e2b8aa6 (#18117)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-06 08:35:01 -04:00
Matthew Momjian
c8b4a7e1f1 fix(docs): update nginx reverse proxy (#18104)
update nginx reverse proxy
2025-05-05 21:09:42 -05:00
Jason Rasmussen
f34f83e164 refactor: controller tests (#18100) 2025-05-05 18:57:32 -04:00
Alex
df2cf5d106 refactor: use UI library variable for table (#18105) 2025-05-05 22:39:52 +00:00
Daniel Dietzler
52975eadb3 refactor: all user admin page modals (#18097) 2025-05-05 23:54:42 +02:00
Sergey Katsubo
12610e4a9f fix(server): handle orientation of imported face regions (#18021)
* Transform imported face RegionInfo according to Exif Orientation

* Add unit tests for re-orienting metadata face regions

* Make code DRY using ImmichTagsWithFaces instead of NonNullable

* Add e2e test for importing metadata face regions when orientation is RotateCW90

* Disable new e2e test until its asset is added to the test-assets project

* Simplify unit tests by using the same face tag definition

* Combine similar e2e tests

* Disable new e2e test until portrait-orientation-6.jpg is added to test-assets

* Fix lint error: Expected property shorthand

* Update test-assets ref to latest

* Enable new e2e test after updating test-assets
2025-05-05 11:11:21 -04:00
renovate[bot]
2b3efa02d8 chore(deps): update dependency vite to v6.3.4 [security] (#18003)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 07:26:32 -07:00
Peter Denham
a21a997f21 fix: documentation - synology install docker link (#18084)
* fix docker link

* fix docker link

---------

Co-authored-by: Peter Denham <peter@denham>
2025-05-05 08:08:11 -05:00
David
7d61ed7ce4 feat(web): Map in albums & shared albums (#17906)
* add btn, map and marker

* Fix bug in navigation assetviewer

* Correct bug on main Viewer

* Add to user album the map of his pictures

* change icon to outline

* lint & format

* with manager instead of variable

* remove duplicate

* chore: minor styling change

* formatting

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-05-05 02:58:44 +00:00
Daniel Dietzler
8f7baf8336 chore: add language requests from weblate (#18050) 2025-05-04 21:04:53 +02:00
Weblate (bot)
44923acfd6 chore(web): update translations (#17817)
Co-authored-by: Ali Afzal <ali.afzalt20@gmail.com>
Co-authored-by: Andreas Johansen <andreas@josern.com>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: Bonov <bonov@mail.ru>
Co-authored-by: CanbiZ <mickey.leskowitz@gmail.com>
Co-authored-by: Conrad <conrad@grosser.group>
Co-authored-by: Daniel A <aquino.daniel1994@ikmail.com>
Co-authored-by: Denis Pacquier <denis.pacquier@gmail.com>
Co-authored-by: Diomed <diomed@tuta.io>
Co-authored-by: Dragonslayer <chybzik@gmail.com>
Co-authored-by: Felipe Garcia <garcia.o.felipe@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: HanYuan <lion70332@gmail.com>
Co-authored-by: Indrek Haav <indrek.haav@hotmail.com>
Co-authored-by: Javier Villanueva García <jvg2203@gmail.com>
Co-authored-by: Jesús Jiménez <jesjimenez@gmail.com>
Co-authored-by: John Kapelakos <johnkapelakos5@gmail.com>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Leo Bottaro <github@leobottaro.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Luna <me@devkit.dk>
Co-authored-by: Malhelo <weblate@malhelo.de>
Co-authored-by: Marco Vockner <marco.vockner@outlook.com>
Co-authored-by: Matjaž T <matjaz@moj-svet.si>
Co-authored-by: Matthew Momjian <mmomjian@gmail.com>
Co-authored-by: Micash <micash_545@protonmail.com>
Co-authored-by: Mārtiņš Bruņenieks <martinsb@gmail.com>
Co-authored-by: NoopyD <antish85@gmail.com>
Co-authored-by: Olaf Nielsen <solluh@mail.de>
Co-authored-by: PixelAxolotl <catmeowmeow009@gmail.com>
Co-authored-by: Raul <raul.plesa@gmail.com>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Stan P <g97d6liib@mozmail.com>
Co-authored-by: Stanislav <stanislavnastasiu0@gmail.com>
Co-authored-by: Sylvain Pichon <service@spichon.fr>
Co-authored-by: Taiki M <vexingly-many-mace@duck.com>
Co-authored-by: Tobias Calcetin <arbelos@gmail.com>
Co-authored-by: Tomi Pöyskö <tomi.poysko@gmail.com>
Co-authored-by: User 123456789 <user123456789@users.noreply.hosted.weblate.org>
Co-authored-by: User 123456789 <w0g-1es-5qq@cld3.com>
Co-authored-by: Vinyas N S <vinyasns@gmail.com>
Co-authored-by: Väino Daum <vainodaum@gmail.com>
Co-authored-by: Zack Pollard <zack@futo.org>
Co-authored-by: Zvonimir <zzrakic@protonmail.com>
Co-authored-by: chamdim <chamdim@protonmail.com>
Co-authored-by: dvbthien <dvbthien@users.noreply.hosted.weblate.org>
Co-authored-by: eav5jhl0 <eav5jhl0@users.noreply.hosted.weblate.org>
Co-authored-by: fmis13 <76878393+fmis13@users.noreply.github.com>
Co-authored-by: fmis136696a34093be41a0 <miskovicfrano2@gmail.com>
Co-authored-by: godzinilla <godzinilla@gmail.com>
Co-authored-by: jojo <e80f8c6f-ccb0-423e-9526-614163e44d51@anonaddy.me>
Co-authored-by: jonas-bonas <frage.zeichen@posteo.at>
Co-authored-by: labolstad <lasse.bolstad@gmail.com>
Co-authored-by: lsy223622 <lsy223622@outlook.com>
Co-authored-by: millallo <millallo@tiscali.it>
Co-authored-by: stephane Carrié <spcc70@gmail.com>
Co-authored-by: tct123 <tct1234@protonmail.com>
Co-authored-by: vzvl <lojewski.vitus@gmail.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
2025-05-04 20:47:46 +02:00
Matthew Momjian
ab95881ebb fix(mobile): Share page URL (#17834)
* Update share_intent.page.dart

* Update share_intent.page.dart

* unused stores

* remove unused duplicate function
2025-05-04 08:58:45 -05:00
Alex
8801ae5821 fix(web): text dim in darkmode (#18072) 2025-05-04 08:30:21 -04:00
Jason Rasmussen
ea9f11bf39 refactor: controller tests (#18035)
* feat: controller unit tests

* refactor: controller tests
2025-05-03 09:39:44 -04:00
Daniel Dietzler
62fc5b3c7d refactor: introduce modal manager (#18039) 2025-05-02 18:41:42 -04:00
Daniel Dietzler
15d431ba6a refactor: dialog callbacks (#18034) 2025-05-02 13:34:53 -04:00
Jason Rasmussen
5d21ba3166 chore: logging clean up (#18031) 2025-05-02 12:34:35 -05:00
Thomas
da7a81b752 chore(server): split album update notifications into multiple jobs (#17879)
We would like to move away from the concept of finding and removing pending
jobs. The only place this is used is for album update notifications, and this
is done so that users who initially uploaded assets to an album will also
receive a notification if someone else then adds assets to the same album. This
can also be achieved with a job for each recipient. Multiple jobs also has the
advantage that it will scale better for albums with many users, it's possible
to send notifications concurrently, retries are possible without sending
duplicate notifications, and it's clear what recipient a job failed for.
2025-04-30 17:45:35 -04:00
Jason Rasmussen
becdc3dcf5 refactor: job on-done (#18004) 2025-04-30 17:02:53 -04:00
Eli Gao
84b51e3cbb fix(server): double rotation on HEIF files (#18002)
* fix(server): double rotation on HEIF/HEIC files

* Update server/src/services/media.service.ts

* formatting

---------

Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com>
2025-04-30 20:33:18 +00:00
Jason Rasmussen
b845184c80 chore: remove old memory lane implementation (#18000) 2025-04-30 14:23:32 -04:00
Jason Rasmussen
1fde02ee1e chore: remove unused types and code (#17999) 2025-04-30 13:41:23 -04:00
Jason Rasmussen
526c02297c refactor: stream queue migration (#17997) 2025-04-30 16:23:13 +00:00
Alex
732b06eec8 refactor: stream for sidecar (#17995)
* refactor: stream for sidecar

* chore: make sql

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2025-04-30 10:53:51 -05:00
Daniel Dietzler
436cff72b5 refactor: activity manager (#17943) 2025-04-30 15:50:38 +00:00
Jason Rasmussen
be5cc2cdf5 refactor: stream detect faces (#17996) 2025-04-30 11:25:30 -04:00
Jason Rasmussen
094a41ac9a chore: remove audit file report (#17994) 2025-04-30 11:17:23 -04:00
Daniel Dietzler
ebad6a008f fix: add missing translations to face editor (#17993) 2025-04-30 10:07:21 -05:00
Jason Rasmussen
0c261ffbe2 fix: queue in batches (#17989) 2025-04-30 10:52:51 -04:00
Jason Rasmussen
6df6103c67 chore: better immich-web logging (#17992) 2025-04-30 09:48:24 -05:00
Jason Rasmussen
8c5116bc1d refactor: stream search duplicates (#17991) 2025-04-30 10:40:32 -04:00
bo0tzz
e3812a0e36 chore: also run e2e tests on arm64 (#17822)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-30 14:22:10 +02:00
Min Idzelis
4b1ced439b feat: improve/refactor focus handling (#17796)
* feat: improve focus

* test

* lint

* use modulus in loop
2025-04-30 00:19:38 -04:00
Jason Rasmussen
2e8a286540 refactor: smart search queue (#17977) 2025-04-29 17:44:28 -04:00
Jason Rasmussen
038a82c4f1 refactor: theme manager (#17976) 2025-04-29 17:44:09 -04:00
renovate[bot]
2c2dd01bf0 fix(deps): update machine-learning (#17951) 2025-04-29 20:02:58 +00:00
Ben
ac73e163df chore(mobile): translate toast messages (#17964) 2025-04-29 14:26:41 -05:00
Jason Rasmussen
d89e88bb3f feat: configure token endpoint auth method (#17968) 2025-04-29 15:17:48 -04:00
Thomas
3ce353393a chore(server): don't insert embeddings if the model has changed (#17885)
* chore(server): don't insert embeddings if the model has changed

We're moving away from the heuristic of waiting for queues to complete. The job
which inserts embeddings can simply check if the model has changed before
inserting, rather than attempting to lock the queue.

* more robust dim size update

* use check constraint

* index command cleanup

* add create statement

* update medium test, create appropriate extension

* new line

* set dimension size when running on all assets

* why does it want braces smh

* take 2

---------

Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
2025-04-29 14:23:01 -04:00
Min Idzelis
0e4cf9ac57 feat(web): responsive date group header height (#17944)
* feat: responsive date group header height

* update tests

* feat(web): improve perf when changing mobile orientation (#17945)

fix: improve perf when changing mobile orientation
2025-04-29 13:59:06 -04:00
Min Idzelis
07290580a6 feat: improve semantic nav/main tags (#17800)
feat: nav/main elements

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-04-29 13:51:39 -04:00
AverageHelper
d9ce74b896 chore: add security.txt (#17952)
* feat: Create .well-known/security.txt

* feat: Add another security.txt for the main website

* fix: deploy hidden files

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2025-04-29 13:48:06 -04:00
Jason Rasmussen
4c0f79b162 fix: use lint:p in checkall script (#17969) 2025-04-29 17:34:36 +00:00
renovate[bot]
9851d24628 chore(deps): update docker.io/valkey/valkey:8-bookworm docker digest to c855f98 (#17948)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 12:08:50 +01:00
renovate[bot]
fe6cbd93b1 chore(deps): pin dependencies (#17947)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 12:08:40 +01:00
renovate[bot]
df20788088 chore(deps): update grafana/grafana docker tag to v11.6.1 (#17955)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 12:08:08 +01:00
renovate[bot]
3d042cc7f1 fix(deps): update typescript-projects (#17961)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 13:00:37 +02:00
renovate[bot]
85446c5862 chore(deps): update redis:6.2-alpine docker digest to 3211c33 (#17950)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 10:09:25 +00:00
renovate[bot]
fb52ac0f5b chore(deps): update node.js to v22.15.0 (#17956)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 12:08:32 +02:00
Eli Gao
48bcbee6ed feat(server): JXL previews from DNG 1.7+ (#17861)
* feat(server): JXL previews from RAW

* refactor(server): use var name assumedExtractedFormat for clarity

* test(server): fix existing media.extract() returning JPEG

* chore(openapi): regen

* style(server): lint

* fix(server): ignore undefined decode orientation

* fix(server): correct orientation assignment in media decode options

* test(server): unit tests of JXL-encoded DNG

* refactor(server): return buffer and format from mediaRepository.extract()

* chore(open-api): regen

* refactor

---------

Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
2025-04-28 18:18:46 -04:00
Daniel Dietzler
f621f8ef2c refactor: more job queries (#17745) 2025-04-29 00:03:20 +02:00
Jason Rasmussen
7f69abbf0d refactor: app init event (#17937) 2025-04-28 14:48:33 -04:00
Jason Rasmussen
895b2bf5cd refactor: download manager (#17935) 2025-04-28 14:21:24 -04:00
Jason Rasmussen
f64e6f5dc3 refactor: auth login event (#17934) 2025-04-28 14:13:14 -04:00
Luke Towers
64e738f79d feat(web): move duplicates controls above preview of duplicate images (#17837)
Move duplicates controls above preview of duplicate images
2025-04-28 16:10:40 +00:00
Daniel Dietzler
a17390a422 refactor: move managers to new folder (#17929) 2025-04-28 16:56:04 +02:00
Jason Rasmussen
1b5fc9c665 feat: notifications (#17701)
* feat: notifications

* UI works

* chore: pr feedback

* initial fetch and clear notification upon logging out

* fix: merge

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-04-28 10:36:14 -04:00
Yaros
23717ce981 feat(mobile): save grid size on gesture resize (#17891) 2025-04-28 09:23:33 -05:00
Min Idzelis
2fd05e8447 feat: preload and cancel images with a service worker (#16893)
* feat: Service Worker to preload/cancel images and other resources

* Remove caddy configuration, localhost is secure if port-forwarded

* fix e2e tests

* Cache/return the app.html for all web entry points

* Only handle preload/cancel

* fix e2e

* fix e2e

* e2e-2

* that'll do it

* format

* fix test

* lint

* refactor common code to conditionals

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-04-28 14:23:05 +00:00
Min Idzelis
c664d99a34 refactor: vscode - format/organize on save (#17928) 2025-04-28 10:11:19 -04:00
Andreas Tollkötter
21c7d70336 feat(mobile): Capitalize month names in asset grid (#17898)
* capitalize month titles

* capitalize day titles as well
2025-04-28 13:56:36 +00:00
Jason Rasmussen
ad272333db refactor: user avatar color (#17753) 2025-04-28 08:54:51 -05:00
Zack Pollard
460d594791 feat: api response compression (#17878) 2025-04-28 08:54:11 -05:00
Jason Rasmussen
e6c575c33e feat: rtl (#17860) 2025-04-28 08:53:53 -05:00
Andreas Tollkötter
85ac0512a6 fix(web): Make date-time formatting follow locale (#17899)
* fixed missing $locale parameter to .toLocaleString

* Remove unused types and functions in timeline-util

* remove unused export

* re-enable export because it is needed for tests

* format
2025-04-28 08:53:26 -05:00
Alex
205260d31c chore: post release tasks (#17895) 2025-04-27 23:02:03 -05:00
Alex
3858973be5 chore(mobile): translation (#17920) 2025-04-27 23:00:40 -05:00
github-actions
02994883fe chore: version v1.132.3 2025-04-25 19:44:05 +00:00
Alex
a1f8150c30 fix: Authelia OAuth code verifier value contains invalid characters (#17886)
* fix(mobile): Authelia OAuth code verifier value contains invalid characters

* Refactor

* Refactoring with Jason

* Refactoring with Jason
2025-04-25 19:39:14 +00:00
Yaros
d85ef19bfc fix(mobile): revert get location on app start (#17882) 2025-04-25 10:38:30 -05:00
Jason Rasmussen
d0014bdf94 refactor: event manager (#17862)
* refactor: event manager

* refactor: event manager
2025-04-25 08:36:31 -04:00
Martin Mikita
e822e3eca9 docs: update MapTiler name (#17863) 2025-04-25 08:57:44 +00:00
Alex
644defa4a1 chore: post release tasks (#17867) 2025-04-25 04:14:40 +00:00
Matthew Momjian
1fe3c7b9b3 fix(docs): priorities (Capitalization) (#17866)
priorities
2025-04-25 04:07:42 +00:00
github-actions
0d60be3d87 chore: version v1.132.2 2025-04-25 03:07:06 +00:00
Alex
765da7b182 fix(mobile): mobile migration logic (#17865)
* fix(mobile): mobile migration logic

* add exception

* remove unused comment

* finalize
2025-04-25 00:16:54 +00:00
shenlong
b037158028 fix(mobile): auto trash using MANAGE_MEDIA (#17828)
fix: auto trash using MANAGE_MEDIA

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-04-24 19:09:50 -05:00
Daimolean
a03902f174 fix(docs): incorrect date sorting (#17858) 2025-04-24 19:40:52 -04:00
Jason Rasmussen
1d610ad9cb refactor: database connection parsing (#17852) 2025-04-24 12:58:29 -04:00
Min Idzelis
dab4870fed fix: flappy e2e test (#17832)
* fix: flappy e2e test

* lint
2025-04-23 23:30:13 -04:00
github-actions
37f5e6e2cb chore: version v1.132.1 2025-04-23 21:43:47 +00:00
Alex
57d622bc43 chore: post release tasks (#17816) 2025-04-23 16:41:08 -05:00
Alex
c167e46ec7 chore: revert #16732 (#17819)
* chore: revert #16732

* lint
2025-04-23 16:40:59 -05:00
Mert
6ce8a1deeb fix(server): bump sharp (#17818)
* bump sharp

* test linking

* link in prod image too

* force global

* keep unnecessary libraries

* override sharp version

* revert dockerfile changes

* add node-gyp and napi

* dev dependency
2025-04-23 17:08:29 -04:00
github-actions
f659ef4b7a chore: version v1.132.0 2025-04-23 16:44:47 +00:00
Zack Pollard
bb6cdc99ab ci: correct permissions for building mobile during release flow (#17814) 2025-04-23 11:38:43 -05:00
Weblate (bot)
830b4dadcb chore(web): update translations (#17808)
Co-authored-by: Aleksander Vae Haaland <aleksander@vaehaaland.no>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: Bonov <bonov@mail.ru>
Co-authored-by: Bruno López Barcia <brunolopar46@gmail.com>
Co-authored-by: Chris Axell <chris.axell@gmail.com>
Co-authored-by: Dymitr <zasvab@gmail.com>
Co-authored-by: Florian Ostertag <florian.kuepper@gmail.com>
Co-authored-by: GiannosOB <giannos2105@gmail.com>
Co-authored-by: Happy <happygamernintendoswitch@gmail.com>
Co-authored-by: Hurricane-32 <rodrigorimo@hotmail.com>
Co-authored-by: Indrek Haav <indrek.haav@hotmail.com>
Co-authored-by: Jane <asetmalik@gmail.com>
Co-authored-by: Javier Villanueva García <jvg2203@gmail.com>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Karl Solgård <karl.f91@gmail.com>
Co-authored-by: Leo Bottaro <github@leobottaro.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: MannyLama <Manfred@lama.be>
Co-authored-by: Matjaž T <matjaz@moj-svet.si>
Co-authored-by: Miki Mrvos <medolino2009@gmail.com>
Co-authored-by: RWDai <869759838@qq.com>
Co-authored-by: Roi Gabay <roigby@gmail.com>
Co-authored-by: Runskrift <anders@rimfrost.nu>
Co-authored-by: Sebastian <sebastiankiwidk@gmail.com>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Sidewave Tech <tech@sidewave.it>
Co-authored-by: Sylvain Pichon <service@spichon.fr>
Co-authored-by: Temuri Doghonadze <temuri.doghonadze@gmail.com>
Co-authored-by: Xo <xocodokie@users.noreply.hosted.weblate.org>
Co-authored-by: Zvonimir <zzrakic@protonmail.com>
Co-authored-by: adri1m64 <adrien.melle@laposte.net>
Co-authored-by: catelixor <catelixor+weblate@proton.me>
Co-authored-by: eav5jhl0 <eav5jhl0@users.noreply.hosted.weblate.org>
Co-authored-by: kiwinho <kiwicaja@gmail.com>
Co-authored-by: millallo <millallo@tiscali.it>
Co-authored-by: pyccl <changcongliang@163.com>
Co-authored-by: stanciupaul <stanciupaul90@yahoo.com>
Co-authored-by: thehijacker <thehijacker@gmail.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: xuars <yago.rana.gayoso@gmail.com>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
Co-authored-by: 灯笼 <gh_denglong@163.com>
2025-04-23 17:26:58 +01:00
Zack Pollard
d2f2f8d672 fix: retrieve version from lockfile and fallback to cli command (#17812) 2025-04-23 17:10:43 +01:00
Alex
be1062474b chore: memory spacing (#17813)
chore(web): memory spacing
2025-04-23 16:02:49 +00:00
bo0tzz
64000d9d76 feat: static analysis job for gha workflows (#17688)
* fix: set persist-credentials explicitly for checkout

https://woodruffw.github.io/zizmor/audits/#artipacked

* fix: minimize permissions scope for workflows

https://woodruffw.github.io/zizmor/audits/#excessive-permissions

* fix: remove potential template injections

https://woodruffw.github.io/zizmor/audits/#template-injection

* fix: only pass needed secrets in workflow_call

https://woodruffw.github.io/zizmor/audits/#secrets-inherit

* fix: push perm for single-arch build jobs

I hadn't realised these push to the registry too :x

* chore: fix formatting

* fix: $

* fix: retag job quoting

* feat: static analysis job for gha workflows

* chore: fix formatting

* fix: clear last zizmor checks

* fix: broken merge

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-23 15:49:06 +00:00
Toni
59fa8fbd0e perf(mobile): remove small thumbnail and cache generated thumbnails (#17792)
* Remove small thumbnail and cache generated thumbnails

* Creating the small thumbnails takes quite some time, which should not be underestimated.
* The time needed to generate the small or big thumbnail is not too different from each other. Therefore there is no real benefit of the small thumbnail and it only adds frustration to the end user experience. That is because the image appeared to have loaded (the visual move from blur to something better) but it's still so bad that it is basically a blur. The better solution is therefore to stay at the blur until the actual thumbnail has loaded.
* Additionaly to the faster generation of the thumbnail, it now also gets cached similarly to the remote thumbnail which already gets cached. This further speeds up the all over usage of the app and prevents a repeatet thumbnail generation when opening the app.
* Decreased the quality from the default 95 to 80 to provide similar quality with much reduces thumbnail size.
* Use try catch around the read of the cache file.
* Use the key provided in the loadImage method instead of the asset of the constructor.

* Use userId instead of ownerId

* Remove import

* Add checksum to thumbnail cache key
2025-04-23 10:31:35 -05:00
Zack Pollard
19746a8685 fix: cache build versions (#17811) 2025-04-23 16:31:18 +01:00
Thomas
987e5ab76c fix(server): start job workers after DB (#17806)
Job workers are currently started on app init, which means they are started
before the DB is initialised. This can be problematic if jobs which need to use
the DB start running before it's ready. It also means that swapping out the
queue implementation for something which uses the DB won't work.
2025-04-23 15:07:32 +00:00
Jason Rasmussen
1b5e981a45 fix: failing ci checks (#17810) 2025-04-23 10:59:54 -04:00
Tin Pecirep
b7a0cf2470 feat: add oauth2 code verifier
* fix: ensure oauth state param matches before finishing oauth flow

Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com>

* chore: upgrade openid-client to v6

Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com>

* feat: use PKCE for oauth2 on supported clients

Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com>

* feat: use state and PKCE in mobile app

Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com>

* fix: remove obsolete oauth repository init

Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com>

* fix: rewrite callback url if mobile redirect url is enabled

Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com>

* fix: propagate oidc client error cause when oauth callback fails

Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com>

* fix: adapt auth service tests to required state and PKCE params

Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com>

* fix: update sdk types

Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com>

* fix: adapt oauth e2e test to work with PKCE

Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com>

* fix: allow insecure (http) oauth clients

Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com>

---------

Signed-off-by: Tin Pecirep <tin.pecirep@gmail.com>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
2025-04-23 15:08:11 +01:00
Alex
13d6bd67b1 feat: no small local thumbnail (#17787)
* feat: no small local thumbnail

* pr feedback
2025-04-23 14:02:51 +00:00
Toni
1de2eae12d perf(mobile): remove load of thumbnails in the image provider (#17773)
Remove loading of thumbnail in the image provider

* Removed the load of the thumbnail from the local and remote image provider as they shall provide the image, not the thumbnail. The thumbnail gets provided by the thumbnail provider.
* The thumbnail provider is used as the loadingBuilder and the image provider as the imageProvider. Therefore loading the thumbnail in the image provider loads it a second time which is completely redundant, uses precious time and yields no results.

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-04-23 13:55:51 +00:00
Zack Pollard
bc5875ba8d chore: multithreaded web linting (#17809) 2025-04-23 13:05:31 +01:00
renovate[bot]
0426b574fe fix(deps): update typescript-projects (#17625)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Zack Pollard <zackpollard@ymail.com>
2025-04-23 11:45:38 +00:00
renovate[bot]
2c3658e642 fix(deps): update machine-learning (#17769) 2025-04-23 07:44:30 -04:00
renovate[bot]
a493dab294 chore(deps): update github-actions (#17766)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-23 11:41:51 +00:00
Matthew Momjian
699fdd0d1b fix(mobile): recently added -> taken (#17780) 2025-04-23 12:38:25 +01:00
Weblate (bot)
a774153f67 chore(web): update translations (#17627)
Co-authored-by: Aleksander Vae Haaland <aleksander@vaehaaland.no>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: Bonov <bonov@mail.ru>
Co-authored-by: Bruno López Barcia <brunolopar46@gmail.com>
Co-authored-by: Chris Axell <chris.axell@gmail.com>
Co-authored-by: Dymitr <zasvab@gmail.com>
Co-authored-by: Florian Ostertag <florian.kuepper@gmail.com>
Co-authored-by: GiannosOB <giannos2105@gmail.com>
Co-authored-by: Happy <happygamernintendoswitch@gmail.com>
Co-authored-by: Hurricane-32 <rodrigorimo@hotmail.com>
Co-authored-by: Indrek Haav <indrek.haav@hotmail.com>
Co-authored-by: Jane <asetmalik@gmail.com>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Leo Bottaro <github@leobottaro.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: MannyLama <Manfred@lama.be>
Co-authored-by: Matjaž T <matjaz@moj-svet.si>
Co-authored-by: Miki Mrvos <medolino2009@gmail.com>
Co-authored-by: RWDai <869759838@qq.com>
Co-authored-by: Roi Gabay <roigby@gmail.com>
Co-authored-by: Runskrift <anders@rimfrost.nu>
Co-authored-by: Sebastian <sebastiankiwidk@gmail.com>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Sidewave Tech <tech@sidewave.it>
Co-authored-by: Sylvain Pichon <service@spichon.fr>
Co-authored-by: Temuri Doghonadze <temuri.doghonadze@gmail.com>
Co-authored-by: Xo <xocodokie@users.noreply.hosted.weblate.org>
Co-authored-by: Zvonimir <zzrakic@protonmail.com>
Co-authored-by: adri1m64 <adrien.melle@laposte.net>
Co-authored-by: catelixor <catelixor+weblate@proton.me>
Co-authored-by: eav5jhl0 <eav5jhl0@users.noreply.hosted.weblate.org>
Co-authored-by: kiwinho <kiwicaja@gmail.com>
Co-authored-by: millallo <millallo@tiscali.it>
Co-authored-by: pyccl <changcongliang@163.com>
Co-authored-by: stanciupaul <stanciupaul90@yahoo.com>
Co-authored-by: thehijacker <thehijacker@gmail.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: xuars <yago.rana.gayoso@gmail.com>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
Co-authored-by: 灯笼 <gh_denglong@163.com>
2025-04-23 12:30:38 +01:00
Bastian Machek
ca12aff3a4 docs: updated community-projects.tsx: lrc-immich-plugin (#17801) 2025-04-23 12:11:42 +01:00
renovate[bot]
550c1c0a10 chore(deps): update prom/prometheus docker digest to 339ce86 (#17767)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-23 12:04:33 +01:00
Mert
92ac1193e6 fix(server): queue android motion assets for transcoding (#17781) 2025-04-23 12:03:28 +01:00
Min Idzelis
2a95eccf6a fix: vscode vitest ext - missing jsdom dev dependency (#17799) 2025-04-22 23:01:22 -04:00
Łukasz Wawrzyk
ee017803bf fix(mobile): use immutable cache keys for local images (#17794) 2025-04-23 02:32:03 +00:00
Alex
0986a71ce3 fix(mobile): revert cache fixes (#17786)
* Revert "fix(mobile): use immutable cache keys for local images (#17736)"

This reverts commit 010b144754.

* Revert "perf(mobile): remove small thumbnail and cache generated thumbnails (#17682)"

This reverts commit b71039e83c.
2025-04-22 12:15:54 -05:00
Alex
af36eaa61b fix(mobile): video player initialization (#17778)
* fix(mobile): video player initialization

* nit
2025-04-22 11:51:20 -04:00
Alex
fda68f972f fix(web): forceDark control app bar doesn't work (#17759) 2025-04-22 09:25:27 -04:00
renovate[bot]
a8eec92da7 chore(deps): update dependency @types/node to ^22.14.1 (#17770)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2025-04-22 10:18:44 +00:00
Alex
ad8511c978 feat(docs): APK download button (#17768) 2025-04-21 23:27:00 -05:00
Bonne Eggleston
fe8c5e8107 feat: add album start and end dates for storage template (#17188) 2025-04-21 19:54:33 -04:00
Yaros
c70140e707 fix(web): map marker positioning in details pane (#17754)
fix: map marker positioning in details pane
2025-04-21 13:01:38 -05:00
Łukasz Wawrzyk
010b144754 fix(mobile): use immutable cache keys for local images (#17736)
fix(mobile): usse immutable cache keys for local images
2025-04-21 13:00:46 -05:00
Toni
b71039e83c perf(mobile): remove small thumbnail and cache generated thumbnails (#17682)
* Remove small thumbnail and cache generated thumbnails

* Creating the small thumbnails takes quite some time, which should not be underestimated.
* The time needed to generate the small or big thumbnail is not too different from each other. Therefore there is no real benefit of the small thumbnail and it only adds frustration to the end user experience. That is because the image appeared to have loaded (the visual move from blur to something better) but it's still so bad that it is basically a blur. The better solution is therefore to stay at the blur until the actual thumbnail has loaded.
* Additionaly to the faster generation of the thumbnail, it now also gets cached similarly to the remote thumbnail which already gets cached. This further speeds up the all over usage of the app and prevents a repeatet thumbnail generation when opening the app.

* Decrease quality and use try catch

* Decreased the quality from the default 95 to 80 to provide similar quality with much reduces thumbnail size.
* Use try catch around the read of the cache file.

* Replace ImmutableBuffer.fromUint8List with ImmutableBuffer.fromFilePath

* Removed unnecessary comment

* Replace debugPrint with log.severe for catch of error

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-04-21 12:51:37 -05:00
Jason Rasmussen
56a4aa9ffe refactor: email repository (#17746) 2025-04-21 12:53:37 -04:00
Jason Rasmussen
488dc4efbd refactor: notification-admin controller (#17748) 2025-04-21 10:49:26 -04:00
Yaros
f0ff8581da feat(mobile): map improvements (#17714)
* fix: remove unnecessary db operations in map

* feat: use user's location for map thumbnails

* chore: refactored handleMapEvents

* fix: location fails fetching & update geolocator

* chore: minor refactor

* chore: small style tweak

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-04-21 05:55:13 +00:00
Yaros
c49fd2065b chore(mobile): bump ios deployment target (#17715)
* chore: bump ios deployment target

* podfile

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-04-21 05:18:25 +00:00
aviv926
21a6eb30ff feat(docs): documentation update (#17720)
Documentation update
2025-04-20 23:55:58 -05:00
Matthew Momjian
9e063c993c fix(docs): Database dump warnings (#17676)
* docs

* admin page

* roadmap

* whitespace

* whitespace

* no danger
2025-04-20 23:54:37 -05:00
Daniel Dietzler
dd1fcd5be5 chore: remove asset entity (#17703) 2025-04-18 21:39:56 +00:00
Daniel Dietzler
52ae06c119 refactor: remove album entity, update types (#17450) 2025-04-18 23:10:34 +02:00
Daniel Dietzler
854ea13d6a chore: simplify asset getByIds (#17699) 2025-04-18 16:52:41 -04:00
bo0tzz
504930947d fix: various actions workflow security improvements (#17651)
* fix: set persist-credentials explicitly for checkout

https://woodruffw.github.io/zizmor/audits/#artipacked

* fix: minimize permissions scope for workflows

https://woodruffw.github.io/zizmor/audits/#excessive-permissions

* fix: remove potential template injections

https://woodruffw.github.io/zizmor/audits/#template-injection

* fix: only pass needed secrets in workflow_call

https://woodruffw.github.io/zizmor/audits/#secrets-inherit

* fix: push perm for single-arch build jobs

I hadn't realised these push to the registry too :x

* chore: fix formatting

* fix: $

* fix: retag job quoting

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-18 15:10:27 -05:00
Alex
0e6ac87645 feat(mobile): assets + exif stream sync placeholder (#17677)
* feat(mobile): assets + exif stream sync placeholder

* feat(mobile): assets + exif stream sync placeholder

* refactor

* fix: test

* fix:test

* refactor(mobile): sync stream service (#17687)

* refactor: sync stream to use callbacks

* pr feedback

* pr feedback

* pr feedback

* fix: test

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>

---------

Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com>
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-04-18 19:01:16 +00:00
Yaros
bd2deda50c feat(mobile): search on places page (#17679)
* feat: search on places page

* chore: use searchfield on people page
2025-04-18 11:19:51 -05:00
Jason Rasmussen
160bb492a2 fix: skip initial kysely migration for existing installs (#17690)
* fix: skip initial kysely migration for existing installs

* Update docs/src/pages/errors.md

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-04-18 11:19:11 -05:00
Jason Rasmussen
6474a78b8b feat: initial kysely migration file (#17678) 2025-04-17 17:38:47 -04:00
Jason Rasmussen
e275f2d8b3 feat: add foreign key indexes (#17672) 2025-04-17 14:41:06 -04:00
shenlong
81ed54aa61 feat: user sync stream (#16862)
* refactor: user entity

* chore: rebase fixes

* refactor: remove int user Id

* refactor: migrate store userId from int to string

* refactor: rename uid to id

* feat: drift

* pr feedback

* refactor: move common overrides to mixin

* refactor: remove int user Id

* refactor: migrate store userId from int to string

* refactor: rename uid to id

* feat: user & partner sync stream

* pr changes

* refactor: sync service and add tests

* chore: remove generated change

* chore: move sync model

* rebase: convert string ids to byte uuids

* rebase

* add processing logs

* batch db calls

* rewrite isolate manager

* rewrite with worker_manager

* misc fixes

* add sync order test

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-04-17 10:25:27 -05:00
Daniel Dietzler
067338b0ed chore: remove transfer encoding header (#17671) 2025-04-17 09:46:52 -05:00
Min Idzelis
5e68f8c519 fix: longpress triggers contextmenu (#17602) 2025-04-16 19:24:26 -04:00
Mert
242a559e0f refactor: query for fetching faces and people of assets (#17661)
* use json instead of jsonb

* missing condition
2025-04-16 19:00:55 -04:00
Jonathan Jogenfors
ed2b54527c chore(server): don't check null dates (#17664) 2025-04-16 18:40:08 -04:00
Daniel Dietzler
8b38f8a58d fix: do not select album in time bucket query (#17662) 2025-04-16 17:52:22 -04:00
yparitcher
29b30570bf fix: use IMMICH_HOST in microservices (#17659) 2025-04-16 23:05:12 +02:00
Daniel Dietzler
586a7a173b refactor: handle detect faces job query (#17660) 2025-04-16 22:52:54 +02:00
Daniel Dietzler
1bbfacfc09 refactor: more job query stuff (#17658) 2025-04-16 22:10:20 +02:00
Daniel Dietzler
85c2d36d99 refactor: dedicated get album thumbnail files query (#17657) 2025-04-16 21:10:27 +02:00
Jason Rasmussen
8cefa0b84b refactor: migrate some e2e to medium (#17640) 2025-04-16 14:59:08 -04:00
Daniel Dietzler
f50e5d006c refactor: dedicated queries for asset jobs (#17652) 2025-04-16 14:08:49 -04:00
renovate[bot]
8f8ff3adc0 fix(deps): update machine-learning (#17610) 2025-04-16 10:56:40 -04:00
Zack Pollard
c4c35ed140 fix(ci): images missing correct OCI annotations and PR cache (#17378)
Co-authored-by: secustor <sebastian@poxhofer.at>
2025-04-15 22:31:23 +01:00
Nils Uliczka
be2f670d35 fix: skip places that no longer exist in geo import (#17637) 2025-04-15 21:27:47 +00:00
Alex
7efcba2b12 chore(mobile): flutter 3.29.3 (#17638)
* chore(mobile): flutter 3.29.3

* chore(mobile): flutter 3.29.3

* upgrade background_downloader
2025-04-15 21:03:22 +00:00
Paul Puschmann
459c815086 feat(docs): Clarify the usage of immich-cli with Docker (#17595)
Add some explanation how to use the various usage parameters together
with the `immich-cli` in the container.
2025-04-15 20:08:55 +00:00
Alex
36fa61c013 fix(mobile): new loading icon too small (#17636) 2025-04-15 20:08:34 +00:00
Jason Rasmussen
8da5f21fcf refactor: medium tests (#17634) 2025-04-15 15:54:23 -04:00
Jonathan Jogenfors
76db8cf604 refactor(server): remove asset placeholder (#17621)
chore: remove AssetEntityPlaceholder

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2025-04-15 15:53:49 -04:00
Daniel Dietzler
21becbf1b0 refactor: dedicated query for asset migration job (#17631) 2025-04-15 15:49:15 -04:00
Min Idzelis
26f0ea4cb5 feat: responsive controlbar (#17601) 2025-04-15 14:39:30 -05:00
Alex
19e5a6f68f chore(doc): translation instruction for mobile app (#17629) 2025-04-15 14:31:13 -05:00
shenlong
78f8e23834 fix(mobile): exif not updated on sync (#17633)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-04-15 14:30:58 -05:00
Daniel Dietzler
5bceefce75 refactor: stream assets for thumbnail job (#17623) 2025-04-15 19:53:28 +02:00
Jason Rasmussen
b710ad36f3 feat: upgrade kysely (#17630)
* feat: upgrade kysely

* chore: pr feedback
2025-04-15 13:26:56 -04:00
Daniel Dietzler
270d178a2e fix: unsafe cast (#17590) 2025-04-15 11:35:00 -05:00
Daniel Dietzler
309528c807 chore: upgrade package locks (#17626) 2025-04-15 11:34:21 -05:00
Toni
7c422363fb chore(mobile): clear the backup detail view when no backup is in progress (#17619)
Clear the backup detail view when no backup is in progress

* When no backup is in progress, display a simple "-" for the details in the upload file info, instead of the data of the last uploaded asset.
* This prevents confusion if a upload job is stuck or just finished.
2025-04-15 11:30:24 -05:00
Weblate (bot)
3eb316abea chore(web): cleanup unused translations (#17624) 2025-04-15 17:24:29 +01:00
renovate[bot]
b3371e16f2 fix(deps): update typescript-projects (#17611)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-15 15:55:03 +00:00
Alex
b2c903c000 feat(mobile): use Weblate for i18n (2) (#17620)
* feat(mobile): use Weblate for i18n (2)

* remove old translation files

* dedup keys

* remove migration report

* chore

* remove localizely.yml
2025-04-15 15:54:26 +00:00
Jason Rasmussen
17e720440d refactor: new asset-job repository (#17622)
* refactor: new asset-job repository

* fix: broken medium tests on main
2025-04-15 10:24:51 -04:00
Alex
a522130122 feat(mobile): use Weblate for i18n (1) (#17609) 2025-04-15 08:30:01 -05:00
921 changed files with 59412 additions and 55714 deletions

118
.github/actions/image-build/action.yml vendored Normal file
View File

@@ -0,0 +1,118 @@
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

View File

@@ -7,6 +7,15 @@ on:
ref: ref:
required: false required: false
type: string type: string
secrets:
KEY_JKS:
required: true
ALIAS:
required: true
ANDROID_KEY_PASSWORD:
required: true
ANDROID_STORE_PASSWORD:
required: true
pull_request: pull_request:
push: push:
branches: [main] branches: [main]
@@ -15,14 +24,21 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions: {}
jobs: jobs:
pre-job: pre-job:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
outputs: outputs:
should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- id: found_paths - id: found_paths
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
with: with:
@@ -38,22 +54,17 @@ jobs:
build-sign-android: build-sign-android:
name: Build and sign Android name: Build and sign Android
needs: pre-job needs: pre-job
permissions:
contents: read
# Skip when PR from a fork # 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' }} 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: macos-14
steps: steps:
- name: Determine ref
id: get-ref
run: |
input_ref="${{ inputs.ref }}"
github_ref="${{ github.sha }}"
ref="${input_ref:-$github_ref}"
echo "ref=$ref" >> $GITHUB_OUTPUT
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
ref: ${{ steps.get-ref.outputs.ref }} ref: ${{ inputs.ref || github.sha }}
persist-credentials: false
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4
with: with:
@@ -78,6 +89,10 @@ jobs:
working-directory: ./mobile working-directory: ./mobile
run: flutter pub get run: flutter pub get
- name: Generate translation file
run: make translation
working-directory: ./mobile
- name: Build Android App Bundle - name: Build Android App Bundle
working-directory: ./mobile working-directory: ./mobile
env: env:

View File

@@ -8,31 +8,38 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions: {}
jobs: jobs:
cleanup: cleanup:
name: Cleanup name: Cleanup
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
actions: write
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: Cleanup - name: Cleanup
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REF: ${{ github.ref }}
run: | run: |
gh extension install actions/gh-actions-cache gh extension install actions/gh-actions-cache
REPO=${{ github.repository }} REPO=${{ github.repository }}
BRANCH=${{ github.ref }}
echo "Fetching list of cache keys" echo "Fetching list of cache keys"
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 ) cacheKeysForPR=$(gh actions-cache list -R $REPO -B ${REF} -L 100 | cut -f 1 )
## Setting this to not fail the workflow while deleting cache keys. ## Setting this to not fail the workflow while deleting cache keys.
set +e set +e
echo "Deleting caches..." echo "Deleting caches..."
for cacheKey in $cacheKeysForPR for cacheKey in $cacheKeysForPR
do do
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm gh actions-cache delete $cacheKey -R "$REPO" -B "${REF}" --confirm
done done
echo "Done" echo "Done"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -16,19 +16,23 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions: permissions: {}
packages: write
jobs: jobs:
publish: publish:
name: CLI Publish name: CLI Publish
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
defaults: defaults:
run: run:
working-directory: ./cli working-directory: ./cli
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
# Setup .npmrc file to publish to npm # Setup .npmrc file to publish to npm
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
@@ -48,11 +52,16 @@ jobs:
docker: docker:
name: Docker name: Docker
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
packages: write
needs: publish needs: publish
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
@@ -87,7 +96,7 @@ jobs:
type=raw,value=latest,enable=${{ github.event_name == 'release' }} type=raw,value=latest,enable=${{ github.event_name == 'release' }}
- name: Build and push image - name: Build and push image
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
with: with:
file: cli/Dockerfile file: cli/Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64

View File

@@ -24,6 +24,8 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions: {}
jobs: jobs:
analyze: analyze:
name: Analyze name: Analyze
@@ -43,10 +45,12 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@45775bd8235c68ba998cffa5171334d58593da47 # v3 uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -59,7 +63,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@45775bd8235c68ba998cffa5171334d58593da47 # v3 uses: github/codeql-action/autobuild@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -72,6 +76,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh # ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@45775bd8235c68ba998cffa5171334d58593da47 # v3 uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3
with: with:
category: '/language:${{matrix.language}}' category: '/language:${{matrix.language}}'

View File

@@ -12,18 +12,21 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions: permissions: {}
packages: write
jobs: jobs:
pre-job: pre-job:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
outputs: outputs:
should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }}
should_run_ml: ${{ steps.found_paths.outputs.machine-learning == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run_ml: ${{ steps.found_paths.outputs.machine-learning == 'true' || steps.should_force.outputs.should_force == 'true' }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- id: found_paths - id: found_paths
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
with: with:
@@ -37,6 +40,8 @@ jobs:
- 'machine-learning/**' - 'machine-learning/**'
workflow: workflow:
- '.github/workflows/docker.yml' - '.github/workflows/docker.yml'
- '.github/workflows/multi-runner-build.yml'
- '.github/actions/image-build'
- name: Check if we should force jobs to run - name: Check if we should force jobs to run
id: should_force id: should_force
@@ -45,6 +50,9 @@ jobs:
retag_ml: retag_ml:
name: Re-Tag ML name: Re-Tag ML
needs: pre-job needs: pre-job
permissions:
contents: read
packages: write
if: ${{ needs.pre-job.outputs.should_run_ml == 'false' && !github.event.pull_request.head.repo.fork }} if: ${{ needs.pre-job.outputs.should_run_ml == 'false' && !github.event.pull_request.head.repo.fork }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
@@ -58,18 +66,22 @@ jobs:
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Re-tag image - name: Re-tag image
env:
REGISTRY_NAME: 'ghcr.io'
REPOSITORY: ${{ github.repository_owner }}/immich-machine-learning
TAG_OLD: main${{ matrix.suffix }}
TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
run: | run: |
REGISTRY_NAME="ghcr.io" docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
REPOSITORY=${{ github.repository_owner }}/immich-machine-learning docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
TAG_OLD=main${{ matrix.suffix }}
TAG_PR=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
TAG_COMMIT=commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_PR $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_COMMIT $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
retag_server: retag_server:
name: Re-Tag Server name: Re-Tag Server
needs: pre-job needs: pre-job
permissions:
contents: read
packages: write
if: ${{ needs.pre-job.outputs.should_run_server == 'false' && !github.event.pull_request.head.repo.fork }} if: ${{ needs.pre-job.outputs.should_run_server == 'false' && !github.event.pull_request.head.repo.fork }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
@@ -83,370 +95,85 @@ jobs:
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Re-tag image - name: Re-tag image
env:
REGISTRY_NAME: 'ghcr.io'
REPOSITORY: ${{ github.repository_owner }}/immich-server
TAG_OLD: main${{ matrix.suffix }}
TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
run: | run: |
REGISTRY_NAME="ghcr.io" docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
REPOSITORY=${{ github.repository_owner }}/immich-server docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
TAG_OLD=main${{ matrix.suffix }}
TAG_PR=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
TAG_COMMIT=commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_PR $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_COMMIT $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
build_and_push_ml: machine-learning:
name: Build and Push ML name: Build and Push ML
needs: pre-job needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }} if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }}
runs-on: ${{ matrix.runner }}
env:
image: immich-machine-learning
context: machine-learning
file: machine-learning/Dockerfile
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-machine-learning
strategy: strategy:
# Prevent a failure in one image from stopping the other builds
fail-fast: false fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
device: cpu
- platform: linux/arm64
runner: ubuntu-24.04-arm
device: cpu
- platform: linux/amd64
runner: ubuntu-latest
device: cuda
suffix: -cuda
- platform: linux/amd64
runner: mich
device: rocm
suffix: -rocm
- platform: linux/amd64
runner: ubuntu-latest
device: openvino
suffix: -openvino
- platform: linux/arm64
runner: ubuntu-24.04-arm
device: armnn
suffix: -armnn
- platform: linux/arm64
runner: ubuntu-24.04-arm
device: rknn
suffix: -rknn
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate cache key suffix
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV
else
echo "CACHE_KEY_SUFFIX=$(echo ${{ github.ref_name }} | sed 's/[^a-zA-Z0-9]/-/g')" >> $GITHUB_ENV
fi
- name: Generate cache target
id: cache-target
run: |
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
# Essentially just ignore the cache output (forks can't write to registry cache)
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
else
echo "cache-to=type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }},mode=max,compression=zstd" >> $GITHUB_OUTPUT
fi
- name: Build and push image
id: build
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
with:
context: ${{ env.context }}
file: ${{ env.file }}
platforms: ${{ matrix.platforms }}
labels: ${{ steps.metadata.outputs.labels }}
cache-to: ${{ steps.cache-target.outputs.cache-to }}
cache-from: |
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }}
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-main
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }}
build-args: |
DEVICE=${{ matrix.device }}
BUILD_ID=${{ github.run_id }}
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
BUILD_SOURCE_REF=${{ github.ref_name }}
BUILD_SOURCE_COMMIT=${{ github.sha }}
- name: Export digest
run: |
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ml-digests-${{ matrix.device }}-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
merge_ml:
name: Merge & Push ML
runs-on: ubuntu-latest
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' && !github.event.pull_request.head.repo.fork }}
env:
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-machine-learning
DOCKER_REPO: altran1502/immich-machine-learning
strategy:
matrix: matrix:
include: include:
- device: cpu - device: cpu
tag-suffix: ''
- device: cuda - device: cuda
suffix: -cuda tag-suffix: '-cuda'
- device: rocm platforms: linux/amd64
suffix: -rocm
- device: openvino - device: openvino
suffix: -openvino tag-suffix: '-openvino'
platforms: linux/amd64
- device: armnn - device: armnn
suffix: -armnn tag-suffix: '-armnn'
platforms: linux/arm64
- device: rknn - device: rknn
suffix: -rknn tag-suffix: '-rknn'
needs: platforms: linux/arm64
- build_and_push_ml - device: rocm
steps: tag-suffix: '-rocm'
- name: Download digests platforms: linux/amd64
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4 runner-mapping: '{"linux/amd64": "mich"}'
with: uses: ./.github/workflows/multi-runner-build.yml
path: ${{ runner.temp }}/digests permissions:
pattern: ml-digests-${{ matrix.device }}-* contents: read
merge-multiple: true actions: read
packages: write
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
with:
image: immich-machine-learning
context: machine-learning
dockerfile: machine-learning/Dockerfile
platforms: ${{ matrix.platforms }}
runner-mapping: ${{ matrix.runner-mapping }}
tag-suffix: ${{ matrix.tag-suffix }}
dockerhub-push: ${{ github.event_name == 'release' }}
build-args: |
DEVICE=${{ matrix.device }}
- name: Login to Docker Hub server:
if: ${{ github.event_name == 'release' }}
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- name: Generate docker image tags
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
env:
DOCKER_METADATA_PR_HEAD_SHA: 'true'
with:
flavor: |
# Disable latest tag
latest=false
suffix=${{ matrix.suffix }}
images: |
name=${{ env.GHCR_REPO }}
name=${{ env.DOCKER_REPO }},enable=${{ github.event_name == 'release' }}
tags: |
# Tag with branch name
type=ref,event=branch
# Tag with pr-number
type=ref,event=pr
# Tag with long commit sha hash
type=sha,format=long,prefix=commit-
# Tag with git tag on release
type=ref,event=tag
type=raw,value=release,enable=${{ github.event_name == 'release' }}
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *)
build_and_push_server:
name: Build and Push Server name: Build and Push Server
runs-on: ${{ matrix.runner }}
needs: pre-job needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }} if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
env: uses: ./.github/workflows/multi-runner-build.yml
permissions:
contents: read
actions: read
packages: write
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
with:
image: immich-server image: immich-server
context: . context: .
file: server/Dockerfile dockerfile: server/Dockerfile
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server dockerhub-push: ${{ github.event_name == 'release' }}
strategy: build-args: |
fail-fast: false DEVICE=cpu
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate cache key suffix
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV
else
echo "CACHE_KEY_SUFFIX=$(echo ${{ github.ref_name }} | sed 's/[^a-zA-Z0-9]/-/g')" >> $GITHUB_ENV
fi
- name: Generate cache target
id: cache-target
run: |
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
# Essentially just ignore the cache output (forks can't write to registry cache)
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
else
echo "cache-to=type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }},mode=max,compression=zstd" >> $GITHUB_OUTPUT
fi
- name: Build and push image
id: build
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
with:
context: ${{ env.context }}
file: ${{ env.file }}
platforms: ${{ matrix.platform }}
labels: ${{ steps.metadata.outputs.labels }}
cache-to: ${{ steps.cache-target.outputs.cache-to }}
cache-from: |
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ env.CACHE_KEY_SUFFIX }}
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-main
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }}
build-args: |
DEVICE=cpu
BUILD_ID=${{ github.run_id }}
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
BUILD_SOURCE_REF=${{ github.ref_name }}
BUILD_SOURCE_COMMIT=${{ github.sha }}
- name: Export digest
run: |
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: server-digests-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
merge_server:
name: Merge & Push Server
runs-on: ubuntu-latest
if: ${{ needs.pre-job.outputs.should_run_server == 'true' && !github.event.pull_request.head.repo.fork }}
env:
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server
DOCKER_REPO: altran1502/immich-server
needs:
- build_and_push_server
steps:
- name: Download digests
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
with:
path: ${{ runner.temp }}/digests
pattern: server-digests-*
merge-multiple: true
- name: Login to Docker Hub
if: ${{ github.event_name == 'release' }}
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- name: Generate docker image tags
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
env:
DOCKER_METADATA_PR_HEAD_SHA: 'true'
with:
flavor: |
# Disable latest tag
latest=false
suffix=${{ matrix.suffix }}
images: |
name=${{ env.GHCR_REPO }}
name=${{ env.DOCKER_REPO }},enable=${{ github.event_name == 'release' }}
tags: |
# Tag with branch name
type=ref,event=branch
# Tag with pr-number
type=ref,event=pr
# Tag with long commit sha hash
type=sha,format=long,prefix=commit-
# Tag with git tag on release
type=ref,event=tag
type=raw,value=release,enable=${{ github.event_name == 'release' }}
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *)
success-check-server: success-check-server:
name: Docker Build & Push Server Success name: Docker Build & Push Server Success
needs: [merge_server, retag_server] needs: [server, retag_server]
permissions: {}
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: always() if: always()
steps: steps:
@@ -455,11 +182,13 @@ jobs:
run: exit 1 run: exit 1
- name: All jobs passed or skipped - name: All jobs passed or skipped
if: ${{ !(contains(needs.*.result, 'failure')) }} if: ${{ !(contains(needs.*.result, 'failure')) }}
# zizmor: ignore[template-injection]
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}" run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
success-check-ml: success-check-ml:
name: Docker Build & Push ML Success name: Docker Build & Push ML Success
needs: [merge_ml, retag_ml] needs: [machine-learning, retag_ml]
permissions: {}
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: always() if: always()
steps: steps:
@@ -468,4 +197,5 @@ jobs:
run: exit 1 run: exit 1
- name: All jobs passed or skipped - name: All jobs passed or skipped
if: ${{ !(contains(needs.*.result, 'failure')) }} if: ${{ !(contains(needs.*.result, 'failure')) }}
# zizmor: ignore[template-injection]
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}" run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"

View File

@@ -10,14 +10,20 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions: {}
jobs: jobs:
pre-job: pre-job:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
outputs: outputs:
should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.should_force.outputs.should_force == 'true' }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- id: found_paths - id: found_paths
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
with: with:
@@ -33,6 +39,8 @@ jobs:
build: build:
name: Docs Build name: Docs Build
needs: pre-job needs: pre-job
permissions:
contents: read
if: ${{ needs.pre-job.outputs.should_run == 'true' }} if: ${{ needs.pre-job.outputs.should_run == 'true' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults: defaults:
@@ -42,6 +50,8 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
@@ -62,4 +72,5 @@ jobs:
with: with:
name: docs-build-output name: docs-build-output
path: docs/build/ path: docs/build/
include-hidden-files: true
retention-days: 1 retention-days: 1

View File

@@ -1,6 +1,6 @@
name: Docs deploy name: Docs deploy
on: on:
workflow_run: workflow_run: # zizmor: ignore[dangerous-triggers] no attacker inputs are used here
workflows: ['Docs build'] workflows: ['Docs build']
types: types:
- completed - completed
@@ -9,6 +9,9 @@ jobs:
checks: checks:
name: Docs Deploy Checks name: Docs Deploy Checks
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
actions: read
pull-requests: read
outputs: outputs:
parameters: ${{ steps.parameters.outputs.result }} parameters: ${{ steps.parameters.outputs.result }}
artifact: ${{ steps.get-artifact.outputs.result }} artifact: ${{ steps.get-artifact.outputs.result }}
@@ -36,6 +39,8 @@ jobs:
- name: Determine deploy parameters - name: Determine deploy parameters
id: parameters id: parameters
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
env:
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
with: with:
script: | script: |
const eventType = context.payload.workflow_run.event; const eventType = context.payload.workflow_run.event;
@@ -57,7 +62,8 @@ jobs:
} else if (eventType == "pull_request") { } else if (eventType == "pull_request") {
let pull_number = context.payload.workflow_run.pull_requests[0]?.number; let pull_number = context.payload.workflow_run.pull_requests[0]?.number;
if(!pull_number) { if(!pull_number) {
const response = await github.rest.search.issuesAndPullRequests({q: 'repo:${{ github.repository }} is:pr sha:${{ github.event.workflow_run.head_sha }}',per_page: 1,}) const {HEAD_SHA} = process.env;
const response = await github.rest.search.issuesAndPullRequests({q: `repo:${{ github.repository }} is:pr sha:${HEAD_SHA}`,per_page: 1,})
const items = response.data.items const items = response.data.items
if (items.length < 1) { if (items.length < 1) {
throw new Error("No pull request found for the commit") throw new Error("No pull request found for the commit")
@@ -95,30 +101,36 @@ jobs:
name: Docs Deploy name: Docs Deploy
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: checks needs: checks
permissions:
contents: read
actions: read
pull-requests: write
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }} if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: Load parameters - name: Load parameters
id: parameters id: parameters
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
env:
PARAM_JSON: ${{ needs.checks.outputs.parameters }}
with: with:
script: | script: |
const json = `${{ needs.checks.outputs.parameters }}`; const parameters = JSON.parse(process.env.PARAM_JSON);
const parameters = JSON.parse(json);
core.setOutput("event", parameters.event); core.setOutput("event", parameters.event);
core.setOutput("name", parameters.name); core.setOutput("name", parameters.name);
core.setOutput("shouldDeploy", parameters.shouldDeploy); core.setOutput("shouldDeploy", parameters.shouldDeploy);
- run: |
echo "Starting docs deployment for ${{ steps.parameters.outputs.event }} ${{ steps.parameters.outputs.name }}"
- name: Download artifact - name: Download artifact
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
env:
ARTIFACT_JSON: ${{ needs.checks.outputs.artifact }}
with: with:
script: | script: |
let artifact = ${{ needs.checks.outputs.artifact }}; let artifact = JSON.parse(process.env.ARTIFACT_JSON);
let download = await github.rest.actions.downloadArtifact({ let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
@@ -162,9 +174,11 @@ jobs:
- name: Output Cleaning - name: Output Cleaning
id: clean id: clean
env:
TG_OUTPUT: ${{ steps.docs-output.outputs.tg_action_output }}
run: | run: |
TG_OUT=$(echo '${{ steps.docs-output.outputs.tg_action_output }}' | sed 's|%0A|\n|g ; s|%3C|<|g' | jq -c .) CLEANED=$(echo "$TG_OUTPUT" | sed 's|%0A|\n|g ; s|%3C|<|g' | jq -c .)
echo "output=$TG_OUT" >> $GITHUB_OUTPUT echo "output=$CLEANED" >> $GITHUB_OUTPUT
- name: Publish to Cloudflare Pages - name: Publish to Cloudflare Pages
uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca # v1 uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca # v1

View File

@@ -1,15 +1,22 @@
name: Docs destroy name: Docs destroy
on: on:
pull_request_target: pull_request_target: # zizmor: ignore[dangerous-triggers] no attacker inputs are used here
types: [closed] types: [closed]
permissions: {}
jobs: jobs:
deploy: deploy:
name: Docs Destroy name: Docs Destroy
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: Destroy Docs Subdomain - name: Destroy Docs Subdomain
env: env:

View File

@@ -4,16 +4,19 @@ on:
pull_request: pull_request:
types: [labeled] types: [labeled]
permissions: {}
jobs: jobs:
fix-formatting: fix-formatting:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event.label.name == 'fix:formatting' }} if: ${{ github.event.label.name == 'fix:formatting' }}
permissions: permissions:
contents: write
pull-requests: write pull-requests: write
steps: steps:
- name: Generate a token - name: Generate a token
id: generate-token id: generate-token
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2 uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@@ -23,6 +26,7 @@ jobs:
with: with:
ref: ${{ github.event.pull_request.head.ref }} ref: ${{ github.event.pull_request.head.ref }}
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}
persist-credentials: true
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4

185
.github/workflows/multi-runner-build.yml vendored Normal file
View File

@@ -0,0 +1,185 @@
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

View File

@@ -1,9 +1,11 @@
name: PR Label Validation name: PR Label Validation
on: on:
pull_request_target: pull_request_target: # zizmor: ignore[dangerous-triggers] no attacker inputs are used here
types: [opened, labeled, unlabeled, synchronize] types: [opened, labeled, unlabeled, synchronize]
permissions: {}
jobs: jobs:
validate-release-label: validate-release-label:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -1,6 +1,8 @@
name: 'Pull Request Labeler' name: 'Pull Request Labeler'
on: on:
- pull_request_target - pull_request_target # zizmor: ignore[dangerous-triggers] no attacker inputs are used here
permissions: {}
jobs: jobs:
labeler: labeler:

View File

@@ -4,9 +4,13 @@ on:
pull_request: pull_request:
types: [opened, synchronize, reopened, edited] types: [opened, synchronize, reopened, edited]
permissions: {}
jobs: jobs:
validate-pr-title: validate-pr-title:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
pull-requests: write
steps: steps:
- name: PR Conventional Commit Validation - name: PR Conventional Commit Validation
uses: ytanikin/PRConventionalCommits@b628c5a234cc32513014b7bfdd1e47b532124d98 # 1.3.0 uses: ytanikin/PRConventionalCommits@b628c5a234cc32513014b7bfdd1e47b532124d98 # 1.3.0

View File

@@ -21,17 +21,18 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-root group: ${{ github.workflow }}-${{ github.ref }}-root
cancel-in-progress: true cancel-in-progress: true
permissions: {}
jobs: jobs:
bump_version: bump_version:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
ref: ${{ steps.push-tag.outputs.commit_long_sha }} ref: ${{ steps.push-tag.outputs.commit_long_sha }}
permissions: {} # No job-level permissions are needed because it uses the app-token
steps: steps:
- name: Generate a token - name: Generate a token
id: generate-token id: generate-token
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2 uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@@ -40,12 +41,16 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}
persist-credentials: true
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5 uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
- name: Bump version - name: Bump version
run: misc/release/pump-version.sh -s "${{ inputs.serverBump }}" -m "${{ inputs.mobileBump }}" env:
SERVER_BUMP: ${{ inputs.serverBump }}
MOBILE_BUMP: ${{ inputs.mobileBump }}
run: misc/release/pump-version.sh -s "${SERVER_BUMP}" -m "${MOBILE_BUMP}"
- name: Commit and tag - name: Commit and tag
id: push-tag id: push-tag
@@ -59,18 +64,26 @@ jobs:
build_mobile: build_mobile:
uses: ./.github/workflows/build-mobile.yml uses: ./.github/workflows/build-mobile.yml
needs: bump_version needs: bump_version
secrets: inherit permissions:
contents: read
secrets:
KEY_JKS: ${{ secrets.KEY_JKS }}
ALIAS: ${{ secrets.ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
with: with:
ref: ${{ needs.bump_version.outputs.ref }} ref: ${{ needs.bump_version.outputs.ref }}
prepare_release: prepare_release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: build_mobile needs: build_mobile
permissions:
actions: read # To download the app artifact
# No content permissions are needed because it uses the app-token
steps: steps:
- name: Generate a token - name: Generate a token
id: generate-token id: generate-token
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2 uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@@ -79,17 +92,19 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}
persist-credentials: false
- name: Download APK - name: Download APK
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with: with:
name: release-apk-signed name: release-apk-signed
- name: Create draft release - name: Create draft release
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2 uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2
with: with:
draft: true draft: true
tag_name: ${{ env.IMMICH_VERSION }} tag_name: ${{ env.IMMICH_VERSION }}
token: ${{ steps.generate-token.outputs.token }}
generate_release_notes: true generate_release_notes: true
body_path: misc/release/notes.tmpl body_path: misc/release/notes.tmpl
files: | files: |

View File

@@ -4,6 +4,8 @@ on:
pull_request: pull_request:
types: [labeled, closed] types: [labeled, closed]
permissions: {}
jobs: jobs:
comment-status: comment-status:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -4,18 +4,22 @@ on:
release: release:
types: [published] types: [published]
permissions: permissions: {}
packages: write
jobs: jobs:
publish: publish:
name: Publish `@immich/sdk` name: Publish `@immich/sdk`
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
defaults: defaults:
run: run:
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
# Setup .npmrc file to publish to npm # Setup .npmrc file to publish to npm
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:

View File

@@ -9,14 +9,20 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions: {}
jobs: jobs:
pre-job: pre-job:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
outputs: outputs:
should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- id: found_paths - id: found_paths
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
with: with:
@@ -33,12 +39,14 @@ jobs:
name: Run Dart Code Analysis name: Run Dart Code Analysis
needs: pre-job needs: pre-job
if: ${{ needs.pre-job.outputs.should_run == 'true' }} if: ${{ needs.pre-job.outputs.should_run == 'true' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: Setup Flutter SDK - name: Setup Flutter SDK
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2 uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2
@@ -50,6 +58,10 @@ jobs:
run: dart pub get run: dart pub get
working-directory: ./mobile working-directory: ./mobile
- name: Generate translation file
run: make translation; dart format lib/generated/codegen_loader.g.dart
working-directory: ./mobile
- name: Run Build Runner - name: Run Build Runner
run: make build run: make build
working-directory: ./mobile working-directory: ./mobile
@@ -65,9 +77,11 @@ jobs:
- name: Verify files have not changed - name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true' if: steps.verify-changed-files.outputs.files_changed == 'true'
env:
CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }}
run: | run: |
echo "ERROR: Generated files not up to date! Run make_build inside the mobile directory" echo "ERROR: Generated files not up to date! Run make_build inside the mobile directory"
echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}" echo "Changed files: ${CHANGED_FILES}"
exit 1 exit 1
- name: Run dart analyze - name: Run dart analyze
@@ -81,3 +95,30 @@ jobs:
- name: Run dart custom_lint - name: Run dart custom_lint
run: dart run custom_lint run: dart run custom_lint
working-directory: ./mobile working-directory: ./mobile
zizmor:
name: zizmor
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
- name: Run zizmor 🌈
run: uvx zizmor --format=sarif . > results.sarif
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3
with:
sarif_file: results.sarif
category: zizmor

View File

@@ -9,9 +9,13 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions: {}
jobs: jobs:
pre-job: pre-job:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
outputs: outputs:
should_run_web: ${{ steps.found_paths.outputs.web == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run_web: ${{ steps.found_paths.outputs.web == 'true' || steps.should_force.outputs.should_force == 'true' }}
should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }}
@@ -25,6 +29,9 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- id: found_paths - id: found_paths
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
with: with:
@@ -58,6 +65,8 @@ jobs:
needs: pre-job needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }} if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
defaults: defaults:
run: run:
working-directory: ./server working-directory: ./server
@@ -65,6 +74,8 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
@@ -95,6 +106,8 @@ jobs:
needs: pre-job needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }} if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
defaults: defaults:
run: run:
working-directory: ./cli working-directory: ./cli
@@ -102,6 +115,8 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
@@ -136,6 +151,8 @@ jobs:
needs: pre-job needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }} if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }}
runs-on: windows-latest runs-on: windows-latest
permissions:
contents: read
defaults: defaults:
run: run:
working-directory: ./cli working-directory: ./cli
@@ -143,6 +160,8 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
@@ -165,11 +184,13 @@ jobs:
run: npm run test:cov run: npm run test:cov
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
web-unit-tests: web-lint:
name: Test & Lint Web name: Lint Web
needs: pre-job needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_web == 'true' }} if: ${{ needs.pre-job.outputs.should_run_web == 'true' }}
runs-on: ubuntu-latest runs-on: mich
permissions:
contents: read
defaults: defaults:
run: run:
working-directory: ./web working-directory: ./web
@@ -177,6 +198,8 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
@@ -191,7 +214,7 @@ jobs:
run: npm ci run: npm ci
- name: Run linter - name: Run linter
run: npm run lint run: npm run lint:p
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run formatter - name: Run formatter
@@ -202,6 +225,35 @@ jobs:
run: npm run check:svelte run: npm run check:svelte
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
web-unit-tests:
name: Test Web
needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_web == 'true' }}
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: ./web
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version-file: './web/.nvmrc'
- name: Run setup typescript-sdk
run: npm ci && npm run build
working-directory: ./open-api/typescript-sdk
- name: Run npm install
run: npm ci
- name: Run tsc - name: Run tsc
run: npm run check:typescript run: npm run check:typescript
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
@@ -215,6 +267,8 @@ jobs:
needs: pre-job needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_e2e == 'true' }} if: ${{ needs.pre-job.outputs.should_run_e2e == 'true' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
defaults: defaults:
run: run:
working-directory: ./e2e working-directory: ./e2e
@@ -222,6 +276,8 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
@@ -254,6 +310,8 @@ jobs:
needs: pre-job needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }} if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
defaults: defaults:
run: run:
working-directory: ./server working-directory: ./server
@@ -261,6 +319,8 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
@@ -278,15 +338,21 @@ jobs:
name: End-to-End Tests (Server & CLI) name: End-to-End Tests (Server & CLI)
needs: pre-job needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_e2e_server_cli == 'true' }} if: ${{ needs.pre-job.outputs.should_run_e2e_server_cli == 'true' }}
runs-on: mich runs-on: ${{ matrix.runner }}
permissions:
contents: read
defaults: defaults:
run: run:
working-directory: ./e2e working-directory: ./e2e
strategy:
matrix:
runner: [ubuntu-latest, ubuntu-24.04-arm]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false
submodules: 'recursive' submodules: 'recursive'
- name: Setup Node - name: Setup Node
@@ -320,15 +386,21 @@ jobs:
name: End-to-End Tests (Web) name: End-to-End Tests (Web)
needs: pre-job needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_e2e_web == 'true' }} if: ${{ needs.pre-job.outputs.should_run_e2e_web == 'true' }}
runs-on: mich runs-on: ${{ matrix.runner }}
permissions:
contents: read
defaults: defaults:
run: run:
working-directory: ./e2e working-directory: ./e2e
strategy:
matrix:
runner: [ubuntu-latest, ubuntu-24.04-arm]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false
submodules: 'recursive' submodules: 'recursive'
- name: Setup Node - name: Setup Node
@@ -357,13 +429,33 @@ jobs:
run: npx playwright test run: npx playwright test
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
success-check-e2e:
name: End-to-End Tests Success
needs: [e2e-tests-server-cli, e2e-tests-web]
permissions: {}
runs-on: ubuntu-latest
if: always()
steps:
- name: Any jobs failed?
if: ${{ contains(needs.*.result, 'failure') }}
run: exit 1
- name: All jobs passed or skipped
if: ${{ !(contains(needs.*.result, 'failure')) }}
# zizmor: ignore[template-injection]
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
mobile-unit-tests: mobile-unit-tests:
name: Unit Test Mobile name: Unit Test Mobile
needs: pre-job needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_mobile == 'true' }} if: ${{ needs.pre-job.outputs.should_run_mobile == 'true' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: Setup Flutter SDK - name: Setup Flutter SDK
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2 uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2
with: with:
@@ -378,14 +470,19 @@ jobs:
needs: pre-job needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }} if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
defaults: defaults:
run: run:
working-directory: ./machine-learning working-directory: ./machine-learning
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5 uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
# TODO: add caching when supported (https://github.com/actions/setup-python/pull/818) # TODO: add caching when supported (https://github.com/actions/setup-python/pull/818)
# with: # with:
# python-version: 3.11 # python-version: 3.11
@@ -411,6 +508,8 @@ jobs:
needs: pre-job needs: pre-job
if: ${{ needs.pre-job.outputs['should_run_.github'] == 'true' }} if: ${{ needs.pre-job.outputs['should_run_.github'] == 'true' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
defaults: defaults:
run: run:
working-directory: ./.github working-directory: ./.github
@@ -418,6 +517,8 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
@@ -434,22 +535,31 @@ jobs:
shellcheck: shellcheck:
name: ShellCheck name: ShellCheck
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: Run ShellCheck - name: Run ShellCheck
uses: ludeeus/action-shellcheck@master uses: ludeeus/action-shellcheck@master
with: with:
ignore_paths: >- ignore_paths: >-
**/open-api/** **/open-api/**
**/openapi/** **/openapi**
**/node_modules/** **/node_modules/**
generated-api-up-to-date: generated-api-up-to-date:
name: OpenAPI Clients name: OpenAPI Clients
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
@@ -476,17 +586,21 @@ jobs:
- name: Verify files have not changed - name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true' if: steps.verify-changed-files.outputs.files_changed == 'true'
env:
CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }}
run: | run: |
echo "ERROR: Generated files not up to date!" echo "ERROR: Generated files not up to date!"
echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}" echo "Changed files: ${CHANGED_FILES}"
exit 1 exit 1
generated-typeorm-migrations-up-to-date: sql-schema-up-to-date:
name: TypeORM Checks name: SQL Schema Checks
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
services: services:
postgres: postgres:
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52 image: tensorchord/vchord-postgres:pg14-v0.3.0
env: env:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres POSTGRES_USER: postgres
@@ -505,6 +619,8 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
@@ -521,11 +637,11 @@ jobs:
run: npm run migrations:run run: npm run migrations:run
- name: Test npm run schema:reset command works - name: Test npm run schema:reset command works
run: npm run typeorm:schema:reset run: npm run schema:reset
- name: Generate new migrations - name: Generate new migrations
continue-on-error: true continue-on-error: true
run: npm run migrations:generate TestMigration run: npm run migrations:generate src/TestMigration
- name: Find file changes - name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20 uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20
@@ -535,9 +651,11 @@ jobs:
server/src server/src
- name: Verify migration files have not changed - name: Verify migration files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true' if: steps.verify-changed-files.outputs.files_changed == 'true'
env:
CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }}
run: | run: |
echo "ERROR: Generated migration files not up to date!" echo "ERROR: Generated migration files not up to date!"
echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}" echo "Changed files: ${CHANGED_FILES}"
cat ./src/*-TestMigration.ts cat ./src/*-TestMigration.ts
exit 1 exit 1
@@ -555,9 +673,11 @@ jobs:
- name: Verify SQL files have not changed - name: Verify SQL files have not changed
if: steps.verify-changed-sql-files.outputs.files_changed == 'true' if: steps.verify-changed-sql-files.outputs.files_changed == 'true'
env:
CHANGED_FILES: ${{ steps.verify-changed-sql-files.outputs.changed_files }}
run: | run: |
echo "ERROR: Generated SQL files not up to date!" echo "ERROR: Generated SQL files not up to date!"
echo "Changed files: ${{ steps.verify-changed-sql-files.outputs.changed_files }}" echo "Changed files: ${CHANGED_FILES}"
exit 1 exit 1
# mobile-integration-tests: # mobile-integration-tests:

View File

@@ -4,30 +4,32 @@ on:
pull_request: pull_request:
branches: [main] branches: [main]
permissions: {}
jobs: jobs:
pre-job: pre-job:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
outputs: outputs:
should_run: ${{ steps.found_paths.outputs.i18n == 'true' && github.head_ref != 'chore/translations'}} should_run: ${{ steps.found_paths.outputs.i18n == 'true' && github.head_ref != 'chore/translations'}}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- id: found_paths - id: found_paths
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
with: with:
filters: | filters: |
i18n: i18n:
- 'i18n/!(en)**\.json' - 'i18n/!(en)**\.json'
- name: Debug
run: |
echo "Should run: ${{ steps.found_paths.outputs.i18n == 'true' && github.head_ref != 'chore/translations'}}"
echo "Found i18n paths: ${{ steps.found_paths.outputs.i18n }}"
echo "Head ref: ${{ github.head_ref }}"
enforce-lock: enforce-lock:
name: Check Weblate Lock name: Check Weblate Lock
needs: [pre-job] needs: [pre-job]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: {}
if: ${{ needs.pre-job.outputs.should_run == 'true' }} if: ${{ needs.pre-job.outputs.should_run == 'true' }}
steps: steps:
- name: Check weblate lock - name: Check weblate lock
@@ -47,6 +49,7 @@ jobs:
name: Weblate Lock Check Success name: Weblate Lock Check Success
needs: [enforce-lock] needs: [enforce-lock]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: {}
if: always() if: always()
steps: steps:
- name: Any jobs failed? - name: Any jobs failed?
@@ -54,4 +57,5 @@ jobs:
run: exit 1 run: exit 1
- name: All jobs passed or skipped - name: All jobs passed or skipped
if: ${{ !(contains(needs.*.result, 'failure')) }} if: ${{ !(contains(needs.*.result, 'failure')) }}
# zizmor: ignore[template-injection]
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}" run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"

78
.vscode/settings.json vendored
View File

@@ -1,45 +1,63 @@
{ {
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2,
"editor.formatOnSave": true
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2,
"editor.formatOnSave": true
},
"[css]": { "[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2, "editor.formatOnSave": true,
"editor.formatOnSave": true
},
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode",
"editor.tabSize": 2 "editor.tabSize": 2
}, },
"svelte.enable-ts-plugin": true,
"eslint.validate": [
"javascript",
"svelte"
],
"typescript.preferences.importModuleSpecifier": "non-relative",
"[dart]": { "[dart]": {
"editor.defaultFormatter": "Dart-Code.dart-code",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.selectionHighlight": false, "editor.selectionHighlight": false,
"editor.suggest.snippetsPreventQuickSuggestions": false, "editor.suggest.snippetsPreventQuickSuggestions": false,
"editor.suggestSelection": "first", "editor.suggestSelection": "first",
"editor.tabCompletion": "onlySnippets", "editor.tabCompletion": "onlySnippets",
"editor.wordBasedSuggestions": "off", "editor.wordBasedSuggestions": "off"
"editor.defaultFormatter": "Dart-Code.dart-code"
}, },
"cSpell.words": [ "[javascript]": {
"immich" "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.enabled": true,
"explorer.fileNesting.patterns": { "explorer.fileNesting.patterns": {
"*.ts": "${capture}.spec.ts,${capture}.mock.ts", "*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart",
"*.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"
} }

View File

@@ -17,6 +17,9 @@ e2e:
prod: prod:
docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
prod-down:
docker compose -f ./docker/docker-compose.prod.yml down --remove-orphans
prod-scale: prod-scale:
docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans

View File

@@ -1,4 +1,4 @@
FROM node:22.14.0-alpine3.20@sha256:40be979442621049f40b1d51a26b55e281246b5de4e5f51a18da7beb6e17e3f9 AS core FROM node:22.15.0-alpine3.20@sha256:686b8892b69879ef5bfd6047589666933508f9a5451c67320df3070ba0e9807b AS core
WORKDIR /usr/src/open-api/typescript-sdk WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./

2333
cli/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.61", "version": "2.2.65",
"description": "Command Line Interface (CLI) for Immich", "description": "Command Line Interface (CLI) for Immich",
"type": "module", "type": "module",
"exports": "./dist/index.js", "exports": "./dist/index.js",
@@ -21,7 +21,7 @@
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/micromatch": "^4.0.9", "@types/micromatch": "^4.0.9",
"@types/mock-fs": "^4.13.1", "@types/mock-fs": "^4.13.1",
"@types/node": "^22.14.0", "@types/node": "^22.14.1",
"@vitest/coverage-v8": "^3.0.0", "@vitest/coverage-v8": "^3.0.0",
"byte-size": "^9.0.0", "byte-size": "^9.0.0",
"cli-progress": "^3.12.0", "cli-progress": "^3.12.0",

View File

@@ -116,13 +116,13 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:42cba146593a5ea9a622002c1b7cba5da7be248650cbb64ecb9c6c33d29794b1 image: docker.io/valkey/valkey:8-bookworm@sha256:4a9f847af90037d59b34cd4d4ad14c6e055f46540cf4ff757aaafb266060fa28
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
database: database:
container_name: immich_postgres container_name: immich_postgres
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52 image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0-pgvectors0.2.0
env_file: env_file:
- .env - .env
environment: environment:
@@ -134,24 +134,6 @@ services:
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data - ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
ports: ports:
- 5432:5432 - 5432:5432
healthcheck:
test: >-
pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1;
Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align
--command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')";
echo "checksum failure count is $$Chksum";
[ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
command: >-
postgres
-c shared_preload_libraries=vectors.so
-c 'search_path="$$user", public, vectors'
-c logging_collector=on
-c max_wal_size=2GB
-c shared_buffers=512MB
-c wal_compression=on
# set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics # set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics
# immich-prometheus: # immich-prometheus:

View File

@@ -56,14 +56,14 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:42cba146593a5ea9a622002c1b7cba5da7be248650cbb64ecb9c6c33d29794b1 image: docker.io/valkey/valkey:8-bookworm@sha256:4a9f847af90037d59b34cd4d4ad14c6e055f46540cf4ff757aaafb266060fa28
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
restart: always restart: always
database: database:
container_name: immich_postgres container_name: immich_postgres
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52 image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0-pgvectors0.2.0
env_file: env_file:
- .env - .env
environment: environment:
@@ -75,14 +75,6 @@ services:
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data - ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
ports: ports:
- 5432:5432 - 5432:5432
healthcheck:
test: >-
pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1; Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
command: >-
postgres -c shared_preload_libraries=vectors.so -c 'search_path="$$user", public, vectors' -c logging_collector=on -c max_wal_size=2GB -c shared_buffers=512MB -c wal_compression=on
restart: always restart: always
# set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics # set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics
@@ -90,7 +82,7 @@ services:
container_name: immich_prometheus container_name: immich_prometheus
ports: ports:
- 9090:9090 - 9090:9090
image: prom/prometheus@sha256:502ad90314c7485892ce696cb14a99fceab9fc27af29f4b427f41bd39701a199 image: prom/prometheus@sha256:e2b8aa62b64855956e3ec1e18b4f9387fb6203174a4471936f4662f437f04405
volumes: volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml - ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus - prometheus-data:/prometheus
@@ -102,7 +94,7 @@ services:
command: [ './run.sh', '-disable-reporting' ] command: [ './run.sh', '-disable-reporting' ]
ports: ports:
- 3000:3000 - 3000:3000
image: grafana/grafana:11.6.0-ubuntu@sha256:fd8fa48213c624e1a95122f1d93abbf1cf1cbe85fc73212c1e599dbd76c63ff8 image: grafana/grafana:11.6.1-ubuntu@sha256:6fc273288470ef499dd3c6b36aeade093170d4f608f864c5dd3a7fabeae77b50
volumes: volumes:
- grafana-data:/var/lib/grafana - grafana-data:/var/lib/grafana

View File

@@ -49,14 +49,14 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:42cba146593a5ea9a622002c1b7cba5da7be248650cbb64ecb9c6c33d29794b1 image: docker.io/valkey/valkey:8-bookworm@sha256:4a9f847af90037d59b34cd4d4ad14c6e055f46540cf4ff757aaafb266060fa28
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
restart: always restart: always
database: database:
container_name: immich_postgres container_name: immich_postgres
image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52 image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0-pgvectors0.2.0
environment: environment:
POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME} POSTGRES_USER: ${DB_USERNAME}
@@ -65,14 +65,8 @@ services:
volumes: volumes:
# Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file # Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data - ${DB_DATA_LOCATION}:/var/lib/postgresql/data
healthcheck: # change ssd below to hdd if you are using a hard disk drive or other slow storage
test: >- command: postgres -c config_file=/etc/postgresql/postgresql.ssd.conf
pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1; Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
command: >-
postgres -c shared_preload_libraries=vectors.so -c 'search_path="$$user", public, vectors' -c logging_collector=on -c max_wal_size=2GB -c shared_buffers=512MB -c wal_compression=on
restart: always restart: always
volumes: volumes:

View File

@@ -23,23 +23,32 @@ Refer to the official [postgres documentation](https://www.postgresql.org/docs/c
It is not recommended to directly backup the `DB_DATA_LOCATION` folder. Doing so while the database is running can lead to a corrupted backup that cannot be restored. It is not recommended to directly backup the `DB_DATA_LOCATION` folder. Doing so while the database is running can lead to a corrupted backup that cannot be restored.
::: :::
### Automatic Database Backups ### Automatic Database Dumps
For convenience, Immich will automatically create database backups by default. The backups are stored in `UPLOAD_LOCATION/backups`. :::warning
As mentioned above, you should make your own backup of these together with the asset folders as noted below. The automatic database dumps can be used to restore the database in the event of damage to the Postgres database files.
You can adjust the schedule and amount of kept backups in the [admin settings](http://my.immich.app/admin/system-settings?isOpen=backup). There is no monitoring for these dumps and you will not be notified if they are unsuccessful.
By default, Immich will keep the last 14 backups and create a new backup every day at 2:00 AM. :::
#### Trigger Backup :::caution
The database dumps do **NOT** contain any pictures or videos, only metadata. They are only usable with a copy of the other files in `UPLOAD_LOCATION` as outlined below.
:::
You are able to trigger a backup in the [admin job status page](http://my.immich.app/admin/jobs-status). For disaster-recovery purposes, Immich will automatically create database dumps. The dumps are stored in `UPLOAD_LOCATION/backups`.
Visit the page, open the "Create job" modal from the top right, select "Backup Database" and click "Confirm". Please be sure to make your own, independent backup of the database together with the asset folders as noted below.
A job will run and trigger a backup, you can verify this worked correctly by checking the logs or the backup folder. You can adjust the schedule and amount of kept database dumps in the [admin settings](http://my.immich.app/admin/system-settings?isOpen=backup).
This backup will count towards the last X backups that will be kept based on your settings. By default, Immich will keep the last 14 database dumps and create a new dump every day at 2:00 AM.
#### Trigger Dump
You are able to trigger a database dump in the [admin job status page](http://my.immich.app/admin/jobs-status).
Visit the page, open the "Create job" modal from the top right, select "Create Database Dump" and click "Confirm".
A job will run and trigger a dump, you can verify this worked correctly by checking the logs or the `backups/` folder.
This dumps will count towards the last `X` dumps that will be kept based on your settings.
#### Restoring #### Restoring
We hope to make restoring simpler in future versions, for now you can find the backups in the `UPLOAD_LOCATION/backups` folder on your host. We hope to make restoring simpler in future versions, for now you can find the database dumps in the `UPLOAD_LOCATION/backups` folder on your host.
Then please follow the steps in the following section for restoring the database. Then please follow the steps in the following section for restoring the database.
### Manual Backup and Restore ### Manual Backup and Restore

View File

@@ -10,12 +10,12 @@ Running with a pre-existing Postgres server can unlock powerful administrative f
## Prerequisites ## 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 VectorChord into your instance of Postgres using their [instructions][vchord-install]. After installation, add `shared_preload_libraries = 'vchord.so'` to your `postgresql.conf`. If you already have some `shared_preload_libraries` set, you can separate each extension with a comma. For example, `shared_preload_libraries = 'pg_stat_statements, vchord.so'`.
:::note :::note
Immich is known to work with Postgres versions 14, 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, 15, 16 and 17. Earlier versions are unsupported.
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.4.0`.
::: :::
## Specifying the connection URL ## Specifying the connection URL
@@ -53,16 +53,75 @@ CREATE DATABASE <immichdatabasename>;
\c <immichdatabasename> \c <immichdatabasename>
BEGIN; BEGIN;
ALTER DATABASE <immichdatabasename> OWNER TO <immichdbusername>; ALTER DATABASE <immichdatabasename> OWNER TO <immichdbusername>;
CREATE EXTENSION vectors; CREATE EXTENSION vchord CASCADE;
CREATE EXTENSION earthdistance CASCADE; CREATE EXTENSION earthdistance CASCADE;
ALTER DATABASE <immichdatabasename> SET search_path TO "$user", public, vectors;
ALTER SCHEMA vectors OWNER TO <immichdbusername>;
COMMIT; COMMIT;
``` ```
### Updating 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 by connecting to the Immich database and running `ALTER EXTENSION vchord UPDATE;`.
## Migrating to VectorChord
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.
### 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:
1. Ensure you still have pgvecto.rs installed
2. [Install VectorChord][vchord-install]
3. 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
4. If Immich does not have superuser permissions, run the SQL command `CREATE EXTENSION vchord CASCADE;` using psql or your choice of database client
5. Start Immich and wait for the logs `Reindexed face_index` and `Reindexed clip_index` to be output
6. Remove the `vectors.so` entry from the `shared_preload_libraries` setting
7. Uninstall pgvecto.rs (e.g. `apt-get purge vectors-pg14` on Debian-based environments, replacing `pg14` as appropriate)
If it is not possible to have both VectorChord and pgvector.s installed at the same time, you can perform the migration with more manual steps:
1. While pgvecto.rs is still installed, run the following SQL command using psql or your choice of database client. Take note of the number outputted by this command as you will need it later
```sql
SELECT atttypmod as dimsize
FROM pg_attribute f
JOIN pg_class c ON c.oid = f.attrelid
WHERE c.relkind = 'r'::char
AND f.attnum > 0
AND c.relname = 'smart_search'::text
AND f.attname = 'embedding'::text;
```
2. Remove references to pgvecto.rs using the below SQL commands
```sql
DROP INDEX IF EXISTS clip_index;
DROP INDEX IF EXISTS face_index;
ALTER TABLE smart_search ALTER COLUMN embedding SET DATA TYPE real[];
ALTER TABLE face_search ALTER COLUMN embedding SET DATA TYPE real[];
```
3. [Install VectorChord][vchord-install]
4. Change the columns back to the appropriate vector types, replacing `<number>` with the number from step 1
```sql
CREATE EXTENSION IF NOT EXISTS vchord CASCADE;
ALTER TABLE smart_search ALTER COLUMN embedding SET DATA TYPE vector(<number>);
ALTER TABLE face_search ALTER COLUMN embedding SET DATA TYPE vector(512);
```
5. Start Immich and let it create new indices using VectorChord
### Migrating from pgvector
1. Ensure you have at least 0.7.0 of pgvector installed. If it is below that, please upgrade it and run the SQL command `ALTER EXTENSION vector UPDATE;` using psql or your choice of database client
2. Follow the Prerequisites to install VectorChord
3. If Immich does not have superuser permissions, run the SQL command `CREATE EXTENSION vchord CASCADE;`
4. Start Immich and let it create new indices using VectorChord
Note that VectorChord itself uses pgvector types, so you should not uninstall pgvector after following these steps.
### Common errors ### Common errors
@@ -70,4 +129,4 @@ When installing a new version of pgvecto.rs, you will need to manually update th
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>;`. 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>;`.
[vectors-install]: https://docs.vectorchord.ai/getting-started/installation.html [vchord-install]: https://docs.vectorchord.ai/vectorchord/getting-started/installation.html

View File

@@ -22,7 +22,7 @@ server {
client_max_body_size 50000M; client_max_body_size 50000M;
# Set headers # Set headers
proxy_set_header Host $http_host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;

View File

@@ -1,14 +1,14 @@
# Database Migrations # Database Migrations
After making any changes in the `server/src/entities`, a database migration need to run in order to register the changes in the database. Follow the steps below to create a new migration. After making any changes in the `server/src/schema`, a database migration need to run in order to register the changes in the database. Follow the steps below to create a new migration.
1. Run the command 1. Run the command
```bash ```bash
npm run typeorm:migrations:generate <migration-name> npm run migrations:generate <migration-name>
``` ```
2. Check if the migration file makes sense. 2. Check if the migration file makes sense.
3. Move the migration file to folder `./server/src/migrations` in your code editor. 3. Move the migration file to folder `./server/src/schema/migrations` in your code editor.
The server will automatically detect `*.ts` file changes and restart. Part of the server start-up process includes running any new migrations, so it will be applied immediately. The server will automatically detect `*.ts` file changes and restart. Part of the server start-up process includes running any new migrations, so it will be applied immediately.

View File

@@ -83,9 +83,20 @@ To see local changes to `@immich/ui` in Immich, do the following:
### Mobile app ### Mobile app
The mobile app `(/mobile)` will required Flutter toolchain 3.13.x and FVM to be installed on your system. #### Setup
Please refer to the [Flutter's official documentation](https://flutter.dev/docs/get-started/install) for more information on setting up the toolchain on your machine. 1. Setup Flutter toolchain using FVM.
2. Run `flutter pub get` to install the dependencies.
3. Run `make translation` to generate the translation file.
4. Run `fvm flutter run` to start the app.
#### Translation
To add a new translation text, enter the key-value pair in the `i18n/en.json` in the root of the immich project. Then, from the `mobile/` directory, run
```bash
make translation
```
The mobile app asks you what backend to connect to. You can utilize the demo backend (https://demo.immich.app/) if you don't need to change server code or upload photos. Alternatively, you can run the server yourself per the instructions above. The mobile app asks you what backend to connect to. You can utilize the demo backend (https://demo.immich.app/) if you don't need to change server code or upload photos. Alternatively, you can run the server yourself per the instructions above.

View File

@@ -42,6 +42,12 @@ docker run -it -v "$(pwd)":/import:ro -e IMMICH_INSTANCE_URL=https://your-immich
Please modify the `IMMICH_INSTANCE_URL` and `IMMICH_API_KEY` environment variables as suitable. You can also use a Docker env file to store your sensitive API key. Please modify the `IMMICH_INSTANCE_URL` and `IMMICH_API_KEY` environment variables as suitable. You can also use a Docker env file to store your sensitive API key.
This `docker run` command will directly run the command `immich` inside the container. You can directly append the desired parameters (see under "usage") to the commandline like this:
```bash
docker run -it -v "$(pwd)":/import:ro -e IMMICH_INSTANCE_URL=https://your-immich-instance/api -e IMMICH_API_KEY=your-api-key ghcr.io/immich-app/immich-cli:latest upload -a -c 5 --recursive directory/
```
## Usage ## Usage
<details> <details>

View File

@@ -72,7 +72,7 @@ In rare cases, the library watcher can hang, preventing Immich from starting up.
### Nightly job ### Nightly job
There is an automatic scan job that is scheduled to run once a day. This job also cleans up any libraries stuck in deletion. It is possible to trigger the cleanup by clicking "Scan all libraries" in the library managment page. There is an automatic scan job that is scheduled to run once a day. This job also cleans up any libraries stuck in deletion. It is possible to trigger the cleanup by clicking "Scan all libraries" in the library management page.
## Usage ## Usage

View File

@@ -42,7 +42,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
- The GPU must have compute capability 5.2 or greater. - The GPU must have compute capability 5.2 or greater.
- The server must have the official NVIDIA driver installed. - The server must have the official NVIDIA driver installed.
- The installed driver must be >= 535 (it must support CUDA 12.2). - The installed driver must be >= 545 (it must support CUDA 12.3).
- On Linux (except for WSL2), you also need to have [NVIDIA Container Toolkit][nvct] installed. - On Linux (except for WSL2), you also need to have [NVIDIA Container Toolkit][nvct] installed.
#### ROCm #### ROCm

View File

@@ -5,7 +5,7 @@ import TabItem from '@theme/TabItem';
Immich uses Postgres as its search database for both metadata and contextual CLIP search. Immich uses Postgres as its search database for both metadata and contextual CLIP search.
Contextual CLIP search is powered by the [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 ## Advanced Search Filters
@@ -92,7 +92,7 @@ Memory and execution time estimates were obtained without acceleration on a 7800
**Execution Time (ms)**: After warming up the model with one pass, the mean execution time of 100 passes with the same input. **Execution Time (ms)**: After warming up the model with one pass, the mean execution time of 100 passes with the same input.
**Memory (MiB)**: The peak RSS usage of the process afer performing the above timing benchmark. Does not include image decoding, concurrent processing, the web server, etc., which are relatively constant factors. **Memory (MiB)**: The peak RSS usage of the process after performing the above timing benchmark. Does not include image decoding, concurrent processing, the web server, etc., which are relatively constant factors.
**Recall (%)**: Evaluated on Crossmodal-3600, the average of the recall@1, recall@5 and recall@10 results for zeroshot image retrieval. Chinese (Simplified), English, French, German, Italian, Japanese, Korean, Polish, Russian, Spanish and Turkish are additionally tested on XTD-10. Chinese (Simplified) and English are additionally tested on Flickr30k. The recall metrics are the average across all tested datasets. **Recall (%)**: Evaluated on Crossmodal-3600, the average of the recall@1, recall@5 and recall@10 results for zeroshot image retrieval. Chinese (Simplified), English, French, German, Italian, Japanese, Korean, Polish, Russian, Spanish and Turkish are additionally tested on XTD-10. Chinese (Simplified) and English are additionally tested on Flickr30k. The recall metrics are the average across all tested datasets.

View File

@@ -14,14 +14,14 @@ online generators you can use.
2. Paste the link to your JSON style in either the **Light Style** or **Dark Style**. (You can add different styles which will help make the map style more appropriate depending on whether you set **Immich** to Light or Dark mode.) 2. Paste the link to your JSON style in either the **Light Style** or **Dark Style**. (You can add different styles which will help make the map style more appropriate depending on whether you set **Immich** to Light or Dark mode.)
3. Save your selections. Reload the map, and enjoy your custom map style! 3. Save your selections. Reload the map, and enjoy your custom map style!
## Use Maptiler to build a custom style ## Use MapTiler to build a custom style
Customizing the map style can be done easily using Maptiler, if you do not want to write an entire JSON document by hand. Customizing the map style can be done easily using MapTiler, if you do not want to write an entire JSON document by hand.
1. Create a free account at https://cloud.maptiler.com 1. Create a free account at https://cloud.maptiler.com
2. Once logged in, you can either create a brand new map by clicking on **New Map**, selecting a starter map, and then clicking **Customize**, OR by selecting a **Standard Map** and customizing it from there. 2. Once logged in, you can either create a brand new map by clicking on **New Map**, selecting a starter map, and then clicking **Customize**, OR by selecting a **Standard Map** and customizing it from there.
3. The **editor** interface is self-explanatory. You can change colors, remove visible layers, or add optional layers (e.g., administrative, topo, hydro, etc.) in the composer. 3. The **editor** interface is self-explanatory. You can change colors, remove visible layers, or add optional layers (e.g., administrative, topo, hydro, etc.) in the composer.
4. Once you have your map composed, click on **Save** at the top right. Give it a unique name to save it to your account. 4. Once you have your map composed, click on **Save** at the top right. Give it a unique name to save it to your account.
5. Next, **Publish** your style using the **Publish** button at the top right. This will deploy it to production, which means it is able to be exposed over the Internet. Maptiler will present an interactive side-by-side map with the original and your changes prior to publication.<br/>![Maptiler Publication Settings](img/immich_map_styles_publish.webp) 5. Next, **Publish** your style using the **Publish** button at the top right. This will deploy it to production, which means it is able to be exposed over the Internet. MapTiler will present an interactive side-by-side map with the original and your changes prior to publication.<br/>![MapTiler Publication Settings](img/immich_map_styles_publish.webp)
6. Maptiler will warn you that changing the map will change it across all apps using the map. Since no apps are using the map yet, this is okay. 6. MapTiler will warn you that changing the map will change it across all apps using the map. Since no apps are using the map yet, this is okay.
7. Clicking on the name of your new map at the top left will bring you to the item's **details** page. From here, copy the link to the JSON style under **Use vector style**. This link will automatically contain your personal API key to Maptiler. 7. Clicking on the name of your new map at the top left will bring you to the item's **details** page. From here, copy the link to the JSON style under **Use vector style**. This link will automatically contain your personal API key to MapTiler.

View File

@@ -1,7 +1,7 @@
# Database Queries # Database Queries
:::danger :::danger
Keep in mind that mucking around in the database might set the moon on fire. Avoid modifying the database directly when possible, and always have current backups. Keep in mind that mucking around in the database might set the Moon on fire. Avoid modifying the database directly when possible, and always have current backups.
::: :::
:::tip :::tip

View File

@@ -2,53 +2,13 @@
sidebar_position: 30 sidebar_position: 30
--- ---
import CodeBlock from '@theme/CodeBlock';
import ExampleEnv from '!!raw-loader!../../../docker/example.env';
# Docker Compose [Recommended] # Docker Compose [Recommended]
Docker Compose is the recommended method to run Immich in production. Below are the steps to deploy Immich with Docker Compose. Docker Compose is the recommended method to run Immich in production. Below are the steps to deploy Immich with Docker Compose.
## Step 1 - Download the required files import DockerComposeSteps from '/docs/partials/_docker-compose-install-steps.mdx';
Create a directory of your choice (e.g. `./immich-app`) to hold the `docker-compose.yml` and `.env` files. <DockerComposeSteps />
```bash title="Move to the directory you created"
mkdir ./immich-app
cd ./immich-app
```
Download [`docker-compose.yml`][compose-file] and [`example.env`][env-file] by running the following commands:
```bash title="Get docker-compose.yml file"
wget -O docker-compose.yml https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
```
```bash title="Get .env file"
wget -O .env https://github.com/immich-app/immich/releases/latest/download/example.env
```
You can alternatively download these two files from your browser and move them to the directory that you created, in which case ensure that you rename `example.env` to `.env`.
## Step 2 - Populate the .env file with custom values
<CodeBlock language="bash" title="Default environmental variable content">
{ExampleEnv}
</CodeBlock>
- Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets. It should be a new directory on the server with enough free space.
- Consider changing `DB_PASSWORD` to a custom value. Postgres is not publicly exposed, so this password is only used for local authentication.
To avoid issues with Docker parsing this value, it is best to use only the characters `A-Za-z0-9`. `pwgen` is a handy utility for this.
- Set your timezone by uncommenting the `TZ=` line.
- Populate custom database information if necessary.
## Step 3 - Start the containers
From the directory you created in Step 1 (which should now contain your customized `docker-compose.yml` and `.env` files), run the following command to start Immich as a background service:
```bash title="Start the containers"
docker compose up -d
```
:::info Docker version :::info Docker version
If you get an error such as `unknown shorthand flag: 'd' in -d` or `open <location of your .env file>: permission denied`, you are probably running the wrong Docker version. (This happens, for example, with the docker.io package in Ubuntu 22.04.3 LTS.) You can correct the problem by following the complete [Docker Engine install](https://docs.docker.com/engine/install/) procedure for your distribution, crucially the "Uninstall old versions" and "Install using the apt/rpm repository" sections. These replace the distro's Docker packages with Docker's official ones. If you get an error such as `unknown shorthand flag: 'd' in -d` or `open <location of your .env file>: permission denied`, you are probably running the wrong Docker version. (This happens, for example, with the docker.io package in Ubuntu 22.04.3 LTS.) You can correct the problem by following the complete [Docker Engine install](https://docs.docker.com/engine/install/) procedure for your distribution, crucially the "Uninstall old versions" and "Install using the apt/rpm repository" sections. These replace the distro's Docker packages with Docker's official ones.
@@ -70,6 +30,3 @@ If you get an error `can't set healthcheck.start_interval as feature require Doc
## Next Steps ## Next Steps
Read the [Post Installation](/docs/install/post-install.mdx) steps and [upgrade instructions](/docs/install/upgrading.md). Read the [Post Installation](/docs/install/post-install.mdx) steps and [upgrade instructions](/docs/install/upgrading.md).
[compose-file]: https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
[env-file]: https://github.com/immich-app/immich/releases/latest/download/example.env

View File

@@ -72,20 +72,21 @@ Information on the current workers can be found [here](/docs/administration/jobs
## Database ## Database
| Variable | Description | Default | Containers | | Variable | Description | Default | Containers |
| :---------------------------------- | :----------------------------------------------------------------------- | :----------: | :----------------------------- | | :---------------------------------- | :--------------------------------------------------------------------------- | :--------: | :----------------------------- |
| `DB_URL` | Database URL | | server | | `DB_URL` | Database URL | | server |
| `DB_HOSTNAME` | Database host | `database` | server | | `DB_HOSTNAME` | Database host | `database` | server |
| `DB_PORT` | Database port | `5432` | server | | `DB_PORT` | Database port | `5432` | server |
| `DB_USERNAME` | Database user | `postgres` | server, database<sup>\*1</sup> | | `DB_USERNAME` | Database user | `postgres` | server, database<sup>\*1</sup> |
| `DB_PASSWORD` | Database password | `postgres` | server, database<sup>\*1</sup> | | `DB_PASSWORD` | Database password | `postgres` | server, database<sup>\*1</sup> |
| `DB_DATABASE_NAME` | Database name | `immich` | server, database<sup>\*1</sup> | | `DB_DATABASE_NAME` | Database name | `immich` | server, database<sup>\*1</sup> |
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`pgvector`, `pgvecto.rs`]) | `pgvecto.rs` | server | | `DB_SSL_MODE` | Database SSL mode | | server |
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | 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`. \*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 :::info

View File

@@ -29,7 +29,7 @@ Download [`docker-compose.yml`](https://github.com/immich-app/immich/releases/la
## Step 2 - Populate the .env file with custom values ## Step 2 - Populate the .env file with custom values
Follow [Step 2 in Docker Compose](./docker-compose#step-2---populate-the-env-file-with-custom-values) for instructions on customizing the `.env` file, and then return back to this guide to continue. Follow [Step 2 in Docker Compose](/docs/install/docker-compose#step-2---populate-the-env-file-with-custom-values) for instructions on customizing the `.env` file, and then return back to this guide to continue.
## Step 3 - Create a new project in Container Manager ## Step 3 - Create a new project in Container Manager

View File

@@ -2,7 +2,7 @@
sidebar_position: 80 sidebar_position: 80
--- ---
# TrueNAS SCALE [Community] # TrueNAS [Community]
:::note :::note
This is a community contribution and not officially supported by the Immich team, but included here for convenience. This is a community contribution and not officially supported by the Immich team, but included here for convenience.
@@ -12,17 +12,17 @@ Community support can be found in the dedicated channel on the [Discord Server](
**Please report app issues to the corresponding [Github Repository](https://github.com/truenas/charts/tree/master/community/immich).** **Please report app issues to the corresponding [Github Repository](https://github.com/truenas/charts/tree/master/community/immich).**
::: :::
Immich can easily be installed on TrueNAS SCALE via the **Community** train application. Immich can easily be installed on TrueNAS Community Edition via the **Community** train application.
Consider reviewing the TrueNAS [Apps tutorial](https://www.truenas.com/docs/scale/scaletutorials/apps/) if you have not previously configured applications on your system. Consider reviewing the TrueNAS [Apps resources](https://apps.truenas.com/getting-started/) if you have not previously configured applications on your system.
TrueNAS SCALE makes installing and updating Immich easy, but you must use the Immich web portal and mobile app to configure accounts and access libraries. TrueNAS Community Edition makes installing and updating Immich easy, but you must use the Immich web portal and mobile app to configure accounts and access libraries.
## First Steps ## First Steps
The Immich app in TrueNAS SCALE installs, completes the initial configuration, then starts the Immich web portal. The Immich app in TrueNAS Community Edition installs, completes the initial configuration, then starts the Immich web portal.
When updates become available, SCALE alerts and provides easy updates. When updates become available, TrueNAS alerts and provides easy updates.
Before installing the Immich app in SCALE, review the [Environment Variables](#environment-variables) documentation to see if you want to configure any during installation. Before installing the Immich app in TrueNAS, review the [Environment Variables](#environment-variables) documentation to see if you want to configure any during installation.
You may also configure environment variables at any time after deploying the application. You may also configure environment variables at any time after deploying the application.
### Setting up Storage Datasets ### Setting up Storage Datasets
@@ -126,9 +126,9 @@ className="border rounded-xl"
Accept the default port `30041` in **WebUI Port** or enter a custom port number. Accept the default port `30041` in **WebUI Port** or enter a custom port number.
:::info Allowed Port Numbers :::info Allowed Port Numbers
Only numbers within the range 9000-65535 may be used on SCALE versions below TrueNAS Scale 24.10 Electric Eel. Only numbers within the range 9000-65535 may be used on TrueNAS versions below TrueNAS Community Edition 24.10 Electric Eel.
Regardless of version, to avoid port conflicts, don't use [ports on this list](https://www.truenas.com/docs/references/defaultports/). Regardless of version, to avoid port conflicts, don't use [ports on this list](https://www.truenas.com/docs/solutions/optimizations/security/#truenas-default-ports).
::: :::
### Storage Configuration ### Storage Configuration
@@ -173,7 +173,7 @@ className="border rounded-xl"
You may configure [External Libraries](/docs/features/libraries) by mounting them using **Additional Storage**. You may configure [External Libraries](/docs/features/libraries) by mounting them using **Additional Storage**.
The **Mount Path** is the location you will need to copy and paste into the External Library settings within Immich. The **Mount Path** is the location you will need to copy and paste into the External Library settings within Immich.
The **Host Path** is the location on the TrueNAS SCALE server where your external library is located. The **Host Path** is the location on the TrueNAS Community Edition server where your external library is located.
<!-- A section for Labels would go here but I don't know what they do. --> <!-- A section for Labels would go here but I don't know what they do. -->
@@ -188,17 +188,17 @@ className="border rounded-xl"
Accept the default **CPU** limit of `2` threads or specify the number of threads (CPUs with Multi-/Hyper-threading have 2 threads per core). Accept the default **CPU** limit of `2` threads or specify the number of threads (CPUs with Multi-/Hyper-threading have 2 threads per core).
Accept the default **Memory** limit of `4096` MB or specify the number of MB of RAM. If you're using Machine Learning you should probably set this above 8000 MB. Specify the **Memory** limit in MB of RAM. Immich recommends at least 6000 MB (6GB). If you selected **Enable Machine Learning** in **Immich Configuration**, you should probably set this above 8000 MB.
:::info Older SCALE Versions :::info Older TrueNAS Versions
Before TrueNAS SCALE version 24.10 Electric Eel: Before TrueNAS Community Edition version 24.10 Electric Eel:
The **CPU** value was specified in a different format with a default of `4000m` which is 4 threads. The **CPU** value was specified in a different format with a default of `4000m` which is 4 threads.
The **Memory** value was specified in a different format with a default of `8Gi` which is 8 GiB of RAM. The value was specified in bytes or a number with a measurement suffix. Examples: `129M`, `123Mi`, `1000000000` The **Memory** value was specified in a different format with a default of `8Gi` which is 8 GiB of RAM. The value was specified in bytes or a number with a measurement suffix. Examples: `129M`, `123Mi`, `1000000000`
::: :::
Enable **GPU Configuration** options if you have a GPU that you will use for [Hardware Transcoding](/docs/features/hardware-transcoding) and/or [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md). More info: [GPU Passthrough Docs for TrueNAS Apps](https://www.truenas.com/docs/truenasapps/#gpu-passthrough) Enable **GPU Configuration** options if you have a GPU that you will use for [Hardware Transcoding](/docs/features/hardware-transcoding) and/or [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md). More info: [GPU Passthrough Docs for TrueNAS Apps](https://apps.truenas.com/managing-apps/installing-apps/#gpu-passthrough)
### Install ### Install
@@ -240,7 +240,7 @@ className="border rounded-xl"
/> />
:::info :::info
Some Environment Variables are not available for the TrueNAS SCALE app. This is mainly because they can be configured through GUI options in the [Edit Immich screen](#edit-app-settings). Some Environment Variables are not available for the TrueNAS Community Edition app. This is mainly because they can be configured through GUI options in the [Edit Immich screen](#edit-app-settings).
Some examples are: `IMMICH_VERSION`, `UPLOAD_LOCATION`, `DB_DATA_LOCATION`, `TZ`, `IMMICH_LOG_LEVEL`, `DB_PASSWORD`, `REDIS_PASSWORD`. Some examples are: `IMMICH_VERSION`, `UPLOAD_LOCATION`, `DB_DATA_LOCATION`, `TZ`, `IMMICH_LOG_LEVEL`, `DB_PASSWORD`, `REDIS_PASSWORD`.
::: :::
@@ -251,7 +251,7 @@ Some examples are: `IMMICH_VERSION`, `UPLOAD_LOCATION`, `DB_DATA_LOCATION`, `TZ`
Make sure to read the general [upgrade instructions](/docs/install/upgrading.md). Make sure to read the general [upgrade instructions](/docs/install/upgrading.md).
::: :::
When updates become available, SCALE alerts and provides easy updates. When updates become available, TrueNAS alerts and provides easy updates.
To update the app to the latest version: To update the app to the latest version:
- Go to the **Installed Applications** screen and select Immich from the list of installed applications. - Go to the **Installed Applications** screen and select Immich from the list of installed applications.

View File

@@ -1,5 +1,5 @@
--- ---
sidebar_position: 2 sidebar_position: 3
--- ---
# Comparison # Comparison

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

View File

@@ -1,5 +1,5 @@
--- ---
sidebar_position: 3 sidebar_position: 2
--- ---
# Quick start # Quick start
@@ -10,11 +10,20 @@ to install and use it.
## Requirements ## Requirements
Check the [requirements page](/docs/install/requirements) to get started. - A system with at least 4GB of RAM and 2 CPU cores.
- [Docker](https://docs.docker.com/engine/install/)
> For a more detailed list of requirements, see the [requirements page](/docs/install/requirements).
---
## Set up the server ## Set up the server
Follow the [Docker Compose (Recommended)](/docs/install/docker-compose) instructions to install the server. import DockerComposeSteps from '/docs/partials/_docker-compose-install-steps.mdx';
<DockerComposeSteps />
---
## Try the web app ## Try the web app
@@ -26,6 +35,8 @@ Try uploading a picture from your browser.
<img src={require('./img/upload-button.webp').default} title="Upload button" /> <img src={require('./img/upload-button.webp').default} title="Upload button" />
---
## Try the mobile app ## Try the mobile app
### Download the Mobile App ### Download the Mobile App
@@ -56,6 +67,8 @@ You can select the **Jobs** tab to see Immich processing your photos.
<img src={require('/docs/guides/img/jobs-tab.webp').default} title="Jobs tab" width={300} /> <img src={require('/docs/guides/img/jobs-tab.webp').default} title="Jobs tab" width={300} />
---
## Review the database backup and restore process ## Review the database backup and restore process
Immich has built-in database backups. You can refer to the Immich has built-in database backups. You can refer to the
@@ -65,6 +78,8 @@ Immich has built-in database backups. You can refer to the
The database only contains metadata and user information. You must setup manual backups of the images and videos stored in `UPLOAD_LOCATION`. The database only contains metadata and user information. You must setup manual backups of the images and videos stored in `UPLOAD_LOCATION`.
::: :::
---
## Where to go from here? ## Where to go from here?
You may decide you'd like to install the server a different way; the Install category on the left menu provides many options. You may decide you'd like to install the server a different way; the Install category on the left menu provides many options.

View File

@@ -2,9 +2,13 @@
sidebar_position: 1 sidebar_position: 1
--- ---
# Introduction # Welcome to Immich
<img src={require('./img/feature-panel.webp').default} alt="Immich - Self-hosted photos and videos backup tool" /> <img
src={require('./img/social-preview-light.webp').default}
alt="Immich - Self-hosted photos and videos backup tool"
data-theme="light"
/>
## Welcome! ## Welcome!

View File

@@ -0,0 +1,43 @@
import CodeBlock from '@theme/CodeBlock';
import ExampleEnv from '!!raw-loader!../../../docker/example.env';
### Step 1 - Download the required files
Create a directory of your choice (e.g. `./immich-app`) to hold the `docker-compose.yml` and `.env` files.
```bash title="Move to the directory you created"
mkdir ./immich-app
cd ./immich-app
```
Download [`docker-compose.yml`](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml) and [`example.env`](https://github.com/immich-app/immich/releases/latest/download/example.env) by running the following commands:
```bash title="Get docker-compose.yml file"
wget -O docker-compose.yml https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
```
```bash title="Get .env file"
wget -O .env https://github.com/immich-app/immich/releases/latest/download/example.env
```
You can alternatively download these two files from your browser and move them to the directory that you created, in which case ensure that you rename `example.env` to `.env`.
### Step 2 - Populate the .env file with custom values
<CodeBlock language="bash" title="Default environmental variable content">
{ExampleEnv}
</CodeBlock>
- Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets. It should be a new directory on the server with enough free space.
- Consider changing `DB_PASSWORD` to a custom value. Postgres is not publicly exposed, so this password is only used for local authentication.
To avoid issues with Docker parsing this value, it is best to use only the characters `A-Za-z0-9`. `pwgen` is a handy utility for this.
- Set your timezone by uncommenting the `TZ=` line.
- Populate custom database information if necessary.
### Step 3 - Start the containers
From the directory you created in Step 1 (which should now contain your customized `docker-compose.yml` and `.env` files), run the following command to start Immich as a background service:
```bash title="Start the containers"
docker compose up -d
```

View File

@@ -95,7 +95,7 @@ const config = {
position: 'right', position: 'right',
}, },
{ {
to: '/docs/overview/introduction', to: '/docs/overview/welcome',
position: 'right', position: 'right',
label: 'Docs', label: 'Docs',
}, },
@@ -124,6 +124,12 @@ const config = {
label: 'Discord', label: 'Discord',
position: 'right', position: 'right',
}, },
{
type: 'html',
position: 'right',
value:
'<a href="https://buy.immich.app" target="_blank" class="no-underline hover:no-underline"><button class="buy-button bg-immich-primary dark:bg-immich-dark-primary text-white dark:text-black rounded-xl">Buy Immich</button></a>',
},
], ],
}, },
footer: { footer: {
@@ -134,7 +140,7 @@ const config = {
items: [ items: [
{ {
label: 'Welcome', label: 'Welcome',
to: '/docs/overview/introduction', to: '/docs/overview/welcome',
}, },
{ {
label: 'Installation', label: 'Installation',

5324
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,8 +40,9 @@ const projects: CommunityProjectProps[] = [
}, },
{ {
title: 'Lightroom Immich Plugin: lrc-immich-plugin', title: 'Lightroom Immich Plugin: lrc-immich-plugin',
description: 'Another Lightroom plugin to publish or export photos from Lightroom to Immich.', description:
url: 'https://github.com/bmachek/lrc-immich-plugin', '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', title: 'Immich Duplicate Finder',

View File

@@ -7,14 +7,22 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); @font-face {
font-family: 'Overpass';
body { src: url('/fonts/overpass/Overpass.ttf') format('truetype-variations');
font-family: 'Be Vietnam Pro', serif; font-weight: 1 999;
font-optical-sizing: auto; font-style: normal;
/* font-size: 1.125rem;
ascent-override: 106.25%; ascent-override: 106.25%;
size-adjust: 106.25%; */ size-adjust: 106.25%;
}
@font-face {
font-family: 'Overpass Mono';
src: url('/fonts/overpass/OverpassMono.ttf') format('truetype-variations');
font-weight: 1 999;
font-style: normal;
ascent-override: 106.25%;
size-adjust: 106.25%;
} }
.breadcrumbs__link { .breadcrumbs__link {
@@ -29,6 +37,7 @@ img {
/* You can override the default Infima variables here. */ /* You can override the default Infima variables here. */
:root { :root {
font-family: 'Overpass', sans-serif;
--ifm-color-primary: #4250af; --ifm-color-primary: #4250af;
--ifm-color-primary-dark: #4250af; --ifm-color-primary-dark: #4250af;
--ifm-color-primary-darker: #4250af; --ifm-color-primary-darker: #4250af;
@@ -59,14 +68,12 @@ div[class^='announcementBar_'] {
} }
.menu__link { .menu__link {
padding: 10px; padding: 10px 10px 10px 16px;
padding-left: 16px;
border-radius: 24px; border-radius: 24px;
margin-right: 16px; margin-right: 16px;
} }
.menu__list-item-collapsible { .menu__list-item-collapsible {
border-radius: 10px;
margin-right: 16px; margin-right: 16px;
border-radius: 24px; border-radius: 24px;
} }
@@ -83,3 +90,12 @@ div[class*='navbar__items'] > li:has(a[class*='version-switcher-34ab39']) {
code { code {
font-weight: 600; font-weight: 600;
} }
.buy-button {
padding: 8px 14px;
border: 1px solid transparent;
font-family: 'Overpass', sans-serif;
font-weight: 500;
cursor: pointer;
box-shadow: 0 0 5px 2px rgba(181, 206, 254, 0.4);
}

View File

@@ -2,6 +2,7 @@ import {
mdiBug, mdiBug,
mdiCalendarToday, mdiCalendarToday,
mdiCrosshairsOff, mdiCrosshairsOff,
mdiCrop,
mdiDatabase, mdiDatabase,
mdiLeadPencil, mdiLeadPencil,
mdiLockOff, mdiLockOff,
@@ -22,6 +23,18 @@ const withLanguage = (date: Date) => (language: string) => date.toLocaleDateStri
type Item = Omit<TimelineItem, 'done' | 'getDateLabel'> & { date: Date }; type Item = Omit<TimelineItem, 'done' | 'getDateLabel'> & { date: Date };
const items: Item[] = [ const items: Item[] = [
{
icon: mdiCrop,
iconColor: 'tomato',
title: 'Image dimensions in EXIF metadata are cursed',
description:
'The dimensions in EXIF metadata can be different from the actual dimensions of the image, causing issues with cropping and resizing.',
link: {
url: 'https://github.com/immich-app/immich/pull/17974',
text: '#17974',
},
date: new Date(2025, 5, 5),
},
{ {
icon: mdiMicrosoftWindows, icon: mdiMicrosoftWindows,
iconColor: '#357EC7', iconColor: '#357EC7',

5
docs/src/pages/errors.md Normal file
View File

@@ -0,0 +1,5 @@
# Errors
## TypeORM Upgrade
The upgrade to Immich `v2.x.x` has a required upgrade path to `v1.132.0+`. This means it is required to start up the application at least once on version `1.132.0` (or later). Doing so will complete database schema upgrades that are required for `v2.0.0`. After Immich has successfully booted on this version, shut the system down and try the `v2.x.x` upgrade again.

View File

@@ -4,6 +4,7 @@ import Layout from '@theme/Layout';
import { discordPath, discordViewBox } from '@site/src/components/svg-paths'; import { discordPath, discordViewBox } from '@site/src/components/svg-paths';
import ThemedImage from '@theme/ThemedImage'; import ThemedImage from '@theme/ThemedImage';
import Icon from '@mdi/react'; import Icon from '@mdi/react';
function HomepageHeader() { function HomepageHeader() {
return ( return (
<header> <header>
@@ -12,11 +13,14 @@ function HomepageHeader() {
<div className="w-full h-[120vh] absolute left-0 top-0 backdrop-blur-3xl bg-immich-bg/40 dark:bg-transparent"></div> <div className="w-full h-[120vh] absolute left-0 top-0 backdrop-blur-3xl bg-immich-bg/40 dark:bg-transparent"></div>
</div> </div>
<section className="text-center pt-12 sm:pt-24 bg-immich-bg/50 dark:bg-immich-dark-bg/80"> <section className="text-center pt-12 sm:pt-24 bg-immich-bg/50 dark:bg-immich-dark-bg/80">
<ThemedImage <a href="https://futo.org" target="_blank" rel="noopener noreferrer">
sources={{ dark: 'img/logomark-dark.svg', light: 'img/logomark-light.svg' }} <ThemedImage
className="h-[115px] w-[115px] mb-2 antialiased rounded-none" sources={{ dark: 'img/logomark-dark-with-futo.svg', light: 'img/logomark-light-with-futo.svg' }}
alt="Immich logo" className="h-[125px] w-[125px] mb-2 antialiased rounded-none"
/> alt="Immich logo"
/>
</a>
<div className="mt-8"> <div className="mt-8">
<p className="text-3xl md:text-5xl sm:leading-tight mb-1 font-extrabold text-black/90 dark:text-white px-4"> <p className="text-3xl md:text-5xl sm:leading-tight mb-1 font-extrabold text-black/90 dark:text-white px-4">
Self-hosted{' '} Self-hosted{' '}
@@ -27,7 +31,7 @@ function HomepageHeader() {
solution<span className="block"></span> solution<span className="block"></span>
</p> </p>
<p className="max-w-1/4 m-auto mt-4 px-4"> <p className="max-w-1/4 m-auto mt-4 px-4 text-lg text-gray-700 dark:text-gray-100">
Easily back up, organize, and manage your photos on your own server. Immich helps you Easily back up, organize, and manage your photos on your own server. Immich helps you
<span className="sm:block"></span> browse, search and organize your photos and videos with ease, without <span className="sm:block"></span> browse, search and organize your photos and videos with ease, without
sacrificing your privacy. sacrificing your privacy.
@@ -35,27 +39,21 @@ function HomepageHeader() {
</div> </div>
<div className="flex flex-col sm:flex-row place-items-center place-content-center mt-9 gap-4 "> <div className="flex flex-col sm:flex-row place-items-center place-content-center mt-9 gap-4 ">
<Link <Link
className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary dark:bg-immich-dark-primary rounded-xl no-underline hover:no-underline text-white hover:text-gray-50 dark:text-immich-dark-bg font-bold uppercase" className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary dark:bg-immich-dark-primary rounded-xl no-underline hover:no-underline text-white hover:text-gray-50 dark:text-immich-dark-bg font-bold"
to="docs/overview/introduction" to="docs/overview/quick-start"
> >
Get started Get Started
</Link> </Link>
<Link <Link
className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary/10 dark:bg-gray-300 rounded-xl hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase" className="flex place-items-center place-content-center py-3 px-8 border bg-white/90 dark:bg-gray-300 rounded-xl hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold"
to="https://demo.immich.app/" to="https://demo.immich.app/"
> >
Demo Open Demo
</Link>
<Link
className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary/10 dark:bg-gray-300 rounded-xl hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase"
to="https://immich.store"
>
Buy Merch
</Link> </Link>
</div> </div>
<div className="my-12 flex gap-1 font-medium place-items-center place-content-center text-immich-primary dark:text-immich-dark-primary">
<div className="my-8 flex gap-1 font-medium place-items-center place-content-center text-immich-primary dark:text-immich-dark-primary">
<Icon <Icon
path={discordPath} path={discordPath}
viewBox={discordViewBox} /* viewBox may show an error in your IDE but it is normal. */ viewBox={discordViewBox} /* viewBox may show an error in your IDE but it is normal. */
@@ -88,11 +86,18 @@ function HomepageHeader() {
<img className="h-24" alt="Get it on Google Play" src="/img/google-play-badge.png" /> <img className="h-24" alt="Get it on Google Play" src="/img/google-play-badge.png" />
</a> </a>
</div> </div>
<div className="h-24"> <div className="h-24">
<a href="https://apps.apple.com/sg/app/immich/id1613945652"> <a href="https://apps.apple.com/sg/app/immich/id1613945652">
<img className="h-24 sm:p-3.5 p-3" alt="Download on the App Store" src="/img/ios-app-store-badge.svg" /> <img className="h-24 sm:p-3.5 p-3" alt="Download on the App Store" src="/img/ios-app-store-badge.svg" />
</a> </a>
</div> </div>
<div className="h-24">
<a href="https://github.com/immich-app/immich/releases/latest">
<img className="h-24 sm:p-3.5 p-3" alt="Download APK" src="/img/download-apk-github.svg" />
</a>
</div>
</div> </div>
<ThemedImage <ThemedImage
sources={{ dark: '/img/app-qr-code-dark.svg', light: '/img/app-qr-code-light.svg' }} sources={{ dark: '/img/app-qr-code-dark.svg', light: '/img/app-qr-code-light.svg' }}
@@ -111,7 +116,7 @@ export default function Home(): JSX.Element {
<HomepageHeader /> <HomepageHeader />
<div className="flex flex-col place-items-center text-center place-content-center dark:bg-immich-dark-bg py-8"> <div className="flex flex-col place-items-center text-center place-content-center dark:bg-immich-dark-bg py-8">
<p>This project is available under GNU AGPL v3 license.</p> <p>This project is available under GNU AGPL v3 license.</p>
<p className="text-xs">Privacy should not be a luxury</p> <p className="text-sm">Privacy should not be a luxury</p>
</div> </div>
</Layout> </Layout>
); );

View File

@@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import Link from '@docusaurus/Link';
import Layout from '@theme/Layout'; import Layout from '@theme/Layout';
function HomepageHeader() { function HomepageHeader() {
return ( return (

View File

@@ -76,6 +76,7 @@ import {
mdiWeb, mdiWeb,
mdiDatabaseOutline, mdiDatabaseOutline,
mdiLinkEdit, mdiLinkEdit,
mdiTagFaces,
mdiMovieOpenPlayOutline, mdiMovieOpenPlayOutline,
} from '@mdi/js'; } from '@mdi/js';
import Layout from '@theme/Layout'; import Layout from '@theme/Layout';
@@ -83,6 +84,8 @@ import React from 'react';
import { Item, Timeline } from '../components/timeline'; import { Item, Timeline } from '../components/timeline';
const releases = { const releases = {
'v1.130.0': new Date(2025, 2, 25),
'v1.127.0': new Date(2025, 1, 26),
'v1.122.0': new Date(2024, 11, 5), 'v1.122.0': new Date(2024, 11, 5),
'v1.120.0': new Date(2024, 10, 6), 'v1.120.0': new Date(2024, 10, 6),
'v1.114.0': new Date(2024, 8, 6), 'v1.114.0': new Date(2024, 8, 6),
@@ -242,6 +245,13 @@ const roadmap: Item[] = [
]; ];
const milestones: Item[] = [ const milestones: Item[] = [
withRelease({
icon: mdiFolderMultiple,
iconColor: 'brown',
title: 'Folders view in the mobile app',
description: 'Browse your photos and videos in their folder structure inside the mobile app',
release: 'v1.130.0',
}),
{ {
icon: mdiStar, icon: mdiStar,
iconColor: 'gold', iconColor: 'gold',
@@ -249,6 +259,14 @@ const milestones: Item[] = [
description: 'Reached 60K Stars on GitHub!', description: 'Reached 60K Stars on GitHub!',
getDateLabel: withLanguage(new Date(2025, 2, 4)), getDateLabel: withLanguage(new Date(2025, 2, 4)),
}, },
withRelease({
icon: mdiTagFaces,
iconColor: 'teal',
title: 'Manual face tagging',
description:
'Manually tag or remove faces in photos and videos, even when automatic detection misses or misidentifies them.',
release: 'v1.127.0',
}),
withRelease({ withRelease({
icon: mdiLinkEdit, icon: mdiLinkEdit,
iconColor: 'crimson', iconColor: 'crimson',
@@ -266,8 +284,8 @@ const milestones: Item[] = [
withRelease({ withRelease({
icon: mdiDatabaseOutline, icon: mdiDatabaseOutline,
iconColor: 'brown', iconColor: 'brown',
title: 'Automatic database backups', title: 'Automatic database dumps',
description: 'Database backups are now integrated into the Immich server', description: 'Database dumps are now integrated into the Immich server',
release: 'v1.120.0', release: 'v1.120.0',
}), }),
{ {
@@ -300,7 +318,7 @@ const milestones: Item[] = [
withRelease({ withRelease({
icon: mdiFolderMultiple, icon: mdiFolderMultiple,
iconColor: 'brown', iconColor: 'brown',
title: 'Folders', title: 'Folders view',
description: 'Browse your photos and videos in their folder structure', description: 'Browse your photos and videos in their folder structure',
release: 'v1.113.0', release: 'v1.113.0',
}), }),

5
docs/static/.well-known/security.txt vendored Normal file
View File

@@ -0,0 +1,5 @@
Policy: https://github.com/immich-app/immich/blob/main/SECURITY.md
Contact: mailto:security@immich.app
Preferred-Languages: en
Expires: 2026-05-01T23:59:00.000Z
Canonical: https://immich.app/.well-known/security.txt

View File

@@ -30,3 +30,4 @@
/docs/guides/api-album-sync /docs/community-projects 307 /docs/guides/api-album-sync /docs/community-projects 307
/docs/guides/remove-offline-files /docs/community-projects 307 /docs/guides/remove-offline-files /docs/community-projects 307
/milestones /roadmap 307 /milestones /roadmap 307
/docs/overview/introduction /docs/overview/welcome 307

View File

@@ -1,36 +1,16 @@
[ [
{
"label": "v1.132.3",
"url": "https://v1.132.3.archive.immich.app"
},
{ {
"label": "v1.131.3", "label": "v1.131.3",
"url": "https://v1.131.3.archive.immich.app" "url": "https://v1.131.3.archive.immich.app"
}, },
{
"label": "v1.131.2",
"url": "https://v1.131.2.archive.immich.app"
},
{
"label": "v1.131.1",
"url": "https://v1.131.1.archive.immich.app"
},
{
"label": "v1.131.0",
"url": "https://v1.131.0.archive.immich.app"
},
{ {
"label": "v1.130.3", "label": "v1.130.3",
"url": "https://v1.130.3.archive.immich.app" "url": "https://v1.130.3.archive.immich.app"
}, },
{
"label": "v1.130.2",
"url": "https://v1.130.2.archive.immich.app"
},
{
"label": "v1.130.1",
"url": "https://v1.130.1.archive.immich.app"
},
{
"label": "v1.130.0",
"url": "https://v1.130.0.archive.immich.app"
},
{ {
"label": "v1.129.0", "label": "v1.129.0",
"url": "https://v1.129.0.archive.immich.app" "url": "https://v1.129.0.archive.immich.app"
@@ -47,46 +27,14 @@
"label": "v1.126.1", "label": "v1.126.1",
"url": "https://v1.126.1.archive.immich.app" "url": "https://v1.126.1.archive.immich.app"
}, },
{
"label": "v1.126.0",
"url": "https://v1.126.0.archive.immich.app"
},
{ {
"label": "v1.125.7", "label": "v1.125.7",
"url": "https://v1.125.7.archive.immich.app" "url": "https://v1.125.7.archive.immich.app"
}, },
{
"label": "v1.125.6",
"url": "https://v1.125.6.archive.immich.app"
},
{
"label": "v1.125.5",
"url": "https://v1.125.5.archive.immich.app"
},
{
"label": "v1.125.3",
"url": "https://v1.125.3.archive.immich.app"
},
{
"label": "v1.125.2",
"url": "https://v1.125.2.archive.immich.app"
},
{
"label": "v1.125.1",
"url": "https://v1.125.1.archive.immich.app"
},
{ {
"label": "v1.124.2", "label": "v1.124.2",
"url": "https://v1.124.2.archive.immich.app" "url": "https://v1.124.2.archive.immich.app"
}, },
{
"label": "v1.124.1",
"url": "https://v1.124.1.archive.immich.app"
},
{
"label": "v1.124.0",
"url": "https://v1.124.0.archive.immich.app"
},
{ {
"label": "v1.123.0", "label": "v1.123.0",
"url": "https://v1.123.0.archive.immich.app" "url": "https://v1.123.0.archive.immich.app"
@@ -95,18 +43,6 @@
"label": "v1.122.3", "label": "v1.122.3",
"url": "https://v1.122.3.archive.immich.app" "url": "https://v1.122.3.archive.immich.app"
}, },
{
"label": "v1.122.2",
"url": "https://v1.122.2.archive.immich.app"
},
{
"label": "v1.122.1",
"url": "https://v1.122.1.archive.immich.app"
},
{
"label": "v1.122.0",
"url": "https://v1.122.0.archive.immich.app"
},
{ {
"label": "v1.121.0", "label": "v1.121.0",
"url": "https://v1.121.0.archive.immich.app" "url": "https://v1.121.0.archive.immich.app"
@@ -115,34 +51,14 @@
"label": "v1.120.2", "label": "v1.120.2",
"url": "https://v1.120.2.archive.immich.app" "url": "https://v1.120.2.archive.immich.app"
}, },
{
"label": "v1.120.1",
"url": "https://v1.120.1.archive.immich.app"
},
{
"label": "v1.120.0",
"url": "https://v1.120.0.archive.immich.app"
},
{ {
"label": "v1.119.1", "label": "v1.119.1",
"url": "https://v1.119.1.archive.immich.app" "url": "https://v1.119.1.archive.immich.app"
}, },
{
"label": "v1.119.0",
"url": "https://v1.119.0.archive.immich.app"
},
{ {
"label": "v1.118.2", "label": "v1.118.2",
"url": "https://v1.118.2.archive.immich.app" "url": "https://v1.118.2.archive.immich.app"
}, },
{
"label": "v1.118.1",
"url": "https://v1.118.1.archive.immich.app"
},
{
"label": "v1.118.0",
"url": "https://v1.118.0.archive.immich.app"
},
{ {
"label": "v1.117.0", "label": "v1.117.0",
"url": "https://v1.117.0.archive.immich.app" "url": "https://v1.117.0.archive.immich.app"
@@ -151,14 +67,6 @@
"label": "v1.116.2", "label": "v1.116.2",
"url": "https://v1.116.2.archive.immich.app" "url": "https://v1.116.2.archive.immich.app"
}, },
{
"label": "v1.116.1",
"url": "https://v1.116.1.archive.immich.app"
},
{
"label": "v1.116.0",
"url": "https://v1.116.0.archive.immich.app"
},
{ {
"label": "v1.115.0", "label": "v1.115.0",
"url": "https://v1.115.0.archive.immich.app" "url": "https://v1.115.0.archive.immich.app"
@@ -171,18 +79,10 @@
"label": "v1.113.1", "label": "v1.113.1",
"url": "https://v1.113.1.archive.immich.app" "url": "https://v1.113.1.archive.immich.app"
}, },
{
"label": "v1.113.0",
"url": "https://v1.113.0.archive.immich.app"
},
{ {
"label": "v1.112.1", "label": "v1.112.1",
"url": "https://v1.112.1.archive.immich.app" "url": "https://v1.112.1.archive.immich.app"
}, },
{
"label": "v1.112.0",
"url": "https://v1.112.0.archive.immich.app"
},
{ {
"label": "v1.111.0", "label": "v1.111.0",
"url": "https://v1.111.0.archive.immich.app" "url": "https://v1.111.0.archive.immich.app"
@@ -195,14 +95,6 @@
"label": "v1.109.2", "label": "v1.109.2",
"url": "https://v1.109.2.archive.immich.app" "url": "https://v1.109.2.archive.immich.app"
}, },
{
"label": "v1.109.1",
"url": "https://v1.109.1.archive.immich.app"
},
{
"label": "v1.109.0",
"url": "https://v1.109.0.archive.immich.app"
},
{ {
"label": "v1.108.0", "label": "v1.108.0",
"url": "https://v1.108.0.archive.immich.app" "url": "https://v1.108.0.archive.immich.app"
@@ -211,38 +103,14 @@
"label": "v1.107.2", "label": "v1.107.2",
"url": "https://v1.107.2.archive.immich.app" "url": "https://v1.107.2.archive.immich.app"
}, },
{
"label": "v1.107.1",
"url": "https://v1.107.1.archive.immich.app"
},
{
"label": "v1.107.0",
"url": "https://v1.107.0.archive.immich.app"
},
{ {
"label": "v1.106.4", "label": "v1.106.4",
"url": "https://v1.106.4.archive.immich.app" "url": "https://v1.106.4.archive.immich.app"
}, },
{
"label": "v1.106.3",
"url": "https://v1.106.3.archive.immich.app"
},
{
"label": "v1.106.2",
"url": "https://v1.106.2.archive.immich.app"
},
{
"label": "v1.106.1",
"url": "https://v1.106.1.archive.immich.app"
},
{ {
"label": "v1.105.1", "label": "v1.105.1",
"url": "https://v1.105.1.archive.immich.app" "url": "https://v1.105.1.archive.immich.app"
}, },
{
"label": "v1.105.0",
"url": "https://v1.105.0.archive.immich.app"
},
{ {
"label": "v1.104.0", "label": "v1.104.0",
"url": "https://v1.104.0.archive.immich.app" "url": "https://v1.104.0.archive.immich.app"
@@ -251,26 +119,10 @@
"label": "v1.103.1", "label": "v1.103.1",
"url": "https://v1.103.1.archive.immich.app" "url": "https://v1.103.1.archive.immich.app"
}, },
{
"label": "v1.103.0",
"url": "https://v1.103.0.archive.immich.app"
},
{ {
"label": "v1.102.3", "label": "v1.102.3",
"url": "https://v1.102.3.archive.immich.app" "url": "https://v1.102.3.archive.immich.app"
}, },
{
"label": "v1.102.2",
"url": "https://v1.102.2.archive.immich.app"
},
{
"label": "v1.102.1",
"url": "https://v1.102.1.archive.immich.app"
},
{
"label": "v1.102.0",
"url": "https://v1.102.0.archive.immich.app"
},
{ {
"label": "v1.101.0", "label": "v1.101.0",
"url": "https://v1.101.0.archive.immich.app" "url": "https://v1.101.0.archive.immich.app"

Binary file not shown.

BIN
docs/static/fonts/overpass/Overpass.ttf vendored Normal file

Binary file not shown.

Binary file not shown.

13
docs/static/img/download-apk-github.svg vendored Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 75 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 75 KiB

View File

@@ -34,11 +34,11 @@ services:
- 2285:2285 - 2285:2285
redis: redis:
image: redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8 image: redis:6.2-alpine@sha256:3211c33a618c457e5d241922c975dbc4f446d0bdb2dc75694f5573ef8e2d01fa
database: database:
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52 image: tensorchord/vchord-postgres:pg14-v0.3.0
command: -c fsync=off -c shared_preload_libraries=vectors.so command: -c fsync=off -c shared_preload_libraries=vchord.so
environment: environment:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres POSTGRES_USER: postgres

3267
e2e/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.131.3", "version": "1.132.3",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
@@ -25,7 +25,7 @@
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1", "@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/node": "^22.14.0", "@types/node": "^22.14.1",
"@types/oidc-provider": "^8.5.1", "@types/oidc-provider": "^8.5.1",
"@types/pg": "^8.11.0", "@types/pg": "^8.11.0",
"@types/pngjs": "^6.0.4", "@types/pngjs": "^6.0.4",

View File

@@ -46,38 +46,6 @@ describe('/activities', () => {
}); });
describe('GET /activities', () => { describe('GET /activities', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/activities');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require an albumId', async () => {
const { status, body } = await request(app)
.get('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
});
it('should reject an invalid albumId', async () => {
const { status, body } = await request(app)
.get('/activities')
.query({ albumId: uuidDto.invalid })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
});
it('should reject an invalid assetId', async () => {
const { status, body } = await request(app)
.get('/activities')
.query({ albumId: uuidDto.notFound, assetId: uuidDto.invalid })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID'])));
});
it('should start off empty', async () => { it('should start off empty', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/activities') .get('/activities')
@@ -192,30 +160,6 @@ describe('/activities', () => {
}); });
describe('POST /activities', () => { describe('POST /activities', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/activities');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require an albumId', async () => {
const { status, body } = await request(app)
.post('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: uuidDto.invalid });
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
});
it('should require a comment when type is comment', async () => {
const { status, body } = await request(app)
.post('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: uuidDto.notFound, type: 'comment', comment: null });
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(['comment must be a string', 'comment should not be empty']));
});
it('should add a comment to an album', async () => { it('should add a comment to an album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/activities') .post('/activities')
@@ -330,20 +274,6 @@ describe('/activities', () => {
}); });
describe('DELETE /activities/:id', () => { describe('DELETE /activities/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/activities/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid uuid', async () => {
const { status, body } = await request(app)
.delete(`/activities/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
});
it('should remove a comment from an album', async () => { it('should remove a comment from an album', async () => {
const reaction = await createActivity({ const reaction = await createActivity({
albumId: album.id, albumId: album.id,

View File

@@ -9,7 +9,7 @@ import {
LoginResponseDto, LoginResponseDto,
SharedLinkType, SharedLinkType,
} from '@immich/sdk'; } from '@immich/sdk';
import { createUserDto, uuidDto } from 'src/fixtures'; import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils'; import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
@@ -128,28 +128,6 @@ describe('/albums', () => {
}); });
describe('GET /albums', () => { describe('GET /albums', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/albums');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should reject an invalid shared param', async () => {
const { status, body } = await request(app)
.get('/albums?shared=invalid')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(['shared must be a boolean value']));
});
it('should reject an invalid assetId param', async () => {
const { status, body } = await request(app)
.get('/albums?assetId=invalid')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(['assetId must be a UUID']));
});
it("should not show other users' favorites", async () => { it("should not show other users' favorites", async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/albums/${user1Albums[0].id}?withoutAssets=false`) .get(`/albums/${user1Albums[0].id}?withoutAssets=false`)
@@ -323,12 +301,6 @@ describe('/albums', () => {
}); });
describe('GET /albums/:id', () => { describe('GET /albums/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/albums/${user1Albums[0].id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return album info for own album', async () => { it('should return album info for own album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/albums/${user1Albums[0].id}?withoutAssets=false`) .get(`/albums/${user1Albums[0].id}?withoutAssets=false`)
@@ -421,12 +393,6 @@ describe('/albums', () => {
}); });
describe('GET /albums/statistics', () => { describe('GET /albums/statistics', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/albums/statistics');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return total count of albums the user has access to', async () => { it('should return total count of albums the user has access to', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/albums/statistics') .get('/albums/statistics')
@@ -438,12 +404,6 @@ describe('/albums', () => {
}); });
describe('POST /albums', () => { describe('POST /albums', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/albums').send({ albumName: 'New album' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should create an album', async () => { it('should create an album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/albums') .post('/albums')
@@ -471,12 +431,6 @@ describe('/albums', () => {
}); });
describe('PUT /albums/:id/assets', () => { describe('PUT /albums/:id/assets', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/albums/${user1Albums[0].id}/assets`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should be able to add own asset to own album', async () => { it('should be able to add own asset to own album', async () => {
const asset = await utils.createAsset(user1.accessToken); const asset = await utils.createAsset(user1.accessToken);
const { status, body } = await request(app) const { status, body } = await request(app)
@@ -526,14 +480,6 @@ describe('/albums', () => {
}); });
describe('PATCH /albums/:id', () => { describe('PATCH /albums/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.patch(`/albums/${uuidDto.notFound}`)
.send({ albumName: 'New album name' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should update an album', async () => { it('should update an album', async () => {
const album = await utils.createAlbum(user1.accessToken, { const album = await utils.createAlbum(user1.accessToken, {
albumName: 'New album', albumName: 'New album',
@@ -576,15 +522,6 @@ describe('/albums', () => {
}); });
describe('DELETE /albums/:id/assets', () => { describe('DELETE /albums/:id/assets', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.delete(`/albums/${user1Albums[0].id}/assets`)
.send({ ids: [user1Asset1.id] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => { it('should require authorization', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/albums/${user1Albums[1].id}/assets`) .delete(`/albums/${user1Albums[1].id}/assets`)
@@ -679,13 +616,6 @@ describe('/albums', () => {
}); });
}); });
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/albums/${user1Albums[0].id}/users`).send({ sharedUserIds: [] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should be able to add user to own album', async () => { it('should be able to add user to own album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/albums/${album.id}/users`) .put(`/albums/${album.id}/users`)

View File

@@ -1,5 +1,5 @@
import { LoginResponseDto, Permission, createApiKey } from '@immich/sdk'; import { LoginResponseDto, Permission, createApiKey } from '@immich/sdk';
import { createUserDto, uuidDto } from 'src/fixtures'; import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils'; import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
@@ -24,12 +24,6 @@ describe('/api-keys', () => {
}); });
describe('POST /api-keys', () => { describe('POST /api-keys', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/api-keys').send({ name: 'API Key' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should not work without permission', async () => { it('should not work without permission', async () => {
const { secret } = await create(user.accessToken, [Permission.ApiKeyRead]); const { secret } = await create(user.accessToken, [Permission.ApiKeyRead]);
const { status, body } = await request(app).post('/api-keys').set('x-api-key', secret).send({ name: 'API Key' }); const { status, body } = await request(app).post('/api-keys').set('x-api-key', secret).send({ name: 'API Key' });
@@ -99,12 +93,6 @@ describe('/api-keys', () => {
}); });
describe('GET /api-keys', () => { describe('GET /api-keys', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/api-keys');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should start off empty', async () => { it('should start off empty', async () => {
const { status, body } = await request(app).get('/api-keys').set('Authorization', `Bearer ${admin.accessToken}`); const { status, body } = await request(app).get('/api-keys').set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual([]); expect(body).toEqual([]);
@@ -125,12 +113,6 @@ describe('/api-keys', () => {
}); });
describe('GET /api-keys/:id', () => { describe('GET /api-keys/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/api-keys/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => { it('should require authorization', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]); const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app) const { status, body } = await request(app)
@@ -140,14 +122,6 @@ describe('/api-keys', () => {
expect(body).toEqual(errorDto.badRequest('API Key not found')); expect(body).toEqual(errorDto.badRequest('API Key not found'));
}); });
it('should require a valid uuid', async () => {
const { status, body } = await request(app)
.get(`/api-keys/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
});
it('should get api key details', async () => { it('should get api key details', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]); const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app) const { status, body } = await request(app)
@@ -165,12 +139,6 @@ describe('/api-keys', () => {
}); });
describe('PUT /api-keys/:id', () => { describe('PUT /api-keys/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/api-keys/${uuidDto.notFound}`).send({ name: 'new name' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => { it('should require authorization', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]); const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app) const { status, body } = await request(app)
@@ -181,15 +149,6 @@ describe('/api-keys', () => {
expect(body).toEqual(errorDto.badRequest('API Key not found')); expect(body).toEqual(errorDto.badRequest('API Key not found'));
}); });
it('should require a valid uuid', async () => {
const { status, body } = await request(app)
.put(`/api-keys/${uuidDto.invalid}`)
.send({ name: 'new name' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
});
it('should update api key details', async () => { it('should update api key details', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]); const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app) const { status, body } = await request(app)
@@ -208,12 +167,6 @@ describe('/api-keys', () => {
}); });
describe('DELETE /api-keys/:id', () => { describe('DELETE /api-keys/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/api-keys/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => { it('should require authorization', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]); const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app) const { status, body } = await request(app)
@@ -223,14 +176,6 @@ describe('/api-keys', () => {
expect(body).toEqual(errorDto.badRequest('API Key not found')); expect(body).toEqual(errorDto.badRequest('API Key not found'));
}); });
it('should require a valid uuid', async () => {
const { status, body } = await request(app)
.delete(`/api-keys/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
});
it('should delete an api key', async () => { it('should delete an api key', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]); const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status } = await request(app) const { status } = await request(app)

View File

@@ -3,6 +3,7 @@ import {
AssetMediaStatus, AssetMediaStatus,
AssetResponseDto, AssetResponseDto,
AssetTypeEnum, AssetTypeEnum,
AssetVisibility,
getAssetInfo, getAssetInfo,
getMyUser, getMyUser,
LoginResponseDto, LoginResponseDto,
@@ -22,27 +23,9 @@ import { app, asBearerAuth, tempDir, TEN_TIMES, testAssetDir, utils } from 'src/
import request from 'supertest'; import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const makeUploadDto = (options?: { omit: string }): Record<string, any> => {
const dto: Record<string, any> = {
deviceAssetId: 'example-image',
deviceId: 'TEST',
fileCreatedAt: new Date().toISOString(),
fileModifiedAt: new Date().toISOString(),
isFavorite: 'testing',
duration: '0:00:00.000000',
};
const omit = options?.omit;
if (omit) {
delete dto[omit];
}
return dto;
};
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`; const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`; const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`;
const facesAssetFilepath = `${testAssetDir}/metadata/faces/portrait.jpg`; const facesAssetDir = `${testAssetDir}/metadata/faces`;
const readTags = async (bytes: Buffer, filename: string) => { const readTags = async (bytes: Buffer, filename: string) => {
const filepath = join(tempDir, filename); const filepath = join(tempDir, filename);
@@ -137,9 +120,9 @@ describe('/asset', () => {
// stats // stats
utils.createAsset(statsUser.accessToken), utils.createAsset(statsUser.accessToken),
utils.createAsset(statsUser.accessToken, { isFavorite: true }), utils.createAsset(statsUser.accessToken, { isFavorite: true }),
utils.createAsset(statsUser.accessToken, { isArchived: true }), utils.createAsset(statsUser.accessToken, { visibility: AssetVisibility.Archive }),
utils.createAsset(statsUser.accessToken, { utils.createAsset(statsUser.accessToken, {
isArchived: true, visibility: AssetVisibility.Archive,
isFavorite: true, isFavorite: true,
assetData: { filename: 'example.mp4' }, assetData: { filename: 'example.mp4' },
}), }),
@@ -160,13 +143,6 @@ describe('/asset', () => {
}); });
describe('GET /assets/:id/original', () => { describe('GET /assets/:id/original', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/assets/${uuidDto.notFound}/original`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should download the file', async () => { it('should download the file', async () => {
const response = await request(app) const response = await request(app)
.get(`/assets/${user1Assets[0].id}/original`) .get(`/assets/${user1Assets[0].id}/original`)
@@ -178,20 +154,6 @@ describe('/asset', () => {
}); });
describe('GET /assets/:id', () => { describe('GET /assets/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/assets/${uuidDto.notFound}`);
expect(body).toEqual(errorDto.unauthorized);
expect(status).toBe(401);
});
it('should require a valid id', async () => {
const { status, body } = await request(app)
.get(`/assets/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
});
it('should require access', async () => { it('should require access', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/assets/${user2Assets[0].id}`) .get(`/assets/${user2Assets[0].id}`)
@@ -224,27 +186,19 @@ describe('/asset', () => {
}); });
}); });
it('should get the asset faces', async () => { describe('faces', () => {
const config = await utils.getSystemConfig(admin.accessToken); const metadataFaceTests = [
config.metadata.faces.import = true; {
await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) }); description: 'without orientation',
// asset faces
const facesAsset = await utils.createAsset(admin.accessToken, {
assetData: {
filename: 'portrait.jpg', filename: 'portrait.jpg',
bytes: await readFile(facesAssetFilepath),
}, },
}); {
description: 'adjusting face regions to orientation',
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: facesAsset.id }); filename: 'portrait-orientation-6.jpg',
},
const { status, body } = await request(app) ];
.get(`/assets/${facesAsset.id}`) // should produce same resulting face region coordinates for any orientation
.set('Authorization', `Bearer ${admin.accessToken}`); const expectedFaces = [
expect(status).toBe(200);
expect(body.id).toEqual(facesAsset.id);
expect(body.people).toMatchObject([
{ {
name: 'Marie Curie', name: 'Marie Curie',
birthDate: null, birthDate: null,
@@ -279,7 +233,30 @@ describe('/asset', () => {
}, },
], ],
}, },
]); ];
it.each(metadataFaceTests)('should get the asset faces from $filename $description', async ({ filename }) => {
const config = await utils.getSystemConfig(admin.accessToken);
config.metadata.faces.import = true;
await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) });
const facesAsset = await utils.createAsset(admin.accessToken, {
assetData: {
filename,
bytes: await readFile(`${facesAssetDir}/${filename}`),
},
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: facesAsset.id });
const { status, body } = await request(app)
.get(`/assets/${facesAsset.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body.id).toEqual(facesAsset.id);
expect(body.people).toMatchObject(expectedFaces);
});
}); });
it('should work with a shared link', async () => { it('should work with a shared link', async () => {
@@ -333,7 +310,7 @@ describe('/asset', () => {
}); });
it('disallows viewing archived assets', async () => { it('disallows viewing archived assets', async () => {
const asset = await utils.createAsset(user1.accessToken, { isArchived: true }); const asset = await utils.createAsset(user1.accessToken, { visibility: AssetVisibility.Archive });
const { status } = await request(app) const { status } = await request(app)
.get(`/assets/${asset.id}`) .get(`/assets/${asset.id}`)
@@ -354,13 +331,6 @@ describe('/asset', () => {
}); });
describe('GET /assets/statistics', () => { describe('GET /assets/statistics', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/assets/statistics');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return stats of all assets', async () => { it('should return stats of all assets', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/assets/statistics') .get('/assets/statistics')
@@ -384,7 +354,7 @@ describe('/asset', () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/assets/statistics') .get('/assets/statistics')
.set('Authorization', `Bearer ${statsUser.accessToken}`) .set('Authorization', `Bearer ${statsUser.accessToken}`)
.query({ isArchived: true }); .query({ visibility: AssetVisibility.Archive });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual({ images: 1, videos: 1, total: 2 }); expect(body).toEqual({ images: 1, videos: 1, total: 2 });
@@ -394,7 +364,7 @@ describe('/asset', () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/assets/statistics') .get('/assets/statistics')
.set('Authorization', `Bearer ${statsUser.accessToken}`) .set('Authorization', `Bearer ${statsUser.accessToken}`)
.query({ isFavorite: true, isArchived: true }); .query({ isFavorite: true, visibility: AssetVisibility.Archive });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual({ images: 0, videos: 1, total: 1 }); expect(body).toEqual({ images: 0, videos: 1, total: 1 });
@@ -404,7 +374,7 @@ describe('/asset', () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/assets/statistics') .get('/assets/statistics')
.set('Authorization', `Bearer ${statsUser.accessToken}`) .set('Authorization', `Bearer ${statsUser.accessToken}`)
.query({ isFavorite: false, isArchived: false }); .query({ isFavorite: false, visibility: AssetVisibility.Timeline });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual({ images: 1, videos: 0, total: 1 }); expect(body).toEqual({ images: 1, videos: 0, total: 1 });
@@ -425,13 +395,6 @@ describe('/asset', () => {
await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration'); await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration');
}); });
it('should require authentication', async () => {
const { status, body } = await request(app).get('/assets/random');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it.each(TEN_TIMES)('should return 1 random assets', async () => { it.each(TEN_TIMES)('should return 1 random assets', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/assets/random') .get('/assets/random')
@@ -467,31 +430,9 @@ describe('/asset', () => {
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: user2Assets[0].id })]); expect(body).toEqual([expect.objectContaining({ id: user2Assets[0].id })]);
}); });
it('should return error', async () => {
const { status } = await request(app)
.get('/assets/random?count=ABC')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
});
}); });
describe('PUT /assets/:id', () => { describe('PUT /assets/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/assets/:${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid id', async () => {
const { status, body } = await request(app)
.put(`/assets/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
});
it('should require access', async () => { it('should require access', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/assets/${user2Assets[0].id}`) .put(`/assets/${user2Assets[0].id}`)
@@ -519,7 +460,7 @@ describe('/asset', () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`) .put(`/assets/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ isArchived: true }); .send({ visibility: AssetVisibility.Archive });
expect(body).toMatchObject({ id: user1Assets[0].id, isArchived: true }); expect(body).toMatchObject({ id: user1Assets[0].id, isArchived: true });
expect(status).toEqual(200); expect(status).toEqual(200);
}); });
@@ -619,28 +560,6 @@ describe('/asset', () => {
expect(status).toEqual(200); expect(status).toEqual(200);
}); });
it('should reject invalid gps coordinates', async () => {
for (const test of [
{ latitude: 12 },
{ longitude: 12 },
{ latitude: 12, longitude: 'abc' },
{ latitude: 'abc', longitude: 12 },
{ latitude: null, longitude: 12 },
{ latitude: 12, longitude: null },
{ latitude: 91, longitude: 12 },
{ latitude: -91, longitude: 12 },
{ latitude: 12, longitude: -181 },
{ latitude: 12, longitude: 181 },
]) {
const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`)
.send(test)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
}
});
it('should update gps data', async () => { it('should update gps data', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`) .put(`/assets/${user1Assets[0].id}`)
@@ -712,17 +631,6 @@ describe('/asset', () => {
expect(status).toEqual(200); expect(status).toEqual(200);
}); });
it('should reject invalid rating', async () => {
for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: null }]) {
const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`)
.send(test)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
}
});
it('should return tagged people', async () => { it('should return tagged people', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`) .put(`/assets/${user1Assets[0].id}`)
@@ -746,25 +654,6 @@ describe('/asset', () => {
}); });
describe('DELETE /assets', () => { describe('DELETE /assets', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.delete(`/assets`)
.send({ ids: [uuidDto.notFound] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid uuid', async () => {
const { status, body } = await request(app)
.delete(`/assets`)
.send({ ids: [uuidDto.invalid] })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID']));
});
it('should throw an error when the id is not found', async () => { it('should throw an error when the id is not found', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/assets`) .delete(`/assets`)
@@ -877,13 +766,6 @@ describe('/asset', () => {
}); });
describe('GET /assets/:id/thumbnail', () => { describe('GET /assets/:id/thumbnail', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/assets/${locationAsset.id}/thumbnail`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should not include gps data for webp thumbnails', async () => { it('should not include gps data for webp thumbnails', async () => {
await utils.waitForWebsocketEvent({ await utils.waitForWebsocketEvent({
event: 'assetUpload', event: 'assetUpload',
@@ -919,13 +801,6 @@ describe('/asset', () => {
}); });
describe('GET /assets/:id/original', () => { describe('GET /assets/:id/original', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/assets/${locationAsset.id}/original`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should download the original', async () => { it('should download the original', async () => {
const { status, body, type } = await request(app) const { status, body, type } = await request(app)
.get(`/assets/${locationAsset.id}/original`) .get(`/assets/${locationAsset.id}/original`)
@@ -946,43 +821,9 @@ describe('/asset', () => {
}); });
}); });
describe('PUT /assets', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put('/assets');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
});
describe('POST /assets', () => { describe('POST /assets', () => {
beforeAll(setupTests, 30_000); beforeAll(setupTests, 30_000);
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/assets`);
expect(body).toEqual(errorDto.unauthorized);
expect(status).toBe(401);
});
it.each([
{ should: 'require `deviceAssetId`', dto: { ...makeUploadDto({ omit: 'deviceAssetId' }) } },
{ should: 'require `deviceId`', dto: { ...makeUploadDto({ omit: 'deviceId' }) } },
{ should: 'require `fileCreatedAt`', dto: { ...makeUploadDto({ omit: 'fileCreatedAt' }) } },
{ should: 'require `fileModifiedAt`', dto: { ...makeUploadDto({ omit: 'fileModifiedAt' }) } },
{ should: 'require `duration`', dto: { ...makeUploadDto({ omit: 'duration' }) } },
{ should: 'throw if `isFavorite` is not a boolean', dto: { ...makeUploadDto(), isFavorite: 'not-a-boolean' } },
{ should: 'throw if `isVisible` is not a boolean', dto: { ...makeUploadDto(), isVisible: 'not-a-boolean' } },
{ should: 'throw if `isArchived` is not a boolean', dto: { ...makeUploadDto(), isArchived: 'not-a-boolean' } },
])('should $should', async ({ dto }) => {
const { status, body } = await request(app)
.post('/assets')
.set('Authorization', `Bearer ${user1.accessToken}`)
.attach('assetData', makeRandomImage(), 'example.png')
.field(dto);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
const tests = [ const tests = [
{ {
input: 'formats/avif/8bit-sRGB.avif', input: 'formats/avif/8bit-sRGB.avif',
@@ -1244,31 +1085,21 @@ describe('/asset', () => {
}, },
]; ];
it(`should upload and generate a thumbnail for different file types`, async () => { it.each(tests)(`should upload and generate a thumbnail for different file types`, async ({ input, expected }) => {
// upload in parallel const filepath = join(testAssetDir, input);
const assets = await Promise.all( const response = await utils.createAsset(admin.accessToken, {
tests.map(async ({ input }) => { assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
const filepath = join(testAssetDir, input); });
return utils.createAsset(admin.accessToken, {
assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
});
}),
);
for (const { id, status } of assets) { expect(response.status).toBe(AssetMediaStatus.Created);
expect(status).toBe(AssetMediaStatus.Created); const id = response.id;
// longer timeout as the thumbnail generation from full-size raw files can take a while // longer timeout as the thumbnail generation from full-size raw files can take a while
await utils.waitForWebsocketEvent({ event: 'assetUpload', id }); await utils.waitForWebsocketEvent({ event: 'assetUpload', id });
}
for (const [i, { id }] of assets.entries()) { const asset = await utils.getAssetInfo(admin.accessToken, id);
const { expected } = tests[i]; expect(asset.exifInfo).toBeDefined();
const asset = await utils.getAssetInfo(admin.accessToken, id); expect(asset.exifInfo).toMatchObject(expected.exifInfo);
expect(asset).toMatchObject(expected);
expect(asset.exifInfo).toBeDefined();
expect(asset.exifInfo).toMatchObject(expected.exifInfo);
expect(asset).toMatchObject(expected);
}
}); });
it('should handle a duplicate', async () => { it('should handle a duplicate', async () => {

View File

@@ -1,43 +0,0 @@
import { deleteAssets, getAuditFiles, updateAsset, type LoginResponseDto } from '@immich/sdk';
import { asBearerAuth, utils } from 'src/utils';
import { beforeAll, describe, expect, it } from 'vitest';
describe('/audits', () => {
let admin: LoginResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
await utils.resetFilesystem();
admin = await utils.adminSetup();
});
// TODO: Enable these tests again once #7436 is resolved as these were flaky
describe.skip('GET :/file-report', () => {
it('excludes assets without issues from report', async () => {
const [trashedAsset, archivedAsset] = await Promise.all([
utils.createAsset(admin.accessToken),
utils.createAsset(admin.accessToken),
utils.createAsset(admin.accessToken),
]);
await Promise.all([
deleteAssets({ assetBulkDeleteDto: { ids: [trashedAsset.id] } }, { headers: asBearerAuth(admin.accessToken) }),
updateAsset(
{
id: archivedAsset.id,
updateAssetDto: { isArchived: true },
},
{ headers: asBearerAuth(admin.accessToken) },
),
]);
const body = await getAuditFiles({
headers: asBearerAuth(admin.accessToken),
});
expect(body.orphans).toHaveLength(0);
expect(body.extras).toHaveLength(0);
});
});
});

View File

@@ -5,7 +5,7 @@ import { app, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { beforeEach, describe, expect, it } from 'vitest'; import { beforeEach, describe, expect, it } from 'vitest';
const { name, email, password } = signupDto.admin; const { email, password } = signupDto.admin;
describe(`/auth/admin-sign-up`, () => { describe(`/auth/admin-sign-up`, () => {
beforeEach(async () => { beforeEach(async () => {
@@ -13,58 +13,12 @@ describe(`/auth/admin-sign-up`, () => {
}); });
describe('POST /auth/admin-sign-up', () => { describe('POST /auth/admin-sign-up', () => {
const invalid = [
{
should: 'require an email address',
data: { name, password },
},
{
should: 'require a password',
data: { name, email },
},
{
should: 'require a name',
data: { email, password },
},
{
should: 'require a valid email',
data: { name, email: 'immich', password },
},
];
for (const { should, data } of invalid) {
it(`should ${should}`, async () => {
const { status, body } = await request(app).post('/auth/admin-sign-up').send(data);
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest());
});
}
it(`should sign up the admin`, async () => { it(`should sign up the admin`, async () => {
const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin); const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin);
expect(status).toBe(201); expect(status).toBe(201);
expect(body).toEqual(signupResponseDto.admin); expect(body).toEqual(signupResponseDto.admin);
}); });
it('should sign up the admin with a local domain', async () => {
const { status, body } = await request(app)
.post('/auth/admin-sign-up')
.send({ ...signupDto.admin, email: 'admin@local' });
expect(status).toEqual(201);
expect(body).toEqual({
...signupResponseDto.admin,
email: 'admin@local',
});
});
it('should transform email to lower case', async () => {
const { status, body } = await request(app)
.post('/auth/admin-sign-up')
.send({ ...signupDto.admin, email: 'aDmIn@IMMICH.cloud' });
expect(status).toEqual(201);
expect(body).toEqual(signupResponseDto.admin);
});
it('should not allow a second admin to sign up', async () => { it('should not allow a second admin to sign up', async () => {
await signUpAdmin({ signUpDto: signupDto.admin }); await signUpAdmin({ signUpDto: signupDto.admin });
@@ -92,22 +46,6 @@ describe('/auth/*', () => {
expect(body).toEqual(errorDto.incorrectLogin); expect(body).toEqual(errorDto.incorrectLogin);
}); });
for (const key of Object.keys(loginDto.admin)) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
.post('/auth/login')
.send({ ...loginDto.admin, [key]: null });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
it('should reject an invalid email', async () => {
const { status, body } = await request(app).post('/auth/login').send({ email: [], password });
expect(status).toBe(400);
expect(body).toEqual(errorDto.invalidEmail);
});
}
it('should accept a correct password', async () => { it('should accept a correct password', async () => {
const { status, body, headers } = await request(app).post('/auth/login').send({ email, password }); const { status, body, headers } = await request(app).post('/auth/login').send({ email, password });
expect(status).toBe(201); expect(status).toBe(201);
@@ -162,14 +100,6 @@ describe('/auth/*', () => {
}); });
describe('POST /auth/change-password', () => { describe('POST /auth/change-password', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.post(`/auth/change-password`)
.send({ password, newPassword: 'Password1234' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require the current password', async () => { it('should require the current password', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post(`/auth/change-password`) .post(`/auth/change-password`)

View File

@@ -1,6 +1,5 @@
import { AssetMediaResponseDto, LoginResponseDto } from '@immich/sdk'; import { AssetMediaResponseDto, LoginResponseDto } from '@immich/sdk';
import { readFile, writeFile } from 'node:fs/promises'; import { readFile, writeFile } from 'node:fs/promises';
import { errorDto } from 'src/responses';
import { app, tempDir, utils } from 'src/utils'; import { app, tempDir, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest'; import { beforeAll, describe, expect, it } from 'vitest';
@@ -17,15 +16,6 @@ describe('/download', () => {
}); });
describe('POST /download/info', () => { describe('POST /download/info', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.post(`/download/info`)
.send({ assetIds: [asset1.id] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should download info', async () => { it('should download info', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/download/info') .post('/download/info')
@@ -42,15 +32,6 @@ describe('/download', () => {
}); });
describe('POST /download/archive', () => { describe('POST /download/archive', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.post(`/download/archive`)
.send({ assetIds: [asset1.id, asset2.id] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should download an archive', async () => { it('should download an archive', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/download/archive') .post('/download/archive')

View File

@@ -1,4 +1,4 @@
import { LoginResponseDto } from '@immich/sdk'; import { AssetVisibility, LoginResponseDto } from '@immich/sdk';
import { readFile } from 'node:fs/promises'; import { readFile } from 'node:fs/promises';
import { basename, join } from 'node:path'; import { basename, join } from 'node:path';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
@@ -44,7 +44,7 @@ describe('/map', () => {
it('should get map markers for all non-archived assets', async () => { it('should get map markers for all non-archived assets', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/map/markers') .get('/map/markers')
.query({ isArchived: false }) .query({ visibility: AssetVisibility.Timeline })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);

View File

@@ -6,6 +6,7 @@ import {
startOAuth, startOAuth,
updateConfig, updateConfig,
} from '@immich/sdk'; } from '@immich/sdk';
import { createHash, randomBytes } from 'node:crypto';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
import { OAuthClient, OAuthUser } from 'src/setup/auth-server'; import { OAuthClient, OAuthUser } from 'src/setup/auth-server';
import { app, asBearerAuth, baseUrl, utils } from 'src/utils'; import { app, asBearerAuth, baseUrl, utils } from 'src/utils';
@@ -21,18 +22,30 @@ const mobileOverrideRedirectUri = 'https://photos.immich.app/oauth/mobile-redire
const redirect = async (url: string, cookies?: string[]) => { const redirect = async (url: string, cookies?: string[]) => {
const { headers } = await request(url) const { headers } = await request(url)
.get('/') .get('')
.set('Cookie', cookies || []); .set('Cookie', cookies || []);
return { cookies: (headers['set-cookie'] as unknown as string[]) || [], location: headers.location }; return { cookies: (headers['set-cookie'] as unknown as string[]) || [], location: headers.location };
}; };
// Function to generate a code challenge from the verifier
const generateCodeChallenge = async (codeVerifier: string): Promise<string> => {
const hashed = createHash('sha256').update(codeVerifier).digest();
return hashed.toString('base64url');
};
const loginWithOAuth = async (sub: OAuthUser | string, redirectUri?: string) => { const loginWithOAuth = async (sub: OAuthUser | string, redirectUri?: string) => {
const { url } = await startOAuth({ oAuthConfigDto: { redirectUri: redirectUri ?? `${baseUrl}/auth/login` } }); const state = randomBytes(16).toString('base64url');
const codeVerifier = randomBytes(64).toString('base64url');
const codeChallenge = await generateCodeChallenge(codeVerifier);
const { url } = await startOAuth({
oAuthConfigDto: { redirectUri: redirectUri ?? `${baseUrl}/auth/login`, state, codeChallenge },
});
// login // login
const response1 = await redirect(url.replace(authServer.internal, authServer.external)); const response1 = await redirect(url.replace(authServer.internal, authServer.external));
const response2 = await request(authServer.external + response1.location) const response2 = await request(authServer.external + response1.location)
.post('/') .post('')
.set('Cookie', response1.cookies) .set('Cookie', response1.cookies)
.type('form') .type('form')
.send({ prompt: 'login', login: sub, password: 'password' }); .send({ prompt: 'login', login: sub, password: 'password' });
@@ -40,7 +53,7 @@ const loginWithOAuth = async (sub: OAuthUser | string, redirectUri?: string) =>
// approve // approve
const response3 = await redirect(response2.header.location, response1.cookies); const response3 = await redirect(response2.header.location, response1.cookies);
const response4 = await request(authServer.external + response3.location) const response4 = await request(authServer.external + response3.location)
.post('/') .post('')
.type('form') .type('form')
.set('Cookie', response3.cookies) .set('Cookie', response3.cookies)
.send({ prompt: 'consent' }); .send({ prompt: 'consent' });
@@ -51,9 +64,9 @@ const loginWithOAuth = async (sub: OAuthUser | string, redirectUri?: string) =>
expect(redirectUrl).toBeDefined(); expect(redirectUrl).toBeDefined();
const params = new URL(redirectUrl).searchParams; const params = new URL(redirectUrl).searchParams;
expect(params.get('code')).toBeDefined(); expect(params.get('code')).toBeDefined();
expect(params.get('state')).toBeDefined(); expect(params.get('state')).toBe(state);
return redirectUrl; return { url: redirectUrl, state, codeVerifier };
}; };
const setupOAuth = async (token: string, dto: Partial<SystemConfigOAuthDto>) => { const setupOAuth = async (token: string, dto: Partial<SystemConfigOAuthDto>) => {
@@ -119,9 +132,42 @@ describe(`/oauth`, () => {
expect(body).toEqual(errorDto.badRequest(['url should not be empty'])); expect(body).toEqual(errorDto.badRequest(['url should not be empty']));
}); });
it('should auto register the user by default', async () => { it(`should throw an error if the state is not provided`, async () => {
const url = await loginWithOAuth('oauth-auto-register'); const { url } = await loginWithOAuth('oauth-auto-register');
const { status, body } = await request(app).post('/oauth/callback').send({ url }); const { status, body } = await request(app).post('/oauth/callback').send({ url });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('OAuth state is missing'));
});
it(`should throw an error if the state mismatches`, async () => {
const callbackParams = await loginWithOAuth('oauth-auto-register');
const { state } = await loginWithOAuth('oauth-auto-register');
const { status } = await request(app)
.post('/oauth/callback')
.send({ ...callbackParams, state });
expect(status).toBeGreaterThanOrEqual(400);
});
it(`should throw an error if the codeVerifier is not provided`, async () => {
const { url, state } = await loginWithOAuth('oauth-auto-register');
const { status, body } = await request(app).post('/oauth/callback').send({ url, state });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('OAuth code verifier is missing'));
});
it(`should throw an error if the codeVerifier doesn't match the challenge`, async () => {
const callbackParams = await loginWithOAuth('oauth-auto-register');
const { codeVerifier } = await loginWithOAuth('oauth-auto-register');
const { status, body } = await request(app)
.post('/oauth/callback')
.send({ ...callbackParams, codeVerifier });
console.log(body);
expect(status).toBeGreaterThanOrEqual(400);
});
it('should auto register the user by default', async () => {
const callbackParams = await loginWithOAuth('oauth-auto-register');
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
expect(status).toBe(201); expect(status).toBe(201);
expect(body).toMatchObject({ expect(body).toMatchObject({
accessToken: expect.any(String), accessToken: expect.any(String),
@@ -132,16 +178,30 @@ describe(`/oauth`, () => {
}); });
}); });
it('should allow passing state and codeVerifier via cookies', async () => {
const { url, state, codeVerifier } = await loginWithOAuth('oauth-auto-register');
const { status, body } = await request(app)
.post('/oauth/callback')
.set('Cookie', [`immich_oauth_state=${state}`, `immich_oauth_code_verifier=${codeVerifier}`])
.send({ url });
expect(status).toBe(201);
expect(body).toMatchObject({
accessToken: expect.any(String),
userId: expect.any(String),
userEmail: 'oauth-auto-register@immich.app',
});
});
it('should handle a user without an email', async () => { it('should handle a user without an email', async () => {
const url = await loginWithOAuth(OAuthUser.NO_EMAIL); const callbackParams = await loginWithOAuth(OAuthUser.NO_EMAIL);
const { status, body } = await request(app).post('/oauth/callback').send({ url }); const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('OAuth profile does not have an email address')); expect(body).toEqual(errorDto.badRequest('OAuth profile does not have an email address'));
}); });
it('should set the quota from a claim', async () => { it('should set the quota from a claim', async () => {
const url = await loginWithOAuth(OAuthUser.WITH_QUOTA); const callbackParams = await loginWithOAuth(OAuthUser.WITH_QUOTA);
const { status, body } = await request(app).post('/oauth/callback').send({ url }); const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
expect(status).toBe(201); expect(status).toBe(201);
expect(body).toMatchObject({ expect(body).toMatchObject({
accessToken: expect.any(String), accessToken: expect.any(String),
@@ -154,8 +214,8 @@ describe(`/oauth`, () => {
}); });
it('should set the storage label from a claim', async () => { it('should set the storage label from a claim', async () => {
const url = await loginWithOAuth(OAuthUser.WITH_USERNAME); const callbackParams = await loginWithOAuth(OAuthUser.WITH_USERNAME);
const { status, body } = await request(app).post('/oauth/callback').send({ url }); const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
expect(status).toBe(201); expect(status).toBe(201);
expect(body).toMatchObject({ expect(body).toMatchObject({
accessToken: expect.any(String), accessToken: expect.any(String),
@@ -176,8 +236,8 @@ describe(`/oauth`, () => {
buttonText: 'Login with Immich', buttonText: 'Login with Immich',
signingAlgorithm: 'RS256', signingAlgorithm: 'RS256',
}); });
const url = await loginWithOAuth('oauth-RS256-token'); const callbackParams = await loginWithOAuth('oauth-RS256-token');
const { status, body } = await request(app).post('/oauth/callback').send({ url }); const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
expect(status).toBe(201); expect(status).toBe(201);
expect(body).toMatchObject({ expect(body).toMatchObject({
accessToken: expect.any(String), accessToken: expect.any(String),
@@ -196,8 +256,8 @@ describe(`/oauth`, () => {
buttonText: 'Login with Immich', buttonText: 'Login with Immich',
profileSigningAlgorithm: 'RS256', profileSigningAlgorithm: 'RS256',
}); });
const url = await loginWithOAuth('oauth-signed-profile'); const callbackParams = await loginWithOAuth('oauth-signed-profile');
const { status, body } = await request(app).post('/oauth/callback').send({ url }); const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
expect(status).toBe(201); expect(status).toBe(201);
expect(body).toMatchObject({ expect(body).toMatchObject({
userId: expect.any(String), userId: expect.any(String),
@@ -213,8 +273,8 @@ describe(`/oauth`, () => {
buttonText: 'Login with Immich', buttonText: 'Login with Immich',
signingAlgorithm: 'something-that-does-not-work', signingAlgorithm: 'something-that-does-not-work',
}); });
const url = await loginWithOAuth('oauth-signed-bad'); const callbackParams = await loginWithOAuth('oauth-signed-bad');
const { status, body } = await request(app).post('/oauth/callback').send({ url }); const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
expect(status).toBe(500); expect(status).toBe(500);
expect(body).toMatchObject({ expect(body).toMatchObject({
error: 'Internal Server Error', error: 'Internal Server Error',
@@ -235,8 +295,8 @@ describe(`/oauth`, () => {
}); });
it('should not auto register the user', async () => { it('should not auto register the user', async () => {
const url = await loginWithOAuth('oauth-no-auto-register'); const callbackParams = await loginWithOAuth('oauth-no-auto-register');
const { status, body } = await request(app).post('/oauth/callback').send({ url }); const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('User does not exist and auto registering is disabled.')); expect(body).toEqual(errorDto.badRequest('User does not exist and auto registering is disabled.'));
}); });
@@ -247,8 +307,8 @@ describe(`/oauth`, () => {
email: 'oauth-user3@immich.app', email: 'oauth-user3@immich.app',
password: 'password', password: 'password',
}); });
const url = await loginWithOAuth('oauth-user3'); const callbackParams = await loginWithOAuth('oauth-user3');
const { status, body } = await request(app).post('/oauth/callback').send({ url }); const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
expect(status).toBe(201); expect(status).toBe(201);
expect(body).toMatchObject({ expect(body).toMatchObject({
userId, userId,
@@ -286,13 +346,15 @@ describe(`/oauth`, () => {
}); });
it('should auto register the user by default', async () => { it('should auto register the user by default', async () => {
const url = await loginWithOAuth('oauth-mobile-override', 'app.immich:///oauth-callback'); const callbackParams = await loginWithOAuth('oauth-mobile-override', 'app.immich:///oauth-callback');
expect(url).toEqual(expect.stringContaining(mobileOverrideRedirectUri)); expect(callbackParams.url).toEqual(expect.stringContaining(mobileOverrideRedirectUri));
// simulate redirecting back to mobile app // simulate redirecting back to mobile app
const redirectUri = url.replace(mobileOverrideRedirectUri, 'app.immich:///oauth-callback'); const url = callbackParams.url.replace(mobileOverrideRedirectUri, 'app.immich:///oauth-callback');
const { status, body } = await request(app).post('/oauth/callback').send({ url: redirectUri }); const { status, body } = await request(app)
.post('/oauth/callback')
.send({ ...callbackParams, url });
expect(status).toBe(201); expect(status).toBe(201);
expect(body).toMatchObject({ expect(body).toMatchObject({
accessToken: expect.any(String), accessToken: expect.any(String),

View File

@@ -1,9 +1,15 @@
import { AssetMediaResponseDto, AssetResponseDto, deleteAssets, LoginResponseDto, updateAsset } from '@immich/sdk'; import {
AssetMediaResponseDto,
AssetResponseDto,
AssetVisibility,
deleteAssets,
LoginResponseDto,
updateAsset,
} from '@immich/sdk';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { readFile } from 'node:fs/promises'; import { readFile } from 'node:fs/promises';
import { join } from 'node:path'; import { join } from 'node:path';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, TEN_TIMES, testAssetDir, utils } from 'src/utils'; import { app, asBearerAuth, TEN_TIMES, testAssetDir, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest';
@@ -50,7 +56,7 @@ describe('/search', () => {
{ filename: '/formats/motionphoto/samsung-one-ui-6.heic' }, { filename: '/formats/motionphoto/samsung-one-ui-6.heic' },
{ filename: '/formats/motionphoto/samsung-one-ui-5.jpg' }, { filename: '/formats/motionphoto/samsung-one-ui-5.jpg' },
{ filename: '/metadata/gps-position/thompson-springs.jpg', dto: { isArchived: true } }, { filename: '/metadata/gps-position/thompson-springs.jpg', dto: { visibility: AssetVisibility.Archive } },
// used for search suggestions // used for search suggestions
{ filename: '/formats/png/density_plot.png' }, { filename: '/formats/png/density_plot.png' },
@@ -141,65 +147,6 @@ describe('/search', () => {
}); });
describe('POST /search/metadata', () => { describe('POST /search/metadata', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/search/metadata');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
const badTests = [
{
should: 'should reject page as a string',
dto: { page: 'abc' },
expected: ['page must not be less than 1', 'page must be an integer number'],
},
{
should: 'should reject page as a decimal',
dto: { page: 1.5 },
expected: ['page must be an integer number'],
},
{
should: 'should reject page as a negative number',
dto: { page: -10 },
expected: ['page must not be less than 1'],
},
{
should: 'should reject page as 0',
dto: { page: 0 },
expected: ['page must not be less than 1'],
},
{
should: 'should reject size as a string',
dto: { size: 'abc' },
expected: [
'size must not be greater than 1000',
'size must not be less than 1',
'size must be an integer number',
],
},
{
should: 'should reject an invalid size',
dto: { size: -1.5 },
expected: ['size must not be less than 1', 'size must be an integer number'],
},
...['isArchived', 'isFavorite', 'isEncoded', 'isOffline', 'isMotion', 'isVisible'].map((value) => ({
should: `should reject ${value} not a boolean`,
dto: { [value]: 'immich' },
expected: [`${value} must be a boolean value`],
})),
];
for (const { should, dto, expected } of badTests) {
it(should, async () => {
const { status, body } = await request(app)
.post('/search/metadata')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send(dto);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(expected));
});
}
const searchTests = [ const searchTests = [
{ {
should: 'should get my assets', should: 'should get my assets',
@@ -231,12 +178,12 @@ describe('/search', () => {
deferred: () => ({ dto: { size: 1, isFavorite: false }, assets: [assetLast] }), deferred: () => ({ dto: { size: 1, isFavorite: false }, assets: [assetLast] }),
}, },
{ {
should: 'should search by isArchived (true)', should: 'should search by visibility (AssetVisibility.Archive)',
deferred: () => ({ dto: { isArchived: true }, assets: [assetSprings] }), deferred: () => ({ dto: { visibility: AssetVisibility.Archive }, assets: [assetSprings] }),
}, },
{ {
should: 'should search by isArchived (false)', should: 'should search by visibility (AssetVisibility.Timeline)',
deferred: () => ({ dto: { size: 1, isArchived: false }, assets: [assetLast] }), deferred: () => ({ dto: { size: 1, visibility: AssetVisibility.Timeline }, assets: [assetLast] }),
}, },
{ {
should: 'should search by type (image)', should: 'should search by type (image)',
@@ -245,7 +192,7 @@ describe('/search', () => {
{ {
should: 'should search by type (video)', should: 'should search by type (video)',
deferred: () => ({ deferred: () => ({
dto: { type: 'VIDEO' }, dto: { type: 'VIDEO', visibility: AssetVisibility.Hidden },
assets: [ assets: [
// the three live motion photos // the three live motion photos
{ id: expect.any(String) }, { id: expect.any(String) },
@@ -289,13 +236,6 @@ describe('/search', () => {
should: 'should search by takenAfter (no results)', should: 'should search by takenAfter (no results)',
deferred: () => ({ dto: { takenAfter: today.plus({ hour: 1 }).toJSDate() }, assets: [] }), deferred: () => ({ dto: { takenAfter: today.plus({ hour: 1 }).toJSDate() }, assets: [] }),
}, },
// {
// should: 'should search by originalPath',
// deferred: () => ({
// dto: { originalPath: asset1.originalPath },
// assets: [asset1],
// }),
// },
{ {
should: 'should search by originalFilename', should: 'should search by originalFilename',
deferred: () => ({ deferred: () => ({
@@ -325,7 +265,7 @@ describe('/search', () => {
deferred: () => ({ deferred: () => ({
dto: { dto: {
city: '', city: '',
isVisible: true, visibility: AssetVisibility.Timeline,
includeNull: true, includeNull: true,
}, },
assets: [assetLast], assets: [assetLast],
@@ -336,7 +276,7 @@ describe('/search', () => {
deferred: () => ({ deferred: () => ({
dto: { dto: {
city: null, city: null,
isVisible: true, visibility: AssetVisibility.Timeline,
includeNull: true, includeNull: true,
}, },
assets: [assetLast], assets: [assetLast],
@@ -357,7 +297,7 @@ describe('/search', () => {
deferred: () => ({ deferred: () => ({
dto: { dto: {
state: '', state: '',
isVisible: true, visibility: AssetVisibility.Timeline,
withExif: true, withExif: true,
includeNull: true, includeNull: true,
}, },
@@ -369,7 +309,7 @@ describe('/search', () => {
deferred: () => ({ deferred: () => ({
dto: { dto: {
state: null, state: null,
isVisible: true, visibility: AssetVisibility.Timeline,
includeNull: true, includeNull: true,
}, },
assets: [assetLast, assetNotocactus], assets: [assetLast, assetNotocactus],
@@ -390,7 +330,7 @@ describe('/search', () => {
deferred: () => ({ deferred: () => ({
dto: { dto: {
country: '', country: '',
isVisible: true, visibility: AssetVisibility.Timeline,
includeNull: true, includeNull: true,
}, },
assets: [assetLast], assets: [assetLast],
@@ -401,7 +341,7 @@ describe('/search', () => {
deferred: () => ({ deferred: () => ({
dto: { dto: {
country: null, country: null,
isVisible: true, visibility: AssetVisibility.Timeline,
includeNull: true, includeNull: true,
}, },
assets: [assetLast], assets: [assetLast],
@@ -454,14 +394,6 @@ describe('/search', () => {
} }
}); });
describe('POST /search/smart', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/search/smart');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
});
describe('POST /search/random', () => { describe('POST /search/random', () => {
beforeAll(async () => { beforeAll(async () => {
await Promise.all([ await Promise.all([
@@ -476,13 +408,6 @@ describe('/search', () => {
await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration'); await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration');
}); });
it('should require authentication', async () => {
const { status, body } = await request(app).post('/search/random').send({ size: 1 });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it.each(TEN_TIMES)('should return 1 random assets', async () => { it.each(TEN_TIMES)('should return 1 random assets', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/search/random') .post('/search/random')
@@ -512,12 +437,6 @@ describe('/search', () => {
}); });
describe('GET /search/explore', () => { describe('GET /search/explore', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/search/explore');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get explore data', async () => { it('should get explore data', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/search/explore') .get('/search/explore')
@@ -528,12 +447,6 @@ describe('/search', () => {
}); });
describe('GET /search/places', () => { describe('GET /search/places', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/search/places');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get relevant places', async () => { it('should get relevant places', async () => {
const name = 'Paris'; const name = 'Paris';
@@ -552,12 +465,6 @@ describe('/search', () => {
}); });
describe('GET /search/cities', () => { describe('GET /search/cities', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/search/cities');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get all cities', async () => { it('should get all cities', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/search/cities') .get('/search/cities')
@@ -576,12 +483,6 @@ describe('/search', () => {
}); });
describe('GET /search/suggestions', () => { describe('GET /search/suggestions', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/search/suggestions');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get suggestions for country (including null)', async () => { it('should get suggestions for country (including null)', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/search/suggestions?type=country&includeNull=true') .get('/search/suggestions?type=country&includeNull=true')

View File

@@ -1,4 +1,4 @@
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType, TimeBucketSize } from '@immich/sdk'; import { AssetMediaResponseDto, AssetVisibility, LoginResponseDto, SharedLinkType, TimeBucketSize } from '@immich/sdk';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { createUserDto } from 'src/fixtures'; import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
@@ -104,7 +104,7 @@ describe('/timeline', () => {
const req1 = await request(app) const req1 = await request(app)
.get('/timeline/buckets') .get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`) .set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ size: TimeBucketSize.Month, withPartners: true, isArchived: true }); .query({ size: TimeBucketSize.Month, withPartners: true, visibility: AssetVisibility.Archive });
expect(req1.status).toBe(400); expect(req1.status).toBe(400);
expect(req1.body).toEqual(errorDto.badRequest()); expect(req1.body).toEqual(errorDto.badRequest());
@@ -112,7 +112,7 @@ describe('/timeline', () => {
const req2 = await request(app) const req2 = await request(app)
.get('/timeline/buckets') .get('/timeline/buckets')
.set('Authorization', `Bearer ${user.accessToken}`) .set('Authorization', `Bearer ${user.accessToken}`)
.query({ size: TimeBucketSize.Month, withPartners: true, isArchived: undefined }); .query({ size: TimeBucketSize.Month, withPartners: true, visibility: undefined });
expect(req2.status).toBe(400); expect(req2.status).toBe(400);
expect(req2.body).toEqual(errorDto.badRequest()); expect(req2.body).toEqual(errorDto.badRequest());

View File

@@ -215,6 +215,19 @@ describe('/admin/users', () => {
const user = await getMyUser({ headers: asBearerAuth(token.accessToken) }); const user = await getMyUser({ headers: asBearerAuth(token.accessToken) });
expect(user).toMatchObject({ email: nonAdmin.userEmail }); expect(user).toMatchObject({ email: nonAdmin.userEmail });
}); });
it('should update the avatar color', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}`)
.send({ avatarColor: 'orange' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ avatarColor: 'orange' });
const after = await getUserAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ avatarColor: 'orange' });
});
}); });
describe('PUT /admin/users/:id/preferences', () => { describe('PUT /admin/users/:id/preferences', () => {
@@ -240,19 +253,6 @@ describe('/admin/users', () => {
expect(after).toMatchObject({ memories: { enabled: false } }); expect(after).toMatchObject({ memories: { enabled: false } });
}); });
it('should update the avatar color', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}/preferences`)
.send({ avatar: { color: 'orange' } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ avatar: { color: 'orange' } });
const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ avatar: { color: 'orange' } });
});
it('should update download archive size', async () => { it('should update download archive size', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}/preferences`) .put(`/admin/users/${admin.userId}/preferences`)

View File

@@ -31,33 +31,7 @@ describe('/users', () => {
); );
}); });
describe('GET /users', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/users');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get users', async () => {
const { status, body } = await request(app).get('/users').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200);
expect(body).toHaveLength(2);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ email: 'admin@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }),
]),
);
});
});
describe('GET /users/me', () => { describe('GET /users/me', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/users/me`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should not work for shared links', async () => { it('should not work for shared links', async () => {
const album = await utils.createAlbum(admin.accessToken, { albumName: 'Album' }); const album = await utils.createAlbum(admin.accessToken, { albumName: 'Album' });
const sharedLink = await utils.createSharedLink(admin.accessToken, { const sharedLink = await utils.createSharedLink(admin.accessToken, {
@@ -99,24 +73,6 @@ describe('/users', () => {
}); });
describe('PUT /users/me', () => { describe('PUT /users/me', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/users/me`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
for (const key of ['email', 'name']) {
it(`should not allow null ${key}`, async () => {
const dto = { [key]: null };
const { status, body } = await request(app)
.put(`/users/me`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send(dto);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
}
it('should update first and last name', async () => { it('should update first and last name', async () => {
const before = await getMyUser({ headers: asBearerAuth(admin.accessToken) }); const before = await getMyUser({ headers: asBearerAuth(admin.accessToken) });
@@ -183,6 +139,19 @@ describe('/users', () => {
profileChangedAt: expect.anything(), profileChangedAt: expect.anything(),
}); });
}); });
it('should update avatar color', async () => {
const { status, body } = await request(app)
.put(`/users/me`)
.send({ avatarColor: 'blue' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ avatarColor: 'blue' });
const after = await getMyUser({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ avatarColor: 'blue' });
});
}); });
describe('PUT /users/me/preferences', () => { describe('PUT /users/me/preferences', () => {
@@ -202,19 +171,6 @@ describe('/users', () => {
expect(after).toMatchObject({ memories: { enabled: false } }); expect(after).toMatchObject({ memories: { enabled: false } });
}); });
it('should update avatar color', async () => {
const { status, body } = await request(app)
.put(`/users/me/preferences`)
.send({ avatar: { color: 'blue' } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ avatar: { color: 'blue' } });
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ avatar: { color: 'blue' } });
});
it('should require an integer for download archive size', async () => { it('should require an integer for download archive size', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/users/me/preferences`) .put(`/users/me/preferences`)
@@ -269,11 +225,6 @@ describe('/users', () => {
}); });
describe('GET /users/:id', () => { describe('GET /users/:id', () => {
it('should require authentication', async () => {
const { status } = await request(app).get(`/users/${admin.userId}`);
expect(status).toEqual(401);
});
it('should get the user', async () => { it('should get the user', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/users/${admin.userId}`) .get(`/users/${admin.userId}`)
@@ -292,12 +243,6 @@ describe('/users', () => {
}); });
describe('GET /server/license', () => { describe('GET /server/license', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/users/me/license');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return the user license', async () => { it('should return the user license', async () => {
await request(app) await request(app)
.put('/users/me/license') .put('/users/me/license')
@@ -315,11 +260,6 @@ describe('/users', () => {
}); });
describe('PUT /users/me/license', () => { describe('PUT /users/me/license', () => {
it('should require authentication', async () => {
const { status } = await request(app).put(`/users/me/license`);
expect(status).toEqual(401);
});
it('should set the user license', async () => { it('should set the user license', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/users/me/license`) .put(`/users/me/license`)

View File

@@ -3,6 +3,7 @@ import {
AssetMediaCreateDto, AssetMediaCreateDto,
AssetMediaResponseDto, AssetMediaResponseDto,
AssetResponseDto, AssetResponseDto,
AssetVisibility,
CheckExistingAssetsDto, CheckExistingAssetsDto,
CreateAlbumDto, CreateAlbumDto,
CreateLibraryDto, CreateLibraryDto,
@@ -429,7 +430,10 @@ export const utils = {
}, },
archiveAssets: (accessToken: string, ids: string[]) => archiveAssets: (accessToken: string, ids: string[]) =>
updateAssets({ assetBulkUpdateDto: { ids, isArchived: true } }, { headers: asBearerAuth(accessToken) }), updateAssets(
{ assetBulkUpdateDto: { ids, visibility: AssetVisibility.Archive } },
{ headers: asBearerAuth(accessToken) },
),
deleteAssets: (accessToken: string, ids: string[]) => deleteAssets: (accessToken: string, ids: string[]) =>
deleteAssets({ assetBulkDeleteDto: { ids } }, { headers: asBearerAuth(accessToken) }), deleteAssets({ assetBulkDeleteDto: { ids } }, { headers: asBearerAuth(accessToken) }),

View File

@@ -25,7 +25,7 @@ test.describe('Registration', () => {
// login // login
await expect(page).toHaveTitle(/Login/); await expect(page).toHaveTitle(/Login/);
await page.goto('/auth/login'); await page.goto('/auth/login?autoLaunch=0');
await page.getByLabel('Email').fill('admin@immich.app'); await page.getByLabel('Email').fill('admin@immich.app');
await page.getByLabel('Password').fill('password'); await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Login' }).click(); await page.getByRole('button', { name: 'Login' }).click();
@@ -59,7 +59,7 @@ test.describe('Registration', () => {
await context.clearCookies(); await context.clearCookies();
// login // login
await page.goto('/auth/login'); await page.goto('/auth/login?autoLaunch=0');
await page.getByLabel('Email').fill('user@immich.cloud'); await page.getByLabel('Email').fill('user@immich.cloud');
await page.getByLabel('Password').fill('password'); await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Login' }).click(); await page.getByRole('button', { name: 'Login' }).click();
@@ -72,7 +72,7 @@ test.describe('Registration', () => {
await page.getByRole('button', { name: 'Change password' }).click(); await page.getByRole('button', { name: 'Change password' }).click();
// login with new password // login with new password
await expect(page).toHaveURL('/auth/login'); await expect(page).toHaveURL('/auth/login?autoLaunch=0');
await page.getByLabel('Email').fill('user@immich.cloud'); await page.getByLabel('Email').fill('user@immich.cloud');
await page.getByLabel('Password').fill('new-password'); await page.getByLabel('Password').fill('new-password');
await page.getByRole('button', { name: 'Login' }).click(); await page.getByRole('button', { name: 'Login' }).click();

View File

@@ -21,23 +21,9 @@ test.describe('Photo Viewer', () => {
test.beforeEach(async ({ context, page }) => { test.beforeEach(async ({ context, page }) => {
// before each test, login as user // before each test, login as user
await utils.setAuthCookies(context, admin.accessToken); await utils.setAuthCookies(context, admin.accessToken);
await page.goto('/photos');
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
}); });
test('initially shows a loading spinner', async ({ page }) => {
await page.route(`/api/assets/${asset.id}/thumbnail**`, async (route) => {
// slow down the request for thumbnail, so spinner has chance to show up
await new Promise((f) => setTimeout(f, 2000));
await route.continue();
});
await page.goto(`/photos/${asset.id}`);
await page.waitForLoadState('load');
// this is the spinner
await page.waitForSelector('svg[role=status]');
await expect(page.getByTestId('loading-spinner')).toBeVisible();
});
test('loads original photo when zoomed', async ({ page }) => { test('loads original photo when zoomed', async ({ page }) => {
await page.goto(`/photos/${asset.id}`); await page.goto(`/photos/${asset.id}`);
await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('thumbnail'); await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('thumbnail');

View File

@@ -47,16 +47,13 @@ test.describe('Shared Links', () => {
await page.locator(`[data-asset-id="${asset.id}"]`).hover(); await page.locator(`[data-asset-id="${asset.id}"]`).hover();
await page.waitForSelector('[data-group] svg'); await page.waitForSelector('[data-group] svg');
await page.getByRole('checkbox').click(); await page.getByRole('checkbox').click();
await page.getByRole('button', { name: 'Download' }).click(); await Promise.all([page.waitForEvent('download'), page.getByRole('button', { name: 'Download' }).click()]);
await page.waitForEvent('download');
}); });
test('download all from shared link', async ({ page }) => { test('download all from shared link', async ({ page }) => {
await page.goto(`/share/${sharedLink.key}`); await page.goto(`/share/${sharedLink.key}`);
await page.getByRole('heading', { name: 'Test Album' }).waitFor(); await page.getByRole('heading', { name: 'Test Album' }).waitFor();
await page.getByRole('button', { name: 'Download' }).click(); await Promise.all([page.waitForEvent('download'), page.getByRole('button', { name: 'Download' }).click()]);
await page.getByText('DOWNLOADING', { exact: true }).waitFor();
await page.waitForEvent('download');
}); });
test('enter password for a shared link', async ({ page }) => { test('enter password for a shared link', async ({ page }) => {

View File

@@ -4,6 +4,7 @@
"account_settings": "إعدادات الحساب", "account_settings": "إعدادات الحساب",
"acknowledge": "أُدرك ذلك", "acknowledge": "أُدرك ذلك",
"action": "التحكم", "action": "التحكم",
"action_common_update": "تحديث",
"actions": "العمليات", "actions": "العمليات",
"active": "نشط", "active": "نشط",
"activity": "النشاط", "activity": "النشاط",
@@ -13,6 +14,7 @@
"add_a_location": "إضافة موقع", "add_a_location": "إضافة موقع",
"add_a_name": "إضافة إسم", "add_a_name": "إضافة إسم",
"add_a_title": "إضافة عنوان", "add_a_title": "إضافة عنوان",
"add_endpoint": "Add endpoint",
"add_exclusion_pattern": "إضافة نمط إستثناء", "add_exclusion_pattern": "إضافة نمط إستثناء",
"add_import_path": "إضافة مسار الإستيراد", "add_import_path": "إضافة مسار الإستيراد",
"add_location": "إضافة موقع", "add_location": "إضافة موقع",
@@ -22,6 +24,8 @@
"add_photos": "إضافة صور", "add_photos": "إضافة صور",
"add_to": "إضافة إلى…", "add_to": "إضافة إلى…",
"add_to_album": "إضافة إلى ألبوم", "add_to_album": "إضافة إلى ألبوم",
"add_to_album_bottom_sheet_added": "تمت الاضافة{album}",
"add_to_album_bottom_sheet_already_exists": "موجودة مسبقا {album}",
"add_to_shared_album": "إضافة إلى ألبوم مشترك", "add_to_shared_album": "إضافة إلى ألبوم مشترك",
"add_url": "إضافة رابط", "add_url": "إضافة رابط",
"added_to_archive": "أُضيفت للأرشيف", "added_to_archive": "أُضيفت للأرشيف",
@@ -183,20 +187,13 @@
"oauth_auto_register": "التسجيل التلقائي", "oauth_auto_register": "التسجيل التلقائي",
"oauth_auto_register_description": "التسجيل التلقائي للمستخدمين الجدد بعد تسجيل الدخول باستخدام OAuth", "oauth_auto_register_description": "التسجيل التلقائي للمستخدمين الجدد بعد تسجيل الدخول باستخدام OAuth",
"oauth_button_text": "نص الزر", "oauth_button_text": "نص الزر",
"oauth_client_id": "معرف العميل",
"oauth_client_secret": "الرمز السري للعميل",
"oauth_enable_description": "تسجيل الدخول باستخدام OAuth", "oauth_enable_description": "تسجيل الدخول باستخدام OAuth",
"oauth_issuer_url": "عنوان URL الخاص بجهة الإصدار",
"oauth_mobile_redirect_uri": "عنوان URI لإعادة التوجيه على الهاتف", "oauth_mobile_redirect_uri": "عنوان URI لإعادة التوجيه على الهاتف",
"oauth_mobile_redirect_uri_override": "تجاوز عنوان URI لإعادة التوجيه على الهاتف", "oauth_mobile_redirect_uri_override": "تجاوز عنوان URI لإعادة التوجيه على الهاتف",
"oauth_mobile_redirect_uri_override_description": "قم بتفعيله عندما لا يسمح موفر OAuth بمعرف URI للجوال، مثل '{callback}'", "oauth_mobile_redirect_uri_override_description": "قم بتفعيله عندما لا يسمح موفر OAuth بمعرف URI للجوال، مثل '{callback}'",
"oauth_profile_signing_algorithm": "خوارزمية توقيع الملف الشخصي",
"oauth_profile_signing_algorithm_description": "الخوارزمية المستخدمة للتوقيع على ملف تعريف المستخدم.",
"oauth_scope": "النطاق",
"oauth_settings": "OAuth", "oauth_settings": "OAuth",
"oauth_settings_description": "إدارة إعدادات تسجيل الدخول OAuth", "oauth_settings_description": "إدارة إعدادات تسجيل الدخول OAuth",
"oauth_settings_more_details": "لمزيد من التفاصيل حول هذه الميزة، يرجى الرجوع إلى <link>الوثائق</link>.", "oauth_settings_more_details": "لمزيد من التفاصيل حول هذه الميزة، يرجى الرجوع إلى <link>الوثائق</link>.",
"oauth_signing_algorithm": "خوارزمية التوقيع",
"oauth_storage_label_claim": "المطالبة بتصنيف التخزين", "oauth_storage_label_claim": "المطالبة بتصنيف التخزين",
"oauth_storage_label_claim_description": "قم تلقائيًا بتعيين تصنيف التخزين الخاص بالمستخدم على قيمة هذه المطالبة.", "oauth_storage_label_claim_description": "قم تلقائيًا بتعيين تصنيف التخزين الخاص بالمستخدم على قيمة هذه المطالبة.",
"oauth_storage_quota_claim": "المطالبة بحصة التخزين", "oauth_storage_quota_claim": "المطالبة بحصة التخزين",
@@ -362,6 +359,16 @@
"admin_password": "كلمة سر المشرف", "admin_password": "كلمة سر المشرف",
"administration": "الإدارة", "administration": "الإدارة",
"advanced": "متقدم", "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": "تمكين الميزات الإضافية لاستكشاف الأخطاء وإصلاحها",
"advanced_settings_troubleshooting_title": "استكشاف الأخطاء وإصلاحها",
"age_months": "عمر {months, plural, one {# شهر} other {# أشهر}}", "age_months": "عمر {months, plural, one {# شهر} other {# أشهر}}",
"age_year_months": "عمر سنة واحدة، {months, plural, one {# شهر} other {# أشهر}}", "age_year_months": "عمر سنة واحدة، {months, plural, one {# شهر} other {# أشهر}}",
"age_years": "{years, plural, other {العمر #}}", "age_years": "{years, plural, other {العمر #}}",
@@ -370,6 +377,8 @@
"album_cover_updated": "تم تحديث غلاف الألبوم", "album_cover_updated": "تم تحديث غلاف الألبوم",
"album_delete_confirmation": "هل أنت متأكد أنك تريد حذف الألبوم {album}؟", "album_delete_confirmation": "هل أنت متأكد أنك تريد حذف الألبوم {album}؟",
"album_delete_confirmation_description": "إذا تمت مشاركة هذا الألبوم، فلن يتمكن المستخدمون الآخرون من الوصول إليه بعد الآن.", "album_delete_confirmation_description": "إذا تمت مشاركة هذا الألبوم، فلن يتمكن المستخدمون الآخرون من الوصول إليه بعد الآن.",
"album_info_card_backup_album_excluded": "مستبعد",
"album_info_card_backup_album_included": "متضمنة",
"album_info_updated": "تم تحديث معلومات الألبوم", "album_info_updated": "تم تحديث معلومات الألبوم",
"album_leave": "هل تريد مغادرة الألبوم؟", "album_leave": "هل تريد مغادرة الألبوم؟",
"album_leave_confirmation": "هل أنت متأكد أنك تريد مغادرة {album}؟", "album_leave_confirmation": "هل أنت متأكد أنك تريد مغادرة {album}؟",
@@ -378,10 +387,22 @@
"album_remove_user": "هل ترغب في إزالة المستخدم؟", "album_remove_user": "هل ترغب في إزالة المستخدم؟",
"album_remove_user_confirmation": "هل أنت متأكد أنك تريد إزالة {user}؟", "album_remove_user_confirmation": "هل أنت متأكد أنك تريد إزالة {user}؟",
"album_share_no_users": "يبدو أنك قمت بمشاركة هذا الألبوم مع جميع المستخدمين أو ليس لديك أي مستخدم للمشاركة معه.", "album_share_no_users": "يبدو أنك قمت بمشاركة هذا الألبوم مع جميع المستخدمين أو ليس لديك أي مستخدم للمشاركة معه.",
"album_thumbnail_card_item": "عنصر واحد",
"album_thumbnail_card_items": "{} items",
"album_thumbnail_card_shared": " · . مشترك",
"album_thumbnail_shared_by": "Shared by {}",
"album_updated": "تم تحديث الألبوم", "album_updated": "تم تحديث الألبوم",
"album_updated_setting_description": "تلقي إشعارًا عبر البريد الإلكتروني عندما يحتوي الألبوم المشترك على محتويات جديدة", "album_updated_setting_description": "تلقي إشعارًا عبر البريد الإلكتروني عندما يحتوي الألبوم المشترك على محتويات جديدة",
"album_user_left": "تم ترك {album}", "album_user_left": "تم ترك {album}",
"album_user_removed": "تم إزالة {user}", "album_user_removed": "تم إزالة {user}",
"album_viewer_appbar_delete_confirm": "هل أنت متأكد أنك تريد حذف هذا الألبوم من حسابك؟",
"album_viewer_appbar_share_err_delete": "فشل في حذف الألبوم",
"album_viewer_appbar_share_err_leave": "فشل في ترك الألبوم",
"album_viewer_appbar_share_err_remove": "هناك مشاكل في إزالة الأصول من الألبوم",
"album_viewer_appbar_share_err_title": "فشل في تغيير عنوان الألبوم",
"album_viewer_appbar_share_leave": "ترك الألبوم",
"album_viewer_appbar_share_to": "حصة ل",
"album_viewer_page_share_add_users": "اضافة مستخدمين",
"album_with_link_access": "السماح لأي شخص لديه الرابط برؤية الصور والأشخاص الموجودين في هذا الألبوم.", "album_with_link_access": "السماح لأي شخص لديه الرابط برؤية الصور والأشخاص الموجودين في هذا الألبوم.",
"albums": "الألبومات", "albums": "الألبومات",
"albums_count": "{count, plural, one {{count, number} ألبوم} other {{count, number} ألبومات}}", "albums_count": "{count, plural, one {{count, number} ألبوم} other {{count, number} ألبومات}}",
@@ -399,42 +420,133 @@
"api_key_description": "سيتم عرض هذه القيمة مرة واحدة فقط. يرجى التأكد من نسخها قبل إغلاق النافذة.", "api_key_description": "سيتم عرض هذه القيمة مرة واحدة فقط. يرجى التأكد من نسخها قبل إغلاق النافذة.",
"api_key_empty": "يجب ألا يكون اسم مفتاح API فارغًا", "api_key_empty": "يجب ألا يكون اسم مفتاح API فارغًا",
"api_keys": "مفاتيح واجهة برمجة التطبيقات", "api_keys": "مفاتيح واجهة برمجة التطبيقات",
"app_bar_signout_dialog_content": "هل أنت متأكد أنك تريد الخروج",
"app_bar_signout_dialog_ok": "نعم",
"app_bar_signout_dialog_title": "خروج",
"app_settings": "إعدادات التطبيق", "app_settings": "إعدادات التطبيق",
"appears_in": "يظهر في", "appears_in": "يظهر في",
"archive": "الأرشيف", "archive": "الأرشيف",
"archive_or_unarchive_photo": "أرشفة الصورة أو إلغاء أرشفتها", "archive_or_unarchive_photo": "أرشفة الصورة أو إلغاء أرشفتها",
"archive_page_no_archived_assets": "لم يتم العثور على الأصول المؤرشفة",
"archive_page_title": "Archive ({})",
"archive_size": "حجم الأرشيف", "archive_size": "حجم الأرشيف",
"archive_size_description": "تكوين حجم الأرشيف للتنزيلات (بالجيجابايت)", "archive_size_description": "تكوين حجم الأرشيف للتنزيلات (بالجيجابايت)",
"archived": "Archived",
"archived_count": "{count, plural, other {الأرشيف #}}", "archived_count": "{count, plural, other {الأرشيف #}}",
"are_these_the_same_person": "هل هؤلاء هم نفس الشخص؟", "are_these_the_same_person": "هل هؤلاء هم نفس الشخص؟",
"are_you_sure_to_do_this": "هل انت متأكد من أنك تريد أن تفعل هذا؟", "are_you_sure_to_do_this": "هل انت متأكد من أنك تريد أن تفعل هذا؟",
"asset_action_delete_err_read_only": "لا يمكن حذف الأصول ذات للقراءة فقط، وسوف يتم التخطي",
"asset_action_share_err_offline": "لا يمكن جلب الأصول غير المتصلة بالإنترنت، وسوف يتم التخطي",
"asset_added_to_album": "تمت إضافته إلى الألبوم", "asset_added_to_album": "تمت إضافته إلى الألبوم",
"asset_adding_to_album": "جارٍ الإضافة إلى الألبوم…", "asset_adding_to_album": "جارٍ الإضافة إلى الألبوم…",
"asset_description_updated": "تم تحديث وصف المحتوى", "asset_description_updated": "تم تحديث وصف المحتوى",
"asset_filename_is_offline": "الأصل {filename} غير متصل", "asset_filename_is_offline": "الأصل {filename} غير متصل",
"asset_has_unassigned_faces": "يحتوي الأصل على وجوه غير مخصصة", "asset_has_unassigned_faces": "يحتوي الأصل على وجوه غير مخصصة",
"asset_hashing": "التجزئة…", "asset_hashing": "التجزئة…",
"asset_list_group_by_sub_title": "تنظيم بواسطة",
"asset_list_layout_settings_dynamic_layout_title": "تخطيط ديناميكي",
"asset_list_layout_settings_group_automatically": "تلقائي",
"asset_list_layout_settings_group_by": "مجموعة الأصول حسب",
"asset_list_layout_settings_group_by_month_day": "شهر + يوم",
"asset_list_layout_sub_title": "تصميم",
"asset_list_settings_subtitle": "إعدادات تخطيط شبكة الصور",
"asset_list_settings_title": "شبكة الصور",
"asset_offline": "المحتوى غير اتصال", "asset_offline": "المحتوى غير اتصال",
"asset_offline_description": "لم يعد هذا الأصل الخارجي موجودًا على القرص. يرجى الاتصال بمسؤول Immich للحصول على المساعدة.", "asset_offline_description": "لم يعد هذا الأصل الخارجي موجودًا على القرص. يرجى الاتصال بمسؤول Immich للحصول على المساعدة.",
"asset_restored_successfully": "Asset restored successfully",
"asset_skipped": "تم تخطيه", "asset_skipped": "تم تخطيه",
"asset_skipped_in_trash": "في سلة المهملات", "asset_skipped_in_trash": "في سلة المهملات",
"asset_uploaded": "تم الرفع", "asset_uploaded": "تم الرفع",
"asset_uploading": "جارٍ الرفع…", "asset_uploading": "جارٍ الرفع…",
"asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
"asset_viewer_settings_title": "عارض الأصول",
"assets": "المحتويات", "assets": "المحتويات",
"assets_added_count": "تمت إضافة {count, plural, one {# محتوى} other {# محتويات}}", "assets_added_count": "تمت إضافة {count, plural, one {# محتوى} other {# محتويات}}",
"assets_added_to_album_count": "تمت إضافة {count, plural, one {# الأصل} other {# الأصول}} إلى الألبوم", "assets_added_to_album_count": "تمت إضافة {count, plural, one {# الأصل} other {# الأصول}} إلى الألبوم",
"assets_added_to_name_count": "تم إضافة {count, plural, one {# محتوى} other {# محتويات }} إلى {hasName, select, true {<b>{name}</b>} other {ألبوم جديد}}", "assets_added_to_name_count": "تم إضافة {count, plural, one {# محتوى} other {# محتويات }} إلى {hasName, select, true {<b>{name}</b>} other {ألبوم جديد}}",
"assets_count": "{count, plural, one {# محتوى} other {# محتويات}}", "assets_count": "{count, plural, one {# محتوى} other {# محتويات}}",
"assets_deleted_permanently": "{} asset(s) deleted permanently",
"assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server",
"assets_moved_to_trash_count": "تم نقل {count, plural, one {# محتوى} other {# محتويات}} إلى سلة المهملات", "assets_moved_to_trash_count": "تم نقل {count, plural, one {# محتوى} other {# محتويات}} إلى سلة المهملات",
"assets_permanently_deleted_count": "تم حذف {count, plural, one {# هذا المحتوى} other {# هذه المحتويات}} بشكل دائم", "assets_permanently_deleted_count": "تم حذف {count, plural, one {# هذا المحتوى} other {# هذه المحتويات}} بشكل دائم",
"assets_removed_count": "تمت إزالة {count, plural, one {# محتوى} other {# محتويات}}", "assets_removed_count": "تمت إزالة {count, plural, one {# محتوى} other {# محتويات}}",
"assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device",
"assets_restore_confirmation": "هل أنت متأكد من أنك تريد استعادة جميع الأصول المحذوفة؟ لا يمكنك التراجع عن هذا الإجراء! لاحظ أنه لا يمكن استعادة أي أصول غير متصلة بهذه الطريقة.", "assets_restore_confirmation": "هل أنت متأكد من أنك تريد استعادة جميع الأصول المحذوفة؟ لا يمكنك التراجع عن هذا الإجراء! لاحظ أنه لا يمكن استعادة أي أصول غير متصلة بهذه الطريقة.",
"assets_restored_count": "تمت استعادة {count, plural, one {# محتوى} other {# محتويات}}", "assets_restored_count": "تمت استعادة {count, plural, one {# محتوى} other {# محتويات}}",
"assets_restored_successfully": "{} asset(s) restored successfully",
"assets_trashed": "{} asset(s) trashed",
"assets_trashed_count": "تم إرسال {count, plural, one {# محتوى} other {# محتويات}} إلى سلة المهملات", "assets_trashed_count": "تم إرسال {count, plural, one {# محتوى} other {# محتويات}} إلى سلة المهملات",
"assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
"assets_were_part_of_album_count": "{count, plural, one {هذا المحتوى} other {هذه المحتويات}} في الألبوم بالفعل", "assets_were_part_of_album_count": "{count, plural, one {هذا المحتوى} other {هذه المحتويات}} في الألبوم بالفعل",
"authorized_devices": "الأجهزه المخولة", "authorized_devices": "الأجهزه المخولة",
"automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
"automatic_endpoint_switching_title": "Automatic URL switching",
"back": "خلف", "back": "خلف",
"back_close_deselect": "الرجوع أو الإغلاق أو إلغاء التحديد", "back_close_deselect": "الرجوع أو الإغلاق أو إلغاء التحديد",
"background_location_permission": "Background location permission",
"background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
"backup_album_selection_page_albums_device": "Albums on device ({})",
"backup_album_selection_page_albums_tap": "انقر للتضمين، وانقر نقرًا مزدوجًا للاستثناء",
"backup_album_selection_page_assets_scatter": "يمكن أن تنتشر الأصول عبر ألبومات متعددة. وبالتالي، يمكن تضمين الألبومات أو استبعادها أثناء عملية النسخ الاحتياطي.",
"backup_album_selection_page_select_albums": "حدد الألبومات",
"backup_album_selection_page_selection_info": "معلومات الاختيار",
"backup_album_selection_page_total_assets": "إجمالي الأصول الفريدة",
"backup_all": "الجميع",
"backup_background_service_backup_failed_message": "فشل في النسخ الاحتياطي للأصول. جارٍ إعادة المحاولة...",
"backup_background_service_connection_failed_message": "فشل في الاتصال بالخادم. جارٍ إعادة المحاولة...",
"backup_background_service_current_upload_notification": "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": "تم تعطيل تحديث التطبيق في الخلفية",
"backup_controller_page_background_app_refresh_enable_button_text": "اذهب للاعدادات",
"backup_controller_page_background_battery_info_link": "أرني كيف",
"backup_controller_page_background_battery_info_message": "للحصول على أفضل تجربة نسخ احتياطي في الخلفية، يرجى تعطيل أي تحسينات للبطارية تقيد نشاط الخلفية لـ تطبيق.\n\nنظرًا لأن هذا خاص بالجهاز، يرجى البحث عن المعلومات المطلوبة للشركة المصنعة لجهازك.",
"backup_controller_page_background_battery_info_ok": "نعم",
"backup_controller_page_background_battery_info_title": "تحسين البطارية",
"backup_controller_page_background_charging": "فقط أثناء الشحن",
"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": "النسخ الاحتياطي التلقائي للخلفية قيد التشغيل",
"backup_controller_page_background_turn_off": "قم بإيقاف تشغيل خدمة الخلفية",
"backup_controller_page_background_turn_on": "قم بتشغيل خدمة الخلفية",
"backup_controller_page_background_wifi": "فقط على واي فاي",
"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": "بقية",
"backup_controller_page_remainder_sub": "الصور ومقاطع الفيديو المتبقية للنسخ الاحتياطي من التحديد",
"backup_controller_page_server_storage": "ذاكرة الجهاز",
"backup_controller_page_start_backup": "بدء النسخ الاحتياطي",
"backup_controller_page_status_off": "النسخة الاحتياطية التلقائية غير فعالة",
"backup_controller_page_status_on": "النسخة الاحتياطية التلقائية فعالة",
"backup_controller_page_storage_format": "{} of {} used",
"backup_controller_page_to_backup": "الألبومات الاحتياطية",
"backup_controller_page_total_sub": "جميع الصور ومقاطع الفيديو الفريدة من ألبومات مختارة",
"backup_controller_page_turn_off": "قم بإيقاف تشغيل النسخ الاحتياطي المقدمة",
"backup_controller_page_turn_on": "قم بتشغيل النسخ الاحتياطي المقدمة",
"backup_controller_page_uploading_file_info": "تحميل معلومات الملف",
"backup_err_only_album": "لا يمكن إزالة الألبوم الوحيد",
"backup_info_card_assets": "أصول",
"backup_manual_cancelled": "ملغي",
"backup_manual_in_progress": "قيد التحميل حاول مره اخرى",
"backup_manual_success": "نجاح",
"backup_manual_title": "حالة التحميل",
"backup_options_page_title": "خيارات النسخ الاحتياطي",
"backup_setting_subtitle": "Manage background and foreground upload settings",
"backward": "الى الوراء", "backward": "الى الوراء",
"birthdate_saved": "تم حفظ تاريخ الميلاد بنجاح", "birthdate_saved": "تم حفظ تاريخ الميلاد بنجاح",
"birthdate_set_description": "يتم استخدام تاريخ الميلاد لحساب عمر هذا الشخص وقت التقاط الصورة.", "birthdate_set_description": "يتم استخدام تاريخ الميلاد لحساب عمر هذا الشخص وقت التقاط الصورة.",
@@ -446,24 +558,52 @@
"bulk_keep_duplicates_confirmation": "هل أنت متأكد من أنك تريد الاحتفاظ بـ {count, plural, one {# محتوى مكرر} other {# محتويات مكررة}}؟ سيؤدي هذا إلى حل جميع مجموعات النسخ المكررة دون حذف أي شيء.", "bulk_keep_duplicates_confirmation": "هل أنت متأكد من أنك تريد الاحتفاظ بـ {count, plural, one {# محتوى مكرر} other {# محتويات مكررة}}؟ سيؤدي هذا إلى حل جميع مجموعات النسخ المكررة دون حذف أي شيء.",
"bulk_trash_duplicates_confirmation": "هل أنت متأكد من أنك تريد إرسال {count, plural, one {# محتوى مكرر} other {# محتويات مكررة}} إلى سلة المهملات ؟ سيحتفظ هذا بأكبر محتوى من كل مجموعة ويرسل جميع النسخ المكررة الأخرى إلى سلة المهملات.", "bulk_trash_duplicates_confirmation": "هل أنت متأكد من أنك تريد إرسال {count, plural, one {# محتوى مكرر} other {# محتويات مكررة}} إلى سلة المهملات ؟ سيحتفظ هذا بأكبر محتوى من كل مجموعة ويرسل جميع النسخ المكررة الأخرى إلى سلة المهملات.",
"buy": "شراء immich", "buy": "شراء immich",
"cache_settings_album_thumbnails": "Library page thumbnails ({} assets)",
"cache_settings_clear_cache_button": "مسح ذاكرة التخزين المؤقت",
"cache_settings_clear_cache_button_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": "إعدادات التخزين المؤقت",
"camera": "الكاميرا", "camera": "الكاميرا",
"camera_brand": "علامة الكاميرا التجارية", "camera_brand": "علامة الكاميرا التجارية",
"camera_model": "طراز الكاميرا", "camera_model": "طراز الكاميرا",
"cancel": "إلغاء", "cancel": "إلغاء",
"cancel_search": "الغي البحث", "cancel_search": "الغي البحث",
"canceled": "Canceled",
"cannot_merge_people": "لا يمكن دمج الأشخاص", "cannot_merge_people": "لا يمكن دمج الأشخاص",
"cannot_undo_this_action": "لا يمكنك التراجع عن هذا الإجراء!", "cannot_undo_this_action": "لا يمكنك التراجع عن هذا الإجراء!",
"cannot_update_the_description": "لا يمكن تحديث الوصف", "cannot_update_the_description": "لا يمكن تحديث الوصف",
"change_date": "غيّر التاريخ", "change_date": "غيّر التاريخ",
"change_display_order": "Change display order",
"change_expiration_time": "تغيير وقت انتهاء الصلاحية", "change_expiration_time": "تغيير وقت انتهاء الصلاحية",
"change_location": "غيّر الموقع", "change_location": "غيّر الموقع",
"change_name": "تغيير الإسم", "change_name": "تغيير الإسم",
"change_name_successfully": "تم تغيير الاسم بنجاح", "change_name_successfully": "تم تغيير الاسم بنجاح",
"change_password": "تغيير كلمة المرور", "change_password": "تغيير كلمة المرور",
"change_password_description": "هذه إما هي المرة الأولى التي تقوم فيها بتسجيل الدخول إلى النظام أو أنه تم تقديم طلب لتغيير كلمة المرور الخاصة بك. الرجاء إدخال كلمة المرور الجديدة أدناه.", "change_password_description": "هذه إما هي المرة الأولى التي تقوم فيها بتسجيل الدخول إلى النظام أو أنه تم تقديم طلب لتغيير كلمة المرور الخاصة بك. الرجاء إدخال كلمة المرور الجديدة أدناه.",
"change_password_form_confirm_password": "تأكيد كلمة المرور",
"change_password_form_description": "مرحبًا ،هذه هي المرة الأولى التي تقوم فيها بالتسجيل في النظام أو تم تقديم طلب لتغيير كلمة المرور الخاصة بك.الرجاء إدخال كلمة المرور الجديدة أدناه",
"change_password_form_new_password": "كلمة المرور الجديدة",
"change_password_form_password_mismatch": "كلمة المرور غير مطابقة",
"change_password_form_reenter_new_password": "أعد إدخال كلمة مرور جديدة",
"change_your_password": "غير كلمة المرور الخاصة بك", "change_your_password": "غير كلمة المرور الخاصة بك",
"changed_visibility_successfully": "تم تغيير الرؤية بنجاح", "changed_visibility_successfully": "تم تغيير الرؤية بنجاح",
"check_all": "تحقق من الكل", "check_all": "تحقق من الكل",
"check_corrupt_asset_backup": "Check for corrupt asset backups",
"check_corrupt_asset_backup_button": "Perform check",
"check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
"check_logs": "تحقق من السجلات", "check_logs": "تحقق من السجلات",
"choose_matching_people_to_merge": "اختر الأشخاص المتطابقين لدمجهم", "choose_matching_people_to_merge": "اختر الأشخاص المتطابقين لدمجهم",
"city": "المدينة", "city": "المدينة",
@@ -472,6 +612,14 @@
"clear_all_recent_searches": "مسح جميع عمليات البحث الأخيرة", "clear_all_recent_searches": "مسح جميع عمليات البحث الأخيرة",
"clear_message": "إخلاء الرسالة", "clear_message": "إخلاء الرسالة",
"clear_value": "إخلاء القيمة", "clear_value": "إخلاء القيمة",
"client_cert_dialog_msg_confirm": "OK",
"client_cert_enter_password": "Enter Password",
"client_cert_import": "Import",
"client_cert_import_success_msg": "Client certificate is imported",
"client_cert_invalid_msg": "Invalid certificate file or wrong password",
"client_cert_remove_msg": "Client certificate is removed",
"client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login",
"client_cert_title": "SSL Client Certificate",
"clockwise": "باتجاه عقارب الساعة", "clockwise": "باتجاه عقارب الساعة",
"close": "إغلاق", "close": "إغلاق",
"collapse": "طي", "collapse": "طي",
@@ -482,6 +630,9 @@
"comment_options": "خيارات التعليق", "comment_options": "خيارات التعليق",
"comments_and_likes": "التعليقات والإعجابات", "comments_and_likes": "التعليقات والإعجابات",
"comments_are_disabled": "التعليقات معطلة", "comments_are_disabled": "التعليقات معطلة",
"common_create_new_album": "إنشاء ألبوم جديد",
"common_server_error": "يرجى التحقق من اتصال الشبكة الخاص بك ، والتأكد من أن الجهاز قابل للوصول وإصدارات التطبيق/الجهاز متوافقة.",
"completed": "Completed",
"confirm": "تأكيد", "confirm": "تأكيد",
"confirm_admin_password": "تأكيد كلمة مرور المسؤول", "confirm_admin_password": "تأكيد كلمة مرور المسؤول",
"confirm_delete_face": "هل أنت متأكد من حذف وجه {name} من الأصول؟", "confirm_delete_face": "هل أنت متأكد من حذف وجه {name} من الأصول؟",
@@ -491,6 +642,15 @@
"contain": "محتواة", "contain": "محتواة",
"context": "السياق", "context": "السياق",
"continue": "متابعة", "continue": "متابعة",
"control_bottom_app_bar_album_info_shared": "{} items · Shared",
"control_bottom_app_bar_create_new_album": "إنشاء ألبوم جديد",
"control_bottom_app_bar_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": "تم نسخ الصورة إلى الحافظة.", "copied_image_to_clipboard": "تم نسخ الصورة إلى الحافظة.",
"copied_to_clipboard": "نسخ إلى الحافظة!", "copied_to_clipboard": "نسخ إلى الحافظة!",
"copy_error": "نسخ الخطأ", "copy_error": "نسخ الخطأ",
@@ -505,24 +665,34 @@
"covers": "أغلفة", "covers": "أغلفة",
"create": "انشاء", "create": "انشاء",
"create_album": "إنشاء ألبوم", "create_album": "إنشاء ألبوم",
"create_album_page_untitled": "بدون اسم",
"create_library": "إنشاء مكتبة", "create_library": "إنشاء مكتبة",
"create_link": "إنشاء رابط", "create_link": "إنشاء رابط",
"create_link_to_share": "إنشاء رابط للمشاركة", "create_link_to_share": "إنشاء رابط للمشاركة",
"create_link_to_share_description": "السماح لأي شخص لديه الرابط بمشاهدة الصورة (الصور) المحددة", "create_link_to_share_description": "السماح لأي شخص لديه الرابط بمشاهدة الصورة (الصور) المحددة",
"create_new": "CREATE NEW",
"create_new_person": "إنشاء شخص جديد", "create_new_person": "إنشاء شخص جديد",
"create_new_person_hint": "تعيين المحتويات المحددة لشخص جديد", "create_new_person_hint": "تعيين المحتويات المحددة لشخص جديد",
"create_new_user": "إنشاء مستخدم جديد", "create_new_user": "إنشاء مستخدم جديد",
"create_shared_album_page_share_add_assets": "إضافة الأصول",
"create_shared_album_page_share_select_photos": "حدد الصور",
"create_tag": "إنشاء علامة", "create_tag": "إنشاء علامة",
"create_tag_description": "أنشئ علامة جديدة. بالنسبة للعلامات المتداخلة، يرجى إدخال المسار الكامل للعلامة بما في ذلك الخطوط المائلة للأمام.", "create_tag_description": "أنشئ علامة جديدة. بالنسبة للعلامات المتداخلة، يرجى إدخال المسار الكامل للعلامة بما في ذلك الخطوط المائلة للأمام.",
"create_user": "إنشاء مستخدم", "create_user": "إنشاء مستخدم",
"created": "تم الإنشاء", "created": "تم الإنشاء",
"crop": "Crop",
"curated_object_page_title": "أشياء",
"current_device": "الجهاز الحالي", "current_device": "الجهاز الحالي",
"current_server_address": "Current server address",
"custom_locale": "لغة مخصصة", "custom_locale": "لغة مخصصة",
"custom_locale_description": "تنسيق التواريخ والأرقام بناءً على اللغة والمنطقة", "custom_locale_description": "تنسيق التواريخ والأرقام بناءً على اللغة والمنطقة",
"daily_title_text_date": "E ، MMM DD",
"daily_title_text_date_year": "E ، MMM DD ، yyyy",
"dark": "معتم", "dark": "معتم",
"date_after": "التارخ بعد", "date_after": "التارخ بعد",
"date_and_time": "التاريخ و الوقت", "date_and_time": "التاريخ و الوقت",
"date_before": "التاريخ قبل", "date_before": "التاريخ قبل",
"date_format": "E ، Lll D ، Y • H: MM A",
"date_of_birth_saved": "تم حفظ تاريخ الميلاد بنجاح", "date_of_birth_saved": "تم حفظ تاريخ الميلاد بنجاح",
"date_range": "نطاق الموعد", "date_range": "نطاق الموعد",
"day": "يوم", "day": "يوم",
@@ -536,19 +706,30 @@
"delete": "حذف", "delete": "حذف",
"delete_album": "حذف الألبوم", "delete_album": "حذف الألبوم",
"delete_api_key_prompt": "هل أنت متأكد أنك تريد حذف مفتاح API هذا؟", "delete_api_key_prompt": "هل أنت متأكد أنك تريد حذف مفتاح API هذا؟",
"delete_dialog_alert": " هذه العناصر سيتم حذفها بشكل دائم من جهازك ومن تطبيق",
"delete_dialog_alert_local": " العناصر التي تم حذفها من جهازك ولكنها موجوده في تطبيق",
"delete_dialog_alert_local_non_backed_up": "بعض العناصر التي سيتم حذفها بشكل دائم ولا يوجد لها نسخه احتياطيه في تطبيق ",
"delete_dialog_alert_remote": "العناصر التي سيتم حذفها بشكل دائم من تطبيق",
"delete_dialog_ok_force": "احذف على أي حال",
"delete_dialog_title": "الحذف بشكل نهائي",
"delete_duplicates_confirmation": "هل أنت متأكد أنك تريد حذف هذه التكرارات نهائيًا؟", "delete_duplicates_confirmation": "هل أنت متأكد أنك تريد حذف هذه التكرارات نهائيًا؟",
"delete_face": "حذف الوجه", "delete_face": "حذف الوجه",
"delete_key": "حذف المفتاح", "delete_key": "حذف المفتاح",
"delete_library": "حذف المكتبة", "delete_library": "حذف المكتبة",
"delete_link": "حذف الرابط", "delete_link": "حذف الرابط",
"delete_local_dialog_ok_backed_up_only": "حذف النسخة الاحتياطية فقط",
"delete_local_dialog_ok_force": "احذف على أي حال",
"delete_others": "حذف الأخرى", "delete_others": "حذف الأخرى",
"delete_shared_link": "حذف الرابط المشترك", "delete_shared_link": "حذف الرابط المشترك",
"delete_shared_link_dialog_title": "حذف الرابط المشترك",
"delete_tag": "حذف العلامة", "delete_tag": "حذف العلامة",
"delete_tag_confirmation_prompt": "هل أنت متأكد أنك تريد حذف العلامة {tagName}؟", "delete_tag_confirmation_prompt": "هل أنت متأكد أنك تريد حذف العلامة {tagName}؟",
"delete_user": "حذف المستخدم", "delete_user": "حذف المستخدم",
"deleted_shared_link": "تم حذف الرابط المشارك", "deleted_shared_link": "تم حذف الرابط المشارك",
"deletes_missing_assets": "حذف الأصول المفقودة من القرص", "deletes_missing_assets": "حذف الأصول المفقودة من القرص",
"description": "وصف", "description": "وصف",
"description_input_hint_text": "اضف وصفا...",
"description_input_submit_error": "خطأ تحديث الوصف ، تحقق من السجل لمزيد من التفاصيل",
"details": "تفاصيل", "details": "تفاصيل",
"direction": "الإتجاه", "direction": "الإتجاه",
"disabled": "معطل", "disabled": "معطل",
@@ -565,12 +746,26 @@
"documentation": "الوثائق", "documentation": "الوثائق",
"done": "تم", "done": "تم",
"download": "تنزيل", "download": "تنزيل",
"download_canceled": "Download canceled",
"download_complete": "Download complete",
"download_enqueue": "Download enqueued",
"download_error": "Download Error",
"download_failed": "Download failed",
"download_filename": "file: {}",
"download_finished": "Download finished",
"download_include_embedded_motion_videos": "مقاطع الفيديو المدمجة", "download_include_embedded_motion_videos": "مقاطع الفيديو المدمجة",
"download_include_embedded_motion_videos_description": "تضمين مقاطع الفيديو المضمنة في الصور المتحركة كملف منفصل", "download_include_embedded_motion_videos_description": "تضمين مقاطع الفيديو المضمنة في الصور المتحركة كملف منفصل",
"download_notfound": "Download not found",
"download_paused": "Download paused",
"download_settings": "التنزيلات", "download_settings": "التنزيلات",
"download_settings_description": "إدارة الإعدادات المتعلقة بتنزيل المحتويات", "download_settings_description": "إدارة الإعدادات المتعلقة بتنزيل المحتويات",
"download_started": "Download started",
"download_sucess": "Download success",
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
"download_waiting_to_retry": "Waiting to retry",
"downloading": "جارٍ التنزيل", "downloading": "جارٍ التنزيل",
"downloading_asset_filename": "{filename} قيد التنزيل", "downloading_asset_filename": "{filename} قيد التنزيل",
"downloading_media": "Downloading media",
"drop_files_to_upload": "قم بإسقاط الملفات في أي مكان لرفعها", "drop_files_to_upload": "قم بإسقاط الملفات في أي مكان لرفعها",
"duplicates": "التكرارات", "duplicates": "التكرارات",
"duplicates_description": "قم بحل كل مجموعة من خلال الإشارة إلى التكرارات، إن وجدت", "duplicates_description": "قم بحل كل مجموعة من خلال الإشارة إلى التكرارات، إن وجدت",
@@ -587,6 +782,7 @@
"edit_key": "تعديل المفتاح", "edit_key": "تعديل المفتاح",
"edit_link": "تغيير الرابط", "edit_link": "تغيير الرابط",
"edit_location": "تعديل الموقع", "edit_location": "تعديل الموقع",
"edit_location_dialog_title": "موقع",
"edit_name": "تعديل الاسم", "edit_name": "تعديل الاسم",
"edit_people": "تعديل الأشخاص", "edit_people": "تعديل الأشخاص",
"edit_tag": "تعديل العلامة", "edit_tag": "تعديل العلامة",
@@ -599,14 +795,19 @@
"editor_crop_tool_h2_aspect_ratios": "نسب العرض إلى الارتفاع", "editor_crop_tool_h2_aspect_ratios": "نسب العرض إلى الارتفاع",
"editor_crop_tool_h2_rotation": "التدوير", "editor_crop_tool_h2_rotation": "التدوير",
"email": "البريد الإلكتروني", "email": "البريد الإلكتروني",
"empty_folder": "This folder is empty",
"empty_trash": "أفرغ سلة المهملات", "empty_trash": "أفرغ سلة المهملات",
"empty_trash_confirmation": "هل أنت متأكد أنك تريد إفراغ سلة المهملات؟ سيؤدي هذا إلى إزالة جميع المحتويات الموجودة في سلة المهملات بشكل نهائي من Immich.\nلا يمكنك التراجع عن هذا الإجراء!", "empty_trash_confirmation": "هل أنت متأكد أنك تريد إفراغ سلة المهملات؟ سيؤدي هذا إلى إزالة جميع المحتويات الموجودة في سلة المهملات بشكل نهائي من Immich.\nلا يمكنك التراجع عن هذا الإجراء!",
"enable": "تفعيل", "enable": "تفعيل",
"enabled": "مفعل", "enabled": "مفعل",
"end_date": "تاريخ الإنتهاء", "end_date": "تاريخ الإنتهاء",
"enqueued": "Enqueued",
"enter_wifi_name": "Enter WiFi name",
"error": "خطأ", "error": "خطأ",
"error_change_sort_album": "Failed to change album sort order",
"error_delete_face": "حدث خطأ في حذف الوجه من الأصول", "error_delete_face": "حدث خطأ في حذف الوجه من الأصول",
"error_loading_image": "حدث خطأ أثناء تحميل الصورة", "error_loading_image": "حدث خطأ أثناء تحميل الصورة",
"error_saving_image": "Error: {}",
"error_title": "خطأ - حدث خللٌ ما", "error_title": "خطأ - حدث خللٌ ما",
"errors": { "errors": {
"cannot_navigate_next_asset": "لا يمكن الانتقال إلى المحتوى التالي", "cannot_navigate_next_asset": "لا يمكن الانتقال إلى المحتوى التالي",
@@ -735,8 +936,21 @@
"unable_to_upload_file": "تعذر رفع الملف" "unable_to_upload_file": "تعذر رفع الملف"
}, },
"exif": "Exif (صيغة ملف صوري قابل للتبادل)", "exif": "Exif (صيغة ملف صوري قابل للتبادل)",
"exif_bottom_sheet_description": "اضف وصفا...",
"exif_bottom_sheet_details": "تفاصيل",
"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": "خروج من العرض التقديمي", "exit_slideshow": "خروج من العرض التقديمي",
"expand_all": "توسيع الكل", "expand_all": "توسيع الكل",
"experimental_settings_new_asset_list_subtitle": "أعمال جارية",
"experimental_settings_new_asset_list_title": "تمكين شبكة الصور التجريبية",
"experimental_settings_subtitle": "استخدام على مسؤوليتك الخاصة!",
"experimental_settings_title": "تجريبي",
"expire_after": "تنتهي بعد", "expire_after": "تنتهي بعد",
"expired": "منتهي الصلاحية", "expired": "منتهي الصلاحية",
"expires_date": "تنتهي الصلاحية في {date}", "expires_date": "تنتهي الصلاحية في {date}",
@@ -747,11 +961,16 @@
"extension": "الإمتداد", "extension": "الإمتداد",
"external": "خارجي", "external": "خارجي",
"external_libraries": "المكتبات الخارجية", "external_libraries": "المكتبات الخارجية",
"external_network": "External network",
"external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
"face_unassigned": "غير معين", "face_unassigned": "غير معين",
"failed": "Failed",
"failed_to_load_assets": "فشل تحميل الأصول", "failed_to_load_assets": "فشل تحميل الأصول",
"failed_to_load_folder": "Failed to load folder",
"favorite": "مفضل", "favorite": "مفضل",
"favorite_or_unfavorite_photo": "تفضيل أو إلغاء تفضيل الصورة", "favorite_or_unfavorite_photo": "تفضيل أو إلغاء تفضيل الصورة",
"favorites": "المفضلة", "favorites": "المفضلة",
"favorites_page_no_favorites": "لم يتم العثور على الأصول المفضلة",
"feature_photo_updated": "تم تحديث الصورة المميزة", "feature_photo_updated": "تم تحديث الصورة المميزة",
"features": "الميزات", "features": "الميزات",
"features_setting_description": "إدارة ميزات التطبيق", "features_setting_description": "إدارة ميزات التطبيق",
@@ -759,25 +978,38 @@
"file_name_or_extension": "اسم الملف أو امتداده", "file_name_or_extension": "اسم الملف أو امتداده",
"filename": "اسم الملف", "filename": "اسم الملف",
"filetype": "نوع الملف", "filetype": "نوع الملف",
"filter": "Filter",
"filter_people": "تصفية الاشخاص", "filter_people": "تصفية الاشخاص",
"find_them_fast": "يمكنك العثور عليها بسرعة بالاسم من خلال البحث", "find_them_fast": "يمكنك العثور عليها بسرعة بالاسم من خلال البحث",
"fix_incorrect_match": "إصلاح المطابقة غير الصحيحة", "fix_incorrect_match": "إصلاح المطابقة غير الصحيحة",
"folder": "Folder",
"folder_not_found": "Folder not found",
"folders": "المجلدات", "folders": "المجلدات",
"folders_feature_description": "تصفح عرض المجلد للصور ومقاطع الفيديو الموجودة على نظام الملفات", "folders_feature_description": "تصفح عرض المجلد للصور ومقاطع الفيديو الموجودة على نظام الملفات",
"forward": "إلى الأمام", "forward": "إلى الأمام",
"general": "عام", "general": "عام",
"get_help": "الحصول على المساعدة", "get_help": "الحصول على المساعدة",
"get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
"getting_started": "البدء", "getting_started": "البدء",
"go_back": "الرجوع للخلف", "go_back": "الرجوع للخلف",
"go_to_folder": "اذهب إلى المجلد", "go_to_folder": "اذهب إلى المجلد",
"go_to_search": "اذهب إلى البحث", "go_to_search": "اذهب إلى البحث",
"grant_permission": "Grant permission",
"group_albums_by": "تجميع الألبومات حسب...", "group_albums_by": "تجميع الألبومات حسب...",
"group_country": "مجموعة البلد", "group_country": "مجموعة البلد",
"group_no": "بدون تجميع", "group_no": "بدون تجميع",
"group_owner": "تجميع حسب المالك", "group_owner": "تجميع حسب المالك",
"group_places_by": "تجميع الأماكن حسب...", "group_places_by": "تجميع الأماكن حسب...",
"group_year": "تجميع حسب السنة", "group_year": "تجميع حسب السنة",
"haptic_feedback_switch": "تمكين ردود الفعل اللمسية",
"haptic_feedback_title": "ردود فعل لمسية",
"has_quota": "محدد بحصة", "has_quota": "محدد بحصة",
"header_settings_add_header_tip": "Add Header",
"header_settings_field_validator_msg": "Value cannot be empty",
"header_settings_header_name_input": "Header name",
"header_settings_header_value_input": "Header value",
"headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request",
"headers_settings_tile_title": "Custom proxy headers",
"hi_user": "مرحبا {name} ({email})", "hi_user": "مرحبا {name} ({email})",
"hide_all_people": "إخفاء جميع الأشخاص", "hide_all_people": "إخفاء جميع الأشخاص",
"hide_gallery": "اخفاء المعرض", "hide_gallery": "اخفاء المعرض",
@@ -785,8 +1017,24 @@
"hide_password": "اخفاء كلمة المرور", "hide_password": "اخفاء كلمة المرور",
"hide_person": "اخفاء الشخص", "hide_person": "اخفاء الشخص",
"hide_unnamed_people": "إخفاء الأشخاص بدون إسم", "hide_unnamed_people": "إخفاء الأشخاص بدون إسم",
"home_page_add_to_album_conflicts": "تمت إضافة {تمت إضافة} الأصول إلى الألبوم {الألبوم}.{فشل} الأصول موجودة بالفعل في الألبوم.",
"home_page_add_to_album_err_local": "لا يمكن إضافة الأصول المحلية إلى الألبومات حتى الآن ، سوف يتخطى",
"home_page_add_to_album_success": "تمت إضافة {تمت إضافة} الأصول إلى الألبوم {الألبوم}.",
"home_page_album_err_partner": "لا يمكن إضافة أصول شريكة إلى ألبوم حتى الآن ، سوف يتخطى",
"home_page_archive_err_local": "لا يمكن أرشفة الأصول المحلية حتى الآن ، سوف يتخطى",
"home_page_archive_err_partner": "لا يمكن أرشفة الأصول الشريكة ، سوف يتخطى",
"home_page_building_timeline": "بناء الجدول الزمني",
"home_page_delete_err_partner": "لا يمكن حذف الأصول الشريكة ,سوف يتخطى",
"home_page_delete_remote_err_local": "الأصول المحلية في التحديد البعيد المحذوف، سوف يتخطى",
"home_page_favorite_err_local": "لا يمكن تفضيل الأصول المحلية بعد، سوف يتخطى",
"home_page_favorite_err_partner": "لا يمكن الأصول الشريكة المفضلة بعد ، سوف يتخطى",
"home_page_first_time_notice": "إذا كانت هذه هي المرة الأولى التي تستخدم فيها التطبيق، فيرجى التأكد من اختيار ألبوم (ألبومات) احتياطية حتى يتمكن المخطط الزمني من ملء الصور ومقاطع الفيديو في الألبوم (الألبومات).",
"home_page_share_err_local": "لا يمكن مشاركة الأصول المحلية عبر الرابط ، سوف يتخطى",
"home_page_upload_err_limit": "لا يمكن إلا تحميل 30 أحد الأصول في وقت واحد ، سوف يتخطى",
"host": "المضيف", "host": "المضيف",
"hour": "ساعة", "hour": "ساعة",
"ignore_icloud_photos": "Ignore iCloud photos",
"ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
"image": "صورة", "image": "صورة",
"image_alt_text_date": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {date}", "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {date}",
"image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} تم التقاطها مع {person1} في {date}", "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} تم التقاطها مع {person1} في {date}",
@@ -798,6 +1046,10 @@
"image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}، {country} مع {person1} و{person2} في {date}", "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}، {country} مع {person1} و{person2} في {date}",
"image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}، {country} مع {person1}، {person2}، و{person3} في {date}", "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}، {country} مع {person1}، {person2}، و{person3} في {date}",
"image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}, {country} with {person1}, {person2}, مع {additionalCount, number} آخرين في {date}", "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}, {country} with {person1}, {person2}, مع {additionalCount, number} آخرين في {date}",
"image_saved_successfully": "Image saved",
"image_viewer_page_state_provider_download_started": "بدأ التنزيل",
"image_viewer_page_state_provider_download_success": "تم التنزيل بنجاح",
"image_viewer_page_state_provider_share_error": "خطأ في المشاركة",
"immich_logo": "شعار immich", "immich_logo": "شعار immich",
"immich_web_interface": "واجهة ويب immich", "immich_web_interface": "واجهة ويب immich",
"import_from_json": "استيراد من JSON", "import_from_json": "استيراد من JSON",
@@ -816,6 +1068,8 @@
"night_at_midnight": "كل ليلة عند منتصف الليل", "night_at_midnight": "كل ليلة عند منتصف الليل",
"night_at_twoam": "كل ليلة الساعة 2 صباحا" "night_at_twoam": "كل ليلة الساعة 2 صباحا"
}, },
"invalid_date": "Invalid date",
"invalid_date_format": "Invalid date format",
"invite_people": "دعوة الأشخاص", "invite_people": "دعوة الأشخاص",
"invite_to_album": "دعوة إلى الألبوم", "invite_to_album": "دعوة إلى الألبوم",
"items_count": "{count, plural, one {# عنصر} other {# عناصر}}", "items_count": "{count, plural, one {# عنصر} other {# عناصر}}",
@@ -836,6 +1090,12 @@
"level": "المستوى", "level": "المستوى",
"library": "مكتبة", "library": "مكتبة",
"library_options": "خيارات المكتبة", "library_options": "خيارات المكتبة",
"library_page_device_albums": "ألبومات على الجهاز",
"library_page_new_album": "البوم جديد",
"library_page_sort_asset_count": "عدد الأصول",
"library_page_sort_created": "تاريخ الإنشاء",
"library_page_sort_last_modified": "آخر تعديل",
"library_page_sort_title": "عنوان الألبوم",
"light": "المضيئ", "light": "المضيئ",
"like_deleted": "تم حذف الإعجاب", "like_deleted": "تم حذف الإعجاب",
"link_motion_video": "رابط فيديو الحركة", "link_motion_video": "رابط فيديو الحركة",
@@ -845,12 +1105,42 @@
"list": "قائمة", "list": "قائمة",
"loading": "تحميل", "loading": "تحميل",
"loading_search_results_failed": "فشل تحميل نتائج البحث", "loading_search_results_failed": "فشل تحميل نتائج البحث",
"local_network": "Local network",
"local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
"location_permission": "Location permission",
"location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
"location_picker_choose_on_map": "اختر على الخريطة",
"location_picker_latitude_error": "أدخل خط عرض صالح",
"location_picker_latitude_hint": "أدخل خط العرض الخاص بك هنا",
"location_picker_longitude_error": "أدخل خط الطول الصحيح",
"location_picker_longitude_hint": "أدخل خط الطول هنا",
"log_out": "تسجيل خروج", "log_out": "تسجيل خروج",
"log_out_all_devices": "تسجيل الخروج من كافة الأجهزة", "log_out_all_devices": "تسجيل الخروج من كافة الأجهزة",
"logged_out_all_devices": "تم تسجيل الخروج من جميع الأجهزة", "logged_out_all_devices": "تم تسجيل الخروج من جميع الأجهزة",
"logged_out_device": "تم تسجيل الخروج من الجهاز", "logged_out_device": "تم تسجيل الخروج من الجهاز",
"login": "تسجيل الدخول", "login": "تسجيل الدخول",
"login_disabled": "تم تعطيل تسجيل الدخول",
"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": "بريد إلكتروني خاطئ",
"login_form_err_invalid_url": "URL غير صالح",
"login_form_err_leading_whitespace": "قيادة المساحة البيضاء",
"login_form_err_trailing_whitespace": "زائدة بيضاء",
"login_form_failed_get_oauth_server_config": "تسجيل الخطأ باستخدام OAUTH ، تحقق من عنوان URL لخادم",
"login_form_failed_get_oauth_server_disable": "ميزة OAuth غير متوفرة على هذا الخادم",
"login_form_failed_login": "خطأ في تسجيل الدخول ، تحقق من عنوان URL للخادم والبريد الإلكتروني وكلمة المرور",
"login_form_handshake_exception": "كان هناك استثناء مصافحة مع الخادم.تمكين دعم الشهادة الموقعة ذاتيا في الإعدادات إذا كنت تستخدم شهادة موقعة ذاتيا.",
"login_form_password_hint": "كلمة المرور",
"login_form_save_login": "ابق متصلا",
"login_form_server_empty": "أدخل عنوان URL الخادم.",
"login_form_server_error": "لا يمكن الاتصال بالخادم.",
"login_has_been_disabled": "تم تعطيل تسجيل الدخول.", "login_has_been_disabled": "تم تعطيل تسجيل الدخول.",
"login_password_changed_error": "كان هناك خطأ في تحديث كلمة المرور الخاصة بك",
"login_password_changed_success": "تم تحديث كلمة السر بنجاح",
"logout_all_device_confirmation": "هل أنت متأكد أنك تريد تسجيل الخروج من جميع الأجهزة؟", "logout_all_device_confirmation": "هل أنت متأكد أنك تريد تسجيل الخروج من جميع الأجهزة؟",
"logout_this_device_confirmation": "هل أنت متأكد أنك تريد تسجيل الخروج من هذا الجهاز؟", "logout_this_device_confirmation": "هل أنت متأكد أنك تريد تسجيل الخروج من هذا الجهاز؟",
"longitude": "خط الطول", "longitude": "خط الطول",
@@ -867,13 +1157,40 @@
"manage_your_devices": "إدارة الأجهزة التي تم تسجيل الدخول إليها", "manage_your_devices": "إدارة الأجهزة التي تم تسجيل الدخول إليها",
"manage_your_oauth_connection": "إدارة اتصال OAuth الخاص بك", "manage_your_oauth_connection": "إدارة اتصال OAuth الخاص بك",
"map": "الخريطة", "map": "الخريطة",
"map_assets_in_bound": "{} photo",
"map_assets_in_bounds": "{} photos",
"map_cannot_get_user_location": "لا يمكن الحصول على موقع المستخدم",
"map_location_dialog_yes": "نعم",
"map_location_picker_page_use_location": "استخدم هذا الموقع",
"map_location_service_disabled_content": "يجب تمكين خدمة الموقع لعرض الأصول من موقعك الحالي.هل تريد تمكينه الآن؟",
"map_location_service_disabled_title": "خدمة الموقع معطل",
"map_marker_for_images": "علامة الخريطة للصور الملتقطة في {city}، {country}", "map_marker_for_images": "علامة الخريطة للصور الملتقطة في {city}، {country}",
"map_marker_with_image": "علامة الخريطة مع الصورة", "map_marker_with_image": "علامة الخريطة مع الصورة",
"map_no_assets_in_bounds": "لا توجد صور في هذا المجال",
"map_no_location_permission_content": "هناك حاجة إلى إذن الموقع لعرض الأصول من موقعك الحالي.هل تريد السماح به الآن؟",
"map_no_location_permission_title": "تم رفض إذن الموقع",
"map_settings": "إعدادات الخريطة", "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": "تضمين الشركاء",
"map_settings_only_show_favorites": "اظهار المفضلة فقط",
"map_settings_theme_settings": "مظهر الخريطة",
"map_zoom_to_see_photos": "قم بتصغيرها لرؤية الصور",
"matches": "تطابقات", "matches": "تطابقات",
"media_type": "نوع الوسائط", "media_type": "نوع الوسائط",
"memories": "الذكريات", "memories": "الذكريات",
"memories_all_caught_up": "كل شيء محدث",
"memories_check_back_tomorrow": "التحقق مرة أخرى غدا لمزيد من الذكريات",
"memories_setting_description": "إدارة ما تراه في ذكرياتك", "memories_setting_description": "إدارة ما تراه في ذكرياتك",
"memories_start_over": "ابدأ من جديد",
"memories_swipe_to_close": "اسحب لأعلى للإغلاق",
"memories_year_ago": "A year ago",
"memories_years_ago": "{} years ago",
"memory": "ذكرى", "memory": "ذكرى",
"memory_lane_title": "ذكرياتٌ من {title}", "memory_lane_title": "ذكرياتٌ من {title}",
"menu": "القائمة", "menu": "القائمة",
@@ -888,12 +1205,17 @@
"missing": "المفقودة", "missing": "المفقودة",
"model": "نموذج", "model": "نموذج",
"month": "شهر", "month": "شهر",
"monthly_title_text_date_format": "ط ط ط",
"more": "المزيد", "more": "المزيد",
"moved_to_trash": "تم النقل إلى سلة المهملات", "moved_to_trash": "تم النقل إلى سلة المهملات",
"multiselect_grid_edit_date_time_err_read_only": "لا يمكن تعديل تاريخ الأصول (المواد) للقراءة فقط، سوف يتخطى",
"multiselect_grid_edit_gps_err_read_only": "لا يمكن تعديل موقع الأصول (المواد) للقراءة فقط، سوف يتخطى",
"mute_memories": "كتم الذكريات", "mute_memories": "كتم الذكريات",
"my_albums": "ألبوماتي", "my_albums": "ألبوماتي",
"name": "الاسم", "name": "الاسم",
"name_or_nickname": "الاسم أو اللقب", "name_or_nickname": "الاسم أو اللقب",
"networking_settings": "Networking",
"networking_subtitle": "Manage the server endpoint settings",
"never": "أبداً", "never": "أبداً",
"new_album": "البوم جديد", "new_album": "البوم جديد",
"new_api_key": "مفتاح API جديد", "new_api_key": "مفتاح API جديد",
@@ -910,6 +1232,7 @@
"no_albums_yet": "يبدو أنه ليس لديك أي ألبومات حتى الآن.", "no_albums_yet": "يبدو أنه ليس لديك أي ألبومات حتى الآن.",
"no_archived_assets_message": "أرشفة الصور ومقاطع الفيديو لإخفائها من عرض الصور لديك", "no_archived_assets_message": "أرشفة الصور ومقاطع الفيديو لإخفائها من عرض الصور لديك",
"no_assets_message": "انقر لتحميل صورتك الأولى", "no_assets_message": "انقر لتحميل صورتك الأولى",
"no_assets_to_show": "لا توجد أصول لعرضها",
"no_duplicates_found": "لم يتم العثور على أي تكرارات.", "no_duplicates_found": "لم يتم العثور على أي تكرارات.",
"no_exif_info_available": "لا تتوفر معلومات exif", "no_exif_info_available": "لا تتوفر معلومات exif",
"no_explore_results_message": "قم برفع المزيد من الصور لاستكشاف مجموعتك.", "no_explore_results_message": "قم برفع المزيد من الصور لاستكشاف مجموعتك.",
@@ -921,8 +1244,13 @@
"no_results_description": "جرب كلمة رئيسية مرادفة أو أكثر عمومية", "no_results_description": "جرب كلمة رئيسية مرادفة أو أكثر عمومية",
"no_shared_albums_message": "قم بإنشاء ألبوم لمشاركة الصور ومقاطع الفيديو مع الأشخاص في شبكتك", "no_shared_albums_message": "قم بإنشاء ألبوم لمشاركة الصور ومقاطع الفيديو مع الأشخاص في شبكتك",
"not_in_any_album": "ليست في أي ألبوم", "not_in_any_album": "ليست في أي ألبوم",
"not_selected": "Not selected",
"note_apply_storage_label_to_previously_uploaded assets": "ملاحظة: لتطبيق تسمية التخزين على المحتويات التي تم رفعها مسبقًا، قم بتشغيل", "note_apply_storage_label_to_previously_uploaded assets": "ملاحظة: لتطبيق تسمية التخزين على المحتويات التي تم رفعها مسبقًا، قم بتشغيل",
"notes": "ملاحظات", "notes": "ملاحظات",
"notification_permission_dialog_content": "لتمكين الإخطارات ، انتقل إلى الإعدادات و اختار السماح.",
"notification_permission_list_tile_content": "منح إذن لتمكين الإخطارات.",
"notification_permission_list_tile_enable_button": "تمكين الإخطارات",
"notification_permission_list_tile_title": "إذن الإخطار",
"notification_toggle_setting_description": "تفعيل إشعارات البريد الإلكتروني", "notification_toggle_setting_description": "تفعيل إشعارات البريد الإلكتروني",
"notifications": "إشعارات", "notifications": "إشعارات",
"notifications_setting_description": "إدارة الإشعارات", "notifications_setting_description": "إدارة الإشعارات",
@@ -933,6 +1261,7 @@
"offline_paths_description": "قد تكون هذه النتائج بسبب الحذف اليدوي للملفات التي لا تشكل جزءًا من مكتبة خارجية.", "offline_paths_description": "قد تكون هذه النتائج بسبب الحذف اليدوي للملفات التي لا تشكل جزءًا من مكتبة خارجية.",
"ok": "نعم", "ok": "نعم",
"oldest_first": "الأقدم أولا", "oldest_first": "الأقدم أولا",
"on_this_device": "On this device",
"onboarding": "الإعداد الأولي", "onboarding": "الإعداد الأولي",
"onboarding_privacy_description": "تعتمد الميزات التالية (اختياري) على خدمات خارجية، ويمكن تعطيلها في أي وقت في إعدادات الإدارة.", "onboarding_privacy_description": "تعتمد الميزات التالية (اختياري) على خدمات خارجية، ويمكن تعطيلها في أي وقت في إعدادات الإدارة.",
"onboarding_theme_description": "اختر نسق الألوان للنسخة الخاصة بك. يمكنك تغيير ذلك لاحقًا في إعداداتك.", "onboarding_theme_description": "اختر نسق الألوان للنسخة الخاصة بك. يمكنك تغيير ذلك لاحقًا في إعداداتك.",
@@ -956,6 +1285,14 @@
"partner_can_access": "يستطيع {partner} الوصول", "partner_can_access": "يستطيع {partner} الوصول",
"partner_can_access_assets": "جميع الصور ومقاطع الفيديو الخاصة بك باستثناء تلك الموجودة في المؤرشفة والمحذوفة", "partner_can_access_assets": "جميع الصور ومقاطع الفيديو الخاصة بك باستثناء تلك الموجودة في المؤرشفة والمحذوفة",
"partner_can_access_location": "الموقع الذي تم التقاط صورك فيه", "partner_can_access_location": "الموقع الذي تم التقاط صورك فيه",
"partner_list_user_photos": "{user}'s photos",
"partner_list_view_all": "عرض الكل",
"partner_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": "مشاركة الشركاء", "partner_sharing": "مشاركة الشركاء",
"partners": "الشركاء", "partners": "الشركاء",
"password": "كلمة المرور", "password": "كلمة المرور",
@@ -984,6 +1321,14 @@
"permanently_delete_assets_prompt": "هل أنت متأكد أنك تريد حذف {count, plural, one {هذا العنصر؟} other {هذه العناصر <b>#</b>؟}} سيتم أيضًا إزالته {count, plural, one {من ألبومه} other {من ألبوماتهم}}.", "permanently_delete_assets_prompt": "هل أنت متأكد أنك تريد حذف {count, plural, one {هذا العنصر؟} other {هذه العناصر <b>#</b>؟}} سيتم أيضًا إزالته {count, plural, one {من ألبومه} other {من ألبوماتهم}}.",
"permanently_deleted_asset": "تم حذف الأصل بشكل نهائي", "permanently_deleted_asset": "تم حذف الأصل بشكل نهائي",
"permanently_deleted_assets_count": "تم حذف {count, plural, one {# محتوى} other {# المحتويات}} نهائيًا", "permanently_deleted_assets_count": "تم حذف {count, plural, one {# محتوى} other {# المحتويات}} نهائيًا",
"permission_onboarding_back": "خلف",
"permission_onboarding_continue_anyway": "تواصل على أي حال",
"permission_onboarding_get_started": "البدء",
"permission_onboarding_go_to_settings": "اذهب للاعدادات",
"permission_onboarding_permission_denied": "تم رفض الإذن. لاستخدام التطبيق، قم بمنح أذونات الصور والفيديو في الإعدادات ",
"permission_onboarding_permission_granted": "تم تأمين التصريح! وضعك تمام.",
"permission_onboarding_permission_limited": "إذن محدود. للسماح بالنسخ الاحتياطي للتطبيق وإدارة مجموعة المعرض بالكامل، امنح أذونات الصور والفيديو في الإعدادات.",
"permission_onboarding_request": "يتطلب التطبيق إذنًا لعرض الصور ومقاطع الفيديو الخاصة بك",
"person": "شخص", "person": "شخص",
"person_birthdate": "تاريخ الميلاد {التاريخ}", "person_birthdate": "تاريخ الميلاد {التاريخ}",
"person_hidden": "{name}{hidden, select, true { (مخفي)} other {}}", "person_hidden": "{name}{hidden, select, true { (مخفي)} other {}}",
@@ -1001,6 +1346,8 @@
"play_motion_photo": "تشغيل الصور المتحركة", "play_motion_photo": "تشغيل الصور المتحركة",
"play_or_pause_video": "تشغيل الفيديو أو إيقافه مؤقتًا", "play_or_pause_video": "تشغيل الفيديو أو إيقافه مؤقتًا",
"port": "المنفذ", "port": "المنفذ",
"preferences_settings_subtitle": "Manage the app's preferences",
"preferences_settings_title": "التفضيلات",
"preset": "الإعداد المسبق", "preset": "الإعداد المسبق",
"preview": "معاينة", "preview": "معاينة",
"previous": "السابق", "previous": "السابق",
@@ -1008,6 +1355,13 @@
"previous_or_next_photo": "الصورة السابقة أو التالية", "previous_or_next_photo": "الصورة السابقة أو التالية",
"primary": "أساسي", "primary": "أساسي",
"privacy": "الخصوصية", "privacy": "الخصوصية",
"profile_drawer_app_logs": "السجلات",
"profile_drawer_client_out_of_date_major": "تطبيق الهاتف المحمول قديم.يرجى التحديث إلى أحدث إصدار رئيسي.",
"profile_drawer_client_out_of_date_minor": "تطبيق الهاتف المحمول قديم.يرجى التحديث إلى أحدث إصدار صغير.",
"profile_drawer_client_server_up_to_date": "العميل والخادم محدثان",
"profile_drawer_github": "Github",
"profile_drawer_server_out_of_date_major": "الخادم قديم.يرجى التحديث إلى أحدث إصدار رئيسي.",
"profile_drawer_server_out_of_date_minor": "الخادم قديم.يرجى التحديث إلى أحدث إصدار صغير.",
"profile_image_of_user": "صورة الملف الشخصي لـ {user}", "profile_image_of_user": "صورة الملف الشخصي لـ {user}",
"profile_picture_set": "مجموعة الصور الشخصية.", "profile_picture_set": "مجموعة الصور الشخصية.",
"public_album": "الألبوم العام", "public_album": "الألبوم العام",
@@ -1057,6 +1411,8 @@
"recent": "حديث", "recent": "حديث",
"recent-albums": "ألبومات الحديثة", "recent-albums": "ألبومات الحديثة",
"recent_searches": "عمليات البحث الأخيرة", "recent_searches": "عمليات البحث الأخيرة",
"recently_added": "Recently added",
"recently_added_page_title": "أضيف مؤخرا",
"refresh": "تحديث", "refresh": "تحديث",
"refresh_encoded_videos": "تحديث مقاطع الفيديو المشفرة", "refresh_encoded_videos": "تحديث مقاطع الفيديو المشفرة",
"refresh_faces": "تحديث الوجوه", "refresh_faces": "تحديث الوجوه",
@@ -1113,10 +1469,12 @@
"role_editor": "المحرر", "role_editor": "المحرر",
"role_viewer": "العارض", "role_viewer": "العارض",
"save": "حفظ", "save": "حفظ",
"save_to_gallery": "Save to gallery",
"saved_api_key": "تم حفظ مفتاح الـ API", "saved_api_key": "تم حفظ مفتاح الـ API",
"saved_profile": "تم حفظ الملف", "saved_profile": "تم حفظ الملف",
"saved_settings": "تم حفظ الإعدادات", "saved_settings": "تم حفظ الإعدادات",
"say_something": "قل شيئًا", "say_something": "قل شيئًا",
"scaffold_body_error_occurred": "حدث خطأ",
"scan_all_libraries": "فحص كل المكتبات", "scan_all_libraries": "فحص كل المكتبات",
"scan_library": "مسح", "scan_library": "مسح",
"scan_settings": "إعدادات الفحص", "scan_settings": "إعدادات الفحص",
@@ -1132,16 +1490,45 @@
"search_camera_model": "البحث حسب موديل الكاميرا...", "search_camera_model": "البحث حسب موديل الكاميرا...",
"search_city": "البحث حسب المدينة...", "search_city": "البحث حسب المدينة...",
"search_country": "البحث حسب الدولة...", "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": "البحث عن",
"search_for_existing_person": "البحث عن شخص موجود", "search_for_existing_person": "البحث عن شخص موجود",
"search_no_more_result": "No more results",
"search_no_people": "لا يوجد أشخاص", "search_no_people": "لا يوجد أشخاص",
"search_no_people_named": "لا يوجد أشخاص بالاسم \"{name}\"", "search_no_people_named": "لا يوجد أشخاص بالاسم \"{name}\"",
"search_no_result": "No results found, try a different search term or combination",
"search_options": "خيارات البحث", "search_options": "خيارات البحث",
"search_page_categories": "فئات",
"search_page_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": "عرض الكل",
"search_page_your_activity": "نشاطك",
"search_page_your_map": "خريطتك",
"search_people": "البحث عن الأشخاص", "search_people": "البحث عن الأشخاص",
"search_places": "البحث عن الأماكن", "search_places": "البحث عن الأماكن",
"search_rating": "البحث حسب التقييم...", "search_rating": "البحث حسب التقييم...",
"search_result_page_new_search_hint": "بحث جديد",
"search_settings": "إعدادات البحث", "search_settings": "إعدادات البحث",
"search_state": "البحث حسب الولاية...", "search_state": "البحث حسب الولاية...",
"search_suggestion_list_smart_search_hint_1": "يتم تمكين البحث الذكي افتراضيًا ، للبحث عن البيانات الوصفية ، استخدم بناء الجملة",
"search_suggestion_list_smart_search_hint_2": "م: البحث الخاص بك",
"search_tags": "البحث عن العلامات...", "search_tags": "البحث عن العلامات...",
"search_timezone": "البحث حسب المنطقة الزمنية...", "search_timezone": "البحث حسب المنطقة الزمنية...",
"search_type": "نوع البحث", "search_type": "نوع البحث",
@@ -1162,10 +1549,14 @@
"select_new_face": "تحديد وجه جديد", "select_new_face": "تحديد وجه جديد",
"select_photos": "تحديد الصور", "select_photos": "تحديد الصور",
"select_trash_all": "تحديد حذف الكلِ", "select_trash_all": "تحديد حذف الكلِ",
"select_user_for_sharing_page_err_album": "فشل في إنشاء ألبوم",
"selected": "التحديد", "selected": "التحديد",
"selected_count": "{count, plural, other {# محددة }}", "selected_count": "{count, plural, other {# محددة }}",
"send_message": "‏إرسال رسالة", "send_message": "‏إرسال رسالة",
"send_welcome_email": "إرسال بريدًا إلكترونيًا ترحيبيًا", "send_welcome_email": "إرسال بريدًا إلكترونيًا ترحيبيًا",
"server_endpoint": "Server Endpoint",
"server_info_box_app_version": "نسخة التطبيق",
"server_info_box_server_url": "عنوان URL الخادم",
"server_offline": "الخادم غير متصل", "server_offline": "الخادم غير متصل",
"server_online": "الخادم متصل", "server_online": "الخادم متصل",
"server_stats": "إحصائيات الخادم", "server_stats": "إحصائيات الخادم",
@@ -1177,22 +1568,91 @@
"set_date_of_birth": "تحديد تاريخ الميلاد", "set_date_of_birth": "تحديد تاريخ الميلاد",
"set_profile_picture": "تحديد صورة الملف الشخصي", "set_profile_picture": "تحديد صورة الملف الشخصي",
"set_slideshow_to_fullscreen": "تحديد عرض الشرائح على وضع ملء الشاشة", "set_slideshow_to_fullscreen": "تحديد عرض الشرائح على وضع ملء الشاشة",
"setting_image_viewer_help": "يقوم عارض التفاصيل بتحميل الصورة المصغرة الصغيرة أولاً ، ثم يقوم بتحميل المعاينة متوسطة الحجم (إذا تم تمكينها) ، ويقوم أخيرًا بتحميل الأصل (إذا تم تمكينه).",
"setting_image_viewer_original_subtitle": "تمكين تحميل الصورة الكاملة الدقة الأصلية (كبيرة!).تعطيل لتقليل استخدام البيانات (كل من الشبكة وعلى ذاكرة التخزين المؤقت للجهاز).",
"setting_image_viewer_original_title": "تحميل الصورة الأصلية",
"setting_image_viewer_preview_subtitle": "تمكين تحميل صورة متوسطة الدقة.تعطيل إما لتحميل مباشرة أو استخدام الصورة المصغرة مباشرة.",
"setting_image_viewer_preview_title": "تحميل صورة معاينة",
"setting_image_viewer_title": "الصور",
"setting_languages_apply": "تغيير الإعدادات",
"setting_languages_subtitle": "Change the app's language",
"setting_languages_title": "اللغات",
"setting_notifications_notify_failures_grace_period": "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": "الإعدادات",
"settings_require_restart": "يرجى إعادة تشغيل لتطبيق هذا الإعداد",
"settings_saved": "تم حفظ الإعدادات", "settings_saved": "تم حفظ الإعدادات",
"share": "مشاركة", "share": "مشاركة",
"share_add_photos": "إضافة الصور",
"share_assets_selected": "{} selected",
"share_dialog_preparing": "تحضير...",
"shared": "مُشتَرك", "shared": "مُشتَرك",
"shared_album_activities_input_disable": "التعليق معطل",
"shared_album_activity_remove_content": "هل تريد حذف هذا النشاط؟",
"shared_album_activity_remove_title": "حذف النشاط",
"shared_album_section_people_action_error": "خطأ ترك/إزالة من الألبوم",
"shared_album_section_people_action_leave": "إزالة المستخدم من الألبوم",
"shared_album_section_people_action_remove_user": "إزالة المستخدم من الألبوم",
"shared_album_section_people_title": "الناس",
"shared_by": "تمت مشاركته بواسطة", "shared_by": "تمت مشاركته بواسطة",
"shared_by_user": "تمت المشاركة بواسطة {user}", "shared_by_user": "تمت المشاركة بواسطة {user}",
"shared_by_you": "تمت مشاركته من قِبلك", "shared_by_you": "تمت مشاركته من قِبلك",
"shared_from_partner": "صور من {partner}", "shared_from_partner": "صور من {partner}",
"shared_intent_upload_button_progress_text": "{} / {} Uploaded",
"shared_link_app_bar_title": "روابط مشتركة",
"shared_link_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_link_options": "خيارات الرابط المشترك",
"shared_links": "روابط مشتركة", "shared_links": "روابط مشتركة",
"shared_links_description": "وصف الروابط المشتركة", "shared_links_description": "وصف الروابط المشتركة",
"shared_photos_and_videos_count": "{assetCount, plural, other {# الصور ومقاطع الفيديو المُشارَكة.}}", "shared_photos_and_videos_count": "{assetCount, plural, other {# الصور ومقاطع الفيديو المُشارَكة.}}",
"shared_with_me": "Shared with me",
"shared_with_partner": "تمت المشاركة مع {partner}", "shared_with_partner": "تمت المشاركة مع {partner}",
"sharing": "مشاركة", "sharing": "مشاركة",
"sharing_enter_password": "الرجاء إدخال كلمة المرور لعرض هذه الصفحة.", "sharing_enter_password": "الرجاء إدخال كلمة المرور لعرض هذه الصفحة.",
"sharing_page_album": "ألبومات مشتركة",
"sharing_page_description": "قم بإنشاء ألبومات مشتركة لمشاركة الصور ومقاطع الفيديو مع أشخاص في شبكتك.",
"sharing_page_empty_list": "قائمة فارغة",
"sharing_sidebar_description": "اعرض رابطًا للمشاركة في الشريط الجانبي", "sharing_sidebar_description": "اعرض رابطًا للمشاركة في الشريط الجانبي",
"sharing_silver_appbar_create_shared_album": "ألبوم مشترك جديد",
"sharing_silver_appbar_share_partner": "شارك مع الشريك",
"shift_to_permanent_delete": "اضغط على ⇧ لحذف المحتوى نهائيًا", "shift_to_permanent_delete": "اضغط على ⇧ لحذف المحتوى نهائيًا",
"show_album_options": "إظهار خيارات الألبوم", "show_album_options": "إظهار خيارات الألبوم",
"show_albums": "إظهار الألبومات", "show_albums": "إظهار الألبومات",
@@ -1259,6 +1719,9 @@
"support_third_party_description": "تم حزم تثبيت immich الخاص بك بواسطة جهة خارجية. قد تكون المشكلات التي تواجهها ناجمة عن هذه الحزمة، لذا يرجى طرح المشكلات معهم في المقام الأول باستخدام الروابط أدناه.", "support_third_party_description": "تم حزم تثبيت immich الخاص بك بواسطة جهة خارجية. قد تكون المشكلات التي تواجهها ناجمة عن هذه الحزمة، لذا يرجى طرح المشكلات معهم في المقام الأول باستخدام الروابط أدناه.",
"swap_merge_direction": "تبديل اتجاه الدمج", "swap_merge_direction": "تبديل اتجاه الدمج",
"sync": "مزامنة", "sync": "مزامنة",
"sync_albums": "Sync albums",
"sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
"sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
"tag": "العلامة", "tag": "العلامة",
"tag_assets": "أصول العلامة", "tag_assets": "أصول العلامة",
"tag_created": "تم إنشاء العلامة: {tag}", "tag_created": "تم إنشاء العلامة: {tag}",
@@ -1272,6 +1735,19 @@
"theme": "مظهر", "theme": "مظهر",
"theme_selection": "اختيار السمة", "theme_selection": "اختيار السمة",
"theme_selection_description": "قم بتعيين السمة تلقائيًا على اللون الفاتح أو الداكن بناءً على تفضيلات نظام المتصفح الخاص بك", "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": "قد يزيد التحميل من ثلاث مراحل من أداء التحميل ولكنه يسبب تحميل شبكة أعلى بكثير",
"theme_setting_three_stage_loading_title": "تمكين تحميل ثلاث مراحل",
"they_will_be_merged_together": "سيتم دمجهم معًا", "they_will_be_merged_together": "سيتم دمجهم معًا",
"third_party_resources": "موارد الطرف الثالث", "third_party_resources": "موارد الطرف الثالث",
"time_based_memories": "ذكريات استنادًا للوقت", "time_based_memories": "ذكريات استنادًا للوقت",
@@ -1291,7 +1767,15 @@
"trash_all": "نقل الكل إلى سلة المهملات", "trash_all": "نقل الكل إلى سلة المهملات",
"trash_count": "سلة المحملات {count, number}", "trash_count": "سلة المحملات {count, number}",
"trash_delete_asset": "حذف/نقل المحتوى إلى سلة المهملات", "trash_delete_asset": "حذف/نقل المحتوى إلى سلة المهملات",
"trash_emptied": "Emptied trash",
"trash_no_results_message": "ستظهر هنا الصور ومقاطع الفيديو المحذوفة.", "trash_no_results_message": "ستظهر هنا الصور ومقاطع الفيديو المحذوفة.",
"trash_page_delete_all": "حذف الكل",
"trash_page_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 {# أيام }}.", "trashed_items_will_be_permanently_deleted_after": "سيتم حذفُ العناصر المحذوفة نِهائيًا بعد {days, plural, one {# يوم} other {# أيام }}.",
"type": "النوع", "type": "النوع",
"unarchive": "أخرج من الأرشيف", "unarchive": "أخرج من الأرشيف",
@@ -1320,6 +1804,8 @@
"updated_password": "تم تحديث كلمة المرور", "updated_password": "تم تحديث كلمة المرور",
"upload": "رفع", "upload": "رفع",
"upload_concurrency": "الرفع المتزامن", "upload_concurrency": "الرفع المتزامن",
"upload_dialog_info": "هل تريد النسخ الاحتياطي للأصول (الأصول) المحددة إلى الخادم؟",
"upload_dialog_title": "تحميل الأصول",
"upload_errors": "إكتمل الرفع مع {count, plural, one {# خطأ} other {# أخطاء}}, قم بتحديث الصفحة لرؤية المحتويات الجديدة التي تم رفعها.", "upload_errors": "إكتمل الرفع مع {count, plural, one {# خطأ} other {# أخطاء}}, قم بتحديث الصفحة لرؤية المحتويات الجديدة التي تم رفعها.",
"upload_progress": "متبقية {remaining, number} - معالجة {processed, number}/{total, number}", "upload_progress": "متبقية {remaining, number} - معالجة {processed, number}/{total, number}",
"upload_skipped_duplicates": "تم تخطي {count, plural, one {# محتوى مكرر} other {# محتويات مكررة }}", "upload_skipped_duplicates": "تم تخطي {count, plural, one {# محتوى مكرر} other {# محتويات مكررة }}",
@@ -1327,8 +1813,11 @@
"upload_status_errors": "الأخطاء", "upload_status_errors": "الأخطاء",
"upload_status_uploaded": "تم الرفع", "upload_status_uploaded": "تم الرفع",
"upload_success": "تم الرفع بنجاح، قم بتحديث الصفحة لرؤية المحتويات المرفوعة الجديدة.", "upload_success": "تم الرفع بنجاح، قم بتحديث الصفحة لرؤية المحتويات المرفوعة الجديدة.",
"upload_to_immich": "Upload to Immich ({})",
"uploading": "Uploading",
"url": "عنوان URL", "url": "عنوان URL",
"usage": "الاستخدام", "usage": "الاستخدام",
"use_current_connection": "use current connection",
"use_custom_date_range": "استخدم النطاق الزمني المخصص بدلاً من ذلك", "use_custom_date_range": "استخدم النطاق الزمني المخصص بدلاً من ذلك",
"user": "مستخدم", "user": "مستخدم",
"user_id": "معرف المستخدم", "user_id": "معرف المستخدم",
@@ -1343,10 +1832,16 @@
"users": "المستخدمين", "users": "المستخدمين",
"utilities": "أدوات", "utilities": "أدوات",
"validate": "تحقْق", "validate": "تحقْق",
"validate_endpoint_error": "Please enter a valid URL",
"variables": "المتغيرات", "variables": "المتغيرات",
"version": "الإصدار", "version": "الإصدار",
"version_announcement_closing": "صديقك، أليكس", "version_announcement_closing": "صديقك، أليكس",
"version_announcement_message": "مرحبًا! يتوفر إصدار جديد من Immich. يُرجى تخصيص بعض الوقت لقراءة <link>ملاحظات الإصدار</link> للتأكد من تحديث إعداداتك لمنع أي أخطاء في التكوين، خاصة إذا كنت تستخدم WatchTower أو أي آلية تتولى تحديث مثيل Immich الخاص بك تلقائيًا.", "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": "تاريخ الإصدار",
"version_history_item": "تم تثبيت {version} في {date}", "version_history_item": "تم تثبيت {version} في {date}",
"video": "فيديو", "video": "فيديو",
@@ -1365,15 +1860,20 @@
"view_next_asset": "عرض المحتوى التالي", "view_next_asset": "عرض المحتوى التالي",
"view_previous_asset": "عرض المحتوى السابق", "view_previous_asset": "عرض المحتوى السابق",
"view_stack": "عرض التكديس", "view_stack": "عرض التكديس",
"viewer_remove_from_stack": "حذف من الكومه أو المجموعة",
"viewer_stack_use_as_main_asset": "استخدم كأصل رئيسي",
"viewer_unstack": "فك الكومه",
"visibility_changed": "الرؤية تغيرت لـ {count, plural, one {شخص واحد} other {# عدة أشخاص}}", "visibility_changed": "الرؤية تغيرت لـ {count, plural, one {شخص واحد} other {# عدة أشخاص}}",
"waiting": "في الانتظار", "waiting": "في الانتظار",
"warning": "تحذير", "warning": "تحذير",
"week": "أسبوع", "week": "أسبوع",
"welcome": "مرحباً", "welcome": "مرحباً",
"welcome_to_immich": "مرحباً بك في Immich", "welcome_to_immich": "مرحباً بك في Immich",
"wifi_name": "WiFi Name",
"year": "سنة", "year": "سنة",
"years_ago": "منذ {years, plural, one {# سنة} other {# سنوات}}", "years_ago": "منذ {years, plural, one {# سنة} other {# سنوات}}",
"yes": "نعم", "yes": "نعم",
"you_dont_have_any_shared_links": "ليس لديك أي روابط مشتركة", "you_dont_have_any_shared_links": "ليس لديك أي روابط مشتركة",
"your_wifi_name": "Your WiFi name",
"zoom_image": "تكبير الصورة" "zoom_image": "تكبير الصورة"
} }

View File

@@ -4,6 +4,7 @@
"account_settings": "Налады ўліковага запісу", "account_settings": "Налады ўліковага запісу",
"acknowledge": "Пацвердзіць", "acknowledge": "Пацвердзіць",
"action": "Дзеянне", "action": "Дзеянне",
"action_common_update": "Абнавіць",
"actions": "Дзеянні", "actions": "Дзеянні",
"active": "Актыўны", "active": "Актыўны",
"activity": "Актыўнасць", "activity": "Актыўнасць",
@@ -13,6 +14,7 @@
"add_a_location": "Дадаць месца", "add_a_location": "Дадаць месца",
"add_a_name": "Дадаць імя", "add_a_name": "Дадаць імя",
"add_a_title": "Дадаць загаловак", "add_a_title": "Дадаць загаловак",
"add_endpoint": "Дадаць кропку доступу",
"add_exclusion_pattern": "Дадаць шаблон выключэння", "add_exclusion_pattern": "Дадаць шаблон выключэння",
"add_import_path": "Дадаць шлях імпарту", "add_import_path": "Дадаць шлях імпарту",
"add_location": "Дадайце месца", "add_location": "Дадайце месца",
@@ -20,8 +22,10 @@
"add_partner": "Дадаць партнёра", "add_partner": "Дадаць партнёра",
"add_path": "Дадаць шлях", "add_path": "Дадаць шлях",
"add_photos": "Дадаць фота", "add_photos": "Дадаць фота",
"add_to": "Дадаць у...", "add_to": "Дадаць у",
"add_to_album": "Дадаць у альбом", "add_to_album": "Дадаць у альбом",
"add_to_album_bottom_sheet_added": "Дададзена да {album}",
"add_to_album_bottom_sheet_already_exists": "Ужо знаходзіцца ў {album}",
"add_to_shared_album": "Дадаць у агульны альбом", "add_to_shared_album": "Дадаць у агульны альбом",
"add_url": "Дадаць URL", "add_url": "Дадаць URL",
"added_to_archive": "Дададзена ў архіў", "added_to_archive": "Дададзена ў архіў",
@@ -39,8 +43,9 @@
"backup_database_enable_description": "Уключыць рэзерваванне базы даных", "backup_database_enable_description": "Уключыць рэзерваванне базы даных",
"backup_keep_last_amount": "Колькасць папярэдніх рэзервовых копій для захавання", "backup_keep_last_amount": "Колькасць папярэдніх рэзервовых копій для захавання",
"backup_settings": "Налады рэзервовага капіявання", "backup_settings": "Налады рэзервовага капіявання",
"backup_settings_description": "Кіраванне наладкамі рэзервовага капіявання базы даных", "backup_settings_description": "Кіраванне наладамі дампа базы дадзеных. Заўвага: гэтыя задачы не кантралююцца, і ў выпадку няўдачы паведамленне адпраўлена не будзе.",
"check_all": "Праверыць усе", "check_all": "Праверыць усе",
"cleanup": "Ачыстка",
"cleared_jobs": "Ачышчаны заданні для: {job}", "cleared_jobs": "Ачышчаны заданні для: {job}",
"config_set_by_file": "Канфігурацыя ў зараз усталявана праз файл канфігурацыі", "config_set_by_file": "Канфігурацыя ў зараз усталявана праз файл канфігурацыі",
"confirm_delete_library": "Вы ўпэўнены што жадаеце выдаліць {library} бібліятэку?", "confirm_delete_library": "Вы ўпэўнены што жадаеце выдаліць {library} бібліятэку?",
@@ -58,8 +63,18 @@
"external_library_created_at": "Знешняя бібліятэка (створана {date})", "external_library_created_at": "Знешняя бібліятэка (створана {date})",
"external_library_management": "Кіраванне знешняй бібліятэкай", "external_library_management": "Кіраванне знешняй бібліятэкай",
"face_detection": "Выяўленне твараў", "face_detection": "Выяўленне твараў",
"face_detection_description": "Выяўляць твары на фотаздымках і відэа з дапамогай машыннага навучання. Для відэа ўлічваецца толькі мініяцюра. \"Абнавіць\" (пера)апрацоўвае ўсе медыя. \"Скінуць\" дадаткова ачышчае ўсе бягучыя дадзеныя пра твары. \"Адсутнічае\" ставіць у чаргу медыя, якія яшчэ не былі апрацаваныя. Выяўленыя твары будуць пастаўлены ў чаргу для распазнавання асоб пасля завяршэння выяўлення твараў, з групаваннем іх па існуючых або новых людзях.",
"facial_recognition_job_description": "Групаваць выяўленыя твары па асобах. Гэты этап выконваецца пасля завяршэння выяўлення твараў. \"Скінуць\" (паўторна) перагрупоўвае ўсе твары. \"Адсутнічае\" ставіць у чаргу твары, якія яшчэ не прыпісаныя да якой-небудзь асобы.",
"failed_job_command": "Каманда {command} не выканалася для задання: {job}",
"force_delete_user_warning": "ПАПЯРЭДЖАННЕ: Гэта дзеянне неадкладна выдаліць карыстальніка і ўсе аб'екты. Гэта дзеянне не можа быць адроблена і файлы немагчыма будзе аднавіць.", "force_delete_user_warning": "ПАПЯРЭДЖАННЕ: Гэта дзеянне неадкладна выдаліць карыстальніка і ўсе аб'екты. Гэта дзеянне не можа быць адроблена і файлы немагчыма будзе аднавіць.",
"forcing_refresh_library_files": "Прымусовае абнаўленне ўсіх файлаў бібліятэкі",
"image_format": "Фармат", "image_format": "Фармат",
"image_format_description": "WebP стварае меншыя файлы, чым JPEG, але павольней кадуе.",
"image_fullsize_description": "Выява ў поўным памеры без метаданых, выкарыстоўваецца пры павелічэнні",
"image_fullsize_enabled": "Уключыць стварэнне выявы ў поўным памеры",
"image_fullsize_enabled_description": "Ствараць выяву ў поўным памеры для фарматаў, што не прыдатныя для вэб. Калі ўключана опцыя \"Аддаваць перавагу ўбудаванай праяве\", прагляды выкарыстоўваюцца непасрэдна без канвертацыі. Не ўплывае на вэб-прыдатныя фарматы, такія як JPEG.",
"image_fullsize_quality_description": "Якасць выявы ў поўным памеры ад 1 да 100. Больш высокае значэнне лепшае, але прыводзіць да павелічэння памеру файла.",
"image_fullsize_title": "Налады выявы ў поўным памеры",
"image_preview_title": "Налады папярэдняга прагляду", "image_preview_title": "Налады папярэдняга прагляду",
"image_quality": "Якасць", "image_quality": "Якасць",
"image_resolution": "Раздзяляльнасць", "image_resolution": "Раздзяляльнасць",

View File

@@ -183,20 +183,13 @@
"oauth_auto_register": "Автоматична регистрация", "oauth_auto_register": "Автоматична регистрация",
"oauth_auto_register_description": "Автоматично регистриране на нови потребители след влизане с OAuth", "oauth_auto_register_description": "Автоматично регистриране на нови потребители след влизане с OAuth",
"oauth_button_text": "Текст на бутона", "oauth_button_text": "Текст на бутона",
"oauth_client_id": "Клиентски ID",
"oauth_client_secret": "Клиентска тайна",
"oauth_enable_description": "Влизане с OAuth", "oauth_enable_description": "Влизане с OAuth",
"oauth_issuer_url": "URL на издателя",
"oauth_mobile_redirect_uri": "URI за мобилно пренасочване", "oauth_mobile_redirect_uri": "URI за мобилно пренасочване",
"oauth_mobile_redirect_uri_override": "URI пренасочване за мобилни устройства", "oauth_mobile_redirect_uri_override": "URI пренасочване за мобилни устройства",
"oauth_mobile_redirect_uri_override_description": "Разреши когато доставчика за OAuth удостоверяване не позволява за мобилни URI идентификатори, като '{callback}'", "oauth_mobile_redirect_uri_override_description": "Разреши когато доставчика за OAuth удостоверяване не позволява за мобилни URI идентификатори, като '{callback}'",
"oauth_profile_signing_algorithm": "Алгоритъм за създаване на профили",
"oauth_profile_signing_algorithm_description": "Алгоритъм използван за вписване на потребителски профил.",
"oauth_scope": "Област/обхват на приложение",
"oauth_settings": "OAuth", "oauth_settings": "OAuth",
"oauth_settings_description": "Управление на настройките за вход с OAuth", "oauth_settings_description": "Управление на настройките за вход с OAuth",
"oauth_settings_more_details": "За повече информация за функционалността, се потърсете в <link>docs</link>.", "oauth_settings_more_details": "За повече информация за функционалността, се потърсете в <link>docs</link>.",
"oauth_signing_algorithm": "Алгоритъм за вписване",
"oauth_storage_label_claim": "Заявка за етикет за съхранение", "oauth_storage_label_claim": "Заявка за етикет за съхранение",
"oauth_storage_label_claim_description": "Автоматично задайте етикета за съхранение на потребителя със стойността от тази заявка.", "oauth_storage_label_claim_description": "Автоматично задайте етикета за съхранение на потребителя със стойността от тази заявка.",
"oauth_storage_quota_claim": "Заявка за квота за съхранение", "oauth_storage_quota_claim": "Заявка за квота за съхранение",

View File

@@ -138,17 +138,12 @@
"oauth_auto_register": "", "oauth_auto_register": "",
"oauth_auto_register_description": "", "oauth_auto_register_description": "",
"oauth_button_text": "", "oauth_button_text": "",
"oauth_client_id": "",
"oauth_client_secret": "",
"oauth_enable_description": "", "oauth_enable_description": "",
"oauth_issuer_url": "",
"oauth_mobile_redirect_uri": "", "oauth_mobile_redirect_uri": "",
"oauth_mobile_redirect_uri_override": "", "oauth_mobile_redirect_uri_override": "",
"oauth_mobile_redirect_uri_override_description": "", "oauth_mobile_redirect_uri_override_description": "",
"oauth_scope": "",
"oauth_settings": "", "oauth_settings": "",
"oauth_settings_description": "", "oauth_settings_description": "",
"oauth_signing_algorithm": "",
"oauth_storage_label_claim": "", "oauth_storage_label_claim": "",
"oauth_storage_label_claim_description": "", "oauth_storage_label_claim_description": "",
"oauth_storage_quota_claim": "", "oauth_storage_quota_claim": "",

View File

@@ -4,6 +4,7 @@
"account_settings": "Configuració del compte", "account_settings": "Configuració del compte",
"acknowledge": "D'acord", "acknowledge": "D'acord",
"action": "Acció", "action": "Acció",
"action_common_update": "Actualitzar",
"actions": "Accions", "actions": "Accions",
"active": "Actiu", "active": "Actiu",
"activity": "Activitat", "activity": "Activitat",
@@ -13,6 +14,7 @@
"add_a_location": "Afegiu una ubicació", "add_a_location": "Afegiu una ubicació",
"add_a_name": "Afegir un nom", "add_a_name": "Afegir un nom",
"add_a_title": "Afegir un títol", "add_a_title": "Afegir un títol",
"add_endpoint": "afegir endpoint",
"add_exclusion_pattern": "Afegir un patró d'exclusió", "add_exclusion_pattern": "Afegir un patró d'exclusió",
"add_import_path": "Afegir una ruta d'importació", "add_import_path": "Afegir una ruta d'importació",
"add_location": "Afegir la ubicació", "add_location": "Afegir la ubicació",
@@ -22,6 +24,8 @@
"add_photos": "Afegir fotografies", "add_photos": "Afegir fotografies",
"add_to": "Afegir a…", "add_to": "Afegir a…",
"add_to_album": "Afegir a un l'àlbum", "add_to_album": "Afegir a un l'àlbum",
"add_to_album_bottom_sheet_added": "Afegit a {album}",
"add_to_album_bottom_sheet_already_exists": "Ja està a {album}",
"add_to_shared_album": "Afegir a un àlbum compartit", "add_to_shared_album": "Afegir a un àlbum compartit",
"add_url": "Afegir URL", "add_url": "Afegir URL",
"added_to_archive": "Afegit als arxivats", "added_to_archive": "Afegit als arxivats",
@@ -188,20 +192,13 @@
"oauth_auto_register": "Registre automàtic", "oauth_auto_register": "Registre automàtic",
"oauth_auto_register_description": "Registra nous usuaris automàticament després d'iniciar sessió amb OAuth", "oauth_auto_register_description": "Registra nous usuaris automàticament després d'iniciar sessió amb OAuth",
"oauth_button_text": "Text del botó", "oauth_button_text": "Text del botó",
"oauth_client_id": "ID Client",
"oauth_client_secret": "Secret de Client",
"oauth_enable_description": "Iniciar sessió amb OAuth", "oauth_enable_description": "Iniciar sessió amb OAuth",
"oauth_issuer_url": "URL de l'emissor",
"oauth_mobile_redirect_uri": "URI de redirecció mòbil", "oauth_mobile_redirect_uri": "URI de redirecció mòbil",
"oauth_mobile_redirect_uri_override": "Sobreescriu l'URI de redirecció mòbil", "oauth_mobile_redirect_uri_override": "Sobreescriu l'URI de redirecció mòbil",
"oauth_mobile_redirect_uri_override_description": "Habilita quan el proveïdor d'OAuth no permet una URI mòbil, com ara '{callback}'", "oauth_mobile_redirect_uri_override_description": "Habilita quan el proveïdor d'OAuth no permet una URI mòbil, com ara '{callback}'",
"oauth_profile_signing_algorithm": "Algoritme de signatura del perfil",
"oauth_profile_signing_algorithm_description": "Algoritme utilitzat per signar el perfil dusuari.",
"oauth_scope": "Abast",
"oauth_settings": "OAuth", "oauth_settings": "OAuth",
"oauth_settings_description": "Gestiona la configuració de l'inici de sessió OAuth", "oauth_settings_description": "Gestiona la configuració de l'inici de sessió OAuth",
"oauth_settings_more_details": "Per a més detalls sobre aquesta funcionalitat, consulteu la <link>documentació</link>.", "oauth_settings_more_details": "Per a més detalls sobre aquesta funcionalitat, consulteu la <link>documentació</link>.",
"oauth_signing_algorithm": "Algorisme de signatura",
"oauth_storage_label_claim": "Petició d'etiquetatge d'emmagatzematge", "oauth_storage_label_claim": "Petició d'etiquetatge d'emmagatzematge",
"oauth_storage_label_claim_description": "Estableix automàticament l'etiquetatge d'emmagatzematge de l'usuari a aquest valor.", "oauth_storage_label_claim_description": "Estableix automàticament l'etiquetatge d'emmagatzematge de l'usuari a aquest valor.",
"oauth_storage_quota_claim": "Quota d'emmagatzematge reclamada", "oauth_storage_quota_claim": "Quota d'emmagatzematge reclamada",
@@ -367,6 +364,16 @@
"admin_password": "Contrasenya de l'administrador", "admin_password": "Contrasenya de l'administrador",
"administration": "Administrador", "administration": "Administrador",
"advanced": "Avançat", "advanced": "Avançat",
"advanced_settings_log_level_title": "Nivell de registre: {}",
"advanced_settings_prefer_remote_subtitle": "Alguns dispositius són molt lents en carregar miniatures dels elements del dispositiu. Activeu aquest paràmetre per carregar imatges remotes en el seu lloc.",
"advanced_settings_prefer_remote_title": "Prefereix imatges remotes",
"advanced_settings_proxy_headers_subtitle": "Definiu les capçaleres de proxy que Immich per enviar amb cada sol·licitud de xarxa",
"advanced_settings_proxy_headers_title": "Capçaleres de proxy",
"advanced_settings_self_signed_ssl_subtitle": "Omet la verificació del certificat SSL del servidor. Requerit per a certificats autosignats.",
"advanced_settings_self_signed_ssl_title": "Permet certificats SSL autosignats",
"advanced_settings_tile_subtitle": "Configuració avançada de l'usuari",
"advanced_settings_troubleshooting_subtitle": "Habilita funcions addicionals per a la resolució de problemes",
"advanced_settings_troubleshooting_title": "Resolució de problemes",
"age_months": "{months, plural, one {# mes} other {# mesos}}", "age_months": "{months, plural, one {# mes} other {# mesos}}",
"age_year_months": "Un any i {months, plural, one {# mes} other {# mesos}}", "age_year_months": "Un any i {months, plural, one {# mes} other {# mesos}}",
"age_years": "{years, plural, one {# any} other {# anys}}", "age_years": "{years, plural, one {# any} other {# anys}}",
@@ -375,6 +382,8 @@
"album_cover_updated": "Portada de l'àlbum actualitzada", "album_cover_updated": "Portada de l'àlbum actualitzada",
"album_delete_confirmation": "Esteu segur que voleu suprimir l'àlbum {album}?", "album_delete_confirmation": "Esteu segur que voleu suprimir l'àlbum {album}?",
"album_delete_confirmation_description": "Si aquest àlbum es comparteix, els altres usuaris ja no podran accedir-hi.", "album_delete_confirmation_description": "Si aquest àlbum es comparteix, els altres usuaris ja no podran accedir-hi.",
"album_info_card_backup_album_excluded": "Exclosos",
"album_info_card_backup_album_included": "Inclosos",
"album_info_updated": "Informació de l'àlbum actualitzada", "album_info_updated": "Informació de l'àlbum actualitzada",
"album_leave": "Sortir de l'àlbum?", "album_leave": "Sortir de l'àlbum?",
"album_leave_confirmation": "N'esteu segur que voleu sortir de {album}?", "album_leave_confirmation": "N'esteu segur que voleu sortir de {album}?",
@@ -383,10 +392,22 @@
"album_remove_user": "Eliminar l'usuari?", "album_remove_user": "Eliminar l'usuari?",
"album_remove_user_confirmation": "Esteu segurs que voleu eliminar {user}?", "album_remove_user_confirmation": "Esteu segurs que voleu eliminar {user}?",
"album_share_no_users": "Sembla que has compartit aquest àlbum amb tots els usuaris o no tens cap usuari amb qui compartir-ho.", "album_share_no_users": "Sembla que has compartit aquest àlbum amb tots els usuaris o no tens cap usuari amb qui compartir-ho.",
"album_thumbnail_card_item": "1 element",
"album_thumbnail_card_items": "{} elements",
"album_thumbnail_card_shared": " · Compartit",
"album_thumbnail_shared_by": "Compartit per {}",
"album_updated": "Àlbum actualitzat", "album_updated": "Àlbum actualitzat",
"album_updated_setting_description": "Rep una notificació per correu electrònic quan un àlbum compartit tingui recursos nous", "album_updated_setting_description": "Rep una notificació per correu electrònic quan un àlbum compartit tingui recursos nous",
"album_user_left": "Surt de {album}", "album_user_left": "Surt de {album}",
"album_user_removed": "{user} eliminat", "album_user_removed": "{user} eliminat",
"album_viewer_appbar_delete_confirm": "Confirmes que vols suprimir aquest àlbum del teu compte?",
"album_viewer_appbar_share_err_delete": "Error al esborrar l'àlbum",
"album_viewer_appbar_share_err_leave": "Error al sortir de l'àlbum",
"album_viewer_appbar_share_err_remove": "Hi ha hagut problemes al treure elements de l'àlbum",
"album_viewer_appbar_share_err_title": "Error al modificar el títol de l'àlbum",
"album_viewer_appbar_share_leave": "Surt de l'àlbum",
"album_viewer_appbar_share_to": "Comparteix amb",
"album_viewer_page_share_add_users": "Afegeix usuaris",
"album_with_link_access": "Permet que qualsevol persona que tingui l'enllaç vegi fotos i persones d'aquest àlbum.", "album_with_link_access": "Permet que qualsevol persona que tingui l'enllaç vegi fotos i persones d'aquest àlbum.",
"albums": "Àlbums", "albums": "Àlbums",
"albums_count": "{count, plural, one {{count, number} Àlbum} other {{count, number} Àlbums}}", "albums_count": "{count, plural, one {{count, number} Àlbum} other {{count, number} Àlbums}}",
@@ -404,42 +425,133 @@
"api_key_description": "Aquest valor només es mostrarà una vegada. Assegureu-vos de copiar-lo abans de tancar la finestra.", "api_key_description": "Aquest valor només es mostrarà una vegada. Assegureu-vos de copiar-lo abans de tancar la finestra.",
"api_key_empty": "El nom de la clau de l'API no pot estar buit", "api_key_empty": "El nom de la clau de l'API no pot estar buit",
"api_keys": "Claus API", "api_keys": "Claus API",
"app_bar_signout_dialog_content": "Estàs segur que vols tancar la sessió?",
"app_bar_signout_dialog_ok": "Sí",
"app_bar_signout_dialog_title": "Tanca la sessió",
"app_settings": "Configuració de l'app", "app_settings": "Configuració de l'app",
"appears_in": "Apareix a", "appears_in": "Apareix a",
"archive": "Arxiu", "archive": "Arxiu",
"archive_or_unarchive_photo": "Arxivar o desarxivar fotografia", "archive_or_unarchive_photo": "Arxivar o desarxivar fotografia",
"archive_page_no_archived_assets": "No s'ha trobat res arxivat",
"archive_page_title": "Arxiu({})",
"archive_size": "Mida de l'arxiu", "archive_size": "Mida de l'arxiu",
"archive_size_description": "Configureu la mida de l'arxiu de les descàrregues (en GiB)", "archive_size_description": "Configureu la mida de l'arxiu de les descàrregues (en GiB)",
"archived": "Arxivat",
"archived_count": "{count, plural, one {Arxivat #} other {Arxivats #}}", "archived_count": "{count, plural, one {Arxivat #} other {Arxivats #}}",
"are_these_the_same_person": "Són la mateixa persona?", "are_these_the_same_person": "Són la mateixa persona?",
"are_you_sure_to_do_this": "Esteu segurs que voleu fer-ho?", "are_you_sure_to_do_this": "Esteu segurs que voleu fer-ho?",
"asset_action_delete_err_read_only": "No es poden esborrar el fitxer(s) de només lectura, ometent",
"asset_action_share_err_offline": "No s'ha pogut obtenir el fitxer(s) sense connexió, ometent",
"asset_added_to_album": "Afegit a l'àlbum", "asset_added_to_album": "Afegit a l'àlbum",
"asset_adding_to_album": "Afegint a l'àlbum…", "asset_adding_to_album": "Afegint a l'àlbum…",
"asset_description_updated": "La descripció del recurs s'ha actualitzat", "asset_description_updated": "La descripció del recurs s'ha actualitzat",
"asset_filename_is_offline": "L'element {filename} està fora de línia", "asset_filename_is_offline": "L'element {filename} està fora de línia",
"asset_has_unassigned_faces": "L'element té cares no assignades", "asset_has_unassigned_faces": "L'element té cares no assignades",
"asset_hashing": "Hasheant…", "asset_hashing": "Hasheant…",
"asset_list_group_by_sub_title": "Agrupar per",
"asset_list_layout_settings_dynamic_layout_title": "Disseny dinàmic",
"asset_list_layout_settings_group_automatically": "Automàtic",
"asset_list_layout_settings_group_by": "Agrupa elements per",
"asset_list_layout_settings_group_by_month_day": "Mes + dia",
"asset_list_layout_sub_title": "Disseny",
"asset_list_settings_subtitle": "Configuració del disseny de la graella de fotos",
"asset_list_settings_title": "Graella de fotos",
"asset_offline": "Element fora de línia", "asset_offline": "Element fora de línia",
"asset_offline_description": "Aquest recurs extern ja no es troba al disc. Poseu-vos en contacte amb el vostre administrador d'Immich per obtenir ajuda.", "asset_offline_description": "Aquest recurs extern ja no es troba al disc. Poseu-vos en contacte amb el vostre administrador d'Immich per obtenir ajuda.",
"asset_restored_successfully": "Element recuperat correctament",
"asset_skipped": "Saltat", "asset_skipped": "Saltat",
"asset_skipped_in_trash": "A la paperera", "asset_skipped_in_trash": "A la paperera",
"asset_uploaded": "Carregat", "asset_uploaded": "Carregat",
"asset_uploading": "S'està carregant…", "asset_uploading": "S'està carregant…",
"asset_viewer_settings_subtitle": "Gestiona la configuració del visualitzador de la galeria",
"asset_viewer_settings_title": "Visor d'arxius",
"assets": "Elements", "assets": "Elements",
"assets_added_count": "{count, plural, one {Afegit un element} other {Afegits # elements}}", "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_album_count": "{count, plural, one {Afegit un element} other {Afegits # elements}} a l'àlbum",
"assets_added_to_name_count": "{count, plural, one {S'ha afegit # recurs} other {S'han afegit # recursos}} a {hasName, select, true {<b>{name}</b>} other {new album}}", "assets_added_to_name_count": "{count, plural, one {S'ha afegit # recurs} other {S'han afegit # recursos}} a {hasName, select, true {<b>{name}</b>} other {new album}}",
"assets_count": "{count, plural, one {# recurs} other {# recursos}}", "assets_count": "{count, plural, one {# recurs} other {# recursos}}",
"assets_deleted_permanently": "{} element(s) esborrats permanentment",
"assets_deleted_permanently_from_server": "{} element(s) esborrats permanentment del servidor d'Immich",
"assets_moved_to_trash_count": "{count, plural, one {# recurs mogut} other {# recursos moguts}} a la paperera", "assets_moved_to_trash_count": "{count, plural, one {# recurs mogut} other {# recursos moguts}} a la paperera",
"assets_permanently_deleted_count": "{count, plural, one {# recurs esborrat} other {# recursos esborrats}} permanentment", "assets_permanently_deleted_count": "{count, plural, one {# recurs esborrat} other {# recursos esborrats}} permanentment",
"assets_removed_count": "{count, plural, one {# element eliminat} other {# elements eliminats}}", "assets_removed_count": "{count, plural, one {# element eliminat} other {# elements eliminats}}",
"assets_removed_permanently_from_device": "{} element(s) esborrat permanentment del dispositiu",
"assets_restore_confirmation": "Esteu segurs que voleu restaurar tots els teus actius? Aquesta acció no es pot desfer! Tingueu en compte que els recursos fora de línia no es poden restaurar d'aquesta manera.", "assets_restore_confirmation": "Esteu segurs que voleu restaurar tots els teus actius? Aquesta acció no es pot desfer! Tingueu en compte que els recursos fora de línia no es poden restaurar d'aquesta manera.",
"assets_restored_count": "{count, plural, one {# element restaurat} other {# elements restaurats}}", "assets_restored_count": "{count, plural, one {# element restaurat} other {# elements restaurats}}",
"assets_restored_successfully": "{} element(s) recuperats correctament",
"assets_trashed": "{} element(s) enviat a la paperera",
"assets_trashed_count": "{count, plural, one {# element enviat} other {# elements enviats}} a la paperera", "assets_trashed_count": "{count, plural, one {# element enviat} other {# elements enviats}} a la paperera",
"assets_trashed_from_server": "{} element(s) enviat a la paperera del servidor d'Immich",
"assets_were_part_of_album_count": "{count, plural, one {L'element ja és} other {Els elements ja són}} part de l'àlbum", "assets_were_part_of_album_count": "{count, plural, one {L'element ja és} other {Els elements ja són}} part de l'àlbum",
"authorized_devices": "Dispositius autoritzats", "authorized_devices": "Dispositius autoritzats",
"automatic_endpoint_switching_subtitle": "Connecteu-vos localment a través de la Wi-Fi designada quan estigui disponible i utilitzeu connexions alternatives en altres llocs",
"automatic_endpoint_switching_title": "Canvi automàtic d'URL",
"back": "Enrere", "back": "Enrere",
"back_close_deselect": "Tornar, tancar o anul·lar la selecció", "back_close_deselect": "Tornar, tancar o anul·lar la selecció",
"background_location_permission": "Permís d'ubicació en segon pla",
"background_location_permission_content": "Per canviar de xarxa quan s'executa en segon pla, Immich ha de *sempre* tenir accés a la ubicació precisa perquè l'aplicació pugui llegir el nom de la xarxa Wi-Fi",
"backup_album_selection_page_albums_device": "Àlbums al dispositiu ({})",
"backup_album_selection_page_albums_tap": "Un toc per incloure, doble toc per excloure",
"backup_album_selection_page_assets_scatter": "Els elements poden dispersar-se en diversos àlbums. Per tant, els àlbums es poden incloure o excloure durant el procés de còpia de seguretat.",
"backup_album_selection_page_select_albums": "Selecciona àlbums",
"backup_album_selection_page_selection_info": "Informació de la selecció",
"backup_album_selection_page_total_assets": "Total d'elements únics",
"backup_all": "Tots",
"backup_background_service_backup_failed_message": "No s'ha pogut copiar els elements. Tornant a intentar…",
"backup_background_service_connection_failed_message": "No s'ha pogut connectar al servidor. Tornant a intentar…",
"backup_background_service_current_upload_notification": "Pujant {}",
"backup_background_service_default_notification": "Cercant nous elements...",
"backup_background_service_error_title": "Error copiant",
"backup_background_service_in_progress_notification": "Copiant els teus elements",
"backup_background_service_upload_failure_notification": "Error al pujar {}",
"backup_controller_page_albums": "Copia els àlbums",
"backup_controller_page_background_app_refresh_disabled_content": "Activa l'actualització en segon pla de l'aplicació a Configuració > General > Actualització en segon pla per utilitzar la copia de seguretat en segon pla.",
"backup_controller_page_background_app_refresh_disabled_title": "Actualització en segon pla desactivada",
"backup_controller_page_background_app_refresh_enable_button_text": "Vés a configuració",
"backup_controller_page_background_battery_info_link": "Mostra'm com",
"backup_controller_page_background_battery_info_message": "Per obtenir la millor experiència de copia de seguretat en segon pla, desactiveu qualsevol optimització de bateria que restringeixi l'activitat en segon pla per a Immich.\n\nAtès que això és específic del dispositiu, busqueu la informació necessària per al fabricant del vostre dispositiu",
"backup_controller_page_background_battery_info_ok": "D'acord",
"backup_controller_page_background_battery_info_title": "Optimitzacions de bateria",
"backup_controller_page_background_charging": "Només mentre es carrega",
"backup_controller_page_background_configure_error": "No s'ha pogut configurar el servei en segon pla",
"backup_controller_page_background_delay": "Retard en la copia de seguretat de nous elements: {}",
"backup_controller_page_background_description": "Activeu el servei en segon pla per copiar automàticament tots els nous elements sense haver d'obrir l'aplicació.",
"backup_controller_page_background_is_off": "La còpia automàtica en segon pla està desactivada",
"backup_controller_page_background_is_on": "La còpia automàtica en segon pla està activada",
"backup_controller_page_background_turn_off": "Desactiva el servei en segon pla",
"backup_controller_page_background_turn_on": "Activa el servei en segon pla",
"backup_controller_page_background_wifi": "Només amb WiFi",
"backup_controller_page_backup": "Còpia",
"backup_controller_page_backup_selected": "Seleccionat: ",
"backup_controller_page_backup_sub": "Fotografies i vídeos copiats",
"backup_controller_page_created": "Creat el: {}",
"backup_controller_page_desc_backup": "Activeu la còpia de seguretat per pujar automàticament els nous elements al servidor en obrir l'aplicació.",
"backup_controller_page_excluded": "Exclosos: ",
"backup_controller_page_failed": "Fallats ({})",
"backup_controller_page_filename": "Nom de l'arxiu: {} [{}]",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Informació de la còpia",
"backup_controller_page_none_selected": "Cap seleccionat",
"backup_controller_page_remainder": "Restant",
"backup_controller_page_remainder_sub": "Fotografies i vídeos restants per copiar de la selecció",
"backup_controller_page_server_storage": "Emmagatzematge del servidor",
"backup_controller_page_start_backup": "Inicia la còpia",
"backup_controller_page_status_off": "La copia de seguretat està desactivada",
"backup_controller_page_status_on": "La copia de seguretat està activada",
"backup_controller_page_storage_format": "{} de {} utilitzats",
"backup_controller_page_to_backup": "Àlbums a copiar",
"backup_controller_page_total_sub": "Totes les fotografies i vídeos dels àlbums seleccionats",
"backup_controller_page_turn_off": "Desactiva la còpia de seguretat",
"backup_controller_page_turn_on": "Activa la còpia de seguretat",
"backup_controller_page_uploading_file_info": "S'està pujant la informació del fitxer",
"backup_err_only_album": "No es pot eliminar l'únic àlbum",
"backup_info_card_assets": "elements",
"backup_manual_cancelled": "Cancel·lat",
"backup_manual_in_progress": "La pujada ja està en curs. Torneu-ho a provar més tard",
"backup_manual_success": "Èxit",
"backup_manual_title": "Estat de pujada",
"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", "backward": "Enrere",
"birthdate_saved": "Data de naixement guardada amb èxit", "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.", "birthdate_set_description": "La data de naixement s'utilitza per calcular l'edat d'aquesta persona en el moment d'una foto.",
@@ -451,24 +563,52 @@
"bulk_keep_duplicates_confirmation": "Esteu segur que voleu mantenir {count, plural, one {# recurs duplicat} other {# recursos duplicats}}? Això resoldrà tots els grups duplicats sense eliminar res.", "bulk_keep_duplicates_confirmation": "Esteu segur que voleu mantenir {count, plural, one {# recurs duplicat} other {# recursos duplicats}}? Això resoldrà tots els grups duplicats sense eliminar res.",
"bulk_trash_duplicates_confirmation": "Esteu segur que voleu enviar a les escombraries {count, plural, one {# recurs duplicat} other {# recursos duplicats}}? Això mantindrà el recurs més gran de cada grup i eliminarà la resta de duplicats.", "bulk_trash_duplicates_confirmation": "Esteu segur que voleu enviar a les escombraries {count, plural, one {# recurs duplicat} other {# recursos duplicats}}? Això mantindrà el recurs més gran de cada grup i eliminarà la resta de duplicats.",
"buy": "Comprar Immich", "buy": "Comprar Immich",
"cache_settings_album_thumbnails": "Miniatures de la pàgina de la biblioteca ({} 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 ({})",
"cache_settings_image_cache_size": "Mida de la memòria cau de imatges ({} elements)",
"cache_settings_statistics_album": "Miniatures de la biblioteca",
"cache_settings_statistics_assets": "{} elements ({})",
"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 ({} 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",
"camera": "Càmera", "camera": "Càmera",
"camera_brand": "Marca de la càmera", "camera_brand": "Marca de la càmera",
"camera_model": "Model de càmera", "camera_model": "Model de càmera",
"cancel": "Cancel·la", "cancel": "Cancel·la",
"cancel_search": "Cancel·la la cerca", "cancel_search": "Cancel·la la cerca",
"canceled": "Cancel·lat",
"cannot_merge_people": "No es pot fusionar gent", "cannot_merge_people": "No es pot fusionar gent",
"cannot_undo_this_action": "Aquesta acció no es pot desfer!", "cannot_undo_this_action": "Aquesta acció no es pot desfer!",
"cannot_update_the_description": "No es pot actualitzar la descripció", "cannot_update_the_description": "No es pot actualitzar la descripció",
"change_date": "Canvia la data", "change_date": "Canvia la data",
"change_display_order": "Canvia l'ordre de visualització",
"change_expiration_time": "Canvia la data d'expiració", "change_expiration_time": "Canvia la data d'expiració",
"change_location": "Canvia la ubicació", "change_location": "Canvia la ubicació",
"change_name": "Canvia el nom", "change_name": "Canvia el nom",
"change_name_successfully": "Nom canviat amb èxit", "change_name_successfully": "Nom canviat amb èxit",
"change_password": "Canvia la contrasenya", "change_password": "Canvia la contrasenya",
"change_password_description": "Aquesta és la primera vegada que inicieu la sessió al sistema o s'ha fet una sol·licitud per canviar la contrasenya. Introduïu la nova contrasenya a continuació.", "change_password_description": "Aquesta és la primera vegada que inicieu la sessió al sistema o s'ha fet una sol·licitud per canviar la contrasenya. Introduïu la nova contrasenya a continuació.",
"change_password_form_confirm_password": "Confirma la contrasenya",
"change_password_form_description": "Hola {name},\n\nAquesta és la primera vegada que inicies sessió al sistema o bé s'ha sol·licitat canviar la teva contrasenya. Si us plau, introdueix la nova contrasenya a continuació.",
"change_password_form_new_password": "Nova contrasenya",
"change_password_form_password_mismatch": "Les contrasenyes no coincideixen",
"change_password_form_reenter_new_password": "Torna a introduir la nova contrasenya",
"change_your_password": "Canvia la teva contrasenya", "change_your_password": "Canvia la teva contrasenya",
"changed_visibility_successfully": "Visibilitat canviada amb èxit", "changed_visibility_successfully": "Visibilitat canviada amb èxit",
"check_all": "Marqueu-ho tot", "check_all": "Marqueu-ho tot",
"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.",
"check_logs": "Comprovar els registres", "check_logs": "Comprovar els registres",
"choose_matching_people_to_merge": "Trieu les persones que coincideixin per combinar-les", "choose_matching_people_to_merge": "Trieu les persones que coincideixin per combinar-les",
"city": "Ciutat", "city": "Ciutat",
@@ -477,6 +617,14 @@
"clear_all_recent_searches": "Esborra totes les cerques recents", "clear_all_recent_searches": "Esborra totes les cerques recents",
"clear_message": "Neteja el missatge", "clear_message": "Neteja el missatge",
"clear_value": "Neteja el valor", "clear_value": "Neteja el valor",
"client_cert_dialog_msg_confirm": "OK",
"client_cert_enter_password": "Introdueix la contrasenya",
"client_cert_import": "Importar",
"client_cert_import_success_msg": "S'ha importat el certificat del client",
"client_cert_invalid_msg": "Fitxer de certificat no vàlid o contrasenya incorrecta",
"client_cert_remove_msg": "S'ha eliminat el certificat del client",
"client_cert_subtitle": "Només admet el format PKCS12 (.p12, .pfx). La importació/eliminació de certificats només està disponible abans d'iniciar sessió",
"client_cert_title": "Certificat de client SSL",
"clockwise": "En sentit horari", "clockwise": "En sentit horari",
"close": "Tanca", "close": "Tanca",
"collapse": "Tanca", "collapse": "Tanca",
@@ -487,6 +635,9 @@
"comment_options": "Opcions de comentari", "comment_options": "Opcions de comentari",
"comments_and_likes": "Comentaris i agradaments", "comments_and_likes": "Comentaris i agradaments",
"comments_are_disabled": "Els comentaris estan desactivats", "comments_are_disabled": "Els comentaris estan desactivats",
"common_create_new_album": "Crea un àlbum nou",
"common_server_error": "Si us plau, comproveu la vostra connexió de xarxa, assegureu-vos que el servidor és accessible i que les versions de l'aplicació i del servidor són compatibles.",
"completed": "Completat",
"confirm": "Confirmar", "confirm": "Confirmar",
"confirm_admin_password": "Confirmeu la contrasenya d'administrador", "confirm_admin_password": "Confirmeu la contrasenya d'administrador",
"confirm_delete_face": "Estàs segur que vols eliminar la cara de {name} de les cares reconegudes?", "confirm_delete_face": "Estàs segur que vols eliminar la cara de {name} de les cares reconegudes?",
@@ -496,6 +647,15 @@
"contain": "Contingut", "contain": "Contingut",
"context": "Context", "context": "Context",
"continue": "Continuar", "continue": "Continuar",
"control_bottom_app_bar_album_info_shared": "{} 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",
"control_bottom_app_bar_edit_location": "Edita la ubicació",
"control_bottom_app_bar_edit_time": "Edita data i hora",
"control_bottom_app_bar_share_link": "Comparteix l'enllaç",
"control_bottom_app_bar_share_to": "Comparteix a",
"control_bottom_app_bar_trash_from_immich": "Mou a paperera",
"copied_image_to_clipboard": "Imatge copiada a porta-retalls.", "copied_image_to_clipboard": "Imatge copiada a porta-retalls.",
"copied_to_clipboard": "Copiada a porta-retalls!", "copied_to_clipboard": "Copiada a porta-retalls!",
"copy_error": "Error de còpia", "copy_error": "Error de còpia",
@@ -510,24 +670,34 @@
"covers": "Portades", "covers": "Portades",
"create": "Crea", "create": "Crea",
"create_album": "Crear un àlbum", "create_album": "Crear un àlbum",
"create_album_page_untitled": "Sense títol",
"create_library": "Crea una llibreria", "create_library": "Crea una llibreria",
"create_link": "Crear enllaç", "create_link": "Crear enllaç",
"create_link_to_share": "Crear enllaç per compartir", "create_link_to_share": "Crear enllaç per compartir",
"create_link_to_share_description": "Deixa que qualsevol persona amb l'enllaç vegi les fotos seleccionades", "create_link_to_share_description": "Deixa que qualsevol persona amb l'enllaç vegi les fotos seleccionades",
"create_new": "CREAR NOU",
"create_new_person": "Crea una nova persona", "create_new_person": "Crea una nova persona",
"create_new_person_hint": "Assigna els elements seleccionats a una persona nova", "create_new_person_hint": "Assigna els elements seleccionats a una persona nova",
"create_new_user": "Crea un usuari nou", "create_new_user": "Crea un usuari nou",
"create_shared_album_page_share_add_assets": "AFEGEIX ELEMENTS",
"create_shared_album_page_share_select_photos": "Escull fotografies",
"create_tag": "Crear etiqueta", "create_tag": "Crear etiqueta",
"create_tag_description": "Crear una nova etiqueta. Per les etiquetes aniuades, escriu la ruta comperta de l'etiqueta, incloses les barres diagonals.", "create_tag_description": "Crear una nova etiqueta. Per les etiquetes aniuades, escriu la ruta comperta de l'etiqueta, incloses les barres diagonals.",
"create_user": "Crea un usuari", "create_user": "Crea un usuari",
"created": "Creat", "created": "Creat",
"crop": "Retalla",
"curated_object_page_title": "Coses",
"current_device": "Dispositiu actual", "current_device": "Dispositiu actual",
"current_server_address": "Adreça actual del servidor",
"custom_locale": "Localització personalitzada", "custom_locale": "Localització personalitzada",
"custom_locale_description": "Format de dates i números segons la llengua i regió", "custom_locale_description": "Format de dates i números segons la llengua i regió",
"daily_title_text_date": "E, dd MMM",
"daily_title_text_date_year": "E, dd MMM, yyyy",
"dark": "Fosc", "dark": "Fosc",
"date_after": "Data posterior a", "date_after": "Data posterior a",
"date_and_time": "Data i hora", "date_and_time": "Data i hora",
"date_before": "Data anterior a", "date_before": "Data anterior a",
"date_format": "E, d LLL, y • hh:mm",
"date_of_birth_saved": "Data de naixement guardada amb èxit", "date_of_birth_saved": "Data de naixement guardada amb èxit",
"date_range": "Interval de dates", "date_range": "Interval de dates",
"day": "Dia", "day": "Dia",
@@ -541,19 +711,30 @@
"delete": "Esborra", "delete": "Esborra",
"delete_album": "Esborra l'àlbum", "delete_album": "Esborra l'àlbum",
"delete_api_key_prompt": "Esteu segurs que voleu eliminar aquesta clau API?", "delete_api_key_prompt": "Esteu segurs que voleu eliminar aquesta clau API?",
"delete_dialog_alert": "Aquests elements seran eliminats de manera permanent d'Immich i del vostre dispositiu.",
"delete_dialog_alert_local": "Aquests elements s'eliminaran permanentment del vostre dispositiu, però encara estaran disponibles al servidor Immich",
"delete_dialog_alert_local_non_backed_up": "Alguns dels elements no tenen còpia de seguretat a Immich i s'eliminaran permanentment del dispositiu",
"delete_dialog_alert_remote": "Aquests elements s'eliminaran permanentment del servidor Immich",
"delete_dialog_ok_force": "Suprimeix de totes maneres",
"delete_dialog_title": "Esborra permanentment",
"delete_duplicates_confirmation": "Esteu segurs que voleu eliminar aquests duplicats permanentment?", "delete_duplicates_confirmation": "Esteu segurs que voleu eliminar aquests duplicats permanentment?",
"delete_face": "Esborrar cara", "delete_face": "Esborrar cara",
"delete_key": "Suprimeix la clau", "delete_key": "Suprimeix la clau",
"delete_library": "Suprimeix la Llibreria", "delete_library": "Suprimeix la Llibreria",
"delete_link": "Esborra l'enllaç", "delete_link": "Esborra l'enllaç",
"delete_local_dialog_ok_backed_up_only": "Esborrar només les que tinguin còpia de seguretat",
"delete_local_dialog_ok_force": "Suprimeix de totes maneres",
"delete_others": "Suprimeix altres", "delete_others": "Suprimeix altres",
"delete_shared_link": "Odstranit sdílený odkaz", "delete_shared_link": "Odstranit sdílený odkaz",
"delete_shared_link_dialog_title": "Suprimeix l'enllaç compartit",
"delete_tag": "Eliminar etiqueta", "delete_tag": "Eliminar etiqueta",
"delete_tag_confirmation_prompt": "Estàs segur que vols eliminar l'etiqueta {tagName}?", "delete_tag_confirmation_prompt": "Estàs segur que vols eliminar l'etiqueta {tagName}?",
"delete_user": "Suprimeix l'usuari", "delete_user": "Suprimeix l'usuari",
"deleted_shared_link": "Suprimeix l'enllaç compartit", "deleted_shared_link": "Suprimeix l'enllaç compartit",
"deletes_missing_assets": "Elimina els actius que falten del disc", "deletes_missing_assets": "Elimina els actius que falten del disc",
"description": "Descripció", "description": "Descripció",
"description_input_hint_text": "Afegeix descripció...",
"description_input_submit_error": "S'ha produït un error en actualitzar la descripció, comproveu el registre per a més detalls",
"details": "Detalls", "details": "Detalls",
"direction": "Direcció", "direction": "Direcció",
"disabled": "Desactivat", "disabled": "Desactivat",
@@ -570,12 +751,26 @@
"documentation": "Documentació", "documentation": "Documentació",
"done": "Fet", "done": "Fet",
"download": "Descarregar", "download": "Descarregar",
"download_canceled": "Descàrrega cancel·lada",
"download_complete": "Descàrrega completada",
"download_enqueue": "Descàrrega en cua",
"download_error": "Error de descàrrega",
"download_failed": "Descàrrega ha fallat",
"download_filename": "arxiu: {}",
"download_finished": "Descàrrega acabada",
"download_include_embedded_motion_videos": "Vídeos incrustats", "download_include_embedded_motion_videos": "Vídeos incrustats",
"download_include_embedded_motion_videos_description": "Incloure vídeos incrustats en fotografies en moviment com un arxiu separat", "download_include_embedded_motion_videos_description": "Incloure vídeos incrustats en fotografies en moviment com un arxiu separat",
"download_notfound": "No s'ha trobat la descàrrega",
"download_paused": "Descàrrega pausada",
"download_settings": "Descarregar", "download_settings": "Descarregar",
"download_settings_description": "Gestioneu la configuració relacionada amb la descàrrega de recursos", "download_settings_description": "Gestioneu la configuració relacionada amb la descàrrega de recursos",
"download_started": "Descàrrega ha començat",
"download_sucess": "Descarregat amb èxit",
"download_sucess_android": "El multimedia s'ha descarregat a DCIM/Immich",
"download_waiting_to_retry": "Esperant per tornar-ho a intentar",
"downloading": "Baixant", "downloading": "Baixant",
"downloading_asset_filename": "Descarregant l'element {filename}", "downloading_asset_filename": "Descarregant l'element {filename}",
"downloading_media": "Descàrrega multimèdia",
"drop_files_to_upload": "Deixeu els fitxers a qualsevol lloc per carregar-los", "drop_files_to_upload": "Deixeu els fitxers a qualsevol lloc per carregar-los",
"duplicates": "Duplicats", "duplicates": "Duplicats",
"duplicates_description": "Resol cada grup indicant quins, si n'hi ha, són duplicats", "duplicates_description": "Resol cada grup indicant quins, si n'hi ha, són duplicats",
@@ -592,6 +787,7 @@
"edit_key": "Edita clau", "edit_key": "Edita clau",
"edit_link": "Edita enllaç", "edit_link": "Edita enllaç",
"edit_location": "Edita ubicació", "edit_location": "Edita ubicació",
"edit_location_dialog_title": "Ubicació",
"edit_name": "Edita el nom", "edit_name": "Edita el nom",
"edit_people": "Edita la gent", "edit_people": "Edita la gent",
"edit_tag": "Editar etiqueta", "edit_tag": "Editar etiqueta",
@@ -604,14 +800,19 @@
"editor_crop_tool_h2_aspect_ratios": "Relació d'aspecte", "editor_crop_tool_h2_aspect_ratios": "Relació d'aspecte",
"editor_crop_tool_h2_rotation": "Rotació", "editor_crop_tool_h2_rotation": "Rotació",
"email": "Correu electrònic", "email": "Correu electrònic",
"empty_folder": "Aquesta carpeta és buida",
"empty_trash": "Buidar la paperera", "empty_trash": "Buidar la paperera",
"empty_trash_confirmation": "Esteu segur que voleu buidar la paperera? Això eliminarà tots els recursos a la paperera permanentment d'Immich.\nNo podeu desfer aquesta acció!", "empty_trash_confirmation": "Esteu segur que voleu buidar la paperera? Això eliminarà tots els recursos a la paperera permanentment d'Immich.\nNo podeu desfer aquesta acció!",
"enable": "Activar", "enable": "Activar",
"enabled": "Activat", "enabled": "Activat",
"end_date": "Data final", "end_date": "Data final",
"enqueued": "En cua",
"enter_wifi_name": "Introdueix el nom de WiFi",
"error": "Error", "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_delete_face": "Error esborrant cara de les cares reconegudes",
"error_loading_image": "Error carregant la imatge", "error_loading_image": "Error carregant la imatge",
"error_saving_image": "Error: {}",
"error_title": "Error - Quelcom ha anat malament", "error_title": "Error - Quelcom ha anat malament",
"errors": { "errors": {
"cannot_navigate_next_asset": "No es pot navegar a l'element següent", "cannot_navigate_next_asset": "No es pot navegar a l'element següent",
@@ -740,8 +941,21 @@
"unable_to_upload_file": "No es pot carregar el fitxer" "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 {}",
"exif_bottom_sheet_person_age_months": "Edat {} mesos",
"exif_bottom_sheet_person_age_year_months": "Edat 1 any, {} mesos",
"exif_bottom_sheet_person_age_years": "Edat {}",
"exit_slideshow": "Surt de la presentació de diapositives", "exit_slideshow": "Surt de la presentació de diapositives",
"expand_all": "Ampliar-ho tot", "expand_all": "Ampliar-ho tot",
"experimental_settings_new_asset_list_subtitle": "Treball en curs",
"experimental_settings_new_asset_list_title": "Habilita la graella de fotos experimental",
"experimental_settings_subtitle": "Utilitzeu-ho sota la vostra responsabilitat!",
"experimental_settings_title": "Experimental",
"expire_after": "Caduca després de", "expire_after": "Caduca després de",
"expired": "Caducat", "expired": "Caducat",
"expires_date": "Caduca el {date}", "expires_date": "Caduca el {date}",
@@ -752,11 +966,16 @@
"extension": "Extensió", "extension": "Extensió",
"external": "Extern", "external": "Extern",
"external_libraries": "Llibreries externes", "external_libraries": "Llibreries externes",
"external_network": "Xarxa externa",
"external_network_sheet_info": "Quan no estigui a la xarxa WiFi preferida, l'aplicació es connectarà al servidor mitjançant el primer dels URL següents a què pot arribar, començant de dalt a baix.",
"face_unassigned": "Sense assignar", "face_unassigned": "Sense assignar",
"failed": "Fallat",
"failed_to_load_assets": "Error carregant recursos", "failed_to_load_assets": "Error carregant recursos",
"failed_to_load_folder": "No s'ha pogut carregar la carpeta",
"favorite": "Preferit", "favorite": "Preferit",
"favorite_or_unfavorite_photo": "Foto preferida o no preferida", "favorite_or_unfavorite_photo": "Foto preferida o no preferida",
"favorites": "Preferits", "favorites": "Preferits",
"favorites_page_no_favorites": "No s'han trobat preferits",
"feature_photo_updated": "Foto destacada actualitzada", "feature_photo_updated": "Foto destacada actualitzada",
"features": "Característiques", "features": "Característiques",
"features_setting_description": "Administrar les funcions de l'aplicació", "features_setting_description": "Administrar les funcions de l'aplicació",
@@ -764,25 +983,38 @@
"file_name_or_extension": "Nom de l'arxiu o extensió", "file_name_or_extension": "Nom de l'arxiu o extensió",
"filename": "Nom del fitxer", "filename": "Nom del fitxer",
"filetype": "Tipus d'arxiu", "filetype": "Tipus d'arxiu",
"filter": "Filtrar",
"filter_people": "Filtra persones", "filter_people": "Filtra persones",
"find_them_fast": "Trobeu-los ràpidament pel nom amb la cerca", "find_them_fast": "Trobeu-los ràpidament pel nom amb la cerca",
"fix_incorrect_match": "Corregiu la coincidència incorrecta", "fix_incorrect_match": "Corregiu la coincidència incorrecta",
"folder": "Carpeta",
"folder_not_found": "Carpeta no trobada",
"folders": "Carpetes", "folders": "Carpetes",
"folders_feature_description": "Explorar la vista de carpetes per les fotos i vídeos del sistema d'arxius", "folders_feature_description": "Explorar la vista de carpetes per les fotos i vídeos del sistema d'arxius",
"forward": "Endavant", "forward": "Endavant",
"general": "General", "general": "General",
"get_help": "Aconseguir ajuda", "get_help": "Aconseguir ajuda",
"get_wifiname_error": "No s'ha pogut obtenir el nom de la Wi-Fi. Assegureu-vos que heu concedit els permisos necessaris i que esteu connectat a una xarxa Wi-Fi",
"getting_started": "Començant", "getting_started": "Començant",
"go_back": "Torna", "go_back": "Torna",
"go_to_folder": "Anar al directori", "go_to_folder": "Anar al directori",
"go_to_search": "Vés a cercar", "go_to_search": "Vés a cercar",
"grant_permission": "Concedir permís",
"group_albums_by": "Agrupa àlbums per...", "group_albums_by": "Agrupa àlbums per...",
"group_country": "Agrupar per país", "group_country": "Agrupar per país",
"group_no": "Cap agrupació", "group_no": "Cap agrupació",
"group_owner": "Agrupar per propietari", "group_owner": "Agrupar per propietari",
"group_places_by": "Agrupar llocs per...", "group_places_by": "Agrupar llocs per...",
"group_year": "Agrupar per any", "group_year": "Agrupar per any",
"haptic_feedback_switch": "Activa la resposta hàptica",
"haptic_feedback_title": "Resposta Hàptica",
"has_quota": "Quota", "has_quota": "Quota",
"header_settings_add_header_tip": "Afegeix Capçalera",
"header_settings_field_validator_msg": "El valor no pot estar buit",
"header_settings_header_name_input": "Nom de la capçalera",
"header_settings_header_value_input": "Valor de la capçalera",
"headers_settings_tile_subtitle": "Definiu les capçaleres de proxy que l'aplicació hauria d'enviar amb cada sol·licitud de xarxa",
"headers_settings_tile_title": "Capçaleres proxy personalitzades",
"hi_user": "Hola {name} ({email})", "hi_user": "Hola {name} ({email})",
"hide_all_people": "Amaga totes les persones", "hide_all_people": "Amaga totes les persones",
"hide_gallery": "Amaga la galeria", "hide_gallery": "Amaga la galeria",
@@ -790,8 +1022,24 @@
"hide_password": "Amaga la contrasenya", "hide_password": "Amaga la contrasenya",
"hide_person": "Amaga la persona", "hide_person": "Amaga la persona",
"hide_unnamed_people": "Amaga persones sense nom", "hide_unnamed_people": "Amaga persones sense nom",
"home_page_add_to_album_conflicts": "S'han afegit {added} elements a l'àlbum {album}. {failed} elements ja existeixen a l'àlbum.",
"home_page_add_to_album_err_local": "Encara no es poden afegir elements locals als àlbums, ometent",
"home_page_add_to_album_success": "S'han afegit {added} elements a l'àlbum {album}.",
"home_page_album_err_partner": "Encara no es poden afegir elements dels companys als àlbums, ometent",
"home_page_archive_err_local": "Encara no es poden arxivar elements locals, ometent",
"home_page_archive_err_partner": "No es poden arxivar els elements de companys, ometent",
"home_page_building_timeline": "Construint la línia de temps",
"home_page_delete_err_partner": "No es poden suprimir els elements de companys, ometent",
"home_page_delete_remote_err_local": "Elements locals a la selecció d'eliminació remota, ometent",
"home_page_favorite_err_local": "Encara no es pot afegir a preferits elements locals, ometent",
"home_page_favorite_err_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_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ó", "host": "Amfitrió",
"hour": "Hora", "hour": "Hora",
"ignore_icloud_photos": "Ignora fotos d'iCloud",
"ignore_icloud_photos_description": "Les fotos emmagatzemades a iCloud no es penjaran al servidor Immich",
"image": "Imatge", "image": "Imatge",
"image_alt_text_date": "{isVideo, select, true {Video} other {Image}} presa el {date}", "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} presa el {date}",
"image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} pres/a amb {person1} el {date}", "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} pres/a amb {person1} el {date}",
@@ -803,6 +1051,10 @@
"image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} pres/a a {city}, {country} amb {person1} i {person2} el {date}", "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} pres/a a {city}, {country} amb {person1} i {person2} el {date}",
"image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} pres/a a {city}, {country} amb {person1}, {person2}, i {person3} el {date}", "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} pres/a a {city}, {country} amb {person1}, {person2}, i {person3} el {date}",
"image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} pres/a a {city}, {country} amb {person1}, {person2}, i {additionalCount, number} altres el {date}", "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} pres/a a {city}, {country} amb {person1}, {person2}, i {additionalCount, number} altres el {date}",
"image_saved_successfully": "Imatge desada",
"image_viewer_page_state_provider_download_started": "Baixada començada",
"image_viewer_page_state_provider_download_success": "Baixada amb èxit",
"image_viewer_page_state_provider_share_error": "Error en compartir",
"immich_logo": "Logotip d'Immich", "immich_logo": "Logotip d'Immich",
"immich_web_interface": "Interfície web Immich", "immich_web_interface": "Interfície web Immich",
"import_from_json": "Importar des de JSON", "import_from_json": "Importar des de JSON",
@@ -821,6 +1073,8 @@
"night_at_midnight": "Cada mitjanit", "night_at_midnight": "Cada mitjanit",
"night_at_twoam": "Cada nit a les 2 del matí" "night_at_twoam": "Cada nit a les 2 del matí"
}, },
"invalid_date": "Data invàlida",
"invalid_date_format": "Format de data invàlid",
"invite_people": "Convida gent", "invite_people": "Convida gent",
"invite_to_album": "Convida a l'àlbum", "invite_to_album": "Convida a l'àlbum",
"items_count": "{count, plural, one {# element} other {# elements}}", "items_count": "{count, plural, one {# element} other {# elements}}",
@@ -841,6 +1095,12 @@
"level": "Nivell", "level": "Nivell",
"library": "Bibilioteca", "library": "Bibilioteca",
"library_options": "Opcions de biblioteca", "library_options": "Opcions de biblioteca",
"library_page_device_albums": "Àlbums al Dispositiu",
"library_page_new_album": "Nou àlbum",
"library_page_sort_asset_count": "Nombre d'elements",
"library_page_sort_created": "Creat més recentment",
"library_page_sort_last_modified": "Darrera modificació",
"library_page_sort_title": "Títol de l'àlbum",
"light": "Llum", "light": "Llum",
"like_deleted": "M'agrada suprimit", "like_deleted": "M'agrada suprimit",
"link_motion_video": "Enllaçar vídeo en moviment", "link_motion_video": "Enllaçar vídeo en moviment",
@@ -850,12 +1110,42 @@
"list": "Llista", "list": "Llista",
"loading": "Carregant", "loading": "Carregant",
"loading_search_results_failed": "No s'han pogut carregar els resultats de la cerca", "loading_search_results_failed": "No s'han pogut carregar els resultats de la cerca",
"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ó",
"location_permission_content": "Per utilitzar la funció de canvi automàtic, Immich necessita un permís de ubicació precisa perquè pugui llegir el nom de la xarxa WiFi actual",
"location_picker_choose_on_map": "Escollir en el mapa",
"location_picker_latitude_error": "Introdueix una latitud vàlida",
"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",
"log_out": "Tanca la sessió", "log_out": "Tanca la sessió",
"log_out_all_devices": "Tanqueu la sessió de tots els dispositius", "log_out_all_devices": "Tanqueu la sessió de tots els dispositius",
"logged_out_all_devices": "S'ha tancat la sessió de tots els dispositius", "logged_out_all_devices": "S'ha tancat la sessió de tots els dispositius",
"logged_out_device": "Dispositiu tancat", "logged_out_device": "Dispositiu tancat",
"login": "Iniciar sessió", "login": "Iniciar sessió",
"login_disabled": "S'ha desactivat l'inici de sessió",
"login_form_api_exception": "Excepció de l'API. Comproveu l'URL del servidor i torneu-ho a provar.",
"login_form_back_button_text": "Enrere",
"login_form_email_hint": "elteu@correu.cat",
"login_form_endpoint_hint": "http://ip-del-servidor:port",
"login_form_endpoint_url": "URL del servidor",
"login_form_err_http": "Especifica http:// o https://",
"login_form_err_invalid_email": "Adreça de correu electrònic no vàlida",
"login_form_err_invalid_url": "URL no vàlid",
"login_form_err_leading_whitespace": "Espai en blanc al principi",
"login_form_err_trailing_whitespace": "Espai en blanc al final",
"login_form_failed_get_oauth_server_config": "Error en iniciar sessió amb OAuth, comprova l'URL del servidor",
"login_form_failed_get_oauth_server_disable": "La funcionalitat OAuth no està disponible en aquest servidor",
"login_form_failed_login": "Error en iniciar sessió, comprova l'URL del servidor, el correu electrònic i la contrasenya.",
"login_form_handshake_exception": "S'ha produït una excepció de handshake amb el servidor. Activa el suport per certificats autofirmats a la configuració si estàs fent servir un certificat autofirmat.",
"login_form_password_hint": "contrasenya",
"login_form_save_login": "Mantingues identificat",
"login_form_server_empty": "Introdueix l'URL del servidor.",
"login_form_server_error": "No s'ha pogut connectar al servidor.",
"login_has_been_disabled": "L'inici de sessió s'ha desactivat.", "login_has_been_disabled": "L'inici de sessió s'ha desactivat.",
"login_password_changed_error": "S'ha produït un error en actualitzar la contrasenya",
"login_password_changed_success": "La contrasenya s'ha canviat correctament",
"logout_all_device_confirmation": "Esteu segur que voleu tancar la sessió de tots els dispositius?", "logout_all_device_confirmation": "Esteu segur que voleu tancar la sessió de tots els dispositius?",
"logout_this_device_confirmation": "Esteu segur que voleu tancar la sessió d'aquest dispositiu?", "logout_this_device_confirmation": "Esteu segur que voleu tancar la sessió d'aquest dispositiu?",
"longitude": "Longitud", "longitude": "Longitud",
@@ -873,13 +1163,40 @@
"manage_your_devices": "Gestioneu els vostres dispositius connectats", "manage_your_devices": "Gestioneu els vostres dispositius connectats",
"manage_your_oauth_connection": "Gestioneu la vostra connexió OAuth", "manage_your_oauth_connection": "Gestioneu la vostra connexió OAuth",
"map": "Mapa", "map": "Mapa",
"map_assets_in_bound": "{} foto",
"map_assets_in_bounds": "{} fotos",
"map_cannot_get_user_location": "No es pot obtenir la ubicació de l'usuari",
"map_location_dialog_yes": "Sí",
"map_location_picker_page_use_location": "Utilitzar aquesta ubicació",
"map_location_service_disabled_content": "El servei de localització s'ha d'activar per mostrar els elements de la teva ubicació actual. Vols activar-lo ara?",
"map_location_service_disabled_title": "Servei de localització desactivat",
"map_marker_for_images": "Marcador de mapa per a imatges fetes a {city}, {country}", "map_marker_for_images": "Marcador de mapa per a imatges fetes a {city}, {country}",
"map_marker_with_image": "Marcador de mapa amb imatge", "map_marker_with_image": "Marcador de mapa amb imatge",
"map_no_assets_in_bounds": "No hi ha fotos en aquesta zona",
"map_no_location_permission_content": "Es necessita el permís de localització per mostrar els elements de la teva ubicació actual. Vols permetre-ho ara?",
"map_no_location_permission_title": "Permís de localització denegat",
"map_settings": "Paràmetres de mapa", "map_settings": "Paràmetres de mapa",
"map_settings_dark_mode": "Mode fosc",
"map_settings_date_range_option_day": "Últimes 24 hores",
"map_settings_date_range_option_days": "Darrers {} dies",
"map_settings_date_range_option_year": "Any passat",
"map_settings_date_range_option_years": "Darrers {} anys",
"map_settings_dialog_title": "Configuració del mapa",
"map_settings_include_show_archived": "Incloure arxivats",
"map_settings_include_show_partners": "Incloure companys",
"map_settings_only_show_favorites": "Mostra només preferits",
"map_settings_theme_settings": "Tema del Mapa",
"map_zoom_to_see_photos": "Allunya per veure fotos",
"matches": "Coincidències", "matches": "Coincidències",
"media_type": "Tipus de mitjà", "media_type": "Tipus de mitjà",
"memories": "Records", "memories": "Records",
"memories_all_caught_up": "Posat al dia",
"memories_check_back_tomorrow": "Torna demà per veure més records",
"memories_setting_description": "Gestiona el que veus als teus records", "memories_setting_description": "Gestiona el que veus als teus records",
"memories_start_over": "Torna a començar",
"memories_swipe_to_close": "Llisca per tancar",
"memories_year_ago": "Fa un any",
"memories_years_ago": "Fa {} anys",
"memory": "Record", "memory": "Record",
"memory_lane_title": "Línia de records {title}", "memory_lane_title": "Línia de records {title}",
"menu": "Menú", "menu": "Menú",
@@ -894,12 +1211,17 @@
"missing": "Restants", "missing": "Restants",
"model": "Model", "model": "Model",
"month": "Mes", "month": "Mes",
"monthly_title_text_date_format": "MMMM y",
"more": "Més", "more": "Més",
"moved_to_trash": "S'ha mogut a la paperera", "moved_to_trash": "S'ha mogut a la paperera",
"multiselect_grid_edit_date_time_err_read_only": "No es pot canviar la data del fitxer(s) de només lectura, ometent",
"multiselect_grid_edit_gps_err_read_only": "No es pot canviar la localització de fitxers de només lectura. Saltant.",
"mute_memories": "Silenciar records", "mute_memories": "Silenciar records",
"my_albums": "Els meus àlbums", "my_albums": "Els meus àlbums",
"name": "Nom", "name": "Nom",
"name_or_nickname": "Nom o sobrenom", "name_or_nickname": "Nom o sobrenom",
"networking_settings": "Xarxes",
"networking_subtitle": "Gestiona la configuració del endpoint del servidor",
"never": "Mai", "never": "Mai",
"new_album": "Nou Àlbum", "new_album": "Nou Àlbum",
"new_api_key": "Nova clau de l'API", "new_api_key": "Nova clau de l'API",
@@ -916,6 +1238,7 @@
"no_albums_yet": "Sembla que encara no tens cap àlbum.", "no_albums_yet": "Sembla que encara no tens cap àlbum.",
"no_archived_assets_message": "Arxiveu fotos i vídeos per ocultar-los de Fotos", "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_message": "FEU CLIC PER PUJAR LA VOSTRA PRIMERA FOTO",
"no_assets_to_show": "No hi ha elements per mostrar",
"no_duplicates_found": "No s'han trobat duplicats.", "no_duplicates_found": "No s'han trobat duplicats.",
"no_exif_info_available": "No hi ha informació d'exif disponible", "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_explore_results_message": "Penja més fotos per explorar la teva col·lecció.",
@@ -927,8 +1250,13 @@
"no_results_description": "Proveu un sinònim o una paraula clau més general", "no_results_description": "Proveu un sinònim o una paraula clau més general",
"no_shared_albums_message": "Creeu un àlbum per compartir fotos i vídeos amb persones a la vostra xarxa", "no_shared_albums_message": "Creeu un àlbum per compartir fotos i vídeos amb persones a la vostra xarxa",
"not_in_any_album": "En cap àlbum", "not_in_any_album": "En cap àlbum",
"not_selected": "No seleccionat",
"note_apply_storage_label_to_previously_uploaded assets": "Nota: per aplicar l'etiqueta d'emmagatzematge als actius penjats anteriorment, executeu el", "note_apply_storage_label_to_previously_uploaded assets": "Nota: per aplicar l'etiqueta d'emmagatzematge als actius penjats anteriorment, executeu el",
"notes": "Notes", "notes": "Notes",
"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",
"notification_permission_list_tile_title": "Permís de notificacions",
"notification_toggle_setting_description": "Activa les notificacions per correu electrònic", "notification_toggle_setting_description": "Activa les notificacions per correu electrònic",
"notifications": "Notificacions", "notifications": "Notificacions",
"notifications_setting_description": "Gestiona les notificacions", "notifications_setting_description": "Gestiona les notificacions",
@@ -939,6 +1267,7 @@
"offline_paths_description": "Aquests resultats poden ser deguts a la supressió manual de fitxers que no formen part d'una biblioteca externa.", "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", "ok": "D'acord",
"oldest_first": "El més vell primer", "oldest_first": "El més vell primer",
"on_this_device": "En aquest dispositiu",
"onboarding": "Incorporació", "onboarding": "Incorporació",
"onboarding_privacy_description": "Les següents funcions (opcionals) depenen de serveis externs i poden desactivarse en qualsevol moment de dels ajustos.", "onboarding_privacy_description": "Les següents funcions (opcionals) depenen de serveis externs i poden desactivarse en qualsevol moment de dels ajustos.",
"onboarding_theme_description": "Trieu un tema de color per a la vostra instància. Podeu canviar-ho més endavant a la vostra configuració.", "onboarding_theme_description": "Trieu un tema de color per a la vostra instància. Podeu canviar-ho més endavant a la vostra configuració.",
@@ -962,6 +1291,14 @@
"partner_can_access": "{partner} hi té accés", "partner_can_access": "{partner} hi té accés",
"partner_can_access_assets": "Totes les vostres fotos i vídeos excepte les arxivades i eliminades", "partner_can_access_assets": "Totes les vostres fotos i vídeos excepte les arxivades i eliminades",
"partner_can_access_location": "Ubicació en què s'han fet les fotos", "partner_can_access_location": "Ubicació en què s'han fet les fotos",
"partner_list_user_photos": "fotos de {user}",
"partner_list_view_all": "Veure tot",
"partner_page_empty_message": "Les teves fotos encara no estan compartides amb cap company.",
"partner_page_no_more_users": "No hi ha més usuaris a afegir",
"partner_page_partner_add_failed": "No s'ha pogut afegir el company",
"partner_page_select_partner": "Escull company",
"partner_page_shared_to_title": "Compartit amb",
"partner_page_stop_sharing_content": "{} ja no podrà accedir a les teves fotos.",
"partner_sharing": "Compartició amb companys", "partner_sharing": "Compartició amb companys",
"partners": "Companys", "partners": "Companys",
"password": "Contrasenya", "password": "Contrasenya",
@@ -990,6 +1327,14 @@
"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_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_asset": "Element eliminat permanentment",
"permanently_deleted_assets_count": "{count, plural, one {S'ha eliminat # element} other {S'han eliminat # elements}} permanentment", "permanently_deleted_assets_count": "{count, plural, one {S'ha eliminat # element} other {S'han eliminat # elements}} permanentment",
"permission_onboarding_back": "Torna",
"permission_onboarding_continue_anyway": "Continua de totes maneres",
"permission_onboarding_get_started": "Comença",
"permission_onboarding_go_to_settings": "Ves a la configuració",
"permission_onboarding_permission_denied": "S'ha denegat el permís. Per utilitzar Immich, concediu permisos de fotos i vídeos a Configuració.",
"permission_onboarding_permission_granted": "Permís concedit! Tot a punt.",
"permission_onboarding_permission_limited": "Permís limitat. Per a permetre que Immich faci còpies de seguretat i gestioni tota la col·lecció de la galeria, concediu permisos de fotos i vídeos a Configuració.",
"permission_onboarding_request": "Immich requereix permís per veure les vostres fotos i vídeos.",
"person": "Persona", "person": "Persona",
"person_birthdate": "Nascut a {date}", "person_birthdate": "Nascut a {date}",
"person_hidden": "{name}{hidden, select, true { (ocultat)} other {}}", "person_hidden": "{name}{hidden, select, true { (ocultat)} other {}}",
@@ -1007,6 +1352,8 @@
"play_motion_photo": "Reproduir Fotos en Moviment", "play_motion_photo": "Reproduir Fotos en Moviment",
"play_or_pause_video": "Reproduir o posar en pausa el vídeo", "play_or_pause_video": "Reproduir o posar en pausa el vídeo",
"port": "Port", "port": "Port",
"preferences_settings_subtitle": "Gestiona les preferències de l'aplicació",
"preferences_settings_title": "Preferències",
"preset": "Preestablert", "preset": "Preestablert",
"preview": "Previsualització", "preview": "Previsualització",
"previous": "Anterior", "previous": "Anterior",
@@ -1014,6 +1361,13 @@
"previous_or_next_photo": "Foto anterior o següent", "previous_or_next_photo": "Foto anterior o següent",
"primary": "Primària", "primary": "Primària",
"privacy": "Privacitat", "privacy": "Privacitat",
"profile_drawer_app_logs": "Registres",
"profile_drawer_client_out_of_date_major": "L'aplicació mòbil està desactualitzada. Si us plau, actualitzeu a l'última versió major.",
"profile_drawer_client_out_of_date_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_image_of_user": "Imatge de perfil de {user}", "profile_image_of_user": "Imatge de perfil de {user}",
"profile_picture_set": "Imatge de perfil configurada.", "profile_picture_set": "Imatge de perfil configurada.",
"public_album": "Àlbum públic", "public_album": "Àlbum públic",
@@ -1063,6 +1417,8 @@
"recent": "Recent", "recent": "Recent",
"recent-albums": "Àlbums recents", "recent-albums": "Àlbums recents",
"recent_searches": "Cerques recents", "recent_searches": "Cerques recents",
"recently_added": "Afegit recentment",
"recently_added_page_title": "Afegit recentment",
"refresh": "Actualitzar", "refresh": "Actualitzar",
"refresh_encoded_videos": "Actualitza vídeos codificats", "refresh_encoded_videos": "Actualitza vídeos codificats",
"refresh_faces": "Actualitzar cares", "refresh_faces": "Actualitzar cares",
@@ -1119,10 +1475,12 @@
"role_editor": "Editor", "role_editor": "Editor",
"role_viewer": "Visor", "role_viewer": "Visor",
"save": "Desa", "save": "Desa",
"save_to_gallery": "Desa a galeria",
"saved_api_key": "Clau d'API guardada", "saved_api_key": "Clau d'API guardada",
"saved_profile": "Perfil guardat", "saved_profile": "Perfil guardat",
"saved_settings": "Configuració guardada", "saved_settings": "Configuració guardada",
"say_something": "Digues quelcom", "say_something": "Digues quelcom",
"scaffold_body_error_occurred": "S'ha produït un error",
"scan_all_libraries": "Escanejar totes les llibreries", "scan_all_libraries": "Escanejar totes les llibreries",
"scan_library": "Escaneja", "scan_library": "Escaneja",
"scan_settings": "Configuració d'escaneig", "scan_settings": "Configuració d'escaneig",
@@ -1138,16 +1496,45 @@
"search_camera_model": "Buscar per model de càmera...", "search_camera_model": "Buscar per model de càmera...",
"search_city": "Buscar per ciutat...", "search_city": "Buscar per ciutat...",
"search_country": "Buscar per país...", "search_country": "Buscar per país...",
"search_filter_apply": "Aplicar filtre",
"search_filter_camera_title": "Selecciona el tipus de càmera",
"search_filter_date": "Data",
"search_filter_date_interval": "{start} a {end}",
"search_filter_date_title": "Selecciona un rang de dates",
"search_filter_display_option_not_in_album": "No en àlbum",
"search_filter_display_options": "Opcions de Visualització",
"search_filter_filename": "Cerca pel nom del fitxer",
"search_filter_location": "Ubicació",
"search_filter_location_title": "Selecciona l'ubicació",
"search_filter_media_type": "Tipus de multimèdia",
"search_filter_media_type_title": "Selecciona tipus de multimèdia",
"search_filter_people_title": "Selecciona persones",
"search_for": "Cercar", "search_for": "Cercar",
"search_for_existing_person": "Busca una persona existent", "search_for_existing_person": "Busca una persona existent",
"search_no_more_result": "No més resultats",
"search_no_people": "Cap persona", "search_no_people": "Cap persona",
"search_no_people_named": "Cap persona anomenada \"{name}\"", "search_no_people_named": "Cap persona anomenada \"{name}\"",
"search_no_result": "No s'han trobat resultats, proveu un terme de cerca o una combinació diferents",
"search_options": "Opcions de cerca", "search_options": "Opcions de cerca",
"search_page_categories": "Categories",
"search_page_motion_photos": "Fotografies animades",
"search_page_no_objects": "No hi ha informació d'objectes disponibles",
"search_page_no_places": "No hi ha informació de llocs disponibles",
"search_page_screenshots": "Captures de pantalla",
"search_page_search_photos_videos": "Cerca les teves fotos i vídeos",
"search_page_selfies": "Autofotos",
"search_page_things": "Coses",
"search_page_view_all_button": "Veure tot",
"search_page_your_activity": "La teva activitat",
"search_page_your_map": "El teu mapa",
"search_people": "Buscar persones", "search_people": "Buscar persones",
"search_places": "Buscar llocs", "search_places": "Buscar llocs",
"search_rating": "Buscar per qualificació...", "search_rating": "Buscar per qualificació...",
"search_result_page_new_search_hint": "Cerca nova",
"search_settings": "Configuració de cerca", "search_settings": "Configuració de cerca",
"search_state": "Buscar per regió...", "search_state": "Buscar per regió...",
"search_suggestion_list_smart_search_hint_1": "La cerca intel·ligent està habilitada per defecte, per a cercar metadades utilitzeu la sintaxi ",
"search_suggestion_list_smart_search_hint_2": "m:el-teu-terme-de-cerca",
"search_tags": "Cercant etiquetes...", "search_tags": "Cercant etiquetes...",
"search_timezone": "Buscar per fus horari...", "search_timezone": "Buscar per fus horari...",
"search_type": "Buscar per tipus", "search_type": "Buscar per tipus",
@@ -1168,10 +1555,14 @@
"select_new_face": "Selecciona nova cara", "select_new_face": "Selecciona nova cara",
"select_photos": "Tria fotografies", "select_photos": "Tria fotografies",
"select_trash_all": "Envia la selecció a la paperera", "select_trash_all": "Envia la selecció a la paperera",
"select_user_for_sharing_page_err_album": "Error al crear l'àlbum",
"selected": "Seleccionat", "selected": "Seleccionat",
"selected_count": "{count, plural, one {# seleccionat} other {# seleccionats}}", "selected_count": "{count, plural, one {# seleccionat} other {# seleccionats}}",
"send_message": "Envia missatge", "send_message": "Envia missatge",
"send_welcome_email": "Envia correu de benvinguda", "send_welcome_email": "Envia correu de benvinguda",
"server_endpoint": "Endpoint de Servidor",
"server_info_box_app_version": "Versió de l'aplicació",
"server_info_box_server_url": "URL del servidor",
"server_offline": "Servidor fora de línia", "server_offline": "Servidor fora de línia",
"server_online": "Servidor en línia", "server_online": "Servidor en línia",
"server_stats": "Estadístiques del servidor", "server_stats": "Estadístiques del servidor",
@@ -1183,22 +1574,91 @@
"set_date_of_birth": "Establir data de naixement", "set_date_of_birth": "Establir data de naixement",
"set_profile_picture": "Establir imatge de perfil", "set_profile_picture": "Establir imatge de perfil",
"set_slideshow_to_fullscreen": "Mostra Diapositives en pantalla completa", "set_slideshow_to_fullscreen": "Mostra Diapositives en pantalla completa",
"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",
"setting_image_viewer_preview_subtitle": "Activa per carregar una imatge de resolució mitjana. Desactiva per carregar directament la imatge original, o bé utilitzar només la miniatura.",
"setting_image_viewer_preview_title": "Carrega la imatge de vista prèvia",
"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: {}",
"setting_notifications_notify_hours": "{} hores",
"setting_notifications_notify_immediately": "immediatament",
"setting_notifications_notify_minutes": "{} minuts",
"setting_notifications_notify_never": "mai",
"setting_notifications_notify_seconds": "{} segons",
"setting_notifications_single_progress_subtitle": "Informació detallada del progrés de la pujada de cada fitxer",
"setting_notifications_single_progress_title": "Mostra el progrés detallat de la còpia de seguretat en segon pla",
"setting_notifications_subtitle": "Ajusta les preferències de notificació",
"setting_notifications_total_progress_subtitle": "Progrés general de la pujada (elements completats/total)",
"setting_notifications_total_progress_title": "Mostra el progrés total de la còpia de seguretat en segon pla",
"setting_video_viewer_looping_title": "Bucle",
"setting_video_viewer_original_video_subtitle": "Quan reproduïu un vídeo des del servidor, reproduïu l'original encara que hi hagi una transcodificació disponible. Pot conduir a l'amortització. Els vídeos disponibles localment es reprodueixen en qualitat original independentment d'aquesta configuració.",
"setting_video_viewer_original_video_title": "Força el vídeo original",
"settings": "Configuració", "settings": "Configuració",
"settings_require_restart": "Si us plau, reinicieu Immich per a aplicar aquest canvi",
"settings_saved": "Configuració desada", "settings_saved": "Configuració desada",
"share": "Comparteix", "share": "Comparteix",
"share_add_photos": "Afegeix fotografies",
"share_assets_selected": "{} seleccionats",
"share_dialog_preparing": "S'està preparant...",
"shared": "Compartit", "shared": "Compartit",
"shared_album_activities_input_disable": "Els comentaris estan desactivats",
"shared_album_activity_remove_content": "Voleu eliminar aquesta activitat?",
"shared_album_activity_remove_title": "Elimina l'activitat",
"shared_album_section_people_action_error": "S'ha produït un error en retirar-se/eliminar l'àlbum",
"shared_album_section_people_action_leave": "Elimina l'usuari de l'àlbum",
"shared_album_section_people_action_remove_user": "Elimina l'usuari de l'àlbum",
"shared_album_section_people_title": "PERSONES",
"shared_by": "Compartit per", "shared_by": "Compartit per",
"shared_by_user": "Compartit per {user}", "shared_by_user": "Compartit per {user}",
"shared_by_you": "Compartit per tu", "shared_by_you": "Compartit per tu",
"shared_from_partner": "Fotos de {partner}", "shared_from_partner": "Fotos de {partner}",
"shared_intent_upload_button_progress_text": "{} / {} Pujat",
"shared_link_app_bar_title": "Enllaços compartits",
"shared_link_clipboard_copied_massage": "S'ha copiat al porta-retalls",
"shared_link_clipboard_text": "Enllaç: {}\nContrasenya: {}",
"shared_link_create_error": "S'ha produït un error en crear l'enllaç compartit",
"shared_link_edit_description_hint": "Introduïu la descripció de compartició",
"shared_link_edit_expire_after_option_day": "1 dia",
"shared_link_edit_expire_after_option_days": "{} dies",
"shared_link_edit_expire_after_option_hour": "1 hora",
"shared_link_edit_expire_after_option_hours": "{} hores",
"shared_link_edit_expire_after_option_minute": "1 minut",
"shared_link_edit_expire_after_option_minutes": "{} minuts",
"shared_link_edit_expire_after_option_months": "{} mesos",
"shared_link_edit_expire_after_option_year": "any {}",
"shared_link_edit_password_hint": "Introduïu la contrasenya de compartició",
"shared_link_edit_submit_button": "Actualitza l'enllaç",
"shared_link_error_server_url_fetch": "No s'ha pogut obtenir l'URL del servidor",
"shared_link_expires_day": "Caduca d'aquí a {} dia",
"shared_link_expires_days": "Caduca d'aquí a {} dies",
"shared_link_expires_hour": "Caduca d'aquí a {} hora",
"shared_link_expires_hours": "Caduca d'aquí a {} hores",
"shared_link_expires_minute": "Caduca d'aquí a {} minut",
"shared_link_expires_minutes": "Caduca d'aquí a {} minuts",
"shared_link_expires_never": "Caduca ∞",
"shared_link_expires_second": "Caduca d'aquí a {} segon",
"shared_link_expires_seconds": "Caduca d'aquí a {} segons",
"shared_link_individual_shared": "Individual compartit",
"shared_link_info_chip_metadata": "EXIF",
"shared_link_manage_links": "Gestiona els enllaços compartits",
"shared_link_options": "Opcions d'enllaços compartits", "shared_link_options": "Opcions d'enllaços compartits",
"shared_links": "Enllaços compartits", "shared_links": "Enllaços compartits",
"shared_links_description": "Comparteix fotos i vídeos amb un enllaç", "shared_links_description": "Comparteix fotos i vídeos amb un enllaç",
"shared_photos_and_videos_count": "{assetCount, plural, other {# fotos i vídeos compartits.}}", "shared_photos_and_videos_count": "{assetCount, plural, other {# fotos i vídeos compartits.}}",
"shared_with_me": "Compartit amb mi",
"shared_with_partner": "Compartit amb {partner}", "shared_with_partner": "Compartit amb {partner}",
"sharing": "Compartit", "sharing": "Compartit",
"sharing_enter_password": "Introduïu la contrasenya per veure aquesta pàgina.", "sharing_enter_password": "Introduïu la contrasenya per veure aquesta pàgina.",
"sharing_page_album": "Àlbums compartits",
"sharing_page_description": "Crea àlbums compartits per compartir fotos i vídeos amb persones de la teva xarxa.",
"sharing_page_empty_list": "LLISTA BUIDA",
"sharing_sidebar_description": "Mostra un enllaç a Compartit a la barra lateral", "sharing_sidebar_description": "Mostra un enllaç a Compartit a la barra lateral",
"sharing_silver_appbar_create_shared_album": "Crea àlbum compartit",
"sharing_silver_appbar_share_partner": "Comparteix amb un company",
"shift_to_permanent_delete": "premeu ⇧ per suprimir el recurs permanentment", "shift_to_permanent_delete": "premeu ⇧ per suprimir el recurs permanentment",
"show_album_options": "Mostra les opcions d'àlbum", "show_album_options": "Mostra les opcions d'àlbum",
"show_albums": "Mostrar àlbums", "show_albums": "Mostrar àlbums",
@@ -1265,6 +1725,9 @@
"support_third_party_description": "La vostra instal·lació immich la va empaquetar un tercer. Els problemes que experimenteu poden ser causats per aquest paquet així que, si us plau, plantegeu els poblemes amb ells en primer lloc mitjançant els enllaços següents.", "support_third_party_description": "La vostra instal·lació immich la va empaquetar un tercer. Els problemes que experimenteu poden ser causats per aquest paquet així que, si us plau, plantegeu els poblemes amb ells en primer lloc mitjançant els enllaços següents.",
"swap_merge_direction": "Canvia la direcció d'unió", "swap_merge_direction": "Canvia la direcció d'unió",
"sync": "Sincronitza", "sync": "Sincronitza",
"sync_albums": "Sincronitzar àlbums",
"sync_albums_manual_subtitle": "Sincronitza tots els vídeos i fotos penjats amb els àlbums de còpia de seguretat seleccionats",
"sync_upload_album_setting_subtitle": "Creeu i pugeu les seves fotos i vídeos als àlbums seleccionats a Immich",
"tag": "Etiqueta", "tag": "Etiqueta",
"tag_assets": "Etiquetar actius", "tag_assets": "Etiquetar actius",
"tag_created": "Etiqueta creada: {tag}", "tag_created": "Etiqueta creada: {tag}",
@@ -1278,6 +1741,19 @@
"theme": "Tema", "theme": "Tema",
"theme_selection": "Selecció de tema", "theme_selection": "Selecció de tema",
"theme_selection_description": "Activa automàticament el tema fosc o clar en funció de les preferències del sistema del navegador", "theme_selection_description": "Activa automàticament el tema fosc o clar en funció de les preferències del sistema del navegador",
"theme_setting_asset_list_storage_indicator_title": "Mostra l'indicador d'emmagatzematge als títols dels elements",
"theme_setting_asset_list_tiles_per_row_title": "Nombre d'elements per fila ({})",
"theme_setting_colorful_interface_subtitle": "Apliqueu color primari a les superfícies de fons.",
"theme_setting_colorful_interface_title": "Interfície colorida",
"theme_setting_image_viewer_quality_subtitle": "Ajusta la qualitat del visor de detalls d'imatges",
"theme_setting_image_viewer_quality_title": "Qualitat del visor d'imatges",
"theme_setting_primary_color_subtitle": "Trieu un color per a les accions i els accents principals.",
"theme_setting_primary_color_title": "Color primari",
"theme_setting_system_primary_color_title": "Utilitza color de sistema",
"theme_setting_system_theme_switch": "Automàtic (Segueix la configuració del sistema)",
"theme_setting_theme_subtitle": "Trieu la configuració del tema de l'aplicació",
"theme_setting_three_stage_loading_subtitle": "La càrrega en tres etapes podria augmentar el rendiment de càrrega, però causa un consum de xarxa significativament més alt",
"theme_setting_three_stage_loading_title": "Activa la càrrega en tres etapes",
"they_will_be_merged_together": "Es combinaran", "they_will_be_merged_together": "Es combinaran",
"third_party_resources": "Recursos de tercers", "third_party_resources": "Recursos de tercers",
"time_based_memories": "Records basats en el temps", "time_based_memories": "Records basats en el temps",
@@ -1297,7 +1773,15 @@
"trash_all": "Envia-ho tot a la paperera", "trash_all": "Envia-ho tot a la paperera",
"trash_count": "Paperera {count, number}", "trash_count": "Paperera {count, number}",
"trash_delete_asset": "Esborra/Elimina element", "trash_delete_asset": "Esborra/Elimina element",
"trash_emptied": "Paperera buidada",
"trash_no_results_message": "Les imatges i vídeos que s'enviïn a la paperera es mostraran aquí.", "trash_no_results_message": "Les imatges i vídeos que s'enviïn a la paperera es mostraran aquí.",
"trash_page_delete_all": "Eliminar-ho tot",
"trash_page_empty_trash_dialog_content": "Segur que voleu eliminar els elements? Aquests elements seran eliminats permanentment de Immich",
"trash_page_info": "Els elements que s'enviïn a la paperera s'eliminaran permanentment després de {} dies",
"trash_page_no_assets": "No hi ha elements a la paperera",
"trash_page_restore_all": "Restaura-ho tot",
"trash_page_select_assets_btn": "Selecciona elements",
"trash_page_title": "Paperera ({})",
"trashed_items_will_be_permanently_deleted_after": "Els elements que s'enviïn a la paperera s'eliminaran permanentment després de {days, plural, one {# dia} other {# dies}}.", "trashed_items_will_be_permanently_deleted_after": "Els elements que s'enviïn a la paperera s'eliminaran permanentment després de {days, plural, one {# dia} other {# dies}}.",
"type": "Tipus", "type": "Tipus",
"unarchive": "Desarxivar", "unarchive": "Desarxivar",
@@ -1326,6 +1810,8 @@
"updated_password": "Contrasenya actualitzada", "updated_password": "Contrasenya actualitzada",
"upload": "Pujar", "upload": "Pujar",
"upload_concurrency": "Concurrència de pujades", "upload_concurrency": "Concurrència de pujades",
"upload_dialog_info": "Vols fer còpia de seguretat dels elements seleccionats al servidor?",
"upload_dialog_title": "Puja elements",
"upload_errors": "Càrrega completada amb {count, plural, one {# error} other {# errors}}, actualitzeu la pàgina per veure els nous elements carregats.", "upload_errors": "Càrrega completada amb {count, plural, one {# error} other {# errors}}, actualitzeu la pàgina per veure els nous elements carregats.",
"upload_progress": "Restant {remaining, number} - Processat {processed, number}/{total, number}", "upload_progress": "Restant {remaining, number} - Processat {processed, number}/{total, number}",
"upload_skipped_duplicates": "{count, plural, one {S'ha omès # recurs duplicat} other {S'han omès # recursos duplicats}}", "upload_skipped_duplicates": "{count, plural, one {S'ha omès # recurs duplicat} other {S'han omès # recursos duplicats}}",
@@ -1333,8 +1819,11 @@
"upload_status_errors": "Errors", "upload_status_errors": "Errors",
"upload_status_uploaded": "Carregat", "upload_status_uploaded": "Carregat",
"upload_success": "Pujada correcta, actualitza la pàgina per veure nous recursos de pujada.", "upload_success": "Pujada correcta, actualitza la pàgina per veure nous recursos de pujada.",
"upload_to_immich": "Puja a Immich ({})",
"uploading": "Pujant",
"url": "URL", "url": "URL",
"usage": "Ús", "usage": "Ús",
"use_current_connection": "utilitzar la connexió actual",
"use_custom_date_range": "Fes servir un rang de dates personalitzat", "use_custom_date_range": "Fes servir un rang de dates personalitzat",
"user": "Usuari", "user": "Usuari",
"user_id": "ID d'usuari", "user_id": "ID d'usuari",
@@ -1349,10 +1838,16 @@
"users": "Usuaris", "users": "Usuaris",
"utilities": "Utilitats", "utilities": "Utilitats",
"validate": "Valida", "validate": "Valida",
"validate_endpoint_error": "Per favor introdueix un URL vàlid",
"variables": "Variables", "variables": "Variables",
"version": "Versió", "version": "Versió",
"version_announcement_closing": "El teu amic Alex", "version_announcement_closing": "El teu amic Alex",
"version_announcement_message": "Hola! Hi ha una nova versió d'Immich, si us plau, preneu-vos una estona per llegir les <link>notes de llançament</link> per assegurar que la teva configuració estigui actualitzada per evitar qualsevol error de configuració, especialment si utilitzeu WatchTower o qualsevol mecanisme que gestioni l'actualització automàtica de la vostra instància Immich.", "version_announcement_message": "Hola! Hi ha una nova versió d'Immich, si us plau, preneu-vos una estona per llegir les <link>notes de llançament</link> per assegurar que la teva configuració estigui actualitzada per evitar qualsevol error de configuració, especialment si utilitzeu WatchTower o qualsevol mecanisme que gestioni l'actualització automàtica de la vostra instància Immich.",
"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": "Historial de versions",
"version_history_item": "Instal·lat {version} el {date}", "version_history_item": "Instal·lat {version} el {date}",
"video": "Vídeo", "video": "Vídeo",
@@ -1372,15 +1867,20 @@
"view_previous_asset": "Mostra l'element anterior", "view_previous_asset": "Mostra l'element anterior",
"view_qr_code": "Veure codi QR", "view_qr_code": "Veure codi QR",
"view_stack": "Veure la pila", "view_stack": "Veure la pila",
"viewer_remove_from_stack": "Elimina de la pila",
"viewer_stack_use_as_main_asset": "Fes servir com a element principal",
"viewer_unstack": "Desapila",
"visibility_changed": "La visibilitat ha canviat per {count, plural, one {# persona} other {# persones}}", "visibility_changed": "La visibilitat ha canviat per {count, plural, one {# persona} other {# persones}}",
"waiting": "Esperant", "waiting": "Esperant",
"warning": "Avís", "warning": "Avís",
"week": "Setmana", "week": "Setmana",
"welcome": "Benvingut", "welcome": "Benvingut",
"welcome_to_immich": "Benvingut a immich", "welcome_to_immich": "Benvingut a immich",
"wifi_name": "Nom WiFi",
"year": "Any", "year": "Any",
"years_ago": "Fa {years, plural, one {# any} other {# anys}}", "years_ago": "Fa {years, plural, one {# any} other {# anys}}",
"yes": "Sí", "yes": "Sí",
"you_dont_have_any_shared_links": "No tens cap enllaç compartit", "you_dont_have_any_shared_links": "No tens cap enllaç compartit",
"your_wifi_name": "El teu nom WiFi",
"zoom_image": "Ampliar Imatge" "zoom_image": "Ampliar Imatge"
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@
"account_settings": "Konto seaded", "account_settings": "Konto seaded",
"acknowledge": "Sain aru", "acknowledge": "Sain aru",
"action": "Tegevus", "action": "Tegevus",
"action_common_update": "Uuenda",
"actions": "Tegevused", "actions": "Tegevused",
"active": "Aktiivne", "active": "Aktiivne",
"activity": "Aktiivsus", "activity": "Aktiivsus",
@@ -13,6 +14,7 @@
"add_a_location": "Lisa asukoht", "add_a_location": "Lisa asukoht",
"add_a_name": "Lisa nimi", "add_a_name": "Lisa nimi",
"add_a_title": "Lisa pealkiri", "add_a_title": "Lisa pealkiri",
"add_endpoint": "Lisa lõpp-punkt",
"add_exclusion_pattern": "Lisa välistamismuster", "add_exclusion_pattern": "Lisa välistamismuster",
"add_import_path": "Lisa imporditee", "add_import_path": "Lisa imporditee",
"add_location": "Lisa asukoht", "add_location": "Lisa asukoht",
@@ -22,6 +24,8 @@
"add_photos": "Lisa fotosid", "add_photos": "Lisa fotosid",
"add_to": "Lisa kohta…", "add_to": "Lisa kohta…",
"add_to_album": "Lisa albumisse", "add_to_album": "Lisa albumisse",
"add_to_album_bottom_sheet_added": "Lisatud albumisse {album}",
"add_to_album_bottom_sheet_already_exists": "On juba albumis {album}",
"add_to_shared_album": "Lisa jagatud albumisse", "add_to_shared_album": "Lisa jagatud albumisse",
"add_url": "Lisa URL", "add_url": "Lisa URL",
"added_to_archive": "Lisatud arhiivi", "added_to_archive": "Lisatud arhiivi",
@@ -35,11 +39,11 @@
"authentication_settings_disable_all": "Kas oled kindel, et soovid kõik sisselogimismeetodid välja lülitada? Sisselogimine lülitatakse täielikult välja.", "authentication_settings_disable_all": "Kas oled kindel, et soovid kõik sisselogimismeetodid välja lülitada? Sisselogimine lülitatakse täielikult välja.",
"authentication_settings_reenable": "Et taas lubada, kasuta <link>serveri käsku</link>.", "authentication_settings_reenable": "Et taas lubada, kasuta <link>serveri käsku</link>.",
"background_task_job": "Tausttegumid", "background_task_job": "Tausttegumid",
"backup_database": "Varunda andmebaas", "backup_database": "Loo andmebaasi tõmmis",
"backup_database_enable_description": "Luba andmebaasi varundamine", "backup_database_enable_description": "Luba andmebaasi tõmmised",
"backup_keep_last_amount": "Varukoopiate arv, mida alles hoida", "backup_keep_last_amount": "Eelmiste tõmmiste arv, mida alles hoida",
"backup_settings": "Varundamise seaded", "backup_settings": "Andmebaasi tõmmiste seaded",
"backup_settings_description": "Halda andmebaasi varundamise seadeid", "backup_settings_description": "Halda andmebaasi tõmmiste seadeid. Märkus: Neid tööteid ei jälgita ning ebaõnnestumisest ei hoiatata.",
"check_all": "Märgi kõik", "check_all": "Märgi kõik",
"cleanup": "Koristus", "cleanup": "Koristus",
"cleared_jobs": "Tööted eemaldatud: {job}", "cleared_jobs": "Tööted eemaldatud: {job}",
@@ -188,26 +192,22 @@
"oauth_auto_register": "Automaatne registreerimine", "oauth_auto_register": "Automaatne registreerimine",
"oauth_auto_register_description": "Registreeri uued kasutajad automaatselt OAuth abil sisselogimisel", "oauth_auto_register_description": "Registreeri uued kasutajad automaatselt OAuth abil sisselogimisel",
"oauth_button_text": "Nupu tekst", "oauth_button_text": "Nupu tekst",
"oauth_client_id": "Kliendi ID", "oauth_client_secret_description": "Nõutud, kui PKCE (Proof Key for Code Exchange) ei ole OAuth pakkuja poolt toetatud",
"oauth_client_secret": "Kliendi saladus",
"oauth_enable_description": "Sisene OAuth abil", "oauth_enable_description": "Sisene OAuth abil",
"oauth_issuer_url": "Väljastaja URL",
"oauth_mobile_redirect_uri": "Mobiilne ümbersuunamise URI", "oauth_mobile_redirect_uri": "Mobiilne ümbersuunamise URI",
"oauth_mobile_redirect_uri_override": "Mobiilse ümbersuunamise URI ülekirjutamine", "oauth_mobile_redirect_uri_override": "Mobiilse ümbersuunamise URI ülekirjutamine",
"oauth_mobile_redirect_uri_override_description": "Lülita sisse, kui OAuth pakkuja ei luba mobiilset URI-d, näiteks '{callback}'", "oauth_mobile_redirect_uri_override_description": "Lülita sisse, kui OAuth pakkuja ei luba mobiilset URI-d, näiteks '{callback}'",
"oauth_profile_signing_algorithm": "Profiili allkirjastamise algoritm",
"oauth_profile_signing_algorithm_description": "Algoritm, mida kasutatakse kasutajaprofiili allkirjastamiseks.",
"oauth_scope": "Skoop",
"oauth_settings": "OAuth", "oauth_settings": "OAuth",
"oauth_settings_description": "Halda OAuth sisselogimise seadeid", "oauth_settings_description": "Halda OAuth sisselogimise seadeid",
"oauth_settings_more_details": "Selle funktsiooni kohta rohkem teada saamiseks loe <link>dokumentatsiooni</link>.", "oauth_settings_more_details": "Selle funktsiooni kohta rohkem teada saamiseks loe <link>dokumentatsiooni</link>.",
"oauth_signing_algorithm": "Allkirjastamise algoritm",
"oauth_storage_label_claim": "Talletussildi väide", "oauth_storage_label_claim": "Talletussildi väide",
"oauth_storage_label_claim_description": "Sea kasutaja talletussildiks automaatselt selle väite väärtus.", "oauth_storage_label_claim_description": "Sea kasutaja talletussildiks automaatselt selle väite väärtus.",
"oauth_storage_quota_claim": "Talletuskvoodi väide", "oauth_storage_quota_claim": "Talletuskvoodi väide",
"oauth_storage_quota_claim_description": "Sea kasutaja talletuskvoodiks automaatselt selle väite väärtus.", "oauth_storage_quota_claim_description": "Sea kasutaja talletuskvoodiks automaatselt selle väite väärtus.",
"oauth_storage_quota_default": "Vaikimisi talletuskvoot (GiB)", "oauth_storage_quota_default": "Vaikimisi talletuskvoot (GiB)",
"oauth_storage_quota_default_description": "Kvoot (GiB), mida kasutada, kui ühtegi väidet pole esitatud (piiramatu kvoodi jaoks sisesta 0).", "oauth_storage_quota_default_description": "Kvoot (GiB), mida kasutada, kui ühtegi väidet pole esitatud (piiramatu kvoodi jaoks sisesta 0).",
"oauth_timeout": "Päringu ajalõpp",
"oauth_timeout_description": "Päringute ajalõpp millisekundites",
"offline_paths": "Ühenduseta failiteed", "offline_paths": "Ühenduseta failiteed",
"offline_paths_description": "Need tulemused võivad olla põhjustatud manuaalselt kustutatud failidest, mis ei ole osa välisest kogust.", "offline_paths_description": "Need tulemused võivad olla põhjustatud manuaalselt kustutatud failidest, mis ei ole osa välisest kogust.",
"password_enable_description": "Logi sisse e-posti aadressi ja parooliga", "password_enable_description": "Logi sisse e-posti aadressi ja parooliga",
@@ -367,6 +367,19 @@
"admin_password": "Administraatori parool", "admin_password": "Administraatori parool",
"administration": "Administratsioon", "administration": "Administratsioon",
"advanced": "Täpsemad valikud", "advanced": "Täpsemad valikud",
"advanced_settings_enable_alternate_media_filter_subtitle": "Kasuta seda valikut, et filtreerida sünkroonimise ajal üksuseid alternatiivsete kriteeriumite alusel. Proovi seda ainult siis, kui rakendusel on probleeme kõigi albumite tuvastamisega.",
"advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTAALNE] Kasuta alternatiivset seadme albumi sünkroonimise filtrit",
"advanced_settings_log_level_title": "Logimistase: {}",
"advanced_settings_prefer_remote_subtitle": "Mõned seadmed laadivad seadmes olevate üksuste pisipilte piinavalt aeglaselt. Aktiveeri see seadistus, et laadida selle asemel kaugpilte.",
"advanced_settings_prefer_remote_title": "Eelista kaugpilte",
"advanced_settings_proxy_headers_subtitle": "Määra vaheserveri päised, mida Immich peaks iga päringuga saatma",
"advanced_settings_proxy_headers_title": "Vaheserveri päised",
"advanced_settings_self_signed_ssl_subtitle": "Jätab serveri lõpp-punkti SSL-sertifikaadi kontrolli vahele. Nõutud endasigneeritud sertifikaatide jaoks.",
"advanced_settings_self_signed_ssl_title": "Luba endasigneeritud SSL-sertifikaadid",
"advanced_settings_sync_remote_deletions_subtitle": "Kustuta või taasta üksus selles seadmes automaatself, kui sama tegevus toimub veebis",
"advanced_settings_sync_remote_deletions_title": "Sünkrooni kaugkustutamised [EKSPERIMENTAALNE]",
"advanced_settings_troubleshooting_subtitle": "Luba lisafunktsioonid tõrkeotsinguks",
"advanced_settings_troubleshooting_title": "Tõrkeotsing",
"age_months": "Vanus {months, plural, one {# kuu} other {# kuud}}", "age_months": "Vanus {months, plural, one {# kuu} other {# kuud}}",
"age_year_months": "Vanus 1 aasta, {months, plural, one {# kuu} other {# kuud}}", "age_year_months": "Vanus 1 aasta, {months, plural, one {# kuu} other {# kuud}}",
"age_years": "{years, plural, other {Vanus #}}", "age_years": "{years, plural, other {Vanus #}}",
@@ -375,6 +388,8 @@
"album_cover_updated": "Albumi kaanepilt muudetud", "album_cover_updated": "Albumi kaanepilt muudetud",
"album_delete_confirmation": "Kas oled kindel, et soovid albumi {album} kustutada?", "album_delete_confirmation": "Kas oled kindel, et soovid albumi {album} kustutada?",
"album_delete_confirmation_description": "Kui see album on jagatud, ei pääse teised kasutajad sellele enam ligi.", "album_delete_confirmation_description": "Kui see album on jagatud, ei pääse teised kasutajad sellele enam ligi.",
"album_info_card_backup_album_excluded": "VÄLJA JÄETUD",
"album_info_card_backup_album_included": "LISATUD",
"album_info_updated": "Albumi info muudetud", "album_info_updated": "Albumi info muudetud",
"album_leave": "Lahku albumist?", "album_leave": "Lahku albumist?",
"album_leave_confirmation": "Kas oled kindel, et soovid albumist {album} lahkuda?", "album_leave_confirmation": "Kas oled kindel, et soovid albumist {album} lahkuda?",
@@ -383,10 +398,22 @@
"album_remove_user": "Eemalda kasutaja?", "album_remove_user": "Eemalda kasutaja?",
"album_remove_user_confirmation": "Kas oled kindel, et soovid kasutaja {user} eemaldada?", "album_remove_user_confirmation": "Kas oled kindel, et soovid kasutaja {user} eemaldada?",
"album_share_no_users": "Paistab, et oled seda albumit kõikide kasutajatega jaganud, või pole ühtegi kasutajat, kellega jagada.", "album_share_no_users": "Paistab, et oled seda albumit kõikide kasutajatega jaganud, või pole ühtegi kasutajat, kellega jagada.",
"album_thumbnail_card_item": "1 üksus",
"album_thumbnail_card_items": "{} üksust",
"album_thumbnail_card_shared": " · Jagatud",
"album_thumbnail_shared_by": "Jagas {}",
"album_updated": "Album muudetud", "album_updated": "Album muudetud",
"album_updated_setting_description": "Saa teavitus e-posti teel, kui jagatud albumis on uusi üksuseid", "album_updated_setting_description": "Saa teavitus e-posti teel, kui jagatud albumis on uusi üksuseid",
"album_user_left": "Lahkutud albumist {album}", "album_user_left": "Lahkutud albumist {album}",
"album_user_removed": "Kasutaja {user} eemaldatud", "album_user_removed": "Kasutaja {user} eemaldatud",
"album_viewer_appbar_delete_confirm": "Kas oled kindel, et soovid selle albumi oma kontolt kustutada?",
"album_viewer_appbar_share_err_delete": "Albumi kustutamine ebaõnnestus",
"album_viewer_appbar_share_err_leave": "Albumist lahkumine ebaõnnestus",
"album_viewer_appbar_share_err_remove": "Üksuste albumist eemaldamisel tekkis probleeme",
"album_viewer_appbar_share_err_title": "Albumi pealkirja muutmine ebaõnnestus",
"album_viewer_appbar_share_leave": "Lahku albumist",
"album_viewer_appbar_share_to": "Jaga",
"album_viewer_page_share_add_users": "Lisa kasutajaid",
"album_with_link_access": "Luba kõigil, kellel on link, näha selle albumi fotosid ja isikuid.", "album_with_link_access": "Luba kõigil, kellel on link, näha selle albumi fotosid ja isikuid.",
"albums": "Albumid", "albums": "Albumid",
"albums_count": "{count, plural, one {{count, number} album} other {{count, number} albumit}}", "albums_count": "{count, plural, one {{count, number} album} other {{count, number} albumit}}",
@@ -404,42 +431,94 @@
"api_key_description": "Seda väärtust kuvatakse ainult üks kord. Kopeeri see enne akna sulgemist.", "api_key_description": "Seda väärtust kuvatakse ainult üks kord. Kopeeri see enne akna sulgemist.",
"api_key_empty": "Su API võtme nimi ei tohiks olla tühi", "api_key_empty": "Su API võtme nimi ei tohiks olla tühi",
"api_keys": "API võtmed", "api_keys": "API võtmed",
"app_bar_signout_dialog_content": "Kas oled kindel, et soovid välja logida?",
"app_bar_signout_dialog_ok": "Jah",
"app_bar_signout_dialog_title": "Logi välja",
"app_settings": "Rakenduse seaded", "app_settings": "Rakenduse seaded",
"appears_in": "Albumid", "appears_in": "Albumid",
"archive": "Arhiiv", "archive": "Arhiiv",
"archive_or_unarchive_photo": "Arhiveeri või taasta foto", "archive_or_unarchive_photo": "Arhiveeri või taasta foto",
"archive_page_no_archived_assets": "Arhiveeritud üksuseid ei leitud",
"archive_page_title": "Arhiveeri ({})",
"archive_size": "Arhiivi suurus", "archive_size": "Arhiivi suurus",
"archive_size_description": "Seadista arhiivi suurus allalaadimiseks (GiB)", "archive_size_description": "Seadista arhiivi suurus allalaadimiseks (GiB)",
"archived": "Arhiveeritud",
"archived_count": "{count, plural, other {# arhiveeritud}}", "archived_count": "{count, plural, other {# arhiveeritud}}",
"are_these_the_same_person": "Kas need on sama isik?", "are_these_the_same_person": "Kas need on sama isik?",
"are_you_sure_to_do_this": "Kas oled kindel, et soovid seda teha?", "are_you_sure_to_do_this": "Kas oled kindel, et soovid seda teha?",
"asset_action_delete_err_read_only": "Kirjutuskaitstud üksuseid ei saa kustutada, jätan vahele",
"asset_added_to_album": "Lisatud albumisse", "asset_added_to_album": "Lisatud albumisse",
"asset_adding_to_album": "Albumisse lisamine…", "asset_adding_to_album": "Albumisse lisamine…",
"asset_description_updated": "Üksuse kirjeldus on muudetud", "asset_description_updated": "Üksuse kirjeldus on muudetud",
"asset_filename_is_offline": "Üksus {filename} ei ole kättesaadav", "asset_filename_is_offline": "Üksus {filename} ei ole kättesaadav",
"asset_has_unassigned_faces": "Üksusel on seostamata nägusid", "asset_has_unassigned_faces": "Üksusel on seostamata nägusid",
"asset_hashing": "Räsimine…", "asset_hashing": "Räsimine…",
"asset_list_group_by_sub_title": "Grupeeri",
"asset_list_layout_settings_dynamic_layout_title": "Dünaamiline asetus",
"asset_list_layout_settings_group_automatically": "Automaatne",
"asset_list_layout_settings_group_by": "Grupeeri üksused",
"asset_list_layout_settings_group_by_month_day": "Kuu + päev",
"asset_list_layout_sub_title": "Asetus",
"asset_list_settings_subtitle": "Fotoruudustiku paigutuse sätted",
"asset_list_settings_title": "Fotoruudustik",
"asset_offline": "Üksus pole kättesaadav", "asset_offline": "Üksus pole kättesaadav",
"asset_offline_description": "Seda välise kogu üksust ei leitud kettalt. Abi saamiseks palun võta ühendust oma Immich'i administraatoriga.", "asset_offline_description": "Seda välise kogu üksust ei leitud kettalt. Abi saamiseks palun võta ühendust oma Immich'i administraatoriga.",
"asset_restored_successfully": "Üksus edukalt taastatud",
"asset_skipped": "Vahele jäetud", "asset_skipped": "Vahele jäetud",
"asset_skipped_in_trash": "Prügikastis", "asset_skipped_in_trash": "Prügikastis",
"asset_uploaded": "Üleslaaditud", "asset_uploaded": "Üleslaaditud",
"asset_uploading": "Üleslaadimine…", "asset_uploading": "Üleslaadimine…",
"asset_viewer_settings_subtitle": "Halda galeriivaaturi seadeid",
"asset_viewer_settings_title": "Üksuste vaatur",
"assets": "Üksused", "assets": "Üksused",
"assets_added_count": "{count, plural, one {# üksus} other {# üksust}} lisatud", "assets_added_count": "{count, plural, one {# üksus} other {# üksust}} lisatud",
"assets_added_to_album_count": "{count, plural, one {# üksus} other {# üksust}} albumisse lisatud", "assets_added_to_album_count": "{count, plural, one {# üksus} other {# üksust}} albumisse lisatud",
"assets_added_to_name_count": "{count, plural, one {# üksus} other {# üksust}} lisatud {hasName, select, true {albumisse <b>{name}</b>} other {uude albumisse}}", "assets_added_to_name_count": "{count, plural, one {# üksus} other {# üksust}} lisatud {hasName, select, true {albumisse <b>{name}</b>} other {uude albumisse}}",
"assets_count": "{count, plural, one {# üksus} other {# üksust}}", "assets_count": "{count, plural, one {# üksus} other {# üksust}}",
"assets_deleted_permanently": "{} üksus(t) jäädavalt kustutatud",
"assets_deleted_permanently_from_server": "{} üksus(t) Immich'i serverist jäädavalt kustutatud",
"assets_moved_to_trash_count": "{count, plural, one {# üksus} other {# üksust}} liigutatud prügikasti", "assets_moved_to_trash_count": "{count, plural, one {# üksus} other {# üksust}} liigutatud prügikasti",
"assets_permanently_deleted_count": "{count, plural, one {# üksus} other {# üksust}} jäädavalt kustutatud", "assets_permanently_deleted_count": "{count, plural, one {# üksus} other {# üksust}} jäädavalt kustutatud",
"assets_removed_count": "{count, plural, one {# üksus} other {# üksust}} eemaldatud", "assets_removed_count": "{count, plural, one {# üksus} other {# üksust}} eemaldatud",
"assets_removed_permanently_from_device": "{} üksus(t) seadmest jäädavalt eemaldatud",
"assets_restore_confirmation": "Kas oled kindel, et soovid oma prügikasti liigutatud üksused taastada? Seda ei saa tagasi võtta! Pane tähele, et sel meetodil ei saa taastada ühenduseta üksuseid.", "assets_restore_confirmation": "Kas oled kindel, et soovid oma prügikasti liigutatud üksused taastada? Seda ei saa tagasi võtta! Pane tähele, et sel meetodil ei saa taastada ühenduseta üksuseid.",
"assets_restored_count": "{count, plural, one {# üksus} other {# üksust}} taastatud", "assets_restored_count": "{count, plural, one {# üksus} other {# üksust}} taastatud",
"assets_restored_successfully": "{} üksus(t) edukalt taastatud",
"assets_trashed": "{} üksus(t) liigutatud prügikasti",
"assets_trashed_count": "{count, plural, one {# üksus} other {# üksust}} liigutatud prügikasti", "assets_trashed_count": "{count, plural, one {# üksus} other {# üksust}} liigutatud prügikasti",
"assets_trashed_from_server": "{} üksus(t) liigutatud Immich'i serveris prügikasti",
"assets_were_part_of_album_count": "{count, plural, one {Üksus oli} other {Üksused olid}} juba osa albumist", "assets_were_part_of_album_count": "{count, plural, one {Üksus oli} other {Üksused olid}} juba osa albumist",
"authorized_devices": "Autoriseeritud seadmed", "authorized_devices": "Autoriseeritud seadmed",
"automatic_endpoint_switching_subtitle": "Ühendu lokaalselt üle valitud WiFi-võrgu, kui see on saadaval, ja kasuta mujal alternatiivseid ühendusi",
"automatic_endpoint_switching_title": "Automaatne URL-i ümberlülitamine",
"back": "Tagasi", "back": "Tagasi",
"back_close_deselect": "Tagasi, sulge või tühista valik", "back_close_deselect": "Tagasi, sulge või tühista valik",
"backup_album_selection_page_select_albums": "Vali albumid",
"backup_album_selection_page_selection_info": "Valiku info",
"backup_album_selection_page_total_assets": "Unikaalseid üksuseid kokku",
"backup_all": "Kõik",
"backup_background_service_default_notification": "Uute üksuste kontrollimine…",
"backup_background_service_error_title": "Varundamise viga",
"backup_controller_page_background_app_refresh_disabled_content": "Taustal varundamise kasutamiseks luba rakenduse taustal värskendamine: Seaded > Üldine > Rakenduse taustal värskendamine.",
"backup_controller_page_background_app_refresh_disabled_title": "Rakenduse taustal värskendamine keelatud",
"backup_controller_page_background_battery_info_link": "Näita mulle, kuidas",
"backup_controller_page_background_battery_info_ok": "OK",
"backup_controller_page_background_configure_error": "Taustateenuse seadistamine ebaõnnestus",
"backup_controller_page_background_description": "Lülita taustateenus sisse, et uusi üksuseid automaatselt varundada, ilma et peaks rakendust avama",
"backup_controller_page_background_is_off": "Automaatne varundamine on välja lülitatud",
"backup_controller_page_background_is_on": "Automaatne varundamine on sisse lülitatud",
"backup_controller_page_background_turn_off": "Lülita taustateenus välja",
"backup_controller_page_background_turn_on": "Lülita taustateenus sisse",
"backup_controller_page_background_wifi": "Ainult WiFi-võrgus",
"backup_controller_page_backup_sub": "Varundatud fotod ja videod",
"backup_controller_page_desc_backup": "Lülita sisse esiplaanil varundamine, et rakenduse avamisel uued üksused automaatselt serverisse üles laadida.",
"backup_controller_page_to_backup": "Albumid, mida varundada",
"backup_controller_page_total_sub": "Kõik unikaalsed fotod ja videod valitud albumitest",
"backup_err_only_album": "Ei saa ainsat albumit eemaldada",
"backup_info_card_assets": "üksused",
"backup_manual_cancelled": "Tühistatud",
"backup_manual_title": "Üleslaadimise staatus",
"backup_setting_subtitle": "Halda taustal ja esiplaanil üleslaadimise seadeid",
"backward": "Tagasi", "backward": "Tagasi",
"birthdate_saved": "Sünnikuupäev salvestatud", "birthdate_saved": "Sünnikuupäev salvestatud",
"birthdate_set_description": "Sünnikuupäeva kasutatakse isiku vanuse arvutamiseks foto tegemise hetkel.", "birthdate_set_description": "Sünnikuupäeva kasutatakse isiku vanuse arvutamiseks foto tegemise hetkel.",
@@ -451,11 +530,19 @@
"bulk_keep_duplicates_confirmation": "Kas oled kindel, et soovid {count, plural, one {# dubleeritud üksuse} other {# dubleeritud üksust}} alles jätta? Sellega märgitakse kõik duplikaadigrupid lahendatuks ilma midagi kustutamata.", "bulk_keep_duplicates_confirmation": "Kas oled kindel, et soovid {count, plural, one {# dubleeritud üksuse} other {# dubleeritud üksust}} alles jätta? Sellega märgitakse kõik duplikaadigrupid lahendatuks ilma midagi kustutamata.",
"bulk_trash_duplicates_confirmation": "Kas oled kindel, et soovid {count, plural, one {# dubleeritud üksuse} other {# dubleeritud üksust}} masskustutada? Sellega jäetakse alles iga grupi suurim üksus ning duplikaadid liigutatakse prügikasti.", "bulk_trash_duplicates_confirmation": "Kas oled kindel, et soovid {count, plural, one {# dubleeritud üksuse} other {# dubleeritud üksust}} masskustutada? Sellega jäetakse alles iga grupi suurim üksus ning duplikaadid liigutatakse prügikasti.",
"buy": "Osta Immich", "buy": "Osta Immich",
"cache_settings_clear_cache_button": "Tühjenda puhver",
"cache_settings_statistics_album": "Kogu pisipildid",
"cache_settings_statistics_full": "Täismõõdus pildid",
"cache_settings_statistics_shared": "Jagatud albumite pisipildid",
"cache_settings_statistics_thumbnail": "Pisipildid",
"cache_settings_statistics_title": "Puhvri kasutus",
"cache_settings_thumbnail_size": "Pisipiltide puhvri suurus ({} üksust)",
"camera": "Kaamera", "camera": "Kaamera",
"camera_brand": "Kaamera mark", "camera_brand": "Kaamera mark",
"camera_model": "Kaamera mudel", "camera_model": "Kaamera mudel",
"cancel": "Katkesta", "cancel": "Katkesta",
"cancel_search": "Katkesta otsing", "cancel_search": "Katkesta otsing",
"canceled": "Tühistatud",
"cannot_merge_people": "Ei saa isikuid ühendada", "cannot_merge_people": "Ei saa isikuid ühendada",
"cannot_undo_this_action": "Sa ei saa seda tagasi võtta!", "cannot_undo_this_action": "Sa ei saa seda tagasi võtta!",
"cannot_update_the_description": "Kirjelduse muutmine ebaõnnestus", "cannot_update_the_description": "Kirjelduse muutmine ebaõnnestus",
@@ -466,6 +553,10 @@
"change_name_successfully": "Nimi edukalt muudetud", "change_name_successfully": "Nimi edukalt muudetud",
"change_password": "Parooli muutmine", "change_password": "Parooli muutmine",
"change_password_description": "See on su esimene kord süsteemi siseneda, või on tehtud taotlus parooli muutmiseks. Palun sisesta allpool uus parool.", "change_password_description": "See on su esimene kord süsteemi siseneda, või on tehtud taotlus parooli muutmiseks. Palun sisesta allpool uus parool.",
"change_password_form_confirm_password": "Kinnita parool",
"change_password_form_new_password": "Uus parool",
"change_password_form_password_mismatch": "Paroolid ei klapi",
"change_password_form_reenter_new_password": "Korda uut parooli",
"change_your_password": "Muuda oma parooli", "change_your_password": "Muuda oma parooli",
"changed_visibility_successfully": "Nähtavus muudetud", "changed_visibility_successfully": "Nähtavus muudetud",
"check_all": "Märgi kõik", "check_all": "Märgi kõik",
@@ -477,6 +568,14 @@
"clear_all_recent_searches": "Tühjenda hiljutised otsingud", "clear_all_recent_searches": "Tühjenda hiljutised otsingud",
"clear_message": "Tühjenda sõnum", "clear_message": "Tühjenda sõnum",
"clear_value": "Tühjenda väärtus", "clear_value": "Tühjenda väärtus",
"client_cert_dialog_msg_confirm": "OK",
"client_cert_enter_password": "Sisesta parool",
"client_cert_import": "Impordi",
"client_cert_import_success_msg": "Klientsertifikaat on imporditud",
"client_cert_invalid_msg": "Vigane sertifikaadi fail või vale parool",
"client_cert_remove_msg": "Klientsertifikaat on eemaldatud",
"client_cert_subtitle": "Toetab ainult PKCS12 (.p12, .pfx) formaati. Sertifikaadi importimine/eemaldamine on saadaval ainult enne sisselogimist",
"client_cert_title": "SSL klientsertifikaat",
"clockwise": "Päripäeva", "clockwise": "Päripäeva",
"close": "Sulge", "close": "Sulge",
"collapse": "Peida", "collapse": "Peida",
@@ -487,6 +586,8 @@
"comment_options": "Kommentaari valikud", "comment_options": "Kommentaari valikud",
"comments_and_likes": "Kommentaarid ja meeldimised", "comments_and_likes": "Kommentaarid ja meeldimised",
"comments_are_disabled": "Kommentaarid on keelatud", "comments_are_disabled": "Kommentaarid on keelatud",
"common_create_new_album": "Lisa uus album",
"completed": "Lõpetatud",
"confirm": "Kinnita", "confirm": "Kinnita",
"confirm_admin_password": "Kinnita administraatori parool", "confirm_admin_password": "Kinnita administraatori parool",
"confirm_delete_face": "Kas oled kindel, et soovid isiku {name} näo üksuselt kustutada?", "confirm_delete_face": "Kas oled kindel, et soovid isiku {name} näo üksuselt kustutada?",
@@ -496,6 +597,10 @@
"contain": "Mahuta ära", "contain": "Mahuta ära",
"context": "Kontekst", "context": "Kontekst",
"continue": "Jätka", "continue": "Jätka",
"control_bottom_app_bar_create_new_album": "Lisa uus album",
"control_bottom_app_bar_delete_from_local": "Kustuta seadmest",
"control_bottom_app_bar_edit_location": "Muuda asukohta",
"control_bottom_app_bar_edit_time": "Muuda kuupäeva ja aega",
"copied_image_to_clipboard": "Pilt kopeeritud lõikelauale.", "copied_image_to_clipboard": "Pilt kopeeritud lõikelauale.",
"copied_to_clipboard": "Kopeeritud lõikelauale!", "copied_to_clipboard": "Kopeeritud lõikelauale!",
"copy_error": "Kopeeri viga", "copy_error": "Kopeeri viga",
@@ -517,6 +622,7 @@
"create_new_person": "Lisa uus isik", "create_new_person": "Lisa uus isik",
"create_new_person_hint": "Seosta valitud üksused uue isikuga", "create_new_person_hint": "Seosta valitud üksused uue isikuga",
"create_new_user": "Lisa uus kasutaja", "create_new_user": "Lisa uus kasutaja",
"create_shared_album_page_share_select_photos": "Vali fotod",
"create_tag": "Lisa silt", "create_tag": "Lisa silt",
"create_tag_description": "Lisa uus silt. Pesastatud siltide jaoks sisesta täielik tee koos kaldkriipsudega.", "create_tag_description": "Lisa uus silt. Pesastatud siltide jaoks sisesta täielik tee koos kaldkriipsudega.",
"create_user": "Lisa kasutaja", "create_user": "Lisa kasutaja",
@@ -541,19 +647,23 @@
"delete": "Kustuta", "delete": "Kustuta",
"delete_album": "Kustuta album", "delete_album": "Kustuta album",
"delete_api_key_prompt": "Kas oled kindel, et soovid selle API võtme kustutada?", "delete_api_key_prompt": "Kas oled kindel, et soovid selle API võtme kustutada?",
"delete_dialog_title": "Kustuta jäädavalt",
"delete_duplicates_confirmation": "Kas oled kindel, et soovid need duplikaadid jäädavalt kustutada?", "delete_duplicates_confirmation": "Kas oled kindel, et soovid need duplikaadid jäädavalt kustutada?",
"delete_face": "Kustuta nägu", "delete_face": "Kustuta nägu",
"delete_key": "Kustuta võti", "delete_key": "Kustuta võti",
"delete_library": "Kustuta kogu", "delete_library": "Kustuta kogu",
"delete_link": "Kustuta link", "delete_link": "Kustuta link",
"delete_local_dialog_ok_backed_up_only": "Kustuta ainult varundatud",
"delete_others": "Kustuta teised", "delete_others": "Kustuta teised",
"delete_shared_link": "Kustuta jagatud link", "delete_shared_link": "Kustuta jagatud link",
"delete_shared_link_dialog_title": "Kustuta jagatud link",
"delete_tag": "Kustuta silt", "delete_tag": "Kustuta silt",
"delete_tag_confirmation_prompt": "Kas oled kindel, et soovid sildi {tagName} kustutada?", "delete_tag_confirmation_prompt": "Kas oled kindel, et soovid sildi {tagName} kustutada?",
"delete_user": "Kustuta kasutaja", "delete_user": "Kustuta kasutaja",
"deleted_shared_link": "Jagatud link kustutatud", "deleted_shared_link": "Jagatud link kustutatud",
"deletes_missing_assets": "Kustutab üksused, mis on kettalt puudu", "deletes_missing_assets": "Kustutab üksused, mis on kettalt puudu",
"description": "Kirjeldus", "description": "Kirjeldus",
"description_input_hint_text": "Lisa kirjeldus...",
"details": "Üksikasjad", "details": "Üksikasjad",
"direction": "Suund", "direction": "Suund",
"disabled": "Välja lülitatud", "disabled": "Välja lülitatud",
@@ -570,10 +680,20 @@
"documentation": "Dokumentatsioon", "documentation": "Dokumentatsioon",
"done": "Tehtud", "done": "Tehtud",
"download": "Laadi alla", "download": "Laadi alla",
"download_canceled": "Allalaadimine katkestatud",
"download_complete": "Allalaadimine lõpetatud",
"download_enqueue": "Allalaadimine ootel",
"download_error": "Allalaadimise viga",
"download_failed": "Allalaadimine ebaõnnestus",
"download_finished": "Allalaadimine lõpetatud",
"download_include_embedded_motion_videos": "Manustatud videod", "download_include_embedded_motion_videos": "Manustatud videod",
"download_include_embedded_motion_videos_description": "Lisa liikuvatesse fotodesse manustatud videod eraldi failidena", "download_include_embedded_motion_videos_description": "Lisa liikuvatesse fotodesse manustatud videod eraldi failidena",
"download_paused": "Allalaadimine peatatud",
"download_settings": "Allalaadimine", "download_settings": "Allalaadimine",
"download_settings_description": "Halda üksuste allalaadimise seadeid", "download_settings_description": "Halda üksuste allalaadimise seadeid",
"download_started": "Allalaadimine alustatud",
"download_sucess": "Allalaadimine õnnestus",
"download_sucess_android": "Meediumid laaditi alla kataloogi DCIM/Immich",
"downloading": "Allalaadimine", "downloading": "Allalaadimine",
"downloading_asset_filename": "Üksuse {filename} allalaadimine", "downloading_asset_filename": "Üksuse {filename} allalaadimine",
"drop_files_to_upload": "Failide üleslaadimiseks sikuta need ükskõik kuhu", "drop_files_to_upload": "Failide üleslaadimiseks sikuta need ükskõik kuhu",
@@ -592,6 +712,7 @@
"edit_key": "Muuda võtit", "edit_key": "Muuda võtit",
"edit_link": "Muuda linki", "edit_link": "Muuda linki",
"edit_location": "Muuda asukohta", "edit_location": "Muuda asukohta",
"edit_location_dialog_title": "Asukoht",
"edit_name": "Muuda nime", "edit_name": "Muuda nime",
"edit_people": "Muuda isikuid", "edit_people": "Muuda isikuid",
"edit_tag": "Muuda silti", "edit_tag": "Muuda silti",
@@ -604,12 +725,15 @@
"editor_crop_tool_h2_aspect_ratios": "Kuvasuhted", "editor_crop_tool_h2_aspect_ratios": "Kuvasuhted",
"editor_crop_tool_h2_rotation": "Pööre", "editor_crop_tool_h2_rotation": "Pööre",
"email": "E-post", "email": "E-post",
"empty_folder": "See kaust on tühi",
"empty_trash": "Tühjenda prügikast", "empty_trash": "Tühjenda prügikast",
"empty_trash_confirmation": "Kas oled kindel, et soovid prügikasti tühjendada? See eemaldab kõik seal olevad üksused Immich'ist jäädavalt.\nSeda tegevust ei saa tagasi võtta!", "empty_trash_confirmation": "Kas oled kindel, et soovid prügikasti tühjendada? See eemaldab kõik seal olevad üksused Immich'ist jäädavalt.\nSeda tegevust ei saa tagasi võtta!",
"enable": "Luba", "enable": "Luba",
"enabled": "Lubatud", "enabled": "Lubatud",
"end_date": "Lõppkuupäev", "end_date": "Lõppkuupäev",
"enter_wifi_name": "Sisesta WiFi-võrgu nimi",
"error": "Viga", "error": "Viga",
"error_change_sort_album": "Albumi sorteerimisjärjestuse muutmine ebaõnnestus",
"error_delete_face": "Viga näo kustutamisel", "error_delete_face": "Viga näo kustutamisel",
"error_loading_image": "Viga pildi laadimisel", "error_loading_image": "Viga pildi laadimisel",
"error_title": "Viga - midagi läks valesti", "error_title": "Viga - midagi läks valesti",
@@ -641,10 +765,12 @@
"failed_to_keep_this_delete_others": "Selle üksuse säilitamine ja ülejäänute kustutamine ebaõnnestus", "failed_to_keep_this_delete_others": "Selle üksuse säilitamine ja ülejäänute kustutamine ebaõnnestus",
"failed_to_load_asset": "Üksuse laadimine ebaõnnestus", "failed_to_load_asset": "Üksuse laadimine ebaõnnestus",
"failed_to_load_assets": "Üksuste laadimine ebaõnnestus", "failed_to_load_assets": "Üksuste laadimine ebaõnnestus",
"failed_to_load_notifications": "Teavituste laadimine ebaõnnestus",
"failed_to_load_people": "Isikute laadimine ebaõnnestus", "failed_to_load_people": "Isikute laadimine ebaõnnestus",
"failed_to_remove_product_key": "Tootevõtme eemaldamine ebaõnnestus", "failed_to_remove_product_key": "Tootevõtme eemaldamine ebaõnnestus",
"failed_to_stack_assets": "Üksuste virnastamine ebaõnnestus", "failed_to_stack_assets": "Üksuste virnastamine ebaõnnestus",
"failed_to_unstack_assets": "Üksuste eraldamine ebaõnnestus", "failed_to_unstack_assets": "Üksuste eraldamine ebaõnnestus",
"failed_to_update_notification_status": "Teavituste seisundi uuendamine ebaõnnestus",
"import_path_already_exists": "See imporditee on juba olemas.", "import_path_already_exists": "See imporditee on juba olemas.",
"incorrect_email_or_password": "Vale e-posti aadress või parool", "incorrect_email_or_password": "Vale e-posti aadress või parool",
"paths_validation_failed": "{paths, plural, one {# tee} other {# teed}} ei valideerunud", "paths_validation_failed": "{paths, plural, one {# tee} other {# teed}} ei valideerunud",
@@ -740,8 +866,14 @@
"unable_to_upload_file": "Faili üleslaadimine ebaõnnestus" "unable_to_upload_file": "Faili üleslaadimine ebaõnnestus"
}, },
"exif": "Exif", "exif": "Exif",
"exif_bottom_sheet_description": "Lisa kirjeldus...",
"exif_bottom_sheet_details": "ÜKSIKASJAD",
"exif_bottom_sheet_location": "ASUKOHT",
"exif_bottom_sheet_people": "ISIKUD",
"exif_bottom_sheet_person_add_person": "Lisa nimi",
"exit_slideshow": "Sulge slaidiesitlus", "exit_slideshow": "Sulge slaidiesitlus",
"expand_all": "Näita kõik", "expand_all": "Näita kõik",
"experimental_settings_title": "Eksperimentaalne",
"expire_after": "Aegub", "expire_after": "Aegub",
"expired": "Aegunud", "expired": "Aegunud",
"expires_date": "Aegub {date}", "expires_date": "Aegub {date}",
@@ -752,6 +884,7 @@
"extension": "Laiend", "extension": "Laiend",
"external": "Väline", "external": "Väline",
"external_libraries": "Välised kogud", "external_libraries": "Välised kogud",
"external_network_sheet_info": "Kui seade ei ole eelistatud WiFi-võrgus, ühendub rakendus serveriga allolevatest URL-idest esimese kättesaadava kaudu, alustades ülevalt",
"face_unassigned": "Seostamata", "face_unassigned": "Seostamata",
"failed_to_load_assets": "Üksuste laadimine ebaõnnestus", "failed_to_load_assets": "Üksuste laadimine ebaõnnestus",
"favorite": "Lemmik", "favorite": "Lemmik",
@@ -767,6 +900,8 @@
"filter_people": "Filtreeri isikuid", "filter_people": "Filtreeri isikuid",
"find_them_fast": "Leia teda kiiresti nime järgi otsides", "find_them_fast": "Leia teda kiiresti nime järgi otsides",
"fix_incorrect_match": "Paranda ebaõige vaste", "fix_incorrect_match": "Paranda ebaõige vaste",
"folder": "Kaust",
"folder_not_found": "Kausta ei leitud",
"folders": "Kaustad", "folders": "Kaustad",
"folders_feature_description": "Kaustavaate abil failisüsteemis olevate fotode ja videote sirvimine", "folders_feature_description": "Kaustavaate abil failisüsteemis olevate fotode ja videote sirvimine",
"forward": "Edasi", "forward": "Edasi",
@@ -782,7 +917,15 @@
"group_owner": "Grupeeri omaniku kaupa", "group_owner": "Grupeeri omaniku kaupa",
"group_places_by": "Grupeeri kohad...", "group_places_by": "Grupeeri kohad...",
"group_year": "Grupeeri aasta kaupa", "group_year": "Grupeeri aasta kaupa",
"haptic_feedback_switch": "Luba haptiline tagasiside",
"haptic_feedback_title": "Haptiline tagasiside",
"has_quota": "On kvoot", "has_quota": "On kvoot",
"header_settings_add_header_tip": "Lisa päis",
"header_settings_field_validator_msg": "Väärtus ei saa olla tühi",
"header_settings_header_name_input": "Päise nimi",
"header_settings_header_value_input": "Päise väärtus",
"headers_settings_tile_subtitle": "Määra vaheserveri päised, mida rakendus peaks iga päringuga saatma",
"headers_settings_tile_title": "Kohandatud vaheserveri päised",
"hi_user": "Tere {name} ({email})", "hi_user": "Tere {name} ({email})",
"hide_all_people": "Peida kõik isikud", "hide_all_people": "Peida kõik isikud",
"hide_gallery": "Peida galerii", "hide_gallery": "Peida galerii",
@@ -790,8 +933,20 @@
"hide_password": "Peida parool", "hide_password": "Peida parool",
"hide_person": "Peida isik", "hide_person": "Peida isik",
"hide_unnamed_people": "Peida nimetud isikud", "hide_unnamed_people": "Peida nimetud isikud",
"home_page_add_to_album_conflicts": "{added} üksust lisati albumisse {album}. {failed} üksust oli juba albumis.",
"home_page_add_to_album_err_local": "Lokaalseid üksuseid ei saa veel albumisse lisada, jätan vahele",
"home_page_add_to_album_success": "{added} üksust lisati albumisse {album}.",
"home_page_album_err_partner": "Partneri üksuseid ei saa veel albumisse lisada, jätan vahele",
"home_page_archive_err_local": "Lokaalseid üksuseid ei saa veel arhiveerida, jätan vahele",
"home_page_archive_err_partner": "Partneri üksuseid ei saa arhiveerida, jätan vahele",
"home_page_building_timeline": "Ajajoone koostamine",
"home_page_delete_err_partner": "Partneri üksuseid ei saa kustutada, jätan vahele",
"home_page_favorite_err_local": "Lokaalseid üksuseid ei saa lemmikuks märkida, jätan vahele",
"home_page_favorite_err_partner": "Partneri üksuseid ei saa lemmikuks märkida, jätan vahele",
"home_page_share_err_local": "Lokaalseid üksuseid ei saa lingiga jagada, jätan vahele",
"host": "Host", "host": "Host",
"hour": "Tund", "hour": "Tund",
"ignore_icloud_photos": "Ignoreeri iCloud fotosid",
"image": "Pilt", "image": "Pilt",
"image_alt_text_date": "{isVideo, select, true {Video} other {Pilt}} tehtud {date}", "image_alt_text_date": "{isVideo, select, true {Video} other {Pilt}} tehtud {date}",
"image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} koos isikuga {person1}", "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} koos isikuga {person1}",
@@ -803,6 +958,8 @@
"image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} kohas {city}, {country} koos isikutega {person1} ja {person2}", "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} kohas {city}, {country} koos isikutega {person1} ja {person2}",
"image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} kohas {city}, {country} koos isikutega {person1}, {person2} ja {person3}", "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} kohas {city}, {country} koos isikutega {person1}, {person2} ja {person3}",
"image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} kohas {city}, {country} koos {person1}, {person2} ja veel {additionalCount, number} isikuga", "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Pilt}} tehtud {date} kohas {city}, {country} koos {person1}, {person2} ja veel {additionalCount, number} isikuga",
"image_viewer_page_state_provider_download_started": "Allalaadimine alustatud",
"image_viewer_page_state_provider_download_success": "Allalaadimine õnnestus",
"immich_logo": "Immich'i logo", "immich_logo": "Immich'i logo",
"immich_web_interface": "Immich'i veebiliides", "immich_web_interface": "Immich'i veebiliides",
"import_from_json": "Impordi JSON-formaadist", "import_from_json": "Impordi JSON-formaadist",
@@ -821,6 +978,8 @@
"night_at_midnight": "Iga päev keskööl", "night_at_midnight": "Iga päev keskööl",
"night_at_twoam": "Iga öö kell 2" "night_at_twoam": "Iga öö kell 2"
}, },
"invalid_date": "Vigane kuupäev",
"invalid_date_format": "Vigane kuupäevaformaat",
"invite_people": "Kutsu inimesi", "invite_people": "Kutsu inimesi",
"invite_to_album": "Kutsu albumisse", "invite_to_album": "Kutsu albumisse",
"items_count": "{count, plural, one {# üksus} other {# üksust}}", "items_count": "{count, plural, one {# üksus} other {# üksust}}",
@@ -841,6 +1000,9 @@
"level": "Tase", "level": "Tase",
"library": "Kogu", "library": "Kogu",
"library_options": "Kogu seaded", "library_options": "Kogu seaded",
"library_page_new_album": "Uus album",
"library_page_sort_asset_count": "Üksuste arv",
"library_page_sort_title": "Albumi pealkiri",
"light": "Hele", "light": "Hele",
"like_deleted": "Meeldimine kustutatud", "like_deleted": "Meeldimine kustutatud",
"link_motion_video": "Lingi liikuv video", "link_motion_video": "Lingi liikuv video",
@@ -850,12 +1012,29 @@
"list": "Loend", "list": "Loend",
"loading": "Laadimine", "loading": "Laadimine",
"loading_search_results_failed": "Otsitulemuste laadimine ebaõnnestus", "loading_search_results_failed": "Otsitulemuste laadimine ebaõnnestus",
"local_network_sheet_info": "Rakendus ühendub valitud Wi-Fi võrgus olles serveriga selle URL-i kaudu",
"location_permission_content": "Automaatseks ümberlülitumiseks vajab Immich täpse asukoha luba, et saaks lugeda aktiivse WiFi-võrgu nime",
"location_picker_choose_on_map": "Vali kaardil",
"log_out": "Logi välja", "log_out": "Logi välja",
"log_out_all_devices": "Logi kõigist seadmetest välja", "log_out_all_devices": "Logi kõigist seadmetest välja",
"logged_out_all_devices": "Kõigist seadmetest välja logitud", "logged_out_all_devices": "Kõigist seadmetest välja logitud",
"logged_out_device": "Seadmest välja logitud", "logged_out_device": "Seadmest välja logitud",
"login": "Logi sisse", "login": "Logi sisse",
"login_form_back_button_text": "Tagasi",
"login_form_email_hint": "sinunimi@email.com",
"login_form_endpoint_hint": "http://serveri-ip:port",
"login_form_endpoint_url": "Serveri lõpp-punkti URL",
"login_form_err_http": "Palun täpsusta http:// või https://",
"login_form_err_invalid_email": "Vigane e-posti aadress",
"login_form_err_invalid_url": "Vigane URL",
"login_form_err_leading_whitespace": "Eelnevad tühikud",
"login_form_err_trailing_whitespace": "Järgnevad tühikud",
"login_form_password_hint": "parool",
"login_form_save_login": "Jää sisselogituks",
"login_form_server_empty": "Sisesta serveri URL.",
"login_form_server_error": "Serveriga ühendumine ebaõnnestus.",
"login_has_been_disabled": "Sisselogimine on keelatud.", "login_has_been_disabled": "Sisselogimine on keelatud.",
"login_password_changed_success": "Parool edukalt uuendatud",
"logout_all_device_confirmation": "Kas oled kindel, et soovid kõigist seadmetest välja logida?", "logout_all_device_confirmation": "Kas oled kindel, et soovid kõigist seadmetest välja logida?",
"logout_this_device_confirmation": "Kas oled kindel, et soovid sellest seadmest välja logida?", "logout_this_device_confirmation": "Kas oled kindel, et soovid sellest seadmest välja logida?",
"longitude": "Pikkuskraad", "longitude": "Pikkuskraad",
@@ -873,13 +1052,27 @@
"manage_your_devices": "Halda oma autenditud seadmeid", "manage_your_devices": "Halda oma autenditud seadmeid",
"manage_your_oauth_connection": "Halda oma OAuth ühendust", "manage_your_oauth_connection": "Halda oma OAuth ühendust",
"map": "Kaart", "map": "Kaart",
"map_assets_in_bound": "{} foto",
"map_assets_in_bounds": "{} fotot",
"map_location_dialog_yes": "Jah",
"map_location_picker_page_use_location": "Kasuta seda asukohta",
"map_marker_for_images": "Kaardimarker kohas {city}, {country} tehtud piltide jaoks", "map_marker_for_images": "Kaardimarker kohas {city}, {country} tehtud piltide jaoks",
"map_marker_with_image": "Kaardimarker pildiga", "map_marker_with_image": "Kaardimarker pildiga",
"map_settings": "Kaardi seaded", "map_settings": "Kaardi seaded",
"map_settings_date_range_option_day": "Viimased 24 tundi",
"map_settings_date_range_option_days": "Viimased {} päeva",
"map_settings_date_range_option_year": "Viimane aasta",
"map_settings_date_range_option_years": "Viimased {} aastat",
"map_settings_dialog_title": "Kaardi seaded",
"mark_all_as_read": "Märgi kõik loetuks",
"mark_as_read": "Märgi loetuks",
"marked_all_as_read": "Kõik märgiti loetuks",
"matches": "Ühtivad failid", "matches": "Ühtivad failid",
"media_type": "Meedia tüüp", "media_type": "Meediumi tüüp",
"memories": "Mälestused", "memories": "Mälestused",
"memories_setting_description": "Halda, mida sa oma mälestustes näed", "memories_setting_description": "Halda, mida sa oma mälestustes näed",
"memories_year_ago": "Aasta tagasi",
"memories_years_ago": "{} aastat tagasi",
"memory": "Mälestus", "memory": "Mälestus",
"memory_lane_title": "Mälestus {title}", "memory_lane_title": "Mälestus {title}",
"menu": "Menüü", "menu": "Menüü",
@@ -894,12 +1087,19 @@
"missing": "Puuduvad", "missing": "Puuduvad",
"model": "Mudel", "model": "Mudel",
"month": "Kuu", "month": "Kuu",
"monthly_title_text_date_format": "MMMM y",
"more": "Rohkem", "more": "Rohkem",
"moved_to_archive": "{count, plural, one {# üksus} other {# üksust}} liigutatud arhiivi",
"moved_to_library": "{count, plural, one {# üksus} other {# üksust}} liigutatud kogusse",
"moved_to_trash": "Liigutatud prügikasti", "moved_to_trash": "Liigutatud prügikasti",
"multiselect_grid_edit_date_time_err_read_only": "Kirjutuskaitsega üksus(t)e kuupäeva ei saa muuta, jätan vahele",
"multiselect_grid_edit_gps_err_read_only": "Kirjutuskaitsega üksus(t)e asukohta ei saa muuta, jätan vahele",
"mute_memories": "Vaigista mälestused", "mute_memories": "Vaigista mälestused",
"my_albums": "Minu albumid", "my_albums": "Minu albumid",
"name": "Nimi", "name": "Nimi",
"name_or_nickname": "Nimi või hüüdnimi", "name_or_nickname": "Nimi või hüüdnimi",
"networking_settings": "Võrguühendus",
"networking_subtitle": "Halda serveri lõpp-punkti seadeid",
"never": "Mitte kunagi", "never": "Mitte kunagi",
"new_album": "Uus album", "new_album": "Uus album",
"new_api_key": "Uus API võti", "new_api_key": "Uus API võti",
@@ -922,6 +1122,7 @@
"no_favorites_message": "Lisa lemmikud, et oma parimaid fotosid ja videosid kiiresti leida", "no_favorites_message": "Lisa lemmikud, et oma parimaid fotosid ja videosid kiiresti leida",
"no_libraries_message": "Lisa väline kogu oma fotode ja videote vaatamiseks", "no_libraries_message": "Lisa väline kogu oma fotode ja videote vaatamiseks",
"no_name": "Nimetu", "no_name": "Nimetu",
"no_notifications": "Teavitusi pole",
"no_places": "Kohti ei ole", "no_places": "Kohti ei ole",
"no_results": "Vasteid pole", "no_results": "Vasteid pole",
"no_results_description": "Proovi sünonüümi või üldisemat märksõna", "no_results_description": "Proovi sünonüümi või üldisemat märksõna",
@@ -929,6 +1130,9 @@
"not_in_any_album": "Pole üheski albumis", "not_in_any_album": "Pole üheski albumis",
"note_apply_storage_label_to_previously_uploaded assets": "Märkus: Et rakendada talletussilt varem üleslaaditud üksustele, käivita", "note_apply_storage_label_to_previously_uploaded assets": "Märkus: Et rakendada talletussilt varem üleslaaditud üksustele, käivita",
"notes": "Märkused", "notes": "Märkused",
"notification_permission_list_tile_content": "Anna luba teavituste saatmiseks.",
"notification_permission_list_tile_enable_button": "Luba teavitused",
"notification_permission_list_tile_title": "Teavituste luba",
"notification_toggle_setting_description": "Luba e-posti teel teavitused", "notification_toggle_setting_description": "Luba e-posti teel teavitused",
"notifications": "Teavitused", "notifications": "Teavitused",
"notifications_setting_description": "Halda teavitusi", "notifications_setting_description": "Halda teavitusi",
@@ -939,6 +1143,7 @@
"offline_paths_description": "Need tulemused võivad olla põhjustatud manuaalselt kustutatud failidest, mis ei ole osa välisest kogust.", "offline_paths_description": "Need tulemused võivad olla põhjustatud manuaalselt kustutatud failidest, mis ei ole osa välisest kogust.",
"ok": "Ok", "ok": "Ok",
"oldest_first": "Vanemad eespool", "oldest_first": "Vanemad eespool",
"on_this_device": "Sellel seadmel",
"onboarding": "Kasutuselevõtt", "onboarding": "Kasutuselevõtt",
"onboarding_privacy_description": "Järgnevad (valikulised) funktsioonid sõltuvad välistest teenustest ning neid saab igal ajal administraatori seadetes välja lülitada.", "onboarding_privacy_description": "Järgnevad (valikulised) funktsioonid sõltuvad välistest teenustest ning neid saab igal ajal administraatori seadetes välja lülitada.",
"onboarding_theme_description": "Vali oma serverile värviteema. Saad seda hiljem seadetes muuta.", "onboarding_theme_description": "Vali oma serverile värviteema. Saad seda hiljem seadetes muuta.",
@@ -946,6 +1151,7 @@
"onboarding_welcome_user": "Tere tulemast, {user}", "onboarding_welcome_user": "Tere tulemast, {user}",
"online": "Ühendatud", "online": "Ühendatud",
"only_favorites": "Ainult lemmikud", "only_favorites": "Ainult lemmikud",
"open": "Ava",
"open_in_map_view": "Ava kaardi vaates", "open_in_map_view": "Ava kaardi vaates",
"open_in_openstreetmap": "Ava OpenStreetMap", "open_in_openstreetmap": "Ava OpenStreetMap",
"open_the_search_filters": "Ava otsingufiltrid", "open_the_search_filters": "Ava otsingufiltrid",
@@ -962,6 +1168,10 @@
"partner_can_access": "{partner} pääseb ligi", "partner_can_access": "{partner} pääseb ligi",
"partner_can_access_assets": "Kõik su fotod ja videod, välja arvatud arhiveeritud ja kustutatud", "partner_can_access_assets": "Kõik su fotod ja videod, välja arvatud arhiveeritud ja kustutatud",
"partner_can_access_location": "Asukohad, kus su fotod tehti", "partner_can_access_location": "Asukohad, kus su fotod tehti",
"partner_list_user_photos": "Kasutaja {user} fotod",
"partner_list_view_all": "Vaata kõiki",
"partner_page_partner_add_failed": "Partneri lisamine ebaõnnestus",
"partner_page_select_partner": "Vali partner",
"partner_sharing": "Partneriga jagamine", "partner_sharing": "Partneriga jagamine",
"partners": "Partnerid", "partners": "Partnerid",
"password": "Parool", "password": "Parool",
@@ -990,6 +1200,8 @@
"permanently_delete_assets_prompt": "Kas oled kindel, et soovid {count, plural, one {selle üksuse} other {need <b>#</b> üksust}} jäädavalt kustutada? Sellega eemaldatakse {count, plural, one {see} other {need}} ka oma albumi(te)st.", "permanently_delete_assets_prompt": "Kas oled kindel, et soovid {count, plural, one {selle üksuse} other {need <b>#</b> üksust}} jäädavalt kustutada? Sellega eemaldatakse {count, plural, one {see} other {need}} ka oma albumi(te)st.",
"permanently_deleted_asset": "Üksus jäädavalt kustutatud", "permanently_deleted_asset": "Üksus jäädavalt kustutatud",
"permanently_deleted_assets_count": "{count, plural, one {# üksus} other {# üksust}} jäädavalt kustutatud", "permanently_deleted_assets_count": "{count, plural, one {# üksus} other {# üksust}} jäädavalt kustutatud",
"permission_onboarding_back": "Tagasi",
"permission_onboarding_continue_anyway": "Jätka sellegipoolest",
"person": "Isik", "person": "Isik",
"person_birthdate": "Sündinud {date}", "person_birthdate": "Sündinud {date}",
"person_hidden": "{name}{hidden, select, true { (peidetud)} other {}}", "person_hidden": "{name}{hidden, select, true { (peidetud)} other {}}",
@@ -1007,6 +1219,8 @@
"play_motion_photo": "Esita liikuv foto", "play_motion_photo": "Esita liikuv foto",
"play_or_pause_video": "Esita või peata video", "play_or_pause_video": "Esita või peata video",
"port": "Port", "port": "Port",
"preferences_settings_subtitle": "Halda rakenduse eelistusi",
"preferences_settings_title": "Eelistused",
"preset": "Eelseadistus", "preset": "Eelseadistus",
"preview": "Eelvaade", "preview": "Eelvaade",
"previous": "Eelmine", "previous": "Eelmine",
@@ -1014,6 +1228,8 @@
"previous_or_next_photo": "Eelmine või järgmine foto", "previous_or_next_photo": "Eelmine või järgmine foto",
"primary": "Peamine", "primary": "Peamine",
"privacy": "Privaatsus", "privacy": "Privaatsus",
"profile_drawer_app_logs": "Logid",
"profile_drawer_github": "GitHub",
"profile_image_of_user": "Kasutaja {user} profiilipilt", "profile_image_of_user": "Kasutaja {user} profiilipilt",
"profile_picture_set": "Profiilipilt määratud.", "profile_picture_set": "Profiilipilt määratud.",
"public_album": "Avalik album", "public_album": "Avalik album",
@@ -1063,6 +1279,8 @@
"recent": "Hiljutine", "recent": "Hiljutine",
"recent-albums": "Hiljutised albumid", "recent-albums": "Hiljutised albumid",
"recent_searches": "Hiljutised otsingud", "recent_searches": "Hiljutised otsingud",
"recently_added": "Hiljuti lisatud",
"recently_added_page_title": "Hiljuti lisatud",
"refresh": "Värskenda", "refresh": "Värskenda",
"refresh_encoded_videos": "Värskenda kodeeritud videod", "refresh_encoded_videos": "Värskenda kodeeritud videod",
"refresh_faces": "Värskenda näod", "refresh_faces": "Värskenda näod",
@@ -1119,6 +1337,7 @@
"role_editor": "Muutja", "role_editor": "Muutja",
"role_viewer": "Vaataja", "role_viewer": "Vaataja",
"save": "Salvesta", "save": "Salvesta",
"save_to_gallery": "Salvesta galeriisse",
"saved_api_key": "API võti salvestatud", "saved_api_key": "API võti salvestatud",
"saved_profile": "Profiil salvestatud", "saved_profile": "Profiil salvestatud",
"saved_settings": "Seaded salvestatud", "saved_settings": "Seaded salvestatud",
@@ -1138,14 +1357,33 @@
"search_camera_model": "Otsi kaamera mudelit...", "search_camera_model": "Otsi kaamera mudelit...",
"search_city": "Otsi linna...", "search_city": "Otsi linna...",
"search_country": "Otsi riiki...", "search_country": "Otsi riiki...",
"search_filter_apply": "Rakenda filter",
"search_filter_camera_title": "Vali kaamera tüüp",
"search_filter_date": "Kuupäev",
"search_filter_date_interval": "{start} kuni {end}",
"search_filter_date_title": "Vali kuupäevavahemik",
"search_filter_display_options": "Kuva valikud",
"search_filter_filename": "Otsi failinime alusel",
"search_filter_location": "Asukoht",
"search_filter_location_title": "Vali asukoht",
"search_filter_media_type": "Meediumi tüüp",
"search_filter_media_type_title": "Vali meediumi tüüp",
"search_filter_people_title": "Vali isikud",
"search_for": "Otsi", "search_for": "Otsi",
"search_for_existing_person": "Otsi olemasolevat isikut", "search_for_existing_person": "Otsi olemasolevat isikut",
"search_no_people": "Isikuid ei ole", "search_no_people": "Isikuid ei ole",
"search_no_people_named": "Ei ole isikuid nimega \"{name}\"", "search_no_people_named": "Ei ole isikuid nimega \"{name}\"",
"search_options": "Otsingu valikud", "search_options": "Otsingu valikud",
"search_page_categories": "Kategooriad",
"search_page_screenshots": "Ekraanipildid",
"search_page_search_photos_videos": "Otsi oma fotosid ja videosid",
"search_page_selfies": "Selfid",
"search_page_things": "Asjad",
"search_page_view_all_button": "Vaata kõiki",
"search_people": "Otsi inimesi", "search_people": "Otsi inimesi",
"search_places": "Otsi kohti", "search_places": "Otsi kohti",
"search_rating": "Otsi hinnangu järgi...", "search_rating": "Otsi hinnangu järgi...",
"search_result_page_new_search_hint": "Uus otsing",
"search_settings": "Otsi seadeid", "search_settings": "Otsi seadeid",
"search_state": "Otsi osariiki...", "search_state": "Otsi osariiki...",
"search_tags": "Otsi silte...", "search_tags": "Otsi silte...",
@@ -1166,12 +1404,17 @@
"select_keep_all": "Vali jäta kõik alles", "select_keep_all": "Vali jäta kõik alles",
"select_library_owner": "Vali kogu omanik", "select_library_owner": "Vali kogu omanik",
"select_new_face": "Vali uus nägu", "select_new_face": "Vali uus nägu",
"select_person_to_tag": "Vali sildistamiseks isik",
"select_photos": "Vali fotod", "select_photos": "Vali fotod",
"select_trash_all": "Vali kõik prügikasti", "select_trash_all": "Vali kõik prügikasti",
"select_user_for_sharing_page_err_album": "Albumi lisamine ebaõnnestus",
"selected": "Valitud", "selected": "Valitud",
"selected_count": "{count, plural, other {# valitud}}", "selected_count": "{count, plural, other {# valitud}}",
"send_message": "Saada sõnum", "send_message": "Saada sõnum",
"send_welcome_email": "Saada tervituskiri", "send_welcome_email": "Saada tervituskiri",
"server_endpoint": "Serveri lõpp-punkt",
"server_info_box_app_version": "Rakenduse versioon",
"server_info_box_server_url": "Serveri URL",
"server_offline": "Serveriga ühendus puudub", "server_offline": "Serveriga ühendus puudub",
"server_online": "Server ühendatud", "server_online": "Server ühendatud",
"server_stats": "Serveri statistika", "server_stats": "Serveri statistika",
@@ -1183,22 +1426,70 @@
"set_date_of_birth": "Määra sünnikuupäev", "set_date_of_birth": "Määra sünnikuupäev",
"set_profile_picture": "Sea profiilipilt", "set_profile_picture": "Sea profiilipilt",
"set_slideshow_to_fullscreen": "Kuva slaidiesitlus täisekraanil", "set_slideshow_to_fullscreen": "Kuva slaidiesitlus täisekraanil",
"setting_image_viewer_help": "Detailivaatur laadib kõigepealt väikese pisipildi, seejärel keskmises mõõdus eelvaate (kui lubatud) ja lõpuks originaalpildi (kui lubatud).",
"setting_image_viewer_preview_subtitle": "Luba keskmise resolutsiooniga pildi laadimine. Keela, et laadida kohe originaalpilt või kasutada ainult pisipilti.",
"setting_image_viewer_preview_title": "Laadi pildi eelvaade",
"setting_image_viewer_title": "Pildid",
"setting_languages_apply": "Rakenda",
"setting_languages_subtitle": "Muuda rakenduse keelt",
"setting_languages_title": "Keeled",
"setting_notifications_notify_hours": "{} tundi",
"setting_notifications_notify_immediately": "kohe",
"setting_notifications_notify_minutes": "{} minutit",
"setting_notifications_notify_never": "mitte kunagi",
"setting_notifications_notify_seconds": "{} sekundit",
"setting_notifications_single_progress_title": "Kuva taustal varundamise detailset edenemist",
"setting_notifications_subtitle": "Halda oma teavituste eelistusi",
"setting_notifications_total_progress_title": "Kuva taustal varundamise üldist edenemist",
"settings": "Seaded", "settings": "Seaded",
"settings_saved": "Seaded salvestatud", "settings_saved": "Seaded salvestatud",
"share": "Jaga", "share": "Jaga",
"share_add_photos": "Lisa fotosid",
"share_assets_selected": "{} valitud",
"shared": "Jagatud", "shared": "Jagatud",
"shared_album_section_people_action_error": "Viga albumist eemaldamisel/lahkumisel",
"shared_album_section_people_action_leave": "Eemalda kasutaja albumist",
"shared_album_section_people_action_remove_user": "Eemalda kasutaja albumist",
"shared_album_section_people_title": "ISIKUD",
"shared_by": "Jagas", "shared_by": "Jagas",
"shared_by_user": "Jagas {user}", "shared_by_user": "Jagas {user}",
"shared_by_you": "Jagasid sina", "shared_by_you": "Jagasid sina",
"shared_from_partner": "Fotod partnerilt {partner}", "shared_from_partner": "Fotod partnerilt {partner}",
"shared_link_app_bar_title": "Jagatud lingid",
"shared_link_clipboard_copied_massage": "Kopeeritud lõikelauale",
"shared_link_clipboard_text": "Link: {}\nParool: {}",
"shared_link_create_error": "Viga jagatud lingi loomisel",
"shared_link_edit_expire_after_option_day": "1 päev",
"shared_link_edit_expire_after_option_days": "{} päeva",
"shared_link_edit_expire_after_option_hour": "1 tund",
"shared_link_edit_expire_after_option_hours": "{} tundi",
"shared_link_edit_expire_after_option_minute": "1 minut",
"shared_link_edit_expire_after_option_minutes": "{} minutit",
"shared_link_edit_expire_after_option_months": "{} kuud",
"shared_link_edit_expire_after_option_year": "{} aasta",
"shared_link_expires_day": "Aegub {} päeva pärast",
"shared_link_expires_days": "Aegub {} päeva pärast",
"shared_link_expires_hour": "Aegub {} tunni pärast",
"shared_link_expires_hours": "Aegub {} tunni pärast",
"shared_link_expires_minute": "Aegub {} minuti pärast",
"shared_link_expires_minutes": "Aegub {} minuti pärast",
"shared_link_expires_never": "Ei aegu",
"shared_link_expires_second": "Aegub {} sekundi pärast",
"shared_link_expires_seconds": "Aegub {} sekundi pärast",
"shared_link_info_chip_metadata": "EXIF",
"shared_link_manage_links": "Halda jagatud linke",
"shared_link_options": "Jagatud lingi valikud", "shared_link_options": "Jagatud lingi valikud",
"shared_links": "Jagatud lingid", "shared_links": "Jagatud lingid",
"shared_links_description": "Jaga fotosid ja videosid lingiga", "shared_links_description": "Jaga fotosid ja videosid lingiga",
"shared_photos_and_videos_count": "{assetCount, plural, other {# jagatud fotot ja videot.}}", "shared_photos_and_videos_count": "{assetCount, plural, other {# jagatud fotot ja videot.}}",
"shared_with_me": "Minuga jagatud",
"shared_with_partner": "Jagatud partneriga {partner}", "shared_with_partner": "Jagatud partneriga {partner}",
"sharing": "Jagamine", "sharing": "Jagamine",
"sharing_enter_password": "Palun sisesta selle lehe vaatamiseks salasõna.", "sharing_enter_password": "Palun sisesta selle lehe vaatamiseks salasõna.",
"sharing_page_album": "Jagatud albumid",
"sharing_sidebar_description": "Kuva külgmenüüs Jagamise linki", "sharing_sidebar_description": "Kuva külgmenüüs Jagamise linki",
"sharing_silver_appbar_create_shared_album": "Uus jagatud album",
"sharing_silver_appbar_share_partner": "Jaga partneriga",
"shift_to_permanent_delete": "vajuta ⇧, et üksus jäädavalt kustutada", "shift_to_permanent_delete": "vajuta ⇧, et üksus jäädavalt kustutada",
"show_album_options": "Näita albumi valikuid", "show_album_options": "Näita albumi valikuid",
"show_albums": "Näita albumeid", "show_albums": "Näita albumeid",
@@ -1265,6 +1556,7 @@
"support_third_party_description": "Sinu Immich'i install on kolmanda osapoole pakendatud. Probleemid, mida täheldad, võivad olla põhjustatud selle pakendamise poolt, seega võta esmajärjekorras nendega ühendust, kasutades allolevaid linke.", "support_third_party_description": "Sinu Immich'i install on kolmanda osapoole pakendatud. Probleemid, mida täheldad, võivad olla põhjustatud selle pakendamise poolt, seega võta esmajärjekorras nendega ühendust, kasutades allolevaid linke.",
"swap_merge_direction": "Muuda ühendamise suunda", "swap_merge_direction": "Muuda ühendamise suunda",
"sync": "Sünkrooni", "sync": "Sünkrooni",
"sync_albums": "Sünkrooni albumid",
"tag": "Silt", "tag": "Silt",
"tag_assets": "Sildista üksuseid", "tag_assets": "Sildista üksuseid",
"tag_created": "Lisatud silt: {tag}", "tag_created": "Lisatud silt: {tag}",
@@ -1278,6 +1570,13 @@
"theme": "Teema", "theme": "Teema",
"theme_selection": "Teema valik", "theme_selection": "Teema valik",
"theme_selection_description": "Sea automaatselt hele või tume teema vastavalt veebilehitseja eelistustele", "theme_selection_description": "Sea automaatselt hele või tume teema vastavalt veebilehitseja eelistustele",
"theme_setting_colorful_interface_subtitle": "Rakenda taustapindadele primaarne värv.",
"theme_setting_colorful_interface_title": "Värviline kasutajaliides",
"theme_setting_image_viewer_quality_subtitle": "Kohanda detailvaaturi kvaliteeti",
"theme_setting_image_viewer_quality_title": "Pildivaaturi kvaliteet",
"theme_setting_primary_color_title": "Põhivärv",
"theme_setting_system_primary_color_title": "Kasuta süsteemset värvi",
"theme_setting_system_theme_switch": "Automaatne (järgi süsteemi seadet)",
"they_will_be_merged_together": "Nad ühendatakse kokku", "they_will_be_merged_together": "Nad ühendatakse kokku",
"third_party_resources": "Kolmanda osapoole ressursid", "third_party_resources": "Kolmanda osapoole ressursid",
"time_based_memories": "Ajapõhised mälestused", "time_based_memories": "Ajapõhised mälestused",
@@ -1297,7 +1596,11 @@
"trash_all": "Kõik prügikasti", "trash_all": "Kõik prügikasti",
"trash_count": "Liiguta {count, number} prügikasti", "trash_count": "Liiguta {count, number} prügikasti",
"trash_delete_asset": "Kustuta üksus", "trash_delete_asset": "Kustuta üksus",
"trash_emptied": "Prügikast tühjendatud",
"trash_no_results_message": "Siia ilmuvad prügikasti liigutatud fotod ja videod.", "trash_no_results_message": "Siia ilmuvad prügikasti liigutatud fotod ja videod.",
"trash_page_delete_all": "Kustuta kõik",
"trash_page_restore_all": "Taasta kõik",
"trash_page_select_assets_btn": "Vali üksused",
"trashed_items_will_be_permanently_deleted_after": "Prügikasti tõstetud üksused kustutatakse jäädavalt {days, plural, one {# päeva} other {# päeva}} pärast.", "trashed_items_will_be_permanently_deleted_after": "Prügikasti tõstetud üksused kustutatakse jäädavalt {days, plural, one {# päeva} other {# päeva}} pärast.",
"type": "Tüüp", "type": "Tüüp",
"unarchive": "Taasta arhiivist", "unarchive": "Taasta arhiivist",
@@ -1333,6 +1636,7 @@
"upload_status_errors": "Vead", "upload_status_errors": "Vead",
"upload_status_uploaded": "Üleslaaditud", "upload_status_uploaded": "Üleslaaditud",
"upload_success": "Üleslaadimine õnnestus, uute üksuste nägemiseks värskenda lehte.", "upload_success": "Üleslaadimine õnnestus, uute üksuste nägemiseks värskenda lehte.",
"uploading": "Üleslaadimine",
"url": "URL", "url": "URL",
"usage": "Kasutus", "usage": "Kasutus",
"use_custom_date_range": "Kasuta kohandatud kuupäevavahemikku", "use_custom_date_range": "Kasuta kohandatud kuupäevavahemikku",
@@ -1353,6 +1657,7 @@
"version": "Versioon", "version": "Versioon",
"version_announcement_closing": "Sinu sõber, Alex", "version_announcement_closing": "Sinu sõber, Alex",
"version_announcement_message": "Hei! Saadaval on uus Immich'i versioon. Palun võta aega, et lugeda <link>väljalasketeadet</link> ning veendu, et su seadistus on ajakohane, et vältida konfiguratsiooniprobleeme, eriti kui kasutad WatchTower'it või muud mehhanismi, mis Immich'it automaatselt uuendab.", "version_announcement_message": "Hei! Saadaval on uus Immich'i versioon. Palun võta aega, et lugeda <link>väljalasketeadet</link> ning veendu, et su seadistus on ajakohane, et vältida konfiguratsiooniprobleeme, eriti kui kasutad WatchTower'it või muud mehhanismi, mis Immich'it automaatselt uuendab.",
"version_announcement_overlay_title": "Uus serveri versioon saadaval 🎉",
"version_history": "Versiooniajalugu", "version_history": "Versiooniajalugu",
"version_history_item": "Versioon {version} paigaldatud {date}", "version_history_item": "Versioon {version} paigaldatud {date}",
"video": "Video", "video": "Video",
@@ -1372,15 +1677,19 @@
"view_previous_asset": "Vaata eelmist üksust", "view_previous_asset": "Vaata eelmist üksust",
"view_qr_code": "Vaata QR-koodi", "view_qr_code": "Vaata QR-koodi",
"view_stack": "Vaata virna", "view_stack": "Vaata virna",
"viewer_remove_from_stack": "Eemalda virnast",
"viewer_unstack": "Eralda",
"visibility_changed": "{count, plural, one {# isiku} other {# isiku}} nähtavus muudetud", "visibility_changed": "{count, plural, one {# isiku} other {# isiku}} nähtavus muudetud",
"waiting": "Ootel", "waiting": "Ootel",
"warning": "Hoiatus", "warning": "Hoiatus",
"week": "Nädal", "week": "Nädal",
"welcome": "Tere tulemast", "welcome": "Tere tulemast",
"welcome_to_immich": "Tere tulemast Immich'isse", "welcome_to_immich": "Tere tulemast Immich'isse",
"wifi_name": "WiFi-võrgu nimi",
"year": "Aasta", "year": "Aasta",
"years_ago": "{years, plural, one {# aasta} other {# aastat}} tagasi", "years_ago": "{years, plural, one {# aasta} other {# aastat}} tagasi",
"yes": "Jah", "yes": "Jah",
"you_dont_have_any_shared_links": "Sul pole ühtegi jagatud linki", "you_dont_have_any_shared_links": "Sul pole ühtegi jagatud linki",
"your_wifi_name": "Sinu WiFi-võrgu nimi",
"zoom_image": "Suumi pilti" "zoom_image": "Suumi pilti"
} }

View File

@@ -153,20 +153,13 @@
"oauth_auto_register": "ثبت خودکار", "oauth_auto_register": "ثبت خودکار",
"oauth_auto_register_description": "کاربران جدید را پس از ورود با OAuth به طور خودکار ثبت نام کن", "oauth_auto_register_description": "کاربران جدید را پس از ورود با OAuth به طور خودکار ثبت نام کن",
"oauth_button_text": "متن دکمه", "oauth_button_text": "متن دکمه",
"oauth_client_id": "شناسه کاربر",
"oauth_client_secret": "شناسه محرمانه کاربر",
"oauth_enable_description": "ورود توسط OAuth", "oauth_enable_description": "ورود توسط OAuth",
"oauth_issuer_url": "نشانی وب صادر کننده",
"oauth_mobile_redirect_uri": "تغییر مسیر URI موبایل", "oauth_mobile_redirect_uri": "تغییر مسیر URI موبایل",
"oauth_mobile_redirect_uri_override": "تغییر مسیر URI تلفن همراه", "oauth_mobile_redirect_uri_override": "تغییر مسیر URI تلفن همراه",
"oauth_mobile_redirect_uri_override_description": "زمانی که 'app.immich:/' یک URI پرش نامعتبر است، فعال کنید.", "oauth_mobile_redirect_uri_override_description": "زمانی که 'app.immich:/' یک URI پرش نامعتبر است، فعال کنید.",
"oauth_profile_signing_algorithm": "الگوریتم امضای پروفایل",
"oauth_profile_signing_algorithm_description": "الگوریتم مورد استفاده برای امضای پروفایل کاربر.",
"oauth_scope": "محدوده",
"oauth_settings": "OAuth", "oauth_settings": "OAuth",
"oauth_settings_description": "مدیریت تنظیمات ورود به سیستم OAuth", "oauth_settings_description": "مدیریت تنظیمات ورود به سیستم OAuth",
"oauth_settings_more_details": "برای جزئیات بیشتر در مورد این ویژگی، به <link>مستندات</link> مراجعه کنید.", "oauth_settings_more_details": "برای جزئیات بیشتر در مورد این ویژگی، به <link>مستندات</link> مراجعه کنید.",
"oauth_signing_algorithm": "الگوریتم امضا",
"oauth_storage_label_claim": "درخواست برچسب فضای ذخیره سازی", "oauth_storage_label_claim": "درخواست برچسب فضای ذخیره سازی",
"oauth_storage_label_claim_description": "تنظیم خودکار برچسب فضای ذخیره‌سازی کاربر به مقدار درخواست شده.", "oauth_storage_label_claim_description": "تنظیم خودکار برچسب فضای ذخیره‌سازی کاربر به مقدار درخواست شده.",
"oauth_storage_quota_claim": "درخواست سهمیه فضای ذخیره سازی", "oauth_storage_quota_claim": "درخواست سهمیه فضای ذخیره سازی",

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