Compare commits

..

35 Commits

Author SHA1 Message Date
Zack Pollard
675c64a22f wip: fix the mess that is the mobile database 2024-07-30 00:12:43 +01:00
Zack Pollard
d3aacbe74b refactor: authentication provider always try network calls and only fail if 401 or no local user 2024-07-29 19:02:04 +01:00
Zack Pollard
41aefffe09 chore: bump flutter sdk path for vscode 2024-07-29 19:01:09 +01:00
Alex
090762f9dd fix(mobile): refactor splash screen to not require online connection 2024-07-29 10:52:53 -05:00
Jared L
3225e33fc1 feat(server): significantly improve Australian reverse geocoding accuracy (#11370)
chore(geocoding): ingest australia PPLXs
2024-07-29 10:59:53 -04:00
Weblate (bot)
85ab916ecf chore(web): update translations (#11416)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: CraftWorks <weblate@craftworks.top>
Co-authored-by: Enoé Mugnaschi <enmuro@gmail.com>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: chapvic <victor@chapaev.org>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: nachtpfoetchen <nachtpfoetchen@posteo.de>
Co-authored-by: tddaij <xdaint@gmail.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: 李奕寯 <eugenelego88@gmail.com>
2024-07-29 14:48:44 +00:00
Michel Heusschen
7445dad0dd fix(web): timeline group date formatting (#11392)
* fix(web): timeline group date formatting

* add isValid check

* remove duplicate type
2024-07-29 10:42:55 -04:00
Michel Heusschen
0237f9baa3 feat(web): more localized number formatting (#11401) 2024-07-29 10:38:27 -04:00
Michel Heusschen
2e059bfbfd fix(web): avoid nesting buttons inside links (#11425) 2024-07-29 10:36:10 -04:00
renovate[bot]
7bb7f63d57 chore(deps): update dependency node to v20.16.0 (#11421)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-29 10:33:01 -04:00
renovate[bot]
66a5a5718f chore(deps): update terraform cloudflare to v4.38.0 (#11423)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-29 10:32:27 -04:00
Alex
ddc4d2f927 fix(mobile): client TLS on ios (#11415) 2024-07-28 17:32:53 -05:00
Weblate (bot)
0beeb61f5c chore(web): update translations (#11365)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/en_devel/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ja/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Cyrl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: AlrightIDidIt <fimofuni.igamunu@gotgel.org>
Co-authored-by: AxGD <guillermeaxel@yahoo.fr>
Co-authored-by: Bartłomiej Ruk <bartek04041993@gmail.com>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: ChoosenMEME <timjankowski259@gmail.com>
Co-authored-by: ConfusedAlex <alex@confusedalex.dev>
Co-authored-by: Coooolfan <coolfan1024@outlook.com>
Co-authored-by: Coxcopi70f00b67b61542fe <hn_vogel@gmx.net>
Co-authored-by: Denis Pacquier <denis.pacquier@gmail.com>
Co-authored-by: Eric Cornish <ao475129@gmail.com>
Co-authored-by: Fredrik Ekdahl <fekdahl@gmail.com>
Co-authored-by: Gilgwath <gilgwath@protonmail.com>
Co-authored-by: Jakub <jakubula.jm@gmail.com>
Co-authored-by: Jordy H <jordy@hoebergen.net>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Miki Mrvos <medolino2009@gmail.com>
Co-authored-by: NikiTricky <niki.sto2010@gmail.com>
Co-authored-by: Sabin Oana <sabin.oana@gmail.com>
Co-authored-by: Sam Smith <ja49619@gmail.com>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Sylvain Pichon <service@spichon.fr>
Co-authored-by: Varga Bence Levente <varga.bence.levente@protonmail.com>
Co-authored-by: Victor Sueiro <kiwicaja@gmail.com>
Co-authored-by: Xo <xocodokie@users.noreply.hosted.weblate.org>
Co-authored-by: aarhor <aaron.horstmann9916@gmail.com>
Co-authored-by: chapvic <victor@chapaev.org>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: krzemyk <krzemyk.official@proton.me>
Co-authored-by: nazo6 <git@nazo6.dev>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: yusufbarisk <yusufbarisk2004@gmail.com>
Co-authored-by: 李奕寯 <eugenelego88@gmail.com>
2024-07-28 20:53:04 +00:00
waclaw66
a321db9f48 fix(web): translation leftovers (#11412)
fix: new album
2024-07-28 15:43:25 -05:00
Matthew Momjian
827136fc8b docs: file custom location (#11413)
* file custom location

* fix microservices
2024-07-28 15:43:09 -05:00
Matthew Momjian
088eea88e0 docs: how to change PG PW (#11414)
* guide to change PG PW

* fix
2024-07-28 15:42:42 -05:00
Yuvraj P
15503784c8 feat(mobile): adds crop and rotate to mobile (#10989)
* Added Crop Feature

* Using LayoutBuilder Fix

* Using Immich Colors

* Using Immich Text Theme

* Chnaging dynamic datatype to nullable

* Fix for the retrivel of the image from the cropscreen

* Using Hooks State

* Small edits

* Finals edits

* Saving to the mobile

* Commented final code

* Commented final code

* Comments and AutoRoute

* Fix AutoRoute Final

* Naming tools and Action when made no edits

* Updating timeline after edit

* chore: lint

* format

* Light Mode Compatible

* fix duplicate page name

* Fix Routing

* Hiding the Button

* lint

* remove unused code

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-07-28 15:41:14 -05:00
Jonathan Jogenfors
bc8e236598 chore(server): make vite-tsconfig-paths a dev dependency instead (#11404) 2024-07-27 21:50:35 +02:00
Michel Heusschen
909bd43e65 fix(web): slideshow settings title (#11396) 2024-07-27 10:46:19 -05:00
Alex
3330885bcc chore(server): email template minor styling (#11387) 2024-07-26 21:58:48 -05:00
Jan
e1ac73718c feat(web): Duplicate-Page shortcut changes (#11183)
* duplicate page assign other shortcut keys, add 'open image' shortcut

* add shortcut info page to duplicates with own list of keys

* edit translations, add translationkeys

* format fix

* remove typo

---------

Co-authored-by: Zack Pollard <zackpollard@ymail.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-07-26 21:47:51 +00:00
Ben
a78eeb9b9c feat(web): search bar keyboard accessibility (#11323)
* feat(web): search bar keyboard accessibility

* fix: adjust aria attributes

* fix: safari announcing the correct option count

* minor adjustments

- CircleIconButton disabled cursor
- more generic selection handler

* fix: more subtle border color in dark mode

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-07-26 16:45:15 -05:00
martin
86b3e3ee13 fix(web): responsive design when selecting assets in an album (#11169)
fix: responsive design when selecting assets in an album
2024-07-26 16:33:20 -05:00
waclaw66
4b2bc8e4ce fix(mobile): search filter translation + fixes (#11141)
translation + fixes
2024-07-26 16:32:19 -05:00
renovate[bot]
f92aee204e chore(deps): update dependency @types/picomatch to v3 (#11096)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-26 16:29:25 -05:00
renovate[bot]
7fd2b7965c chore(deps): update docker.io/redis:6.2-alpine docker digest to e3b17ba (#11302)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-26 16:28:34 -05:00
renovate[bot]
32ba6e3e3f chore(deps): update dependency byte-size to v9 (#11356)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-26 16:27:09 -05:00
Jonathan Jogenfors
0a6e5e0ec1 fix(server): make vitest pick up edited files (#11385)
fix vitest on file edit
2024-07-26 16:26:38 -05:00
Jonathan Jogenfors
65a4f86154 chore: bump vitest to 1.6.0 (#11386)
bump vitest to 1.6.0
2024-07-26 16:26:17 -05:00
ayykamp
147c6e3600 chore(web): improve responsiveness in Album and Shared Album pages on small devices (#11055)
* style: better responsiveness on album and shared album pages

* revert right margin changes

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-07-26 21:06:08 +00:00
Nicolò
ee6f1a010c chore(server): clean mail-templates and add tailwind style (#11296)
With this commit I wanted to complete the react-mail
 structure by properly define the templates styles by
 including tailwind css framework.

The framework is extended by both react-mail and
 tailwindcss-preset-email. Those packages help the rendering
 for various email clients.

If in future there is the necessity to target specific mail
 clients the package `tailwindcss-email-variants` and
 `tailwindcss-mso` can help too. The latter has some
 workarounds for the Ms Outlook that is still lacking
 a lot of the CSS3 funcitonality.
 to target

Signed-off-by: hitech95 <nicveronese@gmail.com>
2024-07-26 15:41:11 -05:00
renovate[bot]
a444ea7361 chore(deps): update dependency flutter to v3.22.3 (#11301)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-26 15:39:33 -05:00
Alex
59b809012f chore(mobile): post release task (#11382) 2024-07-26 15:38:41 -05:00
Ben
c037a8b8fa fix(web): easier alt text translation for other languages (#11124)
* fix(web): alt text translation for non-English languages

* fix: refactor to use full translation key names

* fix: calling the translation function directly
2024-07-26 13:48:40 -05:00
Michel Heusschen
ce15cf6065 fix(web): buy immich translations (#11379) 2024-07-26 13:41:59 -05:00
123 changed files with 4454 additions and 2316 deletions

View File

@@ -1 +1 @@
20.15.1
20.16.0

9
cli/package-lock.json generated
View File

@@ -26,7 +26,7 @@
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"@vitest/coverage-v8": "^1.2.2",
"byte-size": "^8.1.1",
"byte-size": "^9.0.0",
"cli-progress": "^3.12.0",
"commander": "^12.0.0",
"eslint": "^8.56.0",
@@ -1669,10 +1669,11 @@
}
},
"node_modules/byte-size": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/byte-size/-/byte-size-8.1.1.tgz",
"integrity": "sha512-tUkzZWK0M/qdoLEqikxBWe4kumyuwjl3HO6zHTr4yEI23EojPtLYXdG1+AQY7MN0cGyNDvEaJ8wiYQm6P2bPxg==",
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/byte-size/-/byte-size-9.0.0.tgz",
"integrity": "sha512-xrJ8Hki7eQ6xew55mM6TG9zHI852OoAHcPfduWWtR6yxk2upTuIZy13VioRBDyHReHDdbeDPifUboeNkK/sXXA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.17"
}

View File

@@ -22,7 +22,7 @@
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"@vitest/coverage-v8": "^1.2.2",
"byte-size": "^8.1.1",
"byte-size": "^9.0.0",
"cli-progress": "^3.12.0",
"commander": "^12.0.0",
"eslint": "^8.56.0",
@@ -64,6 +64,6 @@
"lodash-es": "^4.17.21"
},
"volta": {
"node": "20.15.1"
"node": "20.16.0"
}
}

View File

@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.37.0"
constraints = "4.37.0"
version = "4.38.0"
constraints = "4.38.0"
hashes = [
"h1:0gOI8arnh2CTcHfGH8iwAe6qz2BRSytmbOiNXZjnrHc=",
"h1:0h0qRJYPHL92Dx3NYZO2WJ21cxyZGEoldzw9aYhPnew=",
"h1:6ri7vZ1MLtQbooicIO4catyIuRq4LHAsIcgd3vGq3AE=",
"h1:7BwVaqxSD9VsmLzs6jDJBJvHPq0dz4I8rCeJAK63Dc4=",
"h1:8tVm+BJvzI14pRbEyt00AvH6oIyqiLRZQ9KxcBeSDhE=",
"h1:FTll1M9rPA7RxEyLB6etQqaqynWWl3WkiwJtHMjPr3Y=",
"h1:L7ysGftn0fstXMjCt3/XEz2giRdEwBsGrdvi4Zw8uzM=",
"h1:PsbAKy7LdSpwZMJZ7bO3lI04hLDTlXke/LCkrKXYwwE=",
"h1:Sjkpr8CKs0rXGcdis5q4Kbqmo5mmosgirnQi65G4sM8=",
"h1:YxJRQdVSzMZR5Ce5M3Gs1SPutXpednxuRwtSSiReHDY=",
"h1:bJrJeBKWEwt4hGQ+3VJR69dsqHORovE8LzuQt9+NTug=",
"h1:hPC7Vk0ZGXCDJ1y5dOepVo1c0PoUulnJUarrMv4gQIQ=",
"h1:joMURZCLUJ2eSlj645xqHWKYbRBYqvajCkhaz7qzi8g=",
"h1:uqo0WgG5lCcG8+gf99VnsKKbJMM1urNZq1FbAT6u3S0=",
"zh:012a6c3e8bf4aca0ebe0884e15bd42fd018659193f2159d5d2bf9948a9be1bc4",
"zh:079666c0a079237af46ed19ffc4143655ee0e8920a274868e44fbc3db88f346d",
"zh:08e7ff86f6848f3109d59ad46f8c0987178eff2f70c8ef03f2d44ae68e42dfb3",
"zh:1ce8a499fdf8f484f7d18ec91566bc0759b07d0ca710990cd60d32b222e416b1",
"zh:348e72338095bffccf7c46c7e6b9d0e063a22d9ae761061b0b31dea1aad22cd9",
"zh:47d39343dea1ef469a2c8e51c8d5993687af427a132da5379796fec27acb5710",
"zh:4cdf8e9579f9af3c72270088fc6e22208f0f91fd4382bc4a860d16040c86917b",
"zh:4fbebb21ecebc7e5ac0ea9e341c5dbea3094fc0579e4dc5b40bfe693164e022e",
"zh:778578dda7dd98576a3fe228132c8b60f646f4cf113638c94f1c40e2b11c027c",
"h1:+27KAHKHBDvv3dqyJv5vhtdKQZJzoZXoMqIyronlHNw=",
"h1:/uV9RgOUhkxElkHhWs8fs5ZbX9vj6RCBfP0oJO0JF30=",
"h1:1DNAdMugJJOAWD/XYiZenYYZLy7fw2ctjT4YZmkRCVQ=",
"h1:1wn4PmCLdT7mvd74JkCGmJDJxTQDkcxc+1jNbmwnMHA=",
"h1:BIHB4fBxHg2bA9KbL92njhyctxKC8b6hNDp60y5QBss=",
"h1:HCQpvKPsMsR4HO5eDqt+Kao7T7CYeEH7KZIO7xMcC6M=",
"h1:HTomuzocukpNLwtWzeSF3yteCVsyVKbwKmN66u9iPac=",
"h1:YDxsUBhBAwHSXLzVwrSlSBOwv1NvLyry7s5SfCV7VqQ=",
"h1:dchVhxo+Acd1l2RuZ88tW9lWj4422QMfgtxKvKCjYrw=",
"h1:eypa+P4ZpsEGMPFuCE+6VkRefu0TZRFmVBOpK+PDOPY=",
"h1:f3yjse2OsRZj7ZhR7BLintJMlI4fpyt8HyDP/zcEavw=",
"h1:mSJ7xj8K+xcnEmGg7lH0jjzyQb157wH94ULTAlIV+HQ=",
"h1:tt+2J2Ze8VIdDq2Hr6uHlTJzAMBRpErBwTYx0uD5ilE=",
"h1:uQW8SKxmulqrAisO+365mIf2FueINAp5PY28bqCPCug=",
"zh:171ab67cccceead4514fafb2d39e4e708a90cce79000aaf3c29aab7ed4457071",
"zh:18aa7228447baaaefc49a43e8eff970817a7491a63d8937e796357a3829dd979",
"zh:2cbaab6092e81ba6f41fa60a50f14e980c8ec327ee11d0b21f16a478be4b7567",
"zh:53b8e49c06f5b31a8c681f8c0669cf43e78abe71657b8182a221d096bb514965",
"zh:6037cfc60b4b647aabae155fcb46d649ed7c650e0287f05db52b2068f1e27c8a",
"zh:62460982ce1a869eebfca675603fbbd50416cf6b69459fb855bfbe5ae2b97607",
"zh:65f6f3a8470917b6398baa5eb4f74b3932b213eac7c0202798bfad6fd1ee17df",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:894071f0f42571f820918d1a4316704923e29c5b2392704c1cbd063a04a641b8",
"zh:8d11dd73dd499c74d89f77a7e1b3d4a077ac88b0c9c3412e9a6a1b4efe17d107",
"zh:991e088be8381a73872cd33bb659e9dd69d7ab1f1f8d89b3cd17ffe59dffc65f",
"zh:9c0848b9c7e6799c9ffcf3afa70ad94a027f3e15a94679d56790714de0b072c5",
"zh:ad71ae800065ffc24b94d994250136ae8a9f6da704cf91b0dc9e14989e947369",
"zh:8b5cebe64bf04105a49178a165b6a8800a9a33bae6767143a47fe4977755f805",
"zh:a5596635db0993ee3c3060fbc2227d91b239466e96d2d82642625a5aa2486988",
"zh:b3a9c63038441f13c311fd4b2c7e69e571445e5a7365a20c7cc9046b7e6c8aba",
"zh:b585e7e4d7648a540b14b9182819214896ca9337729eeb1f2034833b17db754d",
"zh:d2c3c545318ac8542369e9fc8228e29ee585febdf203a450fad3e0eded71ce02",
"zh:e95dd2d6c3525073af47d47b763cb81b6a51b20cabf76f789c69328922da9ecf",
"zh:eee6e590b36d6c6168a7daae8afa74a8721fd7aa9f62a710f04a311975100722",
]
}

View File

@@ -5,7 +5,7 @@ terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "4.37.0"
version = "4.38.0"
}
}
}

View File

@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.37.0"
constraints = "4.37.0"
version = "4.38.0"
constraints = "4.38.0"
hashes = [
"h1:0gOI8arnh2CTcHfGH8iwAe6qz2BRSytmbOiNXZjnrHc=",
"h1:0h0qRJYPHL92Dx3NYZO2WJ21cxyZGEoldzw9aYhPnew=",
"h1:6ri7vZ1MLtQbooicIO4catyIuRq4LHAsIcgd3vGq3AE=",
"h1:7BwVaqxSD9VsmLzs6jDJBJvHPq0dz4I8rCeJAK63Dc4=",
"h1:8tVm+BJvzI14pRbEyt00AvH6oIyqiLRZQ9KxcBeSDhE=",
"h1:FTll1M9rPA7RxEyLB6etQqaqynWWl3WkiwJtHMjPr3Y=",
"h1:L7ysGftn0fstXMjCt3/XEz2giRdEwBsGrdvi4Zw8uzM=",
"h1:PsbAKy7LdSpwZMJZ7bO3lI04hLDTlXke/LCkrKXYwwE=",
"h1:Sjkpr8CKs0rXGcdis5q4Kbqmo5mmosgirnQi65G4sM8=",
"h1:YxJRQdVSzMZR5Ce5M3Gs1SPutXpednxuRwtSSiReHDY=",
"h1:bJrJeBKWEwt4hGQ+3VJR69dsqHORovE8LzuQt9+NTug=",
"h1:hPC7Vk0ZGXCDJ1y5dOepVo1c0PoUulnJUarrMv4gQIQ=",
"h1:joMURZCLUJ2eSlj645xqHWKYbRBYqvajCkhaz7qzi8g=",
"h1:uqo0WgG5lCcG8+gf99VnsKKbJMM1urNZq1FbAT6u3S0=",
"zh:012a6c3e8bf4aca0ebe0884e15bd42fd018659193f2159d5d2bf9948a9be1bc4",
"zh:079666c0a079237af46ed19ffc4143655ee0e8920a274868e44fbc3db88f346d",
"zh:08e7ff86f6848f3109d59ad46f8c0987178eff2f70c8ef03f2d44ae68e42dfb3",
"zh:1ce8a499fdf8f484f7d18ec91566bc0759b07d0ca710990cd60d32b222e416b1",
"zh:348e72338095bffccf7c46c7e6b9d0e063a22d9ae761061b0b31dea1aad22cd9",
"zh:47d39343dea1ef469a2c8e51c8d5993687af427a132da5379796fec27acb5710",
"zh:4cdf8e9579f9af3c72270088fc6e22208f0f91fd4382bc4a860d16040c86917b",
"zh:4fbebb21ecebc7e5ac0ea9e341c5dbea3094fc0579e4dc5b40bfe693164e022e",
"zh:778578dda7dd98576a3fe228132c8b60f646f4cf113638c94f1c40e2b11c027c",
"h1:+27KAHKHBDvv3dqyJv5vhtdKQZJzoZXoMqIyronlHNw=",
"h1:/uV9RgOUhkxElkHhWs8fs5ZbX9vj6RCBfP0oJO0JF30=",
"h1:1DNAdMugJJOAWD/XYiZenYYZLy7fw2ctjT4YZmkRCVQ=",
"h1:1wn4PmCLdT7mvd74JkCGmJDJxTQDkcxc+1jNbmwnMHA=",
"h1:BIHB4fBxHg2bA9KbL92njhyctxKC8b6hNDp60y5QBss=",
"h1:HCQpvKPsMsR4HO5eDqt+Kao7T7CYeEH7KZIO7xMcC6M=",
"h1:HTomuzocukpNLwtWzeSF3yteCVsyVKbwKmN66u9iPac=",
"h1:YDxsUBhBAwHSXLzVwrSlSBOwv1NvLyry7s5SfCV7VqQ=",
"h1:dchVhxo+Acd1l2RuZ88tW9lWj4422QMfgtxKvKCjYrw=",
"h1:eypa+P4ZpsEGMPFuCE+6VkRefu0TZRFmVBOpK+PDOPY=",
"h1:f3yjse2OsRZj7ZhR7BLintJMlI4fpyt8HyDP/zcEavw=",
"h1:mSJ7xj8K+xcnEmGg7lH0jjzyQb157wH94ULTAlIV+HQ=",
"h1:tt+2J2Ze8VIdDq2Hr6uHlTJzAMBRpErBwTYx0uD5ilE=",
"h1:uQW8SKxmulqrAisO+365mIf2FueINAp5PY28bqCPCug=",
"zh:171ab67cccceead4514fafb2d39e4e708a90cce79000aaf3c29aab7ed4457071",
"zh:18aa7228447baaaefc49a43e8eff970817a7491a63d8937e796357a3829dd979",
"zh:2cbaab6092e81ba6f41fa60a50f14e980c8ec327ee11d0b21f16a478be4b7567",
"zh:53b8e49c06f5b31a8c681f8c0669cf43e78abe71657b8182a221d096bb514965",
"zh:6037cfc60b4b647aabae155fcb46d649ed7c650e0287f05db52b2068f1e27c8a",
"zh:62460982ce1a869eebfca675603fbbd50416cf6b69459fb855bfbe5ae2b97607",
"zh:65f6f3a8470917b6398baa5eb4f74b3932b213eac7c0202798bfad6fd1ee17df",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:894071f0f42571f820918d1a4316704923e29c5b2392704c1cbd063a04a641b8",
"zh:8d11dd73dd499c74d89f77a7e1b3d4a077ac88b0c9c3412e9a6a1b4efe17d107",
"zh:991e088be8381a73872cd33bb659e9dd69d7ab1f1f8d89b3cd17ffe59dffc65f",
"zh:9c0848b9c7e6799c9ffcf3afa70ad94a027f3e15a94679d56790714de0b072c5",
"zh:ad71ae800065ffc24b94d994250136ae8a9f6da704cf91b0dc9e14989e947369",
"zh:8b5cebe64bf04105a49178a165b6a8800a9a33bae6767143a47fe4977755f805",
"zh:a5596635db0993ee3c3060fbc2227d91b239466e96d2d82642625a5aa2486988",
"zh:b3a9c63038441f13c311fd4b2c7e69e571445e5a7365a20c7cc9046b7e6c8aba",
"zh:b585e7e4d7648a540b14b9182819214896ca9337729eeb1f2034833b17db754d",
"zh:d2c3c545318ac8542369e9fc8228e29ee585febdf203a450fad3e0eded71ce02",
"zh:e95dd2d6c3525073af47d47b763cb81b6a51b20cabf76f789c69328922da9ecf",
"zh:eee6e590b36d6c6168a7daae8afa74a8721fd7aa9f62a710f04a311975100722",
]
}

View File

@@ -5,7 +5,7 @@ terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "4.37.0"
version = "4.38.0"
}
}
}

View File

@@ -43,7 +43,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/redis:6.2-alpine@sha256:328fe6a5822256d065debb36617a8169dbfbd77b797c525288e465f56c1d392b
image: docker.io/redis:6.2-alpine@sha256:e3b17ba9479deec4b7d1eeec1548a253acc5374d68d3b27937fcfe4df8d18c7e
healthcheck:
test: redis-cli ping || exit 1
restart: always

View File

@@ -1 +1 @@
20.15.1
20.16.0

View File

@@ -13,14 +13,14 @@ In our `.env` file, we will define variables that will help us in the future whe
# Custom location where your uploaded, thumbnails, and transcoded video files are stored
- UPLOAD_LOCATION=./library
+ UPLOAD_LOCATION=/custom/location/on/your/system/immich/immich_files
+ THUMB_LOCATION=/custom/location/on/your/system/immich/thumbs
+ ENCODED_VIDEO_LOCATION=/custom/location/on/your/system/immich/encoded-video
+ PROFILE_LOCATION=/custom/location/on/your/system/immich/profile
+ UPLOAD_LOCATION=/custom/path/immich/immich_files
+ THUMB_LOCATION=/custom/path/immich/thumbs
+ ENCODED_VIDEO_LOCATION=/custom/path/immich/encoded-video
+ PROFILE_LOCATION=/custom/path/immich/profile
...
```
After defining the locations for these files, we will edit the `docker-compose.yml` file accordingly and add the new variables to the `immich-server` and `immich-microservices` containers.
After defining the locations for these files, we will edit the `docker-compose.yml` file accordingly and add the new variables to the `immich-server` container.
```diff title="docker-compose.yml"
services:
@@ -29,16 +29,6 @@ services:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
+ - ${THUMB_LOCATION}:/usr/src/app/upload/thumbs
+ - ${ENCODED_VIDEO_LOCATION}:/usr/src/app/upload/encoded-video
+ - ${PROFILE_LOCATION}:/usr/src/app/upload/profile
- /etc/localtime:/etc/localtime:ro
...
immich-microservices:
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
+ - ${THUMB_LOCATION}:/usr/src/app/upload/thumbs
+ - ${ENCODED_VIDEO_LOCATION}:/usr/src/app/upload/encoded-video
+ - ${PROFILE_LOCATION}:/usr/src/app/upload/profile
- /etc/localtime:/etc/localtime:ro
```
@@ -46,7 +36,6 @@ services:
Restart Immich to register the changes.
```
docker compose down
docker compose up -d
```

View File

@@ -5,7 +5,7 @@ Keep in mind that mucking around in the database might set the moon on fire. Avo
:::
:::tip
Run `docker exec -it immich_postgres psql immich <DB_USERNAME>` to connect to the database via the container directly.
Run `docker exec -it immich_postgres psql --dbname=immich --username=<DB_USERNAME>` to connect to the database via the container directly.
(Replace `<DB_USERNAME>` with the value from your [`.env` file](/docs/install/environment-variables#database)).
:::
@@ -106,3 +106,9 @@ SELECT "key", "value" FROM "system_metadata" WHERE "key" = 'system-config';
```sql title="Delete person and unset it for the faces it was associated with"
DELETE FROM "person" WHERE "name" = 'PersonNameHere';
```
## Postgres internal
```sql title="Change DB_PASSWORD"
ALTER USER <DB_USERNAME> WITH ENCRYPTED PASSWORD 'newpasswordhere';
```

View File

@@ -56,6 +56,6 @@
"node": ">=20"
},
"volta": {
"node": "20.15.1"
"node": "20.16.0"
}
}

View File

@@ -1 +1 @@
20.15.1
20.16.0

4
e2e/package-lock.json generated
View File

@@ -37,7 +37,7 @@
"supertest": "^7.0.0",
"typescript": "^5.3.3",
"utimes": "^5.2.1",
"vitest": "^1.3.0"
"vitest": "^1.6.0"
}
},
"../cli": {
@@ -63,7 +63,7 @@
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"@vitest/coverage-v8": "^1.2.2",
"byte-size": "^8.1.1",
"byte-size": "^9.0.0",
"cli-progress": "^3.12.0",
"commander": "^12.0.0",
"eslint": "^8.56.0",

View File

@@ -47,9 +47,9 @@
"supertest": "^7.0.0",
"typescript": "^5.3.3",
"utimes": "^5.2.1",
"vitest": "^1.3.0"
"vitest": "^1.6.0"
},
"volta": {
"node": "20.15.1"
"node": "20.16.0"
}
}

View File

@@ -13,7 +13,7 @@ test.describe('Registration', () => {
test('admin registration', async ({ page }) => {
// welcome
await page.goto('/');
await page.getByRole('button', { name: 'Getting Started' }).click();
await page.getByRole('link', { name: 'Getting Started' }).click();
// register
await expect(page).toHaveTitle(/Admin Registration/);

View File

@@ -1,3 +1,3 @@
{
"flutter": "3.22.2"
"flutter": "3.22.3"
}

View File

@@ -1,5 +1,5 @@
{
"dart.flutterSdkPath": ".fvm/versions/3.22.1",
"dart.flutterSdkPath": ".fvm/versions/3.22.3",
"search.exclude": {
"**/.fvm": true
},

View File

@@ -3,6 +3,8 @@
"action_common_cancel": "Cancel",
"action_common_clear": "Clear",
"action_common_confirm": "Confirm",
"action_common_save": "Save",
"action_common_select": "Select",
"action_common_update": "Update",
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
@@ -146,6 +148,7 @@
"common_create_new_album": "Create new album",
"common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.",
"common_shared": "Shared",
"contextual_search": "Sunrise on the beach",
"control_bottom_app_bar_add_to_album": "Add to album",
"control_bottom_app_bar_album_info": "{} items",
"control_bottom_app_bar_album_info_shared": "{} items · Shared",
@@ -203,6 +206,7 @@
"experimental_settings_title": "Experimental",
"favorites_page_no_favorites": "No favorite assets found",
"favorites_page_title": "Favorites",
"filename_search": "File name or extension",
"haptic_feedback_switch": "Enable haptic feedback",
"haptic_feedback_title": "Haptic Feedback",
"header_settings_add_header_tip": "Add Header",
@@ -230,6 +234,8 @@
"image_viewer_page_state_provider_download_started": "Download Started",
"image_viewer_page_state_provider_download_success": "Download Success",
"image_viewer_page_state_provider_share_error": "Share Error",
"invalid_date": "Invalid date",
"invalid_date_format": "Invalid date format",
"library_page_albums": "Albums",
"library_page_archive": "Archive",
"library_page_device_albums": "Albums on Device",
@@ -311,6 +317,7 @@
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
"no_assets_to_show": "No assets to show",
"no_name": "No name",
"notification_permission_dialog_cancel": "Cancel",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_dialog_settings": "Settings",
@@ -354,17 +361,30 @@
"scaffold_body_error_occurred": "Error occurred",
"search_bar_hint": "Search your photos",
"search_filter_apply": "Apply filter",
"search_filter_camera": "Camera",
"search_filter_camera_make": "Make",
"search_filter_camera_model": "Model",
"search_filter_camera_title": "Select camera type",
"search_filter_date": "Date",
"search_filter_date_interval": "{start} to {end}",
"search_filter_date_title": "Select a date range",
"search_filter_display_option_archive": "Archive",
"search_filter_display_option_favorite": "Favorite",
"search_filter_display_option_not_in_album": "Not in album",
"search_filter_display_options": "Display Options",
"search_filter_display_options_title": "Display options",
"search_filter_location": "Location",
"search_filter_location_city": "City",
"search_filter_location_country": "Country",
"search_filter_location_state": "State",
"search_filter_location_title": "Select location",
"search_filter_media_type": "Media Type",
"search_filter_media_type_all": "All",
"search_filter_media_type_image": "Image",
"search_filter_media_type_title": "Select media type",
"search_filter_media_type_video": "Video",
"search_filter_people": "People",
"search_filter_people_title": "Select people",
"search_page_categories": "Categories",
"search_page_favorites": "Favorites",
"search_page_motion_photos": "Motion Photos",
@@ -418,15 +438,18 @@
"setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)",
"setting_notifications_total_progress_title": "Show background backup total progress",
"setting_pages_app_bar_settings": "Settings",
"settings_require_restart": "Please restart Immich to apply this setting",
"setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
"setting_video_viewer_looping_title": "Looping",
"setting_video_viewer_title": "Videos",
"settings_require_restart": "Please restart Immich to apply this setting",
"share_add": "Add",
"share_add_photos": "Add photos",
"share_add_title": "Add a title",
"share_assets_selected": "{} selected",
"share_create_album": "Create album",
"share_dialog_preparing": "Preparing...",
"share_done": "Done",
"share_invite": "Invite to album",
"shared_album_activities_input_disable": "Comment is disabled",
"shared_album_activities_input_hint": "Say something",
"shared_album_activity_remove_content": "Do you want to delete this activity?",
@@ -438,7 +461,6 @@
"shared_album_section_people_action_remove_user": "Remove user from album",
"shared_album_section_people_owner_label": "Owner",
"shared_album_section_people_title": "PEOPLE",
"share_dialog_preparing": "Preparing...",
"shared_link_app_bar_title": "Shared Links",
"shared_link_clipboard_copied_massage": "Copied to clipboard",
"shared_link_clipboard_text": "Link: {}\nPassword: {}",
@@ -484,14 +506,12 @@
"shared_link_info_chip_upload": "Upload",
"shared_link_manage_links": "Manage Shared links",
"shared_link_public_album": "Public album",
"share_done": "Done",
"share_invite": "Invite to album",
"sharing_page_album": "Shared albums",
"sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
"sharing_page_empty_list": "EMPTY LIST",
"sharing_silver_appbar_create_shared_album": "New shared album",
"sharing_silver_appbar_shared_links": "Shared links",
"sharing_silver_appbar_share_partner": "Share with partner",
"sharing_silver_appbar_shared_links": "Shared links",
"tab_controller_nav_library": "Library",
"tab_controller_nav_photos": "Photos",
"tab_controller_nav_search": "Search",

View File

@@ -383,7 +383,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 164;
CURRENT_PROJECT_VERSION = 165;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -525,7 +525,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 164;
CURRENT_PROJECT_VERSION = 165;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -553,7 +553,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 164;
CURRENT_PROJECT_VERSION = 165;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -58,11 +58,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.109.0</string>
<string>1.110.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>164</string>
<string>165</string>
<key>FLTEnableImpeller</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>

View File

@@ -19,6 +19,7 @@ class Store {
/// Initializes the store (call exactly once per app start)
static void init(Isar db) {
print("Initializing store");
_db = db;
_populateCache();
_db.storeValues.where().build().watch().listen(_onChangeListener);
@@ -59,6 +60,9 @@ class Store {
/// Removes the value synchronously from the cache and asynchronously from the DB
static Future<void> delete<T>(StoreKey<T> key) {
if (_cache[key.id] == null) return Future.value();
if(key.id == StoreKey.serverEndpoint.id) {
_log.info("Server endpoint changed to null");
}
_cache[key.id] = null;
return _db.writeTxn(() => _db.storeValues.delete(key.id));
}
@@ -76,12 +80,12 @@ class Store {
/// updates the state if a value is updated in any isolate
static void _onChangeListener(List<StoreValue>? data) {
if (data != null) {
final dbValues = _db.txnSync(() => _db.storeValues.getAllSync(data.map((e) => e.id).toList()));
for (StoreValue value in data) {
final key = StoreKey.values.firstWhereOrNull((e) => e.id == value.id);
if (key != null) {
_cache[value.id] = value._extract(key);
} else {
_log.warning("No key available for value id - ${value.id}");
final dbValue = dbValues.firstWhere((e) => e?.id == value.id, orElse: () => null)?._extract(StoreKey.values[value.id]);
_cache[value.id] = dbValue;
if(value.id == StoreKey.serverEndpoint.id) {
_log.info("Server endpoint changed to ${value.strValue}");
}
}
}
@@ -96,7 +100,8 @@ class StoreValue {
int? intValue;
String? strValue;
T? _extract<T>(StoreKey<T> key) {
T? _extract<T>(StoreKey<T>? key) {
if (key == null) return null;
switch (key.type) {
case const (int):
return intValue as T?;

View File

@@ -19,45 +19,22 @@ class SplashScreenPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final apiService = ref.watch(apiServiceProvider);
final serverUrl = Store.tryGet(StoreKey.serverUrl);
final endpoint = Store.tryGet(StoreKey.serverEndpoint);
final accessToken = Store.tryGet(StoreKey.accessToken);
final log = Logger("SplashScreenPage");
void performLoggingIn() async {
bool isSuccess = false;
bool deviceIsOffline = false;
bool isAuthSuccess = false;
if (accessToken != null && serverUrl != null) {
try {
// Resolve API server endpoint from user provided serverUrl
await apiService.resolveAndSetEndpoint(serverUrl);
} on ApiException catch (error, stackTrace) {
log.severe(
"Failed to resolve endpoint [ApiException]",
error,
stackTrace,
);
// okay, try to continue anyway if offline
if (error.code == 503) {
deviceIsOffline = true;
log.warning("Device seems to be offline upon launch");
} else {
log.severe("Failed to resolve endpoint", error);
}
} catch (error, stackTrace) {
log.severe(
"Failed to resolve endpoint [Catch All]",
error,
stackTrace,
);
}
if (accessToken != null && serverUrl != null && endpoint != null) {
apiService.setEndpoint(endpoint);
try {
isSuccess = await ref
isAuthSuccess = await ref
.read(authenticationProvider.notifier)
.setSuccessLoginInfo(
accessToken: accessToken,
serverUrl: serverUrl,
offlineLogin: deviceIsOffline,
);
} catch (error, stackTrace) {
log.severe(
@@ -66,29 +43,29 @@ class SplashScreenPage extends HookConsumerWidget {
stackTrace,
);
}
} else {
isAuthSuccess = false;
log.severe(
'Missing authentication, server, or endpoint info from the local store',
);
}
// If the device is offline and there is a currentUser stored locallly
// Proceed into the app
if (deviceIsOffline && Store.tryGet(StoreKey.currentUser) != null) {
context.replaceRoute(const TabControllerRoute());
} else if (isSuccess) {
// If device was able to login through the internet successfully
final hasPermission =
await ref.read(galleryPermissionNotifier.notifier).hasPermission;
if (hasPermission) {
// Resume backup (if enable) then navigate
ref.watch(backupProvider.notifier).resumeBackup();
}
context.replaceRoute(const TabControllerRoute());
} else {
if (!isAuthSuccess) {
log.severe(
'Unable to login through offline or online methods - logging out completely',
'Unable to login using offline or online methods - Logging out completely',
);
ref.read(authenticationProvider.notifier).logout();
// User was unable to login through either offline or online methods
context.replaceRoute(const LoginRoute());
return;
}
context.replaceRoute(const TabControllerRoute());
final hasPermission =
await ref.read(galleryPermissionNotifier.notifier).hasPermission;
if (hasPermission) {
// Resume backup (if enable) then navigate
ref.watch(backupProvider.notifier).resumeBackup();
}
}

View File

@@ -0,0 +1,203 @@
import 'package:flutter/material.dart';
import 'package:crop_image/crop_image.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart';
import 'edit.page.dart';
import 'package:auto_route/auto_route.dart';
/// A widget for cropping an image.
/// This widget uses [HookWidget] to manage its lifecycle and state. It allows
/// users to crop an image and then navigate to the [EditImagePage] with the
/// cropped image.
@RoutePage()
class CropImagePage extends HookWidget {
final Image image;
const CropImagePage({super.key, required this.image});
@override
Widget build(BuildContext context) {
final cropController = useCropController();
final aspectRatio = useState<double?>(null);
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).bottomAppBarTheme.color,
leading: CloseButton(color: Theme.of(context).iconTheme.color),
actions: [
IconButton(
icon: Icon(
Icons.done_rounded,
color: Theme.of(context).iconTheme.color,
size: 24,
),
onPressed: () async {
final croppedImage = await cropController.croppedImage();
context.pushRoute(EditImageRoute(image: croppedImage));
},
),
],
),
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Column(
children: [
Container(
padding: const EdgeInsets.only(top: 20),
width: double.infinity,
height: constraints.maxHeight * 0.6,
child: CropImage(
controller: cropController,
image: image,
gridColor: Colors.white,
),
),
Expanded(
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).bottomAppBarTheme.color,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
bottom: 10,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: Icon(
Icons.rotate_left,
color: Theme.of(context).iconTheme.color,
),
onPressed: () {
cropController.rotateLeft();
},
),
IconButton(
icon: Icon(
Icons.rotate_right,
color: Theme.of(context).iconTheme.color,
),
onPressed: () {
cropController.rotateRight();
},
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
_AspectRatioButton(
cropController: cropController,
aspectRatio: aspectRatio,
ratio: null,
label: 'Free',
),
_AspectRatioButton(
cropController: cropController,
aspectRatio: aspectRatio,
ratio: 1.0,
label: '1:1',
),
_AspectRatioButton(
cropController: cropController,
aspectRatio: aspectRatio,
ratio: 16.0 / 9.0,
label: '16:9',
),
_AspectRatioButton(
cropController: cropController,
aspectRatio: aspectRatio,
ratio: 3.0 / 2.0,
label: '3:2',
),
_AspectRatioButton(
cropController: cropController,
aspectRatio: aspectRatio,
ratio: 7.0 / 5.0,
label: '7:5',
),
],
),
],
),
),
),
),
],
);
},
),
);
}
}
class _AspectRatioButton extends StatelessWidget {
final CropController cropController;
final ValueNotifier<double?> aspectRatio;
final double? ratio;
final String label;
const _AspectRatioButton({
required this.cropController,
required this.aspectRatio,
required this.ratio,
required this.label,
});
@override
Widget build(BuildContext context) {
IconData iconData;
switch (label) {
case 'Free':
iconData = Icons.crop_free_rounded;
break;
case '1:1':
iconData = Icons.crop_square_rounded;
break;
case '16:9':
iconData = Icons.crop_16_9_rounded;
break;
case '3:2':
iconData = Icons.crop_3_2_rounded;
break;
case '7:5':
iconData = Icons.crop_7_5_rounded;
break;
default:
iconData = Icons.crop_free_rounded;
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(
iconData,
color: aspectRatio.value == ratio
? Colors.indigo
: Theme.of(context).iconTheme.color,
),
onPressed: () {
aspectRatio.value = ratio;
cropController.aspectRatio = ratio;
},
),
Text(label, style: Theme.of(context).textTheme.bodyMedium),
],
);
}
}

View File

@@ -0,0 +1,158 @@
import 'dart:io';
import 'dart:typed_data';
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/widgets/common/immich_image.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:auto_route/auto_route.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
/// A stateless widget that provides functionality for editing an image.
///
/// This widget allows users to edit an image provided either as an [Asset] or
/// directly as an [Image]. It ensures that exactly one of these is provided.
///
/// It also includes a conversion method to convert an [Image] to a [Uint8List] to save the image on the user's phone
/// They automatically navigate to the [HomePage] with the edited image saved and they eventually get backed up to the server.
@immutable
@RoutePage()
class EditImagePage extends ConsumerWidget {
final Asset? asset;
final Image? image;
const EditImagePage({
super.key,
this.image,
this.asset,
}) : assert(
(image != null && asset == null) || (image == null && asset != null),
'Must supply one of asset or image',
);
Future<Uint8List> _imageToUint8List(Image image) async {
final Completer<Uint8List> completer = Completer();
image.image.resolve(const ImageConfiguration()).addListener(
ImageStreamListener(
(ImageInfo info, bool _) {
info.image
.toByteData(format: ImageByteFormat.png)
.then((byteData) {
if (byteData != null) {
completer.complete(byteData.buffer.asUint8List());
} else {
completer.completeError('Failed to convert image to bytes');
}
});
},
onError: (exception, stackTrace) =>
completer.completeError(exception),
),
);
return completer.future;
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final ImageProvider provider = (asset != null)
? ImmichImage.imageProvider(asset: asset!)
: (image != null)
? image!.image
: throw Exception('Invalid image source type');
final Image imageWidget = (asset != null)
? Image(image: ImmichImage.imageProvider(asset: asset!))
: (image != null)
? image!
: throw Exception('Invalid image source type');
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
leading: IconButton(
icon: Icon(
Icons.close_rounded,
color: Theme.of(context).iconTheme.color,
size: 24,
),
onPressed: () =>
Navigator.of(context).popUntil((route) => route.isFirst),
),
actions: <Widget>[
if (image != null)
TextButton(
onPressed: () async {
try {
final Uint8List imageData = await _imageToUint8List(image!);
ImmichToast.show(
durationInSecond: 3,
context: context,
msg: 'Image Saved!',
gravity: ToastGravity.CENTER,
);
await PhotoManager.editor
.saveImage(imageData, title: "_edited.jpg");
await ref.read(albumProvider.notifier).getDeviceAlbums();
Navigator.of(context).popUntil((route) => route.isFirst);
} catch (e) {
ImmichToast.show(
durationInSecond: 6,
context: context,
msg: 'Error: ${e.toString()}',
gravity: ToastGravity.BOTTOM,
);
}
},
child: Text(
'Save to gallery',
style: Theme.of(context).textTheme.displayMedium,
),
),
],
),
body: Column(
children: <Widget>[
Expanded(
child: Image(image: provider),
),
Container(
height: 80,
color: Theme.of(context).bottomAppBarTheme.color,
),
],
),
bottomNavigationBar: Container(
height: 80,
margin: const EdgeInsets.only(bottom: 20, right: 10, left: 10, top: 10),
decoration: BoxDecoration(
color: Theme.of(context).bottomAppBarTheme.color,
borderRadius: BorderRadius.circular(30),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
IconButton(
icon: Icon(
Platform.isAndroid
? Icons.crop_rotate_rounded
: Icons.crop_rotate_rounded,
color: Theme.of(context).iconTheme.color,
),
onPressed: () {
context.pushRoute(CropImageRoute(image: imageWidget));
},
),
Text('Crop', style: Theme.of(context).textTheme.displayMedium),
],
),
),
);
}
}

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -114,7 +115,7 @@ class SearchInputPage extends HookConsumerWidget {
);
peopleCurrentFilterWidget.value = Text(
value.map((e) => e.name != '' ? e.name : "No name").join(', '),
value.map((e) => e.name != '' ? e.name : 'no_name'.tr()).join(', '),
style: context.textTheme.labelLarge,
);
}
@@ -134,7 +135,7 @@ class SearchInputPage extends HookConsumerWidget {
child: FractionallySizedBox(
heightFactor: 0.8,
child: FilterBottomSheetScaffold(
title: 'Select people',
title: 'search_filter_people_title'.tr(),
expanded: true,
onSearch: search,
onClear: handleClear,
@@ -190,7 +191,7 @@ class SearchInputPage extends HookConsumerWidget {
isScrollControlled: true,
isDismissible: false,
child: FilterBottomSheetScaffold(
title: 'Select location',
title: 'search_filter_location_title'.tr(),
onSearch: search,
onClear: handleClear,
child: Padding(
@@ -241,7 +242,7 @@ class SearchInputPage extends HookConsumerWidget {
isScrollControlled: true,
isDismissible: false,
child: FilterBottomSheetScaffold(
title: 'Select camera type',
title: 'search_filter_camera_title'.tr(),
onSearch: search,
onClear: handleClear,
child: Padding(
@@ -268,14 +269,14 @@ class SearchInputPage extends HookConsumerWidget {
start: filter.value.date.takenAfter ?? lastDate,
end: filter.value.date.takenBefore ?? lastDate,
),
helpText: 'Select a date range',
cancelText: 'Cancel',
confirmText: 'Select',
saveText: 'Save',
errorFormatText: 'Invalid date format',
errorInvalidText: 'Invalid date',
fieldStartHintText: 'Start date',
fieldEndHintText: 'End date',
helpText: 'search_filter_date_title'.tr(),
cancelText: 'action_common_cancel'.tr(),
confirmText: 'action_common_select'.tr(),
saveText: 'action_common_save'.tr(),
errorFormatText: 'invalid_date_format'.tr(),
errorInvalidText: 'invalid_date'.tr(),
fieldStartHintText: 'start_date'.tr(),
fieldEndHintText: 'end_date'.tr(),
initialEntryMode: DatePickerEntryMode.input,
);
@@ -305,12 +306,17 @@ class SearchInputPage extends HookConsumerWidget {
// If date range is less than 24 hours, set the end date to the end of the day
if (date.end.difference(date.start).inHours < 24) {
dateRangeCurrentFilterWidget.value = Text(
date.start.toLocal().toIso8601String().split('T').first,
DateFormat.yMMMd().format(date.start.toLocal()),
style: context.textTheme.labelLarge,
);
} else {
dateRangeCurrentFilterWidget.value = Text(
'${date.start.toLocal().toIso8601String().split('T').first} to ${date.end.toLocal().toIso8601String().split('T').first}',
'search_filter_date_interval'.tr(
namedArgs: {
"start": DateFormat.yMMMd().format(date.start.toLocal()),
"end": DateFormat.yMMMd().format(date.end.toLocal()),
},
),
style: context.textTheme.labelLarge,
);
}
@@ -326,7 +332,11 @@ class SearchInputPage extends HookConsumerWidget {
);
mediaTypeCurrentFilterWidget.value = Text(
assetType == AssetType.image ? 'Image' : 'Video',
assetType == AssetType.image
? 'search_filter_media_type_image'.tr()
: assetType == AssetType.video
? 'search_filter_media_type_video'.tr()
: 'search_filter_media_type_all'.tr(),
style: context.textTheme.labelLarge,
);
}
@@ -343,7 +353,7 @@ class SearchInputPage extends HookConsumerWidget {
showFilterBottomSheet(
context: context,
child: FilterBottomSheetScaffold(
title: 'Select media type',
title: 'search_filter_media_type_title'.tr(),
onSearch: search,
onClear: handleClear,
child: MediaTypePicker(
@@ -367,7 +377,10 @@ class SearchInputPage extends HookConsumerWidget {
isNotInAlbum: value,
),
);
if (value) filterText.add('Not in album');
if (value) {
filterText
.add('search_filter_display_option_not_in_album'.tr());
}
break;
case DisplayOption.archive:
filter.value = filter.value.copyWith(
@@ -375,7 +388,9 @@ class SearchInputPage extends HookConsumerWidget {
isArchive: value,
),
);
if (value) filterText.add('Archive');
if (value) {
filterText.add('search_filter_display_option_archive'.tr());
}
break;
case DisplayOption.favorite:
filter.value = filter.value.copyWith(
@@ -383,7 +398,9 @@ class SearchInputPage extends HookConsumerWidget {
isFavorite: value,
),
);
if (value) filterText.add('Favorite');
if (value) {
filterText.add('search_filter_display_option_favorite'.tr());
}
break;
}
});
@@ -410,7 +427,7 @@ class SearchInputPage extends HookConsumerWidget {
showFilterBottomSheet(
context: context,
child: FilterBottomSheetScaffold(
title: 'Display options',
title: 'search_filter_display_options_title'.tr(),
onSearch: search,
onClear: handleClear,
child: DisplayOptionPicker(
@@ -489,8 +506,8 @@ class SearchInputPage extends HookConsumerWidget {
controller: textSearchController,
decoration: InputDecoration(
hintText: isContextualSearch.value
? 'Sunrise on the beach'
: 'File name or extension',
? 'contextual_search'.tr()
: 'filename_search'.tr(),
hintStyle: context.textTheme.bodyLarge?.copyWith(
color: context.themeData.colorScheme.onSurface.withOpacity(0.75),
fontWeight: FontWeight.w500,
@@ -519,37 +536,37 @@ class SearchInputPage extends HookConsumerWidget {
SearchFilterChip(
icon: Icons.people_alt_rounded,
onTap: showPeoplePicker,
label: 'People',
label: 'search_filter_people'.tr(),
currentFilter: peopleCurrentFilterWidget.value,
),
SearchFilterChip(
icon: Icons.location_pin,
onTap: showLocationPicker,
label: 'Location',
label: 'search_filter_location'.tr(),
currentFilter: locationCurrentFilterWidget.value,
),
SearchFilterChip(
icon: Icons.camera_alt_rounded,
onTap: showCameraPicker,
label: 'Camera',
label: 'search_filter_camera'.tr(),
currentFilter: cameraCurrentFilterWidget.value,
),
SearchFilterChip(
icon: Icons.date_range_rounded,
onTap: showDatePicker,
label: 'Date',
label: 'search_filter_date'.tr(),
currentFilter: dateRangeCurrentFilterWidget.value,
),
SearchFilterChip(
icon: Icons.video_collection_outlined,
onTap: showMediaTypePicker,
label: 'Media Type',
label: 'search_filter_media_type'.tr(),
currentFilter: mediaTypeCurrentFilterWidget.value,
),
SearchFilterChip(
icon: Icons.display_settings_outlined,
onTap: showDisplayOptionPicker,
label: 'Display Options',
label: 'search_filter_display_options'.tr(),
currentFilter: displayOptionCurrentFilterWidget.value,
),
],

View File

@@ -156,7 +156,6 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
Future<bool> setSuccessLoginInfo({
required String accessToken,
required String serverUrl,
bool offlineLogin = false,
}) async {
_apiService.setAccessToken(accessToken);
@@ -165,57 +164,56 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid;
bool shouldChangePassword = false;
User? user;
User? user = Store.tryGet(StoreKey.currentUser);
bool retResult = false;
User? offlineUser = Store.tryGet(StoreKey.currentUser);
// If the user is offline and there is a user saved on the device,
// if not try an online login
if (offlineLogin && offlineUser != null) {
user = offlineUser;
retResult = false;
} else {
UserAdminResponseDto? userResponseDto;
UserPreferencesResponseDto? userPreferences;
try {
userResponseDto = await _apiService.usersApi.getMyUser();
userPreferences = await _apiService.usersApi.getMyPreferences();
} on ApiException catch (error, stackTrace) {
_log.severe(
"Error getting user information from the server [API EXCEPTION]",
error,
stackTrace,
);
if (error.innerException is SocketException) {
state = state.copyWith(isAuthenticated: true);
}
} catch (error, stackTrace) {
_log.severe(
"Error getting user information from the server [CATCH ALL]",
error,
stackTrace,
);
}
if (userResponseDto != null) {
Store.put(StoreKey.deviceId, deviceId);
Store.put(StoreKey.deviceIdHash, fastHash(deviceId));
Store.put(
StoreKey.currentUser,
User.fromUserDto(userResponseDto, userPreferences),
);
Store.put(StoreKey.serverUrl, serverUrl);
Store.put(StoreKey.accessToken, accessToken);
shouldChangePassword = userResponseDto.shouldChangePassword;
user = User.fromUserDto(userResponseDto, userPreferences);
retResult = true;
} else {
_log.severe("Unable to get user information from the server.");
UserAdminResponseDto? userResponse;
UserPreferencesResponseDto? userPreferences;
try {
var responses = await Future.wait([
_apiService.usersApi.getMyUser(),
_apiService.usersApi.getMyPreferences(),
]);
userResponse = responses[0] as UserAdminResponseDto;
userPreferences = responses[1] as UserPreferencesResponseDto;
} on ApiException catch (error, stackTrace) {
if (error.code == 401) {
_log.severe("Unauthorized access, token likely expired. Logging out.");
return false;
}
_log.severe(
"Error getting user information from the server [API EXCEPTION]",
stackTrace,
);
} catch (error, stackTrace) {
_log.severe(
"Error getting user information from the server [CATCH ALL]",
error,
stackTrace,
);
}
// If the user information is successfully retrieved, update the store
// Due to the flow of the code, this will always happen on first login
if (userResponse != null) {
Store.put(StoreKey.deviceId, deviceId);
Store.put(StoreKey.deviceIdHash, fastHash(deviceId));
Store.put(
StoreKey.currentUser,
User.fromUserDto(userResponse, userPreferences),
);
Store.put(StoreKey.serverUrl, serverUrl);
Store.put(StoreKey.accessToken, accessToken);
shouldChangePassword = userResponse.shouldChangePassword;
user = User.fromUserDto(userResponse, userPreferences);
} else {
_log.severe("Unable to get user information from the server.");
}
// If the user is null, the login was not successful
// and we don't have a local copy of the user from a prior successful login
if (user == null) {
return false;
}
state = state.copyWith(
@@ -229,7 +227,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
deviceId: deviceId,
);
return retResult;
return true;
}
}

View File

@@ -28,6 +28,8 @@ import 'package:immich_mobile/pages/common/headers_settings.page.dart';
import 'package:immich_mobile/pages/common/settings.page.dart';
import 'package:immich_mobile/pages/common/splash_screen.page.dart';
import 'package:immich_mobile/pages/common/tab_controller.page.dart';
import 'package:immich_mobile/pages/editing/edit.page.dart';
import 'package:immich_mobile/pages/editing/crop.page.dart';
import 'package:immich_mobile/pages/library/archive.page.dart';
import 'package:immich_mobile/pages/library/favorite.page.dart';
import 'package:immich_mobile/pages/library/library.page.dart';
@@ -133,6 +135,8 @@ class AppRouter extends _$AppRouter {
page: CreateAlbumRoute.page,
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(page: EditImageRoute.page),
AutoRoute(page: CropImageRoute.page),
AutoRoute(page: FavoritesRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: AllVideosRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(

View File

@@ -165,6 +165,28 @@ abstract class _$AppRouter extends RootStackRouter {
),
);
},
CropImageRoute.name: (routeData) {
final args = routeData.argsAs<CropImageRouteArgs>();
return AutoRoutePage<dynamic>(
routeData: routeData,
child: CropImagePage(
key: args.key,
image: args.image,
),
);
},
EditImageRoute.name: (routeData) {
final args = routeData.argsAs<EditImageRouteArgs>(
orElse: () => const EditImageRouteArgs());
return AutoRoutePage<dynamic>(
routeData: routeData,
child: EditImagePage(
key: args.key,
image: args.image,
asset: args.asset,
),
);
},
FailedBackupStatusRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
@@ -836,6 +858,87 @@ class CreateAlbumRouteArgs {
}
}
/// generated route for
/// [CropImagePage]
class CropImageRoute extends PageRouteInfo<CropImageRouteArgs> {
CropImageRoute({
Key? key,
required Image image,
List<PageRouteInfo>? children,
}) : super(
CropImageRoute.name,
args: CropImageRouteArgs(
key: key,
image: image,
),
initialChildren: children,
);
static const String name = 'CropImageRoute';
static const PageInfo<CropImageRouteArgs> page =
PageInfo<CropImageRouteArgs>(name);
}
class CropImageRouteArgs {
const CropImageRouteArgs({
this.key,
required this.image,
});
final Key? key;
final Image image;
@override
String toString() {
return 'CropImageRouteArgs{key: $key, image: $image}';
}
}
/// generated route for
/// [EditImagePage]
class EditImageRoute extends PageRouteInfo<EditImageRouteArgs> {
EditImageRoute({
Key? key,
Image? image,
Asset? asset,
List<PageRouteInfo>? children,
}) : super(
EditImageRoute.name,
args: EditImageRouteArgs(
key: key,
image: image,
asset: asset,
),
initialChildren: children,
);
static const String name = 'EditImageRoute';
static const PageInfo<EditImageRouteArgs> page =
PageInfo<EditImageRouteArgs>(name);
}
class EditImageRouteArgs {
const EditImageRouteArgs({
this.key,
this.image,
this.asset,
});
final Key? key;
final Image? image;
final Asset? asset;
@override
String toString() {
return 'EditImageRouteArgs{key: $key, image: $image, asset: $asset}';
}
}
/// generated route for
/// [FailedBackupStatusPage]
class FailedBackupStatusRoute extends PageRouteInfo<void> {

View File

@@ -64,11 +64,13 @@ class ApiService implements Authentication {
}
Future<String> resolveAndSetEndpoint(String serverUrl) async {
final endpoint = await _resolveEndpoint(serverUrl);
var endpoint = Store.tryGet(StoreKey.serverEndpoint);
endpoint ??= await _resolveEndpoint(serverUrl);
setEndpoint(endpoint);
// Save in hivebox for next startup
Store.put(StoreKey.serverEndpoint, endpoint);
await Store.put(StoreKey.serverEndpoint, endpoint);
return endpoint;
}

View File

@@ -0,0 +1,12 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:crop_image/crop_image.dart';
import 'dart:ui'; // Import the dart:ui library for Rect
/// A hook that provides a [CropController] instance.
CropController useCropController() {
return useMemoized(
() => CropController(
defaultCrop: const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9),
),
);
}

View File

@@ -25,9 +25,7 @@ class HttpSSLCertOverride extends HttpOverrides {
try {
_log.info("Setting client certificate");
ctx.usePrivateKeyBytes(cert.data, password: cert.password);
if (!Platform.isIOS) {
ctx.useCertificateChainBytes(cert.data, password: cert.password);
}
ctx.useCertificateChainBytes(cert.data, password: cert.password);
} catch (e) {
_log.severe("Failed to set SSL client cert: $e");
return false;

View File

@@ -21,6 +21,7 @@ import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/pages/editing/edit.page.dart';
class BottomGalleryBar extends ConsumerWidget {
final Asset asset;
@@ -69,6 +70,12 @@ class BottomGalleryBar extends ConsumerWidget {
label: 'control_bottom_app_bar_share'.tr(),
tooltip: 'control_bottom_app_bar_share'.tr(),
),
if (asset.isImage)
BottomNavigationBarItem(
icon: const Icon(Icons.edit_outlined),
label: 'control_bottom_app_bar_edit'.tr(),
tooltip: 'control_bottom_app_bar_edit'.tr(),
),
if (isOwner)
asset.isArchived
? BottomNavigationBarItem(
@@ -280,6 +287,24 @@ class BottomGalleryBar extends ConsumerWidget {
ref.read(imageViewerStateProvider.notifier).shareAsset(asset, context);
}
void handleEdit() async {
if (asset.isOffline) {
ImmichToast.show(
durationInSecond: 1,
context: context,
msg: 'asset_action_edit_err_offline'.tr(),
gravity: ToastGravity.BOTTOM,
);
return;
}
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
EditImagePage(asset: asset), // Send the Asset object
),
);
}
handleArchive() {
ref.read(assetProvider.notifier).toggleArchive([asset]);
if (isParent) {
@@ -343,6 +368,7 @@ class BottomGalleryBar extends ConsumerWidget {
List<Function(int)> actionslist = [
(_) => shareAsset(),
if (asset.isImage) (_) => handleEdit(),
if (isOwner) (_) => handleArchive(),
if (isOwner && stack.isNotEmpty) (_) => showStackActionItems(),
if (isOwner) (_) => handleDelete(),

View File

@@ -273,6 +273,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.1"
crop_image:
dependency: "direct main"
description:
name: crop_image
sha256: "6cf20655ecbfba99c369d43ec7adcfa49bf135af88fb75642173d6224a95d3f1"
url: "https://pub.dev"
source: hosted
version: "1.0.13"
cross_file:
dependency: transitive
description:
@@ -1813,4 +1821,4 @@ packages:
version: "3.1.2"
sdks:
dart: ">=3.4.0 <4.0.0"
flutter: ">=3.22.2"
flutter: ">=3.22.3"

View File

@@ -6,7 +6,7 @@ version: 1.110.0+151
environment:
sdk: '>=3.3.0 <4.0.0'
flutter: 3.22.2
flutter: 3.22.3
dependencies:
flutter:
@@ -62,6 +62,8 @@ dependencies:
thumbhash: 0.1.0+1
async: ^2.11.0
#image editing packages
crop_image: ^1.0.13
openapi:
path: openapi

View File

@@ -1 +1 @@
20.15.1
20.16.0

View File

@@ -28,6 +28,6 @@
"directory": "open-api/typescript-sdk"
},
"volta": {
"node": "20.15.1"
"node": "20.16.0"
}
}

View File

@@ -1 +1 @@
20.15.1
20.16.0

261
server/package-lock.json generated
View File

@@ -60,6 +60,7 @@
"semver": "^7.6.2",
"sharp": "^0.33.0",
"sirv": "^2.0.4",
"tailwindcss-preset-email": "^1.3.2",
"thumbhash": "^0.1.1",
"typeorm": "^0.3.17",
"ua-parser-js": "^1.0.35"
@@ -82,7 +83,7 @@
"@types/multer": "^1.4.7",
"@types/node": "^20.14.12",
"@types/nodemailer": "^6.4.14",
"@types/picomatch": "^2.3.3",
"@types/picomatch": "^3.0.0",
"@types/semver": "^7.5.8",
"@types/supertest": "^6.0.0",
"@types/ua-parser-js": "^0.7.36",
@@ -103,7 +104,8 @@
"typescript": "^5.3.3",
"unplugin-swc": "^1.4.5",
"utimes": "^5.2.1",
"vitest": "^1.5.0"
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -6309,9 +6311,9 @@
}
},
"node_modules/@types/picomatch": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.4.tgz",
"integrity": "sha512-0so8lU8O5zatZS/2Fi4zrwks+vZv7e0dygrgEZXljODXBig97l4cPQD+9LabXfGJOWwoRkTVz6Q4edZvD12UOA==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-3.0.0.tgz",
"integrity": "sha512-iX/Qwk9vU17N/5Q7QrV46wzciloTdCqTRt6z8A7uFFADM2+Sy5oQh9ldZhAiTXH+l0sM/EkXatEhJIs8FUyOBQ==",
"dev": true
},
"node_modules/@types/prismjs": {
@@ -10373,6 +10375,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/globrex": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
"dev": true
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@@ -13831,6 +13839,11 @@
"csstype": "^3.0.2"
}
},
"node_modules/react-email/node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
},
"node_modules/react-email/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@@ -13981,6 +13994,53 @@
"node": ">=0.10.0"
}
},
"node_modules/react-email/node_modules/tailwindcss": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz",
"integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
"chokidar": "^3.5.3",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.3.0",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.19.1",
"lilconfig": "^2.1.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
"object-hash": "^3.0.0",
"picocolors": "^1.0.0",
"postcss": "^8.4.23",
"postcss-import": "^15.1.0",
"postcss-js": "^4.0.1",
"postcss-load-config": "^4.0.1",
"postcss-nested": "^6.0.1",
"postcss-selector-parser": "^6.0.11",
"resolve": "^1.22.2",
"sucrase": "^3.32.0"
},
"bin": {
"tailwind": "lib/cli.js",
"tailwindcss": "lib/cli.js"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/react-email/node_modules/tailwindcss/node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dependencies": {
"is-glob": "^4.0.3"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/react-email/node_modules/typescript": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
@@ -15507,9 +15567,10 @@
}
},
"node_modules/tailwindcss": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz",
"integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==",
"version": "3.4.6",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.6.tgz",
"integrity": "sha512-1uRHzPB+Vzu57ocybfZ4jh5Q3SdlH7XW23J5sQoM9LhE9eIOlzxer/3XPSsycvih3rboRsvt0QCmzSrqyOYUIA==",
"peer": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -15519,7 +15580,7 @@
"fast-glob": "^3.3.0",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.19.1",
"jiti": "^1.21.0",
"lilconfig": "^2.1.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
@@ -15542,15 +15603,48 @@
"node": ">=14.0.0"
}
},
"node_modules/tailwindcss-email-variants": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/tailwindcss-email-variants/-/tailwindcss-email-variants-3.0.1.tgz",
"integrity": "sha512-bRk4R2jnfaW7BBaL2kDgOdBl0SpVP/JPDE/yCkZb1n3YrPK9ZQyQGZoVX3OX06GxjMOrNO3wZACVdHJce7dm8w==",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"tailwindcss": ">=3.4.0"
}
},
"node_modules/tailwindcss-mso": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/tailwindcss-mso/-/tailwindcss-mso-1.4.3.tgz",
"integrity": "sha512-8YfZ4xnIComDrhoSr8FUwm7EGz1FkxsZy07Fs4Jm/JxHrFiubdiZjyxLuHMc3S8o02+U4fjRGHPOzoVXRus10A==",
"peerDependencies": {
"tailwindcss": ">=3.4.0"
}
},
"node_modules/tailwindcss-preset-email": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/tailwindcss-preset-email/-/tailwindcss-preset-email-1.3.2.tgz",
"integrity": "sha512-kSPNZM5+tSi+uhCb4rk1XF9Q6zp8lhoNLCa3GQqe6gKmfI/nTqY8Y+5/DYNpwqhmUPCSHULlyI/LUCaF/q8sLg==",
"dependencies": {
"tailwindcss-email-variants": "^3.0.0",
"tailwindcss-mso": "^1.4.3"
},
"peerDependencies": {
"tailwindcss": ">=3.4.6"
}
},
"node_modules/tailwindcss/node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"peer": true
},
"node_modules/tailwindcss/node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"peer": true,
"dependencies": {
"is-glob": "^4.0.3"
},
@@ -16147,6 +16241,26 @@
}
}
},
"node_modules/tsconfck": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.1.tgz",
"integrity": "sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ==",
"dev": true,
"bin": {
"tsconfck": "bin/tsconfck.js"
},
"engines": {
"node": "^18 || >=20"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/tsconfig-paths": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
@@ -16792,6 +16906,25 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/vite-tsconfig-paths": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz",
"integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==",
"dev": true,
"dependencies": {
"debug": "^4.1.1",
"globrex": "^0.1.2",
"tsconfck": "^3.0.3"
},
"peerDependencies": {
"vite": "*"
},
"peerDependenciesMeta": {
"vite": {
"optional": true
}
}
},
"node_modules/vitest": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz",
@@ -21055,9 +21188,9 @@
}
},
"@types/picomatch": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.4.tgz",
"integrity": "sha512-0so8lU8O5zatZS/2Fi4zrwks+vZv7e0dygrgEZXljODXBig97l4cPQD+9LabXfGJOWwoRkTVz6Q4edZvD12UOA==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-3.0.0.tgz",
"integrity": "sha512-iX/Qwk9vU17N/5Q7QrV46wzciloTdCqTRt6z8A7uFFADM2+Sy5oQh9ldZhAiTXH+l0sM/EkXatEhJIs8FUyOBQ==",
"dev": true
},
"@types/prismjs": {
@@ -24050,6 +24183,12 @@
"slash": "^3.0.0"
}
},
"globrex": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
"dev": true
},
"gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@@ -26373,6 +26512,11 @@
"csstype": "^3.0.2"
}
},
"arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
},
"brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@@ -26476,6 +26620,45 @@
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
},
"tailwindcss": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz",
"integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==",
"requires": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
"chokidar": "^3.5.3",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.3.0",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.19.1",
"lilconfig": "^2.1.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
"object-hash": "^3.0.0",
"picocolors": "^1.0.0",
"postcss": "^8.4.23",
"postcss-import": "^15.1.0",
"postcss-js": "^4.0.1",
"postcss-load-config": "^4.0.1",
"postcss-nested": "^6.0.1",
"postcss-selector-parser": "^6.0.11",
"resolve": "^1.22.2",
"sucrase": "^3.32.0"
},
"dependencies": {
"glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"requires": {
"is-glob": "^4.0.3"
}
}
}
},
"typescript": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
@@ -27591,9 +27774,10 @@
}
},
"tailwindcss": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz",
"integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==",
"version": "3.4.6",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.6.tgz",
"integrity": "sha512-1uRHzPB+Vzu57ocybfZ4jh5Q3SdlH7XW23J5sQoM9LhE9eIOlzxer/3XPSsycvih3rboRsvt0QCmzSrqyOYUIA==",
"peer": true,
"requires": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -27603,7 +27787,7 @@
"fast-glob": "^3.3.0",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.19.1",
"jiti": "^1.21.0",
"lilconfig": "^2.1.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
@@ -27622,18 +27806,41 @@
"arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"peer": true
},
"glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"peer": true,
"requires": {
"is-glob": "^4.0.3"
}
}
}
},
"tailwindcss-email-variants": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/tailwindcss-email-variants/-/tailwindcss-email-variants-3.0.1.tgz",
"integrity": "sha512-bRk4R2jnfaW7BBaL2kDgOdBl0SpVP/JPDE/yCkZb1n3YrPK9ZQyQGZoVX3OX06GxjMOrNO3wZACVdHJce7dm8w==",
"requires": {}
},
"tailwindcss-mso": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/tailwindcss-mso/-/tailwindcss-mso-1.4.3.tgz",
"integrity": "sha512-8YfZ4xnIComDrhoSr8FUwm7EGz1FkxsZy07Fs4Jm/JxHrFiubdiZjyxLuHMc3S8o02+U4fjRGHPOzoVXRus10A==",
"requires": {}
},
"tailwindcss-preset-email": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/tailwindcss-preset-email/-/tailwindcss-preset-email-1.3.2.tgz",
"integrity": "sha512-kSPNZM5+tSi+uhCb4rk1XF9Q6zp8lhoNLCa3GQqe6gKmfI/nTqY8Y+5/DYNpwqhmUPCSHULlyI/LUCaF/q8sLg==",
"requires": {
"tailwindcss-email-variants": "^3.0.0",
"tailwindcss-mso": "^1.4.3"
}
},
"tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
@@ -28090,6 +28297,13 @@
"yn": "3.1.1"
}
},
"tsconfck": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.1.tgz",
"integrity": "sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ==",
"dev": true,
"requires": {}
},
"tsconfig-paths": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
@@ -28435,6 +28649,17 @@
"vite": "^5.0.0"
}
},
"vite-tsconfig-paths": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz",
"integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==",
"dev": true,
"requires": {
"debug": "^4.1.1",
"globrex": "^0.1.2",
"tsconfck": "^3.0.3"
}
},
"vitest": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz",

View File

@@ -86,6 +86,7 @@
"semver": "^7.6.2",
"sharp": "^0.33.0",
"sirv": "^2.0.4",
"tailwindcss-preset-email": "^1.3.2",
"thumbhash": "^0.1.1",
"typeorm": "^0.3.17",
"ua-parser-js": "^1.0.35"
@@ -108,7 +109,7 @@
"@types/multer": "^1.4.7",
"@types/node": "^20.14.12",
"@types/nodemailer": "^6.4.14",
"@types/picomatch": "^2.3.3",
"@types/picomatch": "^3.0.0",
"@types/semver": "^7.5.8",
"@types/supertest": "^6.0.0",
"@types/ua-parser-js": "^0.7.36",
@@ -129,9 +130,10 @@
"typescript": "^5.3.3",
"unplugin-swc": "^1.4.5",
"utimes": "^5.2.1",
"vitest": "^1.5.0"
"vitest": "^1.6.0",
"vite-tsconfig-paths": "^4.3.2"
},
"volta": {
"node": "20.15.1"
"node": "20.16.0"
}
}

View File

@@ -1,21 +1,8 @@
import {
Body,
Button,
Column,
Container,
Head,
Hr,
Html,
Img,
Link,
Preview,
Row,
Section,
Text,
} from '@react-email/components';
import * as CSS from 'csstype';
import { Img, Link, Section, Text } from '@react-email/components';
import * as React from 'react';
import { AlbumInviteEmailProps } from 'src/interfaces/notification.interface';
import { ImmichButton } from './components/button.component';
import ImmichLayout from './components/immich.layout';
export const AlbumInviteEmail = ({
baseUrl,
@@ -25,122 +12,37 @@ export const AlbumInviteEmail = ({
albumId,
cid,
}: AlbumInviteEmailProps) => (
<Html>
<Head />
<Preview>You have been added to a shared album.</Preview>
<Body
style={{
margin: 0,
padding: 0,
backgroundColor: '#ffffff',
color: 'rgb(28,28,28)',
fontFamily: 'Overpass, sans-serif',
fontSize: '18px',
lineHeight: '24px',
}}
>
<Container
style={{
width: '540px',
maxWidth: '100%',
padding: '10px',
margin: '0 auto',
}}
>
<Section
<ImmichLayout preview="You have been added to a shared album.">
<Text className="m-0">
Hey <strong>{recipientName}</strong>!
</Text>
<Text>
{senderName} has added you to the album <strong>{albumName}</strong>.
</Text>
{cid && (
<Section className="flex justify-center my-0">
<Img
className="max-w-[300px] w-full rounded-lg"
src={`cid:${cid}`}
style={{
padding: '36px',
tableLayout: 'fixed',
backgroundColor: 'rgb(226, 232, 240)',
border: 'solid 0px rgb(248 113 113)',
borderRadius: '50px',
textAlign: 'center' as const,
boxShadow: 'rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px',
}}
>
<Img
src="https://immich.app/img/immich-logo-inline-light.png"
alt="Immich"
style={{
height: 'auto',
margin: '0 auto 48px auto',
width: '50%',
alignSelf: 'center',
color: 'white',
}}
/>
/>
</Section>
)}
<Text style={text}>Hey {recipientName}!</Text>
<Section className="flex justify-center my-6">
<ImmichButton href={`${baseUrl}/albums/${albumId}`}>View Album</ImmichButton>
</Section>
<Text style={text}>
{senderName} has added you to the album <strong>{albumName}</strong>.
</Text>
{cid && (
<Row>
<Column align="center">
<Img
src={`cid:${cid}`}
width="300"
style={{
borderRadius: '20px',
boxShadow: 'rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px',
}}
/>
</Column>
</Row>
)}
<Row style={{ marginBottom: '36px', marginTop: '36px' }}>
<Text style={{ ...text }}>To view the album, open the link in a browser, or click the button below.</Text>
</Row>
<Row>
<Column align="center">
<Link style={{ marginTop: '50px' }} href={`${baseUrl}/albums/${albumId}`}>
{baseUrl}/albums/{albumId}
</Link>
</Column>
</Row>
<Row>
<Column align="center">
<Button style={button} href={`${baseUrl}/albums/${albumId}`}>
View album
</Button>
</Column>
</Row>
</Section>
<Hr style={{ color: 'rgb(66, 80, 175)', marginTop: '24px' }} />
<Section style={{ textAlign: 'center' }}>
<Row>
<Column align="center">
<Link href="https://play.google.com/store/apps/details?id=app.alextran.immich">
<Img src={`https://immich.app/img/google-play-badge.png`} height="96px" alt="Immich" />
</Link>
<Link href="https://apps.apple.com/sg/app/immich/id1613945652">
<Img
src={`https://immich.app/img/ios-app-store-badge.png`}
alt="Immich"
style={{ height: '72px', padding: '14px' }}
/>
</Link>
</Column>
</Row>
</Section>
<Text
style={{
color: '#6a737d',
fontSize: '0.8rem',
textAlign: 'center' as const,
marginTop: '12px',
}}
>
<Link href="https://immich.app">Immich</Link> project is available under GNU AGPL v3 license.
</Text>
</Container>
</Body>
</Html>
<Text className="text-xs">
If you cannot click the button use the link below to view the album.
<br />
<Link href={`${baseUrl}/albums/${albumId}`}>{`${baseUrl}/albums/${albumId}`}</Link>
</Text>
</ImmichLayout>
);
AlbumInviteEmail.PreviewProps = {
@@ -148,27 +50,7 @@ AlbumInviteEmail.PreviewProps = {
albumName: 'Trip to Europe',
albumId: 'b63f6dae-e1c9-401b-9a85-9dbbf5612539',
senderName: 'Owner User',
recipientName: 'Guest User',
cid: '',
recipientName: 'Alan Turing',
} as AlbumInviteEmailProps;
export default AlbumInviteEmail;
const text = {
margin: '0 0 24px 0',
textAlign: 'left' as const,
fontSize: '18px',
lineHeight: '24px',
};
const button: CSS.Properties = {
backgroundColor: 'rgb(66, 80, 175)',
margin: '1em 0',
padding: '0.75em 3em',
color: '#fff',
fontSize: '1em',
fontWeight: 700,
lineHeight: 1.5,
textTransform: 'uppercase',
borderRadius: '9999px',
};

View File

@@ -1,165 +1,49 @@
import {
Body,
Button,
Column,
Container,
Head,
Hr,
Html,
Img,
Link,
Preview,
Row,
Section,
Text,
} from '@react-email/components';
import * as CSS from 'csstype';
import { Img, Link, Section, Text } from '@react-email/components';
import * as React from 'react';
import { AlbumUpdateEmailProps } from 'src/interfaces/notification.interface';
import { ImmichButton } from './components/button.component';
import ImmichLayout from './components/immich.layout';
export const AlbumUpdateEmail = ({ baseUrl, albumName, recipientName, albumId, cid }: AlbumUpdateEmailProps) => (
<Html>
<Head />
<Preview>New media has been added to a shared album.</Preview>
<Body
style={{
margin: 0,
padding: 0,
backgroundColor: '#ffffff',
color: 'rgb(28,28,28)',
fontFamily: 'Overpass, sans-serif',
fontSize: '18px',
lineHeight: '24px',
}}
>
<Container
style={{
width: '540px',
maxWidth: '100%',
padding: '10px',
margin: '0 auto',
}}
>
<Section
<ImmichLayout preview="New media has been added to a shared album.">
<Text className="m-0">
Hey <strong>{recipientName}</strong>!
</Text>
<Text>
New media has been added to <strong>{albumName}</strong>,
<br /> check it out!
</Text>
{cid && (
<Section className="flex justify-center my-0">
<Img
className="max-w-[300px] w-full rounded-lg"
src={`cid:${cid}`}
style={{
padding: '36px',
tableLayout: 'fixed',
backgroundColor: 'rgb(226, 232, 240)',
border: 'solid 0px rgb(248 113 113)',
borderRadius: '50px',
textAlign: 'center' as const,
boxShadow: 'rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px',
}}
>
<Img
src="https://immich.app/img/immich-logo-inline-light.png"
alt="Immich"
style={{
height: 'auto',
margin: '0 auto 48px auto',
width: '50%',
alignSelf: 'center',
color: 'white',
}}
/>
/>
</Section>
)}
<Text style={text}>Hey {recipientName}!</Text>
<Section className="flex justify-center my-6">
<ImmichButton href={`${baseUrl}/albums/${albumId}`}>View Album</ImmichButton>
</Section>
<Text style={text}>
New media has been added to <strong>{albumName}</strong>, check it out!
</Text>
{cid && (
<Row>
<Column align="center">
<Img
src={`cid:${cid}`}
width="300"
style={{
borderRadius: '20px',
boxShadow: 'rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px',
}}
/>
</Column>
</Row>
)}
<Row style={{ marginBottom: '36px', marginTop: '36px' }}>
<Text style={{ ...text }}>To view the album, open the link in a browser, or click the button below.</Text>
</Row>
<Row>
<Column align="center">
<Link style={{ marginTop: '50px' }} href={`${baseUrl}/albums/${albumId}`}>
{baseUrl}/albums/{albumId}
</Link>
</Column>
</Row>
<Row>
<Column align="center">
<Button style={button} href={`${baseUrl}/albums/${albumId}`}>
View album
</Button>
</Column>
</Row>
</Section>
<Hr style={{ color: 'rgb(66, 80, 175)', marginTop: '24px' }} />
<Section style={{ textAlign: 'center' }}>
<Row>
<Column align="center">
<Link href="https://play.google.com/store/apps/details?id=app.alextran.immich">
<Img src={`https://immich.app/img/google-play-badge.png`} height="96px" alt="Immich" />
</Link>
<Link href="https://apps.apple.com/sg/app/immich/id1613945652">
<Img
src={`https://immich.app/img/ios-app-store-badge.png`}
alt="Immich"
style={{ height: '72px', padding: '14px' }}
/>
</Link>
</Column>
</Row>
</Section>
<Text
style={{
color: '#6a737d',
fontSize: '0.8rem',
textAlign: 'center' as const,
marginTop: '12px',
}}
>
<Link href="https://immich.app">Immich</Link> project is available under GNU AGPL v3 license.
</Text>
</Container>
</Body>
</Html>
<Text className="text-xs">
If you cannot click the button use the link below to view the album.
<br />
<Link href={`${baseUrl}/albums/${albumId}`}>{`${baseUrl}/albums/${albumId}`}</Link>
</Text>
</ImmichLayout>
);
AlbumUpdateEmail.PreviewProps = {
baseUrl: 'https://demo.immich.app',
albumName: 'Trip to Europe',
albumId: 'b63f6dae-e1c9-401b-9a85-9dbbf5612539',
recipientName: 'Alex Tran',
recipientName: 'Alan Turing',
} as AlbumUpdateEmailProps;
export default AlbumUpdateEmail;
const text = {
margin: '0 0 24px 0',
textAlign: 'left' as const,
fontSize: '18px',
lineHeight: '24px',
};
const button: CSS.Properties = {
backgroundColor: 'rgb(66, 80, 175)',
margin: '1em 0',
padding: '0.75em 3em',
color: '#fff',
fontSize: '1em',
fontWeight: 700,
lineHeight: 1.5,
textTransform: 'uppercase',
borderRadius: '9999px',
};

View File

@@ -0,0 +1,14 @@
import React from 'react';
import { Button, ButtonProps } from '@react-email/components';
interface ImmichButtonProps extends ButtonProps {}
export const ImmichButton = ({ children, ...props }: ImmichButtonProps) => (
<Button
{...props}
className="py-3 px-8 border bg-immich-primary rounded-full no-underline hover:no-underline text-white hover:text-gray-50 font-bold uppercase"
>
{children}
</Button>
);

View File

@@ -0,0 +1,25 @@
import { Column, Img, Link, Row, Text } from '@react-email/components';
import * as React from 'react';
export const ImmichFooter = () => (
<>
<Row className="h-18 w-full">
<Column align="center" className="w-6/12 sm:w-full">
<Link href="https://play.google.com/store/apps/details?id=app.alextran.immich">
<Img className="h-full max-w-full" src={`https://immich.app/img/google-play-badge.png`} />
</Link>
</Column>
<Column align="center" className="w-6/12 sm:w-full">
<div className="h-full p-3">
<Link href="https://apps.apple.com/sg/app/immich/id1613945652">
<Img src={`https://immich.app/img/ios-app-store-badge.png`} alt="Immich" className="max-w-full" />
</Link>
</div>
</Column>
</Row>
<Text className="text-center text-sm text-immich-footer">
<Link href="https://immich.app">Immich</Link> project is available under GNU AGPL v3 license.
</Text>
</>
);

View File

@@ -0,0 +1,93 @@
import {
Body,
Container,
Font,
Head,
Hr,
Html,
Img,
Link,
Preview,
Section,
Tailwind,
Text,
} from '@react-email/components';
import * as React from 'react';
import { ImmichFooter } from './footer.template';
interface FutoLayoutProps {
children: React.ReactNode;
preview: string;
}
export const FutoLayout = ({ children, preview }: FutoLayoutProps) => (
<Html>
<Tailwind
config={{
presets: [require('tailwindcss-preset-email')],
theme: {
extend: {
colors: {
// Light Theme
'immich-primary': '#4250AF',
'futo-primary': '#000000',
'futo-bg': '#F4F4f4',
'futo-gray': '#F6F6F4',
'futo-footer': '#6A737D',
},
fontFamily: {
sans: ['Overpass', 'sans-serif'],
mono: ['Overpass Mono', 'monospace'],
},
},
},
}}
>
<Head>
<Font
fontFamily="Overpass"
fallbackFontFamily="sans-serif"
webFont={{
url: 'https://fonts.gstatic.com/s/overpass/v13/qFdH35WCmI96Ajtm81GrU9vyww.woff2',
format: 'woff2',
}}
fontWeight={'100 900'}
fontStyle="normal"
/>
</Head>
<Preview>{preview}</Preview>
<Body className="bg-futo-bg my-auto mx-auto px-2 font-sans text-base text-futo-primary">
<Container className="my-[40px] mx-auto max-w-[465px]">
<Section className="my-6 p-12 border border-red-400 rounded-[50px] bg-gray-50">
<Section className="flex justify-center mb-12">
<Img
src="https://immich.app/img/immich-logo-inline-light.png"
className="h-12 antialiased rounded-none"
alt="Immich"
/>
</Section>
{children}
</Section>
<Section className="flex justify-center my-6">
<Link href="https://futo.org">
<Img className="h-6" src="https://futo.org/images/FutoMainLogo.svg" alt="FUTO" />
</Link>
</Section>
<Hr className="my-2 text-futo-gray" />
<ImmichFooter />
</Container>
</Body>
</Tailwind>
</Html>
);
FutoLayout.PreviewProps = {
preview: 'This is the preview shown on some mail clients',
children: <Text>Email body goes here.</Text>,
} as FutoLayoutProps;
export default FutoLayout;

View File

@@ -0,0 +1,74 @@
import { Body, Container, Font, Head, Hr, Html, Img, Preview, Section, Tailwind, Text } from '@react-email/components';
import * as React from 'react';
import { ImmichFooter } from './footer.template';
interface ImmichLayoutProps {
children: React.ReactNode;
preview: string;
}
export const ImmichLayout = ({ children, preview }: ImmichLayoutProps) => (
<Html>
<Tailwind
config={{
presets: [require('tailwindcss-preset-email')],
theme: {
extend: {
colors: {
// Light Theme
'immich-primary': '#4250AF',
'immich-bg': 'white',
'immich-fg': 'black',
'immich-gray': '#F6F6F4',
'immich-footer': '#6A737D',
},
fontFamily: {
sans: ['Overpass', 'sans-serif'],
mono: ['Overpass Mono', 'monospace'],
},
},
},
}}
>
<Head>
<Font
fontFamily="Overpass"
fallbackFontFamily="sans-serif"
webFont={{
url: 'https://fonts.gstatic.com/s/overpass/v13/qFdH35WCmI96Ajtm81GrU9vyww.woff2',
format: 'woff2',
}}
fontWeight={'100 900'}
fontStyle="normal"
/>
</Head>
<Preview>{preview}</Preview>
<Body className="bg-[#F4F4f4] my-auto mx-auto px-2 font-sans text-base text-gray-800">
<Container className="my-[40px] mx-auto max-w-[465px]">
<Section className="my-6 p-12 border border-red-400 rounded-[50px] bg-gray-50">
<Section className="flex justify-center mb-12">
<Img
src="https://immich.app/img/immich-logo-inline-light.png"
className="h-12 antialiased rounded-none"
alt="Immich"
/>
</Section>
{children}
</Section>
<Hr className="my-2 text-immich-gray" />
<ImmichFooter />
</Container>
</Body>
</Tailwind>
</Html>
);
ImmichLayout.PreviewProps = {
preview: 'This is the preview shown on some mail clients',
children: <Text>Email body goes here.</Text>,
} as ImmichLayoutProps;
export default ImmichLayout;

View File

@@ -15,172 +15,50 @@ import {
} from '@react-email/components';
import * as CSS from 'csstype';
import * as React from 'react';
import { ImmichButton } from './components/button.component';
import FutoLayout from './components/futo.layout';
/**
* Template to be used for FUTOPay project
* Variable is {{LICENSEKEY}}
* */
export const LicenseEmail = () => (
<Html>
<Head />
<Preview>Your Immich Server License</Preview>
<Body
style={{
margin: 0,
padding: 0,
backgroundColor: '#f4f4f4',
color: 'rgb(28,28,28)',
fontFamily: 'Overpass, sans-serif',
fontSize: '18px',
lineHeight: '24px',
}}
>
<Container
style={{
width: '540px',
maxWidth: '100%',
padding: '10px',
margin: '0 auto',
}}
<FutoLayout preview="Your Immich Server License">
<Text>Thank you for supporting Immich and open-source software</Text>
<Text>
Your <strong>Immich</strong> key is
</Text>
<Section className="my-2 bg-gray-200 rounded-2xl text-center p-4">
<Text className="m-0 text-monospace font-bold text-immich-primary">{'{{LICENSEKEY}}'}</Text>
</Section>
<Text>
To activate your instance, you can click the following button or copy and paste the link below to your browser.
</Text>
<Section className="flex justify-center my-6">
<ImmichButton
href={`https://my.immich.app/link?target=activate_license&licenseKey={{LICENSEKEY}}&activationKey={{ACTIVATIONKEY}}`}
>
<Section
style={{
padding: '36px',
tableLayout: 'fixed',
backgroundColor: '#fefefe',
borderRadius: '16px',
textAlign: 'center' as const,
}}
>
<Img
src="https://immich.app/img/immich-logo-inline-light.png"
alt="Immich"
style={{
height: 'auto',
margin: '0 auto 48px auto',
width: '50%',
alignSelf: 'center',
color: 'white',
}}
/>
Activate
</ImmichButton>
</Section>
<Text style={text}>Thank you for supporting Immich and open-source software</Text>
<Text style={text}>
Your <strong>Immich</strong> license key is
</Text>
<Section
style={{
textAlign: 'center',
background: 'rgb(225, 225, 225)',
borderRadius: '16px',
marginBottom: '25px',
}}
>
<Text style={{ fontFamily: 'monospace', fontWeight: 600, color: 'rgb(66, 80, 175)' }}>
{'{{LICENSEKEY}}'}
</Text>
</Section>
{/* <Text style={text}>
To activate your instance, you can click the following button or copy and paste the link below to your
browser
</Text>
<Row>
<Column align="center">
<Button
style={button}
href={`https://my.immich.app/link?target=activate_license&licenseKey={{LICENSEKEY}}&activationKey={{ACTIVATIONKEY}}`}
>
Activate
</Button>
</Column>
</Row>
<Row>
<Column align="center">
<a
style={{ marginTop: '50px', color: 'rgb(66, 80, 175)', fontSize: '0.9rem' }}
href={`https://my.immich.app/link?target=activate_license&licenseKey={{LICENSEKEY}}&activationKey={{ACTIVATIONKEY}}`}
>
https://my.immich.app/link?target=activate_license&licenseKey={'{{LICENSEKEY}}'}&activationKey=
{'{{ACTIVATIONKEY}}'}
</a>
</Column>
</Row> */}
</Section>
<Section style={{ textAlign: 'center' }}>
<Row>
<Column align="center">
<Link href="https://futo.org">
<Img
src="https://futo.org/images/FutoMainLogo.svg"
alt="FUTO"
style={{
height: '24px',
marginTop: '25px',
marginBottom: '25px',
}}
/>
</Link>
</Column>
</Row>
</Section>
<Hr style={{ color: 'rgb(66, 80, 175)', marginTop: '0' }} />
<Section style={{ textAlign: 'center' }}>
<Column align="center">
<Link href="https://apps.apple.com/sg/app/immich/id1613945652">
<Img
src={`https://immich.app/img/ios-app-store-badge.png`}
alt="Immich"
style={{ height: '72px', padding: '14px' }}
/>
</Link>
<Link href="https://play.google.com/store/apps/details?id=app.alextran.immich">
<Img src={`https://immich.app/img/google-play-badge.png`} height="96px" alt="Immich" />
</Link>
</Column>
</Section>
<Text
style={{
color: '#6a737d',
fontSize: '0.8rem',
textAlign: 'center' as const,
marginTop: '14px',
}}
>
<Link href="https://immich.app">Immich</Link> project is available under GNU AGPL v3 license.
</Text>
</Container>
</Body>
</Html>
<Text className="text-center">
<Link
className="text-immich-primary text-sm"
// style={{ marginTop: '50px', color: 'rgb(66, 80, 175)', fontSize: '0.9rem' }}
href={`https://my.immich.app/link?target=activate_license&licenseKey={{LICENSEKEY}}&activationKey={{ACTIVATIONKEY}}`}
>
https://my.immich.app/link?target=activate_license&licenseKey={'{{LICENSEKEY}}'}&activationKey=
{'{{ACTIVATIONKEY}}'}
</Link>
</Text>
</FutoLayout>
);
LicenseEmail.PreviewProps = {};
export default LicenseEmail;
const text = {
margin: '0 0 24px 0',
textAlign: 'left' as const,
fontSize: '16px',
lineHeight: '24px',
};
const button: CSS.Properties = {
backgroundColor: 'rgb(66, 80, 175)',
margin: '1em 0',
padding: '0.75em 3em',
color: '#fff',
fontSize: '1em',
fontWeight: 600,
lineHeight: 1.5,
textTransform: 'uppercase',
borderRadius: '9999px',
};

View File

@@ -1,134 +1,25 @@
import {
Body,
Button,
Column,
Container,
Head,
Hr,
Html,
Img,
Link,
Preview,
Row,
Section,
Text,
} from '@react-email/components';
import * as CSS from 'csstype';
import { Link, Row, Text } from '@react-email/components';
import * as React from 'react';
import { TestEmailProps } from 'src/interfaces/notification.interface';
import ImmichLayout from './components/immich.layout';
export const TestEmail = ({ baseUrl, displayName }: TestEmailProps) => (
<Html>
<Head />
<Preview>This is a test email from Immich</Preview>
<Body
style={{
margin: 0,
padding: 0,
backgroundColor: '#ffffff',
color: 'rgb(66, 80, 175)',
fontFamily: 'Overpass, sans-serif',
fontSize: '18px',
lineHeight: '24px',
}}
>
<Container
style={{
width: '480px',
maxWidth: '100%',
padding: '10px',
margin: '0 auto',
}}
>
<Section
style={{
padding: '36px',
tableLayout: 'fixed',
backgroundColor: 'rgb(226, 232, 240)',
border: 'solid 0px rgb(248 113 113)',
borderRadius: '50px',
textAlign: 'center' as const,
}}
>
<Img
src="https://immich.app/img/immich-logo-inline-light.png"
alt="Immich"
style={{
height: 'auto',
margin: '0 auto 48px auto',
width: '50%',
alignSelf: 'center',
color: 'white',
}}
/>
<ImmichLayout preview="This is a test email from Immich.">
<Text className="m-0">
Hey <strong>{displayName}</strong>!
</Text>
<Text style={text}>
Hey <strong>{displayName}</strong>, this is the test email from your Immich Instance
</Text>
<Text>This is a test email from your Immich Instance!</Text>
<Row>
<Link style={{ marginTop: '50px' }} href={baseUrl}>
{baseUrl}
</Link>
</Row>
</Section>
<Hr style={{ color: 'rgb(66, 80, 175)', marginTop: '24px' }} />
<Section style={{ textAlign: 'center' }}>
<Row>
<Column align="center">
<Link href="https://play.google.com/store/apps/details?id=app.alextran.immich">
<Img src={`https://immich.app/img/google-play-badge.png`} height="96px" alt="Immich" />
</Link>
<Link href="https://apps.apple.com/sg/app/immich/id1613945652">
<Img
src={`https://immich.app/img/ios-app-store-badge.png`}
alt="Immich"
style={{ height: '72px', padding: '14px' }}
/>
</Link>
</Column>
</Row>
</Section>
<Text
style={{
color: '#6a737d',
fontSize: '0.8rem',
textAlign: 'center' as const,
marginTop: '12px',
}}
>
<Link href="https://immich.app">Immich</Link> project is available under GNU AGPL v3 license.
</Text>
</Container>
</Body>
</Html>
<Row>
<Link href={baseUrl}>{baseUrl}</Link>
</Row>
</ImmichLayout>
);
TestEmail.PreviewProps = {
baseUrl: 'https://demo.immich.app/auth/login',
baseUrl: 'https://demo.immich.app',
displayName: 'Alan Turing',
} as TestEmailProps;
export default TestEmail;
const text = {
margin: '0 0 24px 0',
textAlign: 'left' as const,
fontSize: '18px',
lineHeight: '24px',
};
const button: CSS.Properties = {
backgroundColor: 'rgb(66, 80, 175)',
margin: '1em 0',
padding: '0.75em 3em',
color: '#fff',
fontSize: '1em',
fontWeight: 700,
lineHeight: 1.5,
textTransform: 'uppercase',
borderRadius: '9999px',
};

View File

@@ -1,132 +1,37 @@
import {
Body,
Button,
Column,
Container,
Head,
Hr,
Html,
Img,
Link,
Preview,
Row,
Section,
Text,
} from '@react-email/components';
import * as CSS from 'csstype';
import { Link, Section, Text } from '@react-email/components';
import * as React from 'react';
import { WelcomeEmailProps } from 'src/interfaces/notification.interface';
import { ImmichButton } from './components/button.component';
import ImmichLayout from './components/immich.layout';
export const WelcomeEmail = ({ baseUrl, displayName, username, password }: WelcomeEmailProps) => (
<Html>
<Head />
<Preview>You have been invited to a new Immich instance.</Preview>
<Body
style={{
margin: 0,
padding: 0,
backgroundColor: '#ffffff',
color: 'rgb(66, 80, 175)',
fontFamily: 'Overpass, sans-serif',
fontSize: '18px',
lineHeight: '24px',
}}
>
<Container
style={{
width: '480px',
maxWidth: '100%',
padding: '10px',
margin: '0 auto',
}}
>
<Section
style={{
padding: '36px',
tableLayout: 'fixed',
backgroundColor: 'rgb(226, 232, 240)',
border: 'solid 0px rgb(248 113 113)',
borderRadius: '50px',
textAlign: 'center' as const,
}}
>
<Img
src="https://immich.app/img/immich-logo-inline-light.png"
alt="Immich"
style={{
height: 'auto',
margin: '0 auto 48px auto',
width: '50%',
alignSelf: 'center',
color: 'white',
}}
/>
<ImmichLayout preview="You have been invited to a new Immich instance.">
<Text className="m-0">
Hey <strong>{displayName}</strong>!
</Text>
<Text style={text}>
Hey <strong>{displayName}</strong>!
</Text>
<Text>A new account has been created for you.</Text>
<Text style={text}>A new account has been created for you.</Text>
<Text>
<strong>Username</strong>: {username}
{password && (
<>
<br />
<strong>Password</strong>: {password}
</>
)}
</Text>
<Text style={text}>
<strong>Username</strong>: {username}
{password && (
<>
<br />
<strong>Password</strong>: {password}
</>
)}
</Text>
<Section className="flex justify-center my-6">
<ImmichButton href={`${baseUrl}/auth/login`}>Login</ImmichButton>
</Section>
<Row>
<Text style={{ ...text, marginBottom: '36px' }}>
To login, open the link in a browser, or click the button below.
</Text>
</Row>
<Row>
<Link style={{ marginTop: '50px' }} href={baseUrl}>
{baseUrl}
</Link>
</Row>
<Row>
<Button style={button} href={`${baseUrl}/auth/login`}>
Login
</Button>
</Row>
</Section>
<Hr style={{ color: 'rgb(66, 80, 175)', marginTop: '24px' }} />
<Section style={{ textAlign: 'center' }}>
<Row>
<Column align="center">
<Link href="https://play.google.com/store/apps/details?id=app.alextran.immich">
<Img src={`https://immich.app/img/google-play-badge.png`} height="96px" alt="Immich" />
</Link>
<Link href="https://apps.apple.com/sg/app/immich/id1613945652">
<Img
src={`https://immich.app/img/ios-app-store-badge.png`}
alt="Immich"
style={{ height: '72px', padding: '14px' }}
/>
</Link>
</Column>
</Row>
</Section>
<Text
style={{
color: '#6a737d',
fontSize: '0.8rem',
textAlign: 'center' as const,
marginTop: '12px',
}}
>
<Link href="https://immich.app">Immich</Link> project is available under GNU AGPL v3 license.
</Text>
</Container>
</Body>
</Html>
<Text className="text-xs">
If you cannot click the button use the link below to proceed with first login.
<br />
<Link href={baseUrl}>{baseUrl}</Link>
</Text>
</ImmichLayout>
);
WelcomeEmail.PreviewProps = {
@@ -137,22 +42,3 @@ WelcomeEmail.PreviewProps = {
} as WelcomeEmailProps;
export default WelcomeEmail;
const text = {
margin: '0 0 24px 0',
textAlign: 'left' as const,
fontSize: '18px',
lineHeight: '24px',
};
const button: CSS.Properties = {
backgroundColor: 'rgb(66, 80, 175)',
margin: '1em 0',
padding: '0.75em 3em',
color: '#fff',
fontSize: '1em',
fontWeight: 700,
lineHeight: 1.5,
textTransform: 'uppercase',
borderRadius: '9999px',
};

View File

@@ -297,7 +297,16 @@ export class MapRepository implements IMapRepository {
admin2Name: admin2Map.get(`${lineSplit[8]}.${lineSplit[10]}.${lineSplit[11]}`),
}),
resourcePaths.geodata.cities500,
{ entityFilter: (lineSplit) => lineSplit[7] != 'PPLX' },
{
entityFilter: (lineSplit) => {
if (lineSplit[7] === 'PPLX') {
// Exclude populated subsections of cities that are not in Australia.
// Australia has a lot of PPLX areas, so we include them.
return lineSplit[8] === 'AU';
}
return true;
},
},
);
}

View File

@@ -1,4 +1,5 @@
import swc from 'unplugin-swc';
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
export default defineConfig({
@@ -11,5 +12,5 @@ export default defineConfig({
},
},
},
plugins: [swc.vite()],
plugins: [swc.vite(), tsconfigPaths()],
});

View File

@@ -1 +1 @@
20.15.1
20.16.0

2
web/package-lock.json generated
View File

@@ -65,7 +65,7 @@
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"vite": "^5.1.4",
"vitest": "^1.3.1"
"vitest": "^1.6.0"
}
},
"../open-api/typescript-sdk": {

View File

@@ -58,7 +58,7 @@
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"vite": "^5.1.4",
"vitest": "^1.3.1"
"vitest": "^1.6.0"
},
"type": "module",
"dependencies": {
@@ -83,6 +83,6 @@
"thumbhash": "^0.1.1"
},
"volta": {
"node": "20.15.1"
"node": "20.16.0"
}
}

View File

@@ -108,7 +108,10 @@
{/if}
<span class="text-immich-primary dark:text-immich-dark-primary">
{#if user.quotaSizeInBytes}
({((user.usage / user.quotaSizeInBytes) * 100).toFixed(0)}%)
({(user.usage / user.quotaSizeInBytes).toLocaleString($locale, {
style: 'percent',
maximumFractionDigits: 0,
})})
{:else}
({$t('unlimited')})
{/if}

View File

@@ -33,7 +33,7 @@
<input
use:shortcut={{ shortcut: { key: 'Enter' }, onShortcut: (e) => e.currentTarget.blur() }}
on:blur={handleUpdateName}
class="w-[99%] mb-2 border-b-2 border-transparent text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary {isOwned
class="w-[99%] mb-2 border-b-2 border-transparent text-2xl md:text-4xl lg:text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary {isOwned
? 'hover:border-gray-400'
: 'hover:border-transparent'} bg-immich-bg focus:border-b-2 focus:border-immich-primary focus:outline-none dark:bg-immich-dark-bg dark:focus:border-immich-dark-primary dark:focus:bg-immich-dark-gray"
type="text"

View File

@@ -95,10 +95,10 @@
<main class="relative h-screen overflow-hidden bg-immich-bg px-6 pt-[var(--navbar-height)] dark:bg-immich-dark-bg">
<AssetGrid {album} {assetStore} {assetInteractionStore}>
<section class="pt-24">
<section class="pt-8 md:pt-24">
<!-- ALBUM TITLE -->
<h1
class="bg-immich-bg text-6xl text-immich-primary outline-none transition-all dark:bg-immich-dark-bg dark:text-immich-dark-primary"
class="bg-immich-bg text-2xl md:text-4xl lg:text-6xl text-immich-primary outline-none transition-all dark:bg-immich-dark-bg dark:text-immich-dark-primary"
>
{album.albumName}
</h1>

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import { locale } from '$lib/stores/preferences.store';
import type { ActivityResponseDto } from '@immich/sdk';
import { mdiCommentOutline, mdiHeart, mdiHeartOutline } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
@@ -24,7 +25,7 @@
<div class="flex gap-2 items-center justify-center">
<Icon path={mdiCommentOutline} class="scale-x-[-1]" size={24} />
{#if numberOfComments}
<div class="text-xl">{numberOfComments}</div>
<div class="text-xl">{numberOfComments.toLocaleString($locale)}</div>
{/if}
</div>
</button>

View File

@@ -35,7 +35,9 @@
<div class="h-[7px] rounded-full bg-immich-primary" style={`width: ${download.percentage}%`} />
</div>
<p class="min-w-[4em] whitespace-nowrap text-right">
<span class="text-immich-primary">{download.percentage}%</span>
<span class="text-immich-primary">
{(download.percentage / 100).toLocaleString($locale, { style: 'percent' })}
</span>
</p>
</div>
</div>

View File

@@ -109,7 +109,7 @@
buttonSize="50"
icon={mdiCog}
on:click={() => (showSettings = !showSettings)}
title={$t('next')}
title={$t('slideshow_settings')}
/>
{#if !isFullScreen}
<CircleIconButton

View File

@@ -6,7 +6,7 @@
import { getAltText } from '$lib/utils/thumbnail-util';
import { timeToSeconds } from '$lib/utils/date-time';
import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
import { playVideoThumbnailOnHover } from '$lib/stores/preferences.store';
import { locale, playVideoThumbnailOnHover } from '$lib/stores/preferences.store';
import { getAssetPlaybackUrl } from '$lib/utils';
import {
mdiArchiveArrowDownOutline,
@@ -177,7 +177,7 @@
: 'top-7 right-1'} z-20 flex place-items-center gap-1 text-xs font-medium text-white"
>
<span class="pr-2 pt-2 flex place-items-center gap-1">
<p>{asset.stackCount}</p>
<p>{asset.stackCount.toLocaleString($locale)}</p>
<Icon path={mdiCameraBurst} size="24" />
</span>
</div>

View File

@@ -0,0 +1,20 @@
import Button from '$lib/components/elements/buttons/button.svelte';
import { render, screen } from '@testing-library/svelte';
describe('Button component', () => {
it('should render as a button', () => {
render(Button);
const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
expect(button).toHaveAttribute('type', 'button');
expect(button).not.toHaveAttribute('href');
});
it('should render as a link if href prop is set', () => {
render(Button, { props: { href: '/test' } });
const link = screen.getByRole('link');
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute('href', '/test');
expect(link).not.toHaveAttribute('type');
});
});

View File

@@ -0,0 +1,29 @@
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { render, screen } from '@testing-library/svelte';
describe('CircleIconButton component', () => {
it('should render as a button', () => {
render(CircleIconButton, { icon: '', title: 'test' });
const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
expect(button).toHaveAttribute('type', 'button');
expect(button).not.toHaveAttribute('href');
expect(button).toHaveAttribute('title', 'test');
});
it('should render as a link if href prop is set', () => {
render(CircleIconButton, { props: { href: '/test', icon: '', title: 'test' } });
const link = screen.getByRole('link');
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute('href', '/test');
expect(link).not.toHaveAttribute('type');
});
it('should render icon inside button', () => {
render(CircleIconButton, { icon: '', title: 'test' });
const button = screen.getByRole('button');
const icon = button.querySelector('svg');
expect(icon).toBeInTheDocument();
expect(icon).toHaveAttribute('aria-label', 'test');
});
});

View File

@@ -1,5 +1,6 @@
<script lang="ts" context="module">
export type Type = 'button' | 'submit' | 'reset';
import type { HTMLButtonAttributes, HTMLLinkAttributes } from 'svelte/elements';
export type Color =
| 'primary'
| 'primary-inversed'
@@ -14,45 +15,66 @@
| 'dark-gray'
| 'overlay-primary';
export type Size = 'tiny' | 'icon' | 'link' | 'sm' | 'base' | 'lg';
export type Rounded = 'lg' | '3xl' | 'full' | false;
export type Rounded = 'lg' | '3xl' | 'full' | 'none';
export type Shadow = 'md' | false;
type BaseProps = {
class?: string;
color?: Color;
size?: Size;
rounded?: Rounded;
shadow?: Shadow;
fullwidth?: boolean;
border?: boolean;
};
export type ButtonProps = HTMLButtonAttributes &
BaseProps & {
href?: never;
};
export type LinkProps = HTMLLinkAttributes &
BaseProps & {
type?: never;
};
export type Props = ButtonProps | LinkProps;
</script>
<script lang="ts">
export let type: Type = 'button';
type $$Props = Props;
export let type: $$Props['type'] = 'button';
export let href: $$Props['href'] = undefined;
export let color: Color = 'primary';
export let size: Size = 'base';
export let rounded: Rounded = '3xl';
export let shadow: Shadow = 'md';
export let disabled = false;
export let fullwidth = false;
export let border = false;
export let title: string | undefined = '';
export let form: string | undefined = undefined;
let className = '';
export { className as class };
const colorClasses: Record<Color, string> = {
primary:
'bg-immich-primary dark:bg-immich-dark-primary text-white dark:text-immich-dark-gray enabled:dark:hover:bg-immich-dark-primary/80 enabled:hover:bg-immich-primary/90',
'bg-immich-primary dark:bg-immich-dark-primary text-white dark:text-immich-dark-gray dark:hover:bg-immich-dark-primary/80 hover:bg-immich-primary/90',
secondary:
'bg-gray-500 dark:bg-gray-200 text-white dark:text-immich-dark-gray enabled:hover:bg-gray-500/90 enabled:dark:hover:bg-gray-200/90',
'transparent-primary':
'text-gray-500 dark:text-immich-dark-primary enabled:hover:bg-gray-100 enabled:dark:hover:bg-gray-700',
'bg-gray-500 dark:bg-gray-200 text-white dark:text-immich-dark-gray hover:bg-gray-500/90 dark:hover:bg-gray-200/90',
'transparent-primary': 'text-gray-500 dark:text-immich-dark-primary hover:bg-gray-100 dark:hover:bg-gray-700',
'text-primary':
'text-immich-primary dark:text-immich-dark-primary enabled:dark:hover:bg-immich-dark-primary/10 enabled:hover:bg-immich-primary/10',
'light-red': 'bg-[#F9DEDC] text-[#410E0B] enabled:hover:bg-red-50',
red: 'bg-red-500 text-white enabled:hover:bg-red-400',
green: 'bg-green-400 text-gray-800 enabled:hover:bg-green-400/90',
gray: 'bg-gray-500 dark:bg-gray-200 enabled:hover:bg-gray-500/75 enabled:dark:hover:bg-gray-200/80 text-white dark:text-immich-dark-gray',
'text-immich-primary dark:text-immich-dark-primary dark:hover:bg-immich-dark-primary/10 hover:bg-immich-primary/10',
'light-red': 'bg-[#F9DEDC] text-[#410E0B] hover:bg-red-50',
red: 'bg-red-500 text-white hover:bg-red-400',
green: 'bg-green-400 text-gray-800 hover:bg-green-400/90',
gray: 'bg-gray-500 dark:bg-gray-200 hover:bg-gray-500/75 dark:hover:bg-gray-200/80 text-white dark:text-immich-dark-gray',
'transparent-gray':
'dark:text-immich-dark-fg enabled:hover:bg-immich-primary/5 enabled:hover:text-gray-700 enabled:hover:dark:text-immich-dark-fg enabled:dark:hover:bg-immich-dark-primary/25',
'dark:text-immich-dark-fg hover:bg-immich-primary/5 hover:text-gray-700 hover:dark:text-immich-dark-fg dark:hover:bg-immich-dark-primary/25',
'dark-gray':
'dark:border-immich-dark-gray dark:bg-gray-500 enabled:dark:hover:bg-immich-dark-primary/50 enabled:hover:bg-immich-primary/10 dark:text-white',
'overlay-primary': 'text-gray-500 enabled:hover:bg-gray-100',
'dark:border-immich-dark-gray dark:bg-gray-500 dark:hover:bg-immich-dark-primary/50 hover:bg-immich-primary/10 dark:text-white',
'overlay-primary': 'text-gray-500 hover:bg-gray-100',
'primary-inversed':
'bg-immich-dark-primary dark:bg-immich-primary text-black dark:text-white enabled:hover:bg-immich-dark-primary/80 enabled:dark:hover:bg-immich-primary/90',
'bg-immich-dark-primary dark:bg-immich-primary text-black dark:text-white hover:bg-immich-dark-primary/80 dark:hover:bg-immich-primary/90',
};
const sizeClasses: Record<Size, string> = {
@@ -63,25 +85,37 @@
base: 'px-6 py-3 font-medium',
lg: 'px-6 py-4 font-semibold',
};
const roundedClasses: Record<Rounded, string> = {
none: '',
lg: 'rounded-lg',
'3xl': 'rounded-3xl',
full: 'rounded-full',
};
$: computedClass = [
className,
colorClasses[color],
sizeClasses[size],
roundedClasses[rounded],
shadow === 'md' && 'shadow-md',
fullwidth && 'w-full',
border && 'border',
]
.filter(Boolean)
.join(' ');
</script>
<button
{type}
{disabled}
{title}
{form}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<svelte:element
this={href ? 'a' : 'button'}
type={href ? undefined : type}
{href}
on:click
on:focus
on:blur
class="{className} inline-flex items-center justify-center transition-colors disabled:cursor-not-allowed disabled:opacity-60 {colorClasses[
color
]} {sizeClasses[size]}"
class:rounded-lg={rounded === 'lg'}
class:rounded-3xl={rounded === '3xl'}
class:rounded-full={rounded === 'full'}
class:shadow-md={shadow === 'md'}
class:w-full={fullwidth}
class:border
class="inline-flex items-center justify-center transition-colors disabled:cursor-not-allowed disabled:opacity-60 disabled:pointer-events-none {computedClass}"
{...$$restProps}
>
<slot />
</button>
</svelte:element>

View File

@@ -1,18 +1,48 @@
<script lang="ts" context="module">
import type { HTMLButtonAttributes, HTMLLinkAttributes } from 'svelte/elements';
export type Color = 'transparent' | 'light' | 'dark' | 'gray' | 'primary' | 'opaque';
export type Padding = '1' | '2' | '3';
type BaseProps = {
icon: string;
title: string;
class?: string;
color?: Color;
padding?: Padding;
size?: string;
hideMobile?: true;
buttonSize?: string;
viewBox?: string;
};
export type ButtonProps = HTMLButtonAttributes &
BaseProps & {
href?: never;
};
export type LinkProps = HTMLLinkAttributes &
BaseProps & {
type?: never;
};
export type Props = ButtonProps | LinkProps;
</script>
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
export let type: 'button' | 'submit' | 'reset' = 'button';
type $$Props = Props;
export let type: $$Props['type'] = 'button';
export let href: $$Props['href'] = undefined;
export let icon: string;
export let color: Color = 'transparent';
export let title: string;
/**
* The padding of the button, used by the `p-{padding}` Tailwind CSS class.
*/
export let padding = '3';
export let padding: Padding = '3';
/**
* Size of the button, used for a CSS value.
*/
@@ -23,10 +53,6 @@
* viewBox attribute for the SVG icon.
*/
export let viewBox: string | undefined = undefined;
export let id: string | undefined = undefined;
export let ariaHasPopup: boolean | undefined = undefined;
export let ariaExpanded: boolean | undefined = undefined;
export let ariaControls: string | undefined = undefined;
/**
* Override the default styling of the button for specific use cases, such as the icon color.
@@ -44,22 +70,28 @@
'bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 hover:dark:bg-immich-dark-primary/80 text-white dark:text-immich-dark-gray',
};
const paddingClasses: Record<Padding, string> = {
'1': 'p-1',
'2': 'p-2',
'3': 'p-3',
};
$: colorClass = colorClasses[color];
$: mobileClass = hideMobile ? 'hidden sm:flex' : '';
$: paddingClass = `p-${padding}`;
$: paddingClass = paddingClasses[padding];
</script>
<button
{id}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<svelte:element
this={href ? 'a' : 'button'}
type={href ? undefined : type}
{title}
{type}
{href}
style:width={buttonSize ? buttonSize + 'px' : ''}
style:height={buttonSize ? buttonSize + 'px' : ''}
class="flex place-content-center place-items-center rounded-full {colorClass} {paddingClass} transition-all hover:dark:text-immich-dark-gray {className} {mobileClass}"
aria-haspopup={ariaHasPopup}
aria-expanded={ariaExpanded}
aria-controls={ariaControls}
class="flex place-content-center place-items-center rounded-full {colorClass} {paddingClass} transition-all disabled:cursor-default hover:dark:text-immich-dark-gray {className} {mobileClass}"
on:click
{...$$restProps}
>
<Icon path={icon} {size} ariaLabel={title} {viewBox} color="currentColor" />
</button>
</svelte:element>

View File

@@ -1,16 +1,22 @@
<script lang="ts" context="module">
export type Color = 'transparent-primary' | 'transparent-gray';
type BaseProps = {
color?: Color;
};
export type Props = (LinkProps & BaseProps) | (ButtonProps & BaseProps);
</script>
<script lang="ts">
import Button from './button.svelte';
import Button, { type ButtonProps, type LinkProps } from '$lib/components/elements/buttons/button.svelte';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type $$Props = Props;
export let color: Color = 'transparent-gray';
export let disabled = false;
export let fullwidth = false;
export let title: string | undefined = undefined;
</script>
<Button {title} size="link" {color} shadow={false} rounded="lg" {disabled} on:click {fullwidth}>
<Button size="link" {color} shadow={false} rounded="lg" on:click {...$$restProps}>
<slot />
</Button>

View File

@@ -17,7 +17,7 @@
<div class="absolute z-50 top-2 left-2 transition-transform {isFocused ? 'translate-y-0' : '-translate-y-10 sr-only'}">
<Button
size={'sm'}
rounded={false}
rounded="none"
on:click={moveFocus}
on:focus={() => (isFocused = true)}
on:blur={() => (isFocused = false)}

View File

@@ -38,6 +38,7 @@
import { tweened } from 'svelte/motion';
import { fade } from 'svelte/transition';
import { t } from 'svelte-i18n';
import { locale } from '$lib/stores/preferences.store';
const parseIndex = (s: string | null, max: number | null) =>
Math.max(Math.min(Number.parseInt(s ?? '') || 0, max ?? 0), 0);
@@ -201,7 +202,7 @@
<div>
<p class="text-small">
{assetIndex + 1}/{currentMemory.assets.length}
{(assetIndex + 1).toLocaleString($locale)}/{currentMemory.assets.length.toLocaleString($locale)}
</p>
</div>
</div>

View File

@@ -427,7 +427,9 @@
<!-- Right margin MUST be equal to the width of immich-scrubbable-scrollbar -->
<section
id="asset-grid"
class="scrollbar-hidden h-full overflow-y-auto outline-none pb-[60px] {isEmpty ? 'm-0' : 'ml-4 tall:ml-0 mr-[60px]'}"
class="scrollbar-hidden h-full overflow-y-auto outline-none pb-[60px] {isEmpty
? 'm-0'
: 'ml-4 tall:ml-0 md:mr-[60px]'}"
tabindex="-1"
bind:clientHeight={viewport.height}
bind:clientWidth={viewport.width}

View File

@@ -31,8 +31,9 @@
</script>
<ControlAppBar on:close={clearSelect} backIcon={mdiClose} tailwindClasses="bg-white shadow-md">
<p class="font-medium text-immich-primary dark:text-immich-dark-primary" slot="leading">
{$t('selected_count', { values: { count: assets.size } })}
</p>
<div class="font-medium text-immich-primary dark:text-immich-dark-primary" slot="leading">
<p class="block sm:hidden">{assets.size}</p>
<p class="hidden sm:block">{$t('selected_count', { values: { count: assets.size } })}</p>
</div>
<slot slot="trailing" />
</ControlAppBar>

View File

@@ -86,7 +86,8 @@
<Icon path={mdiPlus} size="30" />
</div>
<p class="">
New Album {#if search.length > 0}<b>{search}</b>{/if}
{$t('new_album')}
{#if search.length > 0}<b>{search}</b>{/if}
</p>
</button>
{#if filteredAlbums.length > 0}

View File

@@ -1,5 +1,8 @@
<script lang="ts">
import CircleIconButton, { type Color } from '$lib/components/elements/buttons/circle-icon-button.svelte';
import CircleIconButton, {
type Color,
type Padding,
} from '$lib/components/elements/buttons/circle-icon-button.svelte';
import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte';
import {
getContextMenuPositionFromBoundingRect,
@@ -24,7 +27,7 @@
export let direction: 'left' | 'right' = 'right';
export let color: Color = 'transparent';
export let size: string | undefined = undefined;
export let padding: string | undefined = undefined;
export let padding: Padding | undefined = undefined;
/**
* Additional classes to apply to the button.
*/
@@ -114,9 +117,9 @@
{padding}
{size}
{title}
ariaControls={menuId}
ariaExpanded={isOpen}
ariaHasPopup={true}
aria-controls={menuId}
aria-expanded={isOpen}
aria-haspopup={true}
class={buttonClass}
id={buttonId}
on:click={handleClick}

View File

@@ -54,11 +54,11 @@
<div in:fly={{ y: 10, duration: 200 }} class="absolute top-0 w-full z-[100] bg-transparent">
<div
id="asset-selection-app-bar"
class={`grid grid-cols-[10%_80%_10%] justify-between md:grid-cols-[15%_70%_15%] lg:grid-cols-[25%_50%_25%] ${appBarBorder} mx-2 mt-2 place-items-center rounded-lg p-2 transition-all ${tailwindClasses} dark:bg-immich-dark-gray ${
class={`grid grid-cols-[10%_80%_10%] justify-between sm:grid-cols-[25%_50%_25%] lg:grid-cols-[25%_50%_25%] ${appBarBorder} mx-2 mt-2 place-items-center rounded-lg p-2 transition-all ${tailwindClasses} dark:bg-immich-dark-gray ${
forceDark && 'bg-immich-dark-gray text-white'
}`}
>
<div class="flex place-items-center gap-6 justify-self-start dark:text-immich-dark-fg">
<div class="flex place-items-center sm:gap-6 justify-self-start dark:text-immich-dark-fg">
{#if showBackButton}
<CircleIconButton title={$t('close')} on:click={handleClose} icon={backIcon} size={'24'} class={buttonClass} />
{/if}

View File

@@ -73,14 +73,19 @@
<p class="text-sm text-gray-500 dark:text-immich-dark-fg">{$user.email}</p>
</div>
<a href={AppRoute.USER_SETTINGS} on:click={() => dispatch('close')}>
<Button color="dark-gray" size="sm" shadow={false} border>
<div class="flex place-content-center place-items-center gap-2 px-2">
<Icon path={mdiCog} size="18" />
{$t('account_settings')}
</div>
</Button>
</a>
<Button
href={AppRoute.USER_SETTINGS}
on:click={() => dispatch('close')}
color="dark-gray"
size="sm"
shadow={false}
border
>
<div class="flex place-content-center place-items-center gap-2 px-2">
<Icon path={mdiCog} size="18" />
{$t('account_settings')}
</div>
</Button>
</div>
<div class="mb-4 flex flex-col">

View File

@@ -60,9 +60,13 @@
<section class="flex place-items-center justify-end gap-4 max-sm:w-full">
{#if $featureFlags.search}
<a href={AppRoute.SEARCH} id="search-button" class="ml-4 sm:hidden">
<CircleIconButton title={$t('go_to_search')} icon={mdiMagnify} />
</a>
<CircleIconButton
href={AppRoute.SEARCH}
id="search-button"
class="ml-4 sm:hidden"
title={$t('go_to_search')}
icon={mdiMagnify}
/>
{/if}
<ThemeButton />

View File

@@ -37,8 +37,6 @@
</div>
</div>
<a href={getProductLink(ImmichProduct.Client)}>
<Button fullwidth>{$t('purchase_button_select')}</Button>
</a>
<Button href={getProductLink(ImmichProduct.Client)} fullwidth>{$t('purchase_button_select')}</Button>
</div>
</div>

View File

@@ -25,6 +25,6 @@
</div>
<div class="mt-6 w-full">
<Button fullwidth on:click={onDone}>OK</Button>
<Button fullwidth on:click={onDone}>{$t('ok')}</Button>
</div>
</div>

View File

@@ -37,8 +37,6 @@
</div>
</div>
<a href={getLicenseLink(ImmichProduct.Server)}>
<Button fullwidth>{$t('purchase_button_select')}</Button>
</a>
<Button href={getLicenseLink(ImmichProduct.Server)} fullwidth>{$t('purchase_button_select')}</Button>
</div>
</div>

View File

@@ -13,21 +13,31 @@
import { focusOutside } from '$lib/actions/focus-outside';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { t } from 'svelte-i18n';
import { generateId } from '$lib/utils/generate-id';
import { tick } from 'svelte';
export let value = '';
export let grayTheme: boolean;
export let searchQuery: MetadataSearchDto | SmartSearchDto = {};
$: showClearIcon = value.length > 0;
let input: HTMLInputElement;
let showHistory = false;
let showSuggestions = false;
let showFilter = false;
$: showClearIcon = value.length > 0;
let isSearchSuggestions = false;
let selectedId: string | undefined;
let moveSelection: (direction: 1 | -1) => void;
let clearSelection: () => void;
let selectActiveOption: () => void;
const listboxId = generateId();
const onSearch = async (payload: SmartSearchDto | MetadataSearchDto) => {
const params = getMetadataSearchQuery(payload);
showHistory = false;
closeDropdown();
showFilter = false;
$isSearchEnabled = false;
await goto(`${AppRoute.SEARCH}?${params}`);
@@ -39,7 +49,8 @@
};
const saveSearchTerm = (saveValue: string) => {
$savedSearchTerms = [saveValue, ...$savedSearchTerms];
const filteredSearchTerms = $savedSearchTerms.filter((item) => item.toLowerCase() !== saveValue.toLowerCase());
$savedSearchTerms = [saveValue, ...filteredSearchTerms];
if ($savedSearchTerms.length > 5) {
$savedSearchTerms = $savedSearchTerms.slice(0, 5);
@@ -52,7 +63,6 @@
};
const onFocusIn = () => {
showHistory = true;
$isSearchEnabled = true;
};
@@ -61,12 +71,13 @@
$preventRaceConditionSearchBar = true;
}
showHistory = false;
closeDropdown();
$isSearchEnabled = false;
showFilter = false;
};
const onHistoryTermClick = async (searchTerm: string) => {
value = searchTerm;
const searchPayload = { query: searchTerm };
await onSearch(searchPayload);
};
@@ -76,7 +87,7 @@
value = '';
if (showFilter) {
showHistory = false;
closeDropdown();
}
};
@@ -84,12 +95,49 @@
handlePromiseError(onSearch({ query: value }));
saveSearchTerm(value);
};
const onClear = () => {
value = '';
input.focus();
};
const onEscape = () => {
closeDropdown();
showFilter = false;
};
const onArrow = async (direction: 1 | -1) => {
openDropdown();
await tick();
moveSelection(direction);
};
const onEnter = (event: KeyboardEvent) => {
if (selectedId) {
event.preventDefault();
selectActiveOption();
}
};
const onInput = () => {
openDropdown();
clearSelection();
};
const openDropdown = () => {
showSuggestions = true;
};
const closeDropdown = () => {
showSuggestions = false;
clearSelection();
};
</script>
<svelte:window
use:shortcuts={[
{ shortcut: { key: 'Escape' }, onShortcut: onFocusOut },
{ shortcut: { ctrl: true, key: 'k' }, onShortcut: () => input.focus() },
{ shortcut: { key: 'Escape' }, onShortcut: onEscape },
{ shortcut: { ctrl: true, key: 'k' }, onShortcut: () => input.select() },
{ shortcut: { ctrl: true, shift: true, key: 'k' }, onShortcut: onFilterClick },
]}
/>
@@ -102,53 +150,69 @@
action={AppRoute.SEARCH}
on:reset={() => (value = '')}
on:submit|preventDefault={onSubmit}
on:focusin={onFocusIn}
role="search"
>
<div class="absolute inset-y-0 left-0 flex items-center pl-2">
<CircleIconButton type="submit" title={$t('search')} icon={mdiMagnify} size="20" />
<div use:focusOutside={{ onFocusOut: closeDropdown }}>
<label for="main-search-bar" class="sr-only">{$t('search_your_photos')}</label>
<input
type="text"
name="q"
id="main-search-bar"
class="w-full transition-all border-2 px-14 py-4 text-immich-fg/75 dark:text-immich-dark-fg
{grayTheme ? 'dark:bg-immich-dark-gray' : 'dark:bg-immich-dark-bg'}
{(showSuggestions && isSearchSuggestions) || showFilter ? 'rounded-t-3xl' : 'rounded-3xl bg-gray-200'}
{$isSearchEnabled ? 'border-gray-200 dark:border-gray-700 bg-white' : 'border-transparent'}"
placeholder={$t('search_your_photos')}
required
pattern="^(?!m:$).*$"
bind:value
bind:this={input}
on:focus={openDropdown}
on:input={onInput}
disabled={showFilter}
role="combobox"
aria-controls={listboxId}
aria-activedescendant={selectedId ?? ''}
aria-expanded={showSuggestions && isSearchSuggestions}
aria-autocomplete="list"
use:shortcuts={[
{ shortcut: { key: 'Escape' }, onShortcut: onEscape },
{ shortcut: { ctrl: true, shift: true, key: 'k' }, onShortcut: onFilterClick },
{ shortcut: { key: 'ArrowUp' }, onShortcut: () => onArrow(-1) },
{ shortcut: { key: 'ArrowDown' }, onShortcut: () => onArrow(1) },
{ shortcut: { key: 'Enter' }, onShortcut: onEnter, preventDefault: false },
{ shortcut: { key: 'ArrowDown', alt: true }, onShortcut: openDropdown },
]}
/>
<!-- SEARCH HISTORY BOX -->
<SearchHistoryBox
id={listboxId}
searchQuery={value}
isOpen={showSuggestions}
bind:isSearchSuggestions
bind:moveSelection
bind:clearSelection
bind:selectActiveOption
onClearAllSearchTerms={clearAllSearchTerms}
onClearSearchTerm={(searchTerm) => clearSearchTerm(searchTerm)}
onSelectSearchTerm={(searchTerm) => handlePromiseError(onHistoryTermClick(searchTerm))}
onActiveSelectionChange={(id) => (selectedId = id)}
/>
</div>
<label for="main-search-bar" class="sr-only">{$t('search_your_photos')}</label>
<input
type="text"
name="q"
id="main-search-bar"
class="w-full {grayTheme
? 'dark:bg-immich-dark-gray'
: 'dark:bg-immich-dark-bg'} px-14 py-4 text-immich-fg/75 dark:text-immich-dark-fg {(showHistory &&
$savedSearchTerms.length > 0) ||
showFilter
? 'rounded-t-3xl border border-gray-200 bg-white dark:border-gray-800'
: 'rounded-3xl border border-transparent bg-gray-200'}"
placeholder={$t('search_your_photos')}
required
pattern="^(?!m:$).*$"
bind:value
bind:this={input}
on:click={onFocusIn}
on:focus={onFocusIn}
disabled={showFilter}
use:shortcuts={[
{ shortcut: { key: 'Escape' }, onShortcut: onFocusOut },
{ shortcut: { ctrl: true, shift: true, key: 'k' }, onShortcut: onFilterClick },
]}
/>
<div class="absolute inset-y-0 {showClearIcon ? 'right-14' : 'right-2'} flex items-center pl-6 transition-all">
<CircleIconButton title={$t('show_search_options')} icon={mdiTune} on:click={onFilterClick} size="20" />
</div>
{#if showClearIcon}
<div class="absolute inset-y-0 right-0 flex items-center pr-2">
<CircleIconButton type="reset" icon={mdiClose} title={$t('clear')} size="20" />
<CircleIconButton on:click={onClear} icon={mdiClose} title={$t('clear')} size="20" />
</div>
{/if}
<!-- SEARCH HISTORY BOX -->
{#if showHistory && $savedSearchTerms.length > 0}
<SearchHistoryBox
on:clearAllSearchTerms={clearAllSearchTerms}
on:clearSearchTerm={({ detail: searchTerm }) => clearSearchTerm(searchTerm)}
on:selectSearchTerm={({ detail: searchTerm }) => handlePromiseError(onHistoryTermClick(searchTerm))}
/>
{/if}
<div class="absolute inset-y-0 left-0 flex items-center pl-2">
<CircleIconButton type="submit" disabled={showFilter} title={$t('search')} icon={mdiMagnify} size="20" />
</div>
</form>
{#if showFilter}

View File

@@ -117,7 +117,7 @@
<div
bind:clientWidth={filterBoxWidth}
transition:fly={{ y: 25, duration: 250 }}
class="absolute w-full rounded-b-3xl border border-t-0 border-gray-200 bg-white shadow-2xl dark:border-gray-800 dark:bg-immich-dark-gray dark:text-gray-300"
class="absolute w-full rounded-b-3xl border-2 border-t-0 border-gray-200 bg-white shadow-2xl dark:border-gray-700 dark:bg-immich-dark-gray dark:text-gray-300"
>
<form
id="search-filter-form"

View File

@@ -2,51 +2,130 @@
import Icon from '$lib/components/elements/icon.svelte';
import { savedSearchTerms } from '$lib/stores/search.store';
import { mdiMagnify, mdiClose } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import { fly } from 'svelte/transition';
import { t } from 'svelte-i18n';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
const dispatch = createEventDispatcher<{
selectSearchTerm: string;
clearSearchTerm: string;
clearAllSearchTerms: void;
}>();
export let id: string;
export let searchQuery: string = '';
export let isSearchSuggestions: boolean = false;
export let isOpen: boolean = false;
export let onSelectSearchTerm: (searchTerm: string) => void;
export let onClearSearchTerm: (searchTerm: string) => void;
export let onClearAllSearchTerms: () => void;
export let onActiveSelectionChange: (selectedId: string | undefined) => void;
$: filteredSearchTerms = $savedSearchTerms.filter((term) => term.toLowerCase().includes(searchQuery.toLowerCase()));
$: isSearchSuggestions = filteredSearchTerms.length > 0;
$: showClearAll = searchQuery === '';
$: suggestionCount = showClearAll ? filteredSearchTerms.length + 1 : filteredSearchTerms.length;
let selectedIndex: number | undefined = undefined;
let element: HTMLDivElement;
export function moveSelection(increment: 1 | -1) {
if (!isSearchSuggestions) {
return;
} else if (selectedIndex === undefined) {
selectedIndex = increment === 1 ? 0 : suggestionCount - 1;
} else if (selectedIndex + increment < 0 || selectedIndex + increment >= suggestionCount) {
clearSelection();
} else {
selectedIndex = (selectedIndex + increment + suggestionCount) % suggestionCount;
}
onActiveSelectionChange(getId(selectedIndex));
}
export function clearSelection() {
selectedIndex = undefined;
onActiveSelectionChange(undefined);
}
export function selectActiveOption() {
if (selectedIndex === undefined) {
return;
}
const selectedElement = element.querySelector(`#${getId(selectedIndex)}`) as HTMLElement;
selectedElement?.click();
}
const handleClearAll = () => {
clearSelection();
onClearAllSearchTerms();
};
const handleClearSingle = (searchTerm: string) => {
clearSelection();
onClearSearchTerm(searchTerm);
};
const handleSelect = (searchTerm: string) => {
clearSelection();
onSelectSearchTerm(searchTerm);
};
const getId = (index: number | undefined) => {
if (index === undefined) {
return undefined;
}
return `${id}-${index}`;
};
</script>
<div
transition:fly={{ y: 25, duration: 250 }}
class="absolute w-full rounded-b-3xl border border-gray-200 bg-white pb-5 shadow-2xl transition-all dark:border-gray-800 dark:bg-immich-dark-gray dark:text-gray-300"
>
{#if $savedSearchTerms.length > 0}
<div class="flex items-center justify-between px-5 pt-5 text-xs">
<p>{$t('recent_searches').toUpperCase()}</p>
<button
type="button"
class="rounded-lg p-2 font-semibold text-immich-primary hover:bg-immich-primary/25 dark:text-immich-dark-primary"
on:click={() => dispatch('clearAllSearchTerms')}>{$t('clear_all')}</button
>
<div role="listbox" {id} aria-label={$t('recent_searches')} bind:this={element}>
{#if isOpen && isSearchSuggestions}
<div
transition:fly={{ y: 25, duration: 150 }}
class="absolute w-full rounded-b-3xl border-2 border-t-0 border-gray-200 bg-white pb-5 shadow-2xl transition-all dark:border-gray-700 dark:bg-immich-dark-gray dark:text-gray-300"
>
<div class="flex items-center justify-between px-5 pt-5 text-xs">
<p class="py-2" aria-hidden={true}>{$t('recent_searches').toUpperCase()}</p>
{#if showClearAll}
<button
id={getId(0)}
type="button"
class="rounded-lg p-2 font-semibold text-immich-primary aria-selected:bg-immich-primary/25 hover:bg-immich-primary/25 dark:text-immich-dark-primary"
role="option"
on:click={() => handleClearAll()}
tabindex="-1"
aria-selected={selectedIndex === 0}
aria-label={$t('clear_all_recent_searches')}
>
{$t('clear_all')}
</button>
{/if}
</div>
{#each filteredSearchTerms as savedSearchTerm, i (i)}
{@const index = showClearAll ? i + 1 : i}
<div class="flex w-full items-center justify-between text-sm text-black dark:text-gray-300">
<div class="relative w-full items-center">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
id={getId(index)}
class="relative flex w-full cursor-pointer gap-3 py-3 pl-5 hover:bg-gray-100 aria-selected:bg-gray-100 dark:aria-selected:bg-gray-500/30 dark:hover:bg-gray-500/30"
on:click={() => handleSelect(savedSearchTerm)}
role="option"
tabindex="-1"
aria-selected={selectedIndex === index}
aria-label={savedSearchTerm}
>
<Icon path={mdiMagnify} size="1.5em" ariaHidden={true} />
{savedSearchTerm}
</div>
<div aria-hidden={true} class="absolute right-5 top-0 items-center justify-center py-3">
<CircleIconButton
icon={mdiClose}
title={$t('remove')}
size="18"
padding="1"
tabindex={-1}
on:click={() => handleClearSingle(savedSearchTerm)}
/>
</div>
</div>
</div>
{/each}
</div>
{/if}
{#each $savedSearchTerms as savedSearchTerm, i (i)}
<div
class="flex w-full items-center justify-between text-sm text-black hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-500/10"
>
<div class="relative w-full items-center">
<button
type="button"
class="relative flex w-full cursor-pointer gap-3 py-3 pl-5"
on:click={() => dispatch('selectSearchTerm', savedSearchTerm)}
>
<Icon path={mdiMagnify} size="1.5em" />
{savedSearchTerm}
</button>
<div class="absolute right-5 top-0 items-center justify-center py-3">
<button type="button" on:click={() => dispatch('clearSearchTerm', savedSearchTerm)}
><Icon path={mdiClose} size="18" /></button
>
</div>
</div>
</div>
{/each}
</div>

View File

@@ -16,7 +16,7 @@
info?: string;
}
const shortcuts: Shortcuts = {
export let shortcuts: Shortcuts = {
general: [
{ key: ['←', '→'], action: $t('previous_or_next_photo') },
{ key: ['Esc'], action: $t('back_close_deselect') },
@@ -40,45 +40,48 @@
<FullScreenModal title={$t('keyboard_shortcuts')} width="auto" onClose={() => dispatch('close')}>
<div class="grid grid-cols-1 gap-4 px-4 pb-4 md:grid-cols-2">
<div class="p-4">
<h2>{$t('general')}</h2>
<div class="text-sm">
{#each shortcuts.general as shortcut}
<div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm">
<div class="flex justify-self-end">
{#each shortcut.key as key}
<p class="mr-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2">
{key}
</p>
{/each}
</div>
<p class="mb-1 mt-1 flex">{shortcut.action}</p>
</div>
{/each}
</div>
</div>
<div class="p-4">
<h2>{$t('actions')}</h2>
<div class="text-sm">
{#each shortcuts.actions as shortcut}
<div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm">
<div class="flex justify-self-end">
{#each shortcut.key as key}
<p class="mr-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2">
{key}
</p>
{/each}
</div>
<div class="flex items-center gap-2">
{#if shortcuts.general.length > 0}
<div class="p-4">
<h2>{$t('general')}</h2>
<div class="text-sm">
{#each shortcuts.general as shortcut}
<div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm">
<div class="flex justify-self-end">
{#each shortcut.key as key}
<p class="mr-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2">
{key}
</p>
{/each}
</div>
<p class="mb-1 mt-1 flex">{shortcut.action}</p>
{#if shortcut.info}
<Icon path={mdiInformationOutline} title={shortcut.info} />
{/if}
</div>
</div>
{/each}
{/each}
</div>
</div>
</div>
{/if}
{#if shortcuts.actions.length > 0}
<div class="p-4">
<h2>{$t('actions')}</h2>
<div class="text-sm">
{#each shortcuts.actions as shortcut}
<div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm">
<div class="flex justify-self-end">
{#each shortcut.key as key}
<p class="mr-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2">
{key}
</p>
{/each}
</div>
<div class="flex items-center gap-2">
<p class="mb-1 mt-1 flex">{shortcut.action}</p>
{#if shortcut.info}
<Icon path={mdiInformationOutline} title={shortcut.info} />
{/if}
</div>
</div>
{/each}
</div>
</div>
{/if}
</div>
</FullScreenModal>

View File

@@ -58,7 +58,7 @@
showBuyButton = getButtonVisibility();
showMessage = false;
} catch (error) {
handleError(error, 'Error hiding buy button');
handleError(error, $t('errors.error_hiding_buy_button'));
}
};
@@ -89,7 +89,7 @@
<div class="h-6 w-6">
<ImmichLogo noText />
</div>
<p class="dark:text-gray-100">Supporter</p>
<p class="dark:text-gray-100">{$t('purchase_account_info')}</p>
</div>
</button>
{:else if !$isPurchased && showBuyButton}

View File

@@ -9,6 +9,7 @@
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import { mdiCog, mdiWindowMinimize, mdiCancel, mdiCloudUploadOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
import { locale } from '$lib/stores/preferences.store';
let showDetail = false;
let showOptions = false;
@@ -73,9 +74,14 @@
})}
</p>
<p class="immich-form-label text-xs">
{$t('upload_status_uploaded')} <span class="text-immich-success">{$successCounter}</span> -
{$t('upload_status_errors')} <span class="text-immich-error">{$errorCounter}</span> -
{$t('upload_status_duplicates')} <span class="text-immich-warning">{$duplicateCounter}</span>
{$t('upload_status_uploaded')}
<span class="text-immich-success">{$successCounter.toLocaleString($locale)}</span>
-
{$t('upload_status_errors')}
<span class="text-immich-error">{$errorCounter.toLocaleString($locale)}</span>
-
{$t('upload_status_duplicates')}
<span class="text-immich-warning">{$duplicateCounter.toLocaleString($locale)}</span>
</p>
</div>
<div class="flex flex-col items-end">
@@ -139,7 +145,7 @@
on:click={() => (showDetail = true)}
class="absolute -left-4 -top-4 flex h-10 w-10 place-content-center place-items-center rounded-full bg-immich-primary p-5 text-xs text-gray-200"
>
{$remainingUploads}
{$remainingUploads.toLocaleString($locale)}
</button>
{#if $hasError}
<button
@@ -148,7 +154,7 @@
on:click={() => (showDetail = true)}
class="absolute -right-4 -top-4 flex h-10 w-10 place-content-center place-items-center rounded-full bg-immich-error p-5 text-xs text-gray-200"
>
{$errorCounter}
{$errorCounter.toLocaleString($locale)}
</button>
{/if}
<button

View File

@@ -63,10 +63,10 @@
const removeIndividualProductKey = async () => {
try {
const isConfirmed = await dialogController.show({
title: 'Remove Product Key',
prompt: 'Are you sure you want to remove the product key?',
confirmText: 'Remove',
cancelText: 'Cancel',
title: $t('purchase_remove_product_key'),
prompt: $t('purchase_remove_product_key_prompt'),
confirmText: $t('remove'),
cancelText: $t('cancel'),
});
if (!isConfirmed) {
@@ -76,17 +76,17 @@
await deleteIndividualProductKey();
purchaseStore.setPurchaseStatus(false);
} catch (error) {
handleError(error, 'Failed to remove product key');
handleError(error, $t('errors.failed_to_remove_product_key'));
}
};
const removeServerProductKey = async () => {
try {
const isConfirmed = await dialogController.show({
title: 'Remove License',
prompt: 'Are you sure you want to remove the Server product key?',
confirmText: 'Remove',
cancelText: 'Cancel',
title: $t('purchase_remove_server_product_key'),
prompt: $t('purchase_remove_server_product_key_prompt'),
confirmText: $t('remove'),
cancelText: $t('cancel'),
});
if (!isConfirmed) {
@@ -96,7 +96,7 @@
await deleteServerProductKey();
purchaseStore.setPurchaseStatus(false);
} catch (error) {
handleError(error, 'Failed to remove product key');
handleError(error, $t('errors.failed_to_remove_product_key'));
}
};
@@ -134,7 +134,7 @@
{#if $user.isAdmin && serverPurchaseInfo?.activatedAt}
<p class="dark:text-white text-sm mt-1 col-start-2">
{$t('purchase_activated_time', {
values: { date: new Date(serverPurchaseInfo.activatedAt).toLocaleDateString() },
values: { date: new Date(serverPurchaseInfo.activatedAt) },
})}
</p>
{:else}
@@ -161,7 +161,7 @@
{#if $user.license?.activatedAt}
<p class="dark:text-white text-sm mt-1 col-start-2">
{$t('purchase_activated_time', {
values: { date: new Date($user.license?.activatedAt).toLocaleDateString() },
values: { date: new Date($user.license?.activatedAt) },
})}
</p>
{/if}

View File

@@ -64,8 +64,14 @@
<svelte:window
use:shortcuts={[
{ shortcut: { key: 'k', shift: true }, onShortcut: onSelectAll },
{ shortcut: { key: 't', shift: true }, onShortcut: onSelectNone },
{ shortcut: { key: 'a' }, onShortcut: onSelectAll },
{
shortcut: { key: 's' },
onShortcut: () => {
setAsset(assets[0]);
},
},
{ shortcut: { key: 'd' }, onShortcut: onSelectNone },
{ shortcut: { key: 'c', shift: true }, onShortcut: handleResolve },
]}
/>

View File

@@ -366,7 +366,7 @@
"api_keys": "مفاتيح واجهة برمجة التطبيقات",
"app_settings": "إعدادات التطبيق",
"appears_in": "يظهر في",
"archive": "أرشيف",
"archive": "الأرشيف",
"archive_or_unarchive_photo": "أرشفة الصورة أو إلغاء أرشفتها",
"archive_size": "حجم الأرشيف",
"archive_size_description": "تكوين حجم الأرشيف للتنزيلات (بالجيجابايت)",
@@ -1215,7 +1215,7 @@
"user_usage_detail": "تفاصيل استخدام المستخدم",
"username": "اسم المستخدم",
"users": "المستخدمين",
"utilities": "مُعدات",
"utilities": "أدوات",
"validate": "تحقْق",
"variables": "المتغيرات",
"version": "الإصدار",

View File

@@ -169,7 +169,7 @@
"oauth_client_secret": "Клиентска тайна",
"oauth_enable_description": "",
"oauth_issuer_url": "",
"oauth_mobile_redirect_uri": "",
"oauth_mobile_redirect_uri": "URI за мобилно пренасочване",
"oauth_mobile_redirect_uri_override": "",
"oauth_mobile_redirect_uri_override_description": "Разреши когато 'app.immich:/' е невалиден пренасочвар адрес/URI.",
"oauth_profile_signing_algorithm": "Алгоритъм за създаване на профили",

View File

@@ -128,6 +128,7 @@
"map_dark_style": "Tema fosc",
"map_enable_description": "Habilita característiques del mapa",
"map_gps_settings": "Configuració de mapa i GPS",
"map_gps_settings_description": "Gestiona la configuració de mapa i GPS (Geocodificació inversa)",
"map_light_style": "Tema clar",
"map_manage_reverse_geocoding_settings": "Gestiona els paràmetres de <link>geocodificació inversa</link>",
"map_reverse_geocoding": "Geocodificació inversa",
@@ -173,6 +174,7 @@
"oauth_mobile_redirect_uri": "URI de redirecció mòbil",
"oauth_mobile_redirect_uri_override": "Sobreescriu l'URI de redirecció mòbil",
"oauth_mobile_redirect_uri_override_description": "Habilita quan 'app.immich:/' és una URI de redirecció invàlida.",
"oauth_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",
@@ -224,82 +226,97 @@
"storage_template_migration_description": "Aplica la <link>{template}</link> actual als elements pujats prèviament",
"storage_template_migration_info": "Els canvis de plantilla només s'aplicaran a nous elements. Per aplicar la plantilla rectroactivament a elements pujats prèviament, executeu la <link>{job}</link>.",
"storage_template_migration_job": "Tasca de migració de la plantilla d'emmagatzematge",
"storage_template_more_details": "Per obtenir més detalls sobre aquesta funció, consulteu la <template-link>Storage Template</template-link> i les seves <implications-link>implications</implications-link>",
"storage_template_onboarding_description": "Quan està activada, aquesta funció organitzarà automàticament els fitxers en funció d'una plantilla definida per l'usuari. A causa de problemes d'estabilitat, la funció s'ha desactivat de manera predeterminada. Per obtenir més informació, consulteu la <link>documentation</link>.",
"storage_template_path_length": "Límit aproximat de longitud de la ruta: <b>{length, number}</b>/{limit, number}",
"storage_template_settings": "Plantilla d'emmagatzematge",
"storage_template_settings_description": "Gestiona l'estructura de les carpetes i el nom del fitxers dels elements pujats",
"storage_template_user_label": "<code>{label}</code> és l'etiqueta d'emmagatzematge de l'usuari",
"system_settings": "Configuració del sistema",
"theme_custom_css_settings": "CSS personalitzat",
"theme_custom_css_settings_description": "",
"theme_custom_css_settings_description": "Els Fulls d'Estil en Cascada permeten personalitzar el disseny d'Immich.",
"theme_settings": "Configuració del tema",
"theme_settings_description": "Gestiona la personalització de la interfície web Immich",
"these_files_matched_by_checksum": "Aquests fitxers coincideixen amb els seus checksums",
"thumbnail_generation_job": "Generar miniatures",
"thumbnail_generation_job_description": "Genera miniatures grans, petites i borroses per a cada element, així com miniatures per a cada persona",
"transcode_policy_description": "",
"transcoding_acceleration_api": "API d'acceleració",
"transcoding_acceleration_api_description": "",
"transcoding_acceleration_api_description": "L'API que interactuarà amb el vostre dispositiu per accelerar la transcodificació. Aquesta configuració és \"millor esforç\": tornarà a la transcodificació del programari en cas d'error. VP9 pot funcionar o no depenent del vostre maquinari.",
"transcoding_acceleration_nvenc": "NVENC (requereix GPU d'NVIDIA)",
"transcoding_acceleration_qsv": "Quick Sync (requereix GPU d'Intel de 7a generació o posterior)",
"transcoding_acceleration_rkmpp": "RKMPP (requereix SoC de Rockchip)",
"transcoding_acceleration_vaapi": "VAAPI",
"transcoding_accepted_audio_codecs": "Còdecs d'àudio acceptats",
"transcoding_accepted_audio_codecs_description": "",
"transcoding_accepted_audio_codecs_description": "Seleccioneu quins còdecs d'àudio no s'han de transcodificar. Només s'utilitza per a determinades polítiques de transcodificació.",
"transcoding_accepted_containers": "Contenidors acceptats",
"transcoding_accepted_containers_description": "Seleccioneu quins formats de contenidor no s'han de redistribuir a MP4. Només s'utilitza per a determinades polítiques de transcodificació.",
"transcoding_accepted_video_codecs": "Còdecs de vídeo acceptats",
"transcoding_accepted_video_codecs_description": "",
"transcoding_advanced_options_description": "",
"transcoding_accepted_video_codecs_description": "Seleccioneu quins còdecs de vídeo no s'han de transcodificar. Només s'utilitza per a determinades polítiques de transcodificació.",
"transcoding_advanced_options_description": "Opcions que la majoria dels usuaris no haurien de canviar",
"transcoding_audio_codec": "Còdec d'àudio",
"transcoding_audio_codec_description": "",
"transcoding_bitrate_description": "",
"transcoding_audio_codec_description": "Opus és l'opció de màxima qualitat, però té menor compatibilitat amb dispositius o programari antics.",
"transcoding_bitrate_description": "Vídeos superiors a la taxa de bits màxima o que no tenen un format acceptat",
"transcoding_codecs_learn_more": "Per obtenir més informació sobre la terminologia utilitzada, consulteu la documentació de FFmpeg per al <h264-link> còdec H.264</h264-link>, <hevc-link> còdec HEVC</hevc-link> i <vp9-link> còdec VP9</vp9-link>.",
"transcoding_constant_quality_mode": "Mode de qualitat constant",
"transcoding_constant_quality_mode_description": "",
"transcoding_constant_rate_factor": "",
"transcoding_constant_rate_factor_description": "",
"transcoding_disabled_description": "",
"transcoding_constant_quality_mode_description": "ICQ és millor que CQP, però alguns dispositius d'acceleració de maquinari no admeten aquest mode. Establir aquesta opció preferirà el mode especificat quan utilitzeu la codificació basada en la qualitat. Ignorat per NVENC perquè no és compatible amb ICQ.",
"transcoding_constant_rate_factor": "Factor de taxa constant (-crf)",
"transcoding_constant_rate_factor_description": "Nivell de qualitat del vídeo. Els valors típics són 23 per a H.264, 28 per a HEVC, 31 per a VP9 i 35 per a AV1. Més baix és millor, però produeix fitxers més grans.",
"transcoding_disabled_description": "No transcodifiqueu cap vídeo, pot interrompre la reproducció en alguns clients",
"transcoding_hardware_acceleration": "Acceleració de maquinari",
"transcoding_hardware_acceleration_description": "Experimental. Molt més ràpid, però tindrà una qualitat més baixa amb la mateixa taxa de bits",
"transcoding_hardware_decoding": "Descodificació de maquinari",
"transcoding_hardware_decoding_setting_description": "",
"transcoding_hardware_decoding_setting_description": "S'aplica només a NVENC, QSV i RKMPP. Permet l'acceleració d'extrem a extrem en lloc d'accelerar només la codificació. És possible que no funcioni en tots els vídeos.",
"transcoding_hevc_codec": "Còdec HEVC",
"transcoding_max_b_frames": "",
"transcoding_max_b_frames_description": "",
"transcoding_max_bitrate": "",
"transcoding_max_bitrate_description": "",
"transcoding_max_keyframe_interval": "",
"transcoding_max_keyframe_interval_description": "",
"transcoding_optimal_description": "",
"transcoding_max_b_frames": "Nombre màxim de B-frames",
"transcoding_max_b_frames_description": "Els valors més alts milloren l'eficiència de la compressió, però alenteixen la codificació. És possible que no sigui compatible amb l'acceleració de maquinari en dispositius antics. 0 desactiva els B-frames, mentre que -1 estableix aquest valor automàticament.",
"transcoding_max_bitrate": "Taxa de bits màxima",
"transcoding_max_bitrate_description": "Establir una taxa de bits màxima pot fer que les mides dels fitxers siguin més previsibles amb un cost menor per a la qualitat. A 720p, els valors típics són 2600k per a VP9 o HEVC, o 4500k per a H.264. Desactivat si s'estableix a 0.",
"transcoding_max_keyframe_interval": "Interval màxim de fotogrames clau",
"transcoding_max_keyframe_interval_description": "Estableix la distància màxima entre fotogrames clau. Els valors més baixos empitjoren l'eficiència de la compressió, però milloren els temps de cerca i poden millorar la qualitat en escenes amb moviment ràpid. 0 estableix aquest valor automàticament.",
"transcoding_optimal_description": "Vídeos superiors a la resolució objectiu o que no tenen un format acceptat",
"transcoding_preferred_hardware_device": "Dispositiu de maquinari preferit",
"transcoding_preferred_hardware_device_description": "",
"transcoding_preset_preset": "",
"transcoding_preset_preset_description": "",
"transcoding_reference_frames": "",
"transcoding_reference_frames_description": "",
"transcoding_required_description": "",
"transcoding_preferred_hardware_device_description": "S'aplica només a VAAPI i QSV. Estableix el node dri utilitzat per a la transcodificació de maquinari.",
"transcoding_preset_preset": "Preestablert (-preset)",
"transcoding_preset_preset_description": "Velocitat de compressió. Els valors predefinits més lents produeixen fitxers més petits i augmenten la qualitat quan s'orienta a una taxa de bits determinada. VP9 ignora les velocitats superiors a \"més ràpides\".",
"transcoding_reference_frames": "Fotogrames de referència",
"transcoding_reference_frames_description": "El nombre de fotogrames a fer referència en comprimir un fotograma determinat. Els valors més alts milloren l'eficiència de la compressió, però alenteixen la codificació. 0 estableix aquest valor automàticament.",
"transcoding_required_description": "Només vídeos que no tenen un format acceptat",
"transcoding_settings": "Configuració de transcodificació de vídeo",
"transcoding_settings_description": "Gestiona la resolució i codificació dels fitxers de vídeo",
"transcoding_target_resolution": "",
"transcoding_target_resolution_description": "",
"transcoding_temporal_aq": "",
"transcoding_temporal_aq_description": "",
"transcoding_target_resolution": "Resolució objectiu",
"transcoding_target_resolution_description": "Les resolucions més altes poden conservar més detalls, però triguen més temps a codificar-se, tenen mides de fitxer més grans i poden reduir la capacitat de resposta de l'aplicació.",
"transcoding_temporal_aq": "AQ temporal",
"transcoding_temporal_aq_description": "S'aplica només a NVENC. Augmenta la qualitat de les escenes de baix moviment i alt detall. És possible que no sigui compatible amb dispositius antics.",
"transcoding_threads": "Fils",
"transcoding_threads_description": "",
"transcoding_tone_mapping": "",
"transcoding_tone_mapping_description": "",
"transcoding_tone_mapping_npl": "",
"transcoding_tone_mapping_npl_description": "",
"transcoding_transcode_policy": "",
"transcoding_two_pass_encoding": "",
"transcoding_two_pass_encoding_setting_description": "",
"transcoding_threads_description": "Els valors més alts condueixen a una codificació més ràpida, però deixen menys espai perquè el servidor processi altres tasques mentre està actiu. Aquest valor no hauria de ser superior al nombre de nuclis de CPU. Maximitza la utilització si s'estableix a 0.",
"transcoding_tone_mapping": "Mapeig de to",
"transcoding_tone_mapping_description": "Intenta preservar l'aspecte dels vídeos HDR quan es converteixen a SDR. Cada algorisme fa diferents compensacions pel color, el detall i la brillantor. Hable conserva els detalls, Mobius conserva el color i Reinhard conserva la brillantor.",
"transcoding_tone_mapping_npl": "NPL de mapatge de to",
"transcoding_tone_mapping_npl_description": "Els colors s'ajustaran perquè semblin normals per a exposicions amb aquesta brillantor. Contra intuïtivament, els valors més baixos augmenten la brillantor del vídeo i viceversa, ja que compensa la brillantor de la pantalla. 0 estableix aquest valor automàticament.",
"transcoding_transcode_policy": "Política de transcodificació",
"transcoding_transcode_policy_description": "Política sobre quan s'ha de transcodificar un vídeo. Els vídeos HDR sempre es transcodificaran (excepte si la transcodificació està desactivada).",
"transcoding_two_pass_encoding": "Codificació de dues passades",
"transcoding_two_pass_encoding_setting_description": "Transcodifica en dos passos per produir vídeos millor codificats. Quan la taxa de bits màxima està habilitada (necessari perquè funcioni amb H.264 i HEVC), aquest mode utilitza un interval de velocitat de bits basat en la taxa de bits màxima i ignora CRF. Per a VP9, es pot utilitzar CRF si la taxa de bits màxima està desactivada.",
"transcoding_video_codec": "Còdec de video",
"transcoding_video_codec_description": "",
"trash_enabled_description": "",
"transcoding_video_codec_description": "VP9 té una alta eficiència i compatibilitat web, però triga més a transcodificar-se. HEVC funciona de manera similar, però té una compatibilitat web inferior. H.264 és àmpliament compatible i de transcodificació ràpida, però produeix fitxers molt més grans. AV1 és el còdec més eficient, però no té suport en dispositius antics.",
"trash_enabled_description": "Activa les funcions de la paperera",
"trash_number_of_days": "Nombre de dies",
"trash_number_of_days_description": "",
"trash_number_of_days_description": "Nombre de dies per mantenir els recursos a la paperera abans de suprimir-los permanentment",
"trash_settings": "Configuració de la paperera",
"trash_settings_description": "Gestiona la configuració de la paperera",
"user_delete_delay_settings": "",
"user_delete_delay_settings_description": "",
"untracked_files": "Fitxers sense seguiment",
"untracked_files_description": "L'aplicació no fa un seguiment d'aquests fitxers. Poden ser el resultat de moviments fallits, càrregues interrompudes o deixades enrere a causa d'un error",
"user_delete_delay": "El compte i els recursos de <b>{user}</b> es programaran per a la supressió permanent en {delay, plural, one {# day} other {# days}}.",
"user_delete_delay_settings": "Retard de la supressió",
"user_delete_delay_settings_description": "Nombre de dies després de la supressió per eliminar permanentment el compte i els elements d'un usuari. El treball de supressió d'usuaris s'executa a mitjanit per comprovar si hi ha usuaris preparats per eliminar. Els canvis en aquesta configuració s'avaluaran en la propera execució.",
"user_delete_immediately": "El compte i els recursos de <b>{user}</b> es posaran a la cua per suprimir-los permanentment <b>immediatament</b>.",
"user_delete_immediately_checkbox": "Posa en cua l'usuari i els recursos per suprimir-los immediatament",
"user_management": "Gestió d'usuaris",
"user_password_has_been_reset": "La contrasenya de l'usuari ha estat restablida:",
"user_password_reset_description": "Si us plau, proporcioneu la contrasenya temporal a l'usuari i informeu-los que haurà de canviar la contrasenya en el proper inici de sessió.",
"user_restore_description": "Es restaurarà el compte <b>{user}</b> .",
"user_restore_scheduled_removal": "Restaura l'usuari - eliminació programada el {date, date, long}",
"user_settings": "Configuració d'usuaris",
"user_settings_description": "Gestiona la configuració dels usuaris",
"user_successfully_removed": "L'usuari {email} s'ha eliminat correctament.",
@@ -327,10 +344,12 @@
"album_options": "Opcions de l'àlbum",
"album_remove_user": "Eliminar l'usuari?",
"album_remove_user_confirmation": "Esteu segurs que voleu eliminar {user}?",
"album_share_no_users": "Sembla que has compartit aquest àlbum amb tots els usuaris o no tens cap usuari amb qui compartir-ho.",
"album_updated": "Àlbum actualitzat",
"album_updated_setting_description": "",
"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_removed": "{user} eliminat",
"album_with_link_access": "Permet que qualsevol persona que tingui l'enllaç vegi fotos i persones d'aquest àlbum.",
"albums": "Àlbums",
"albums_count": "{count, plural, one {{count, number} àlbum} other {{count, number} àlbums}}",
"all": "Tots",
@@ -339,7 +358,11 @@
"all_videos": "Tots els vídeos",
"allow_dark_mode": "Permet el tema fosc",
"allow_edits": "Permet editar",
"allow_public_user_to_download": "Permet que l'usuari públic pugui descarregar",
"allow_public_user_to_upload": "Permet que l'usuari públic pugui carregar",
"api_key": "Clau API",
"api_key_description": "Aquest valor només es mostrarà una vegada. Assegureu-vos de copiar-lo abans de tancar la finestra.",
"api_key_empty": "El nom de la clau de l'API no pot estar buit",
"api_keys": "Claus API",
"app_settings": "Configuració de l'app",
"appears_in": "Apareix a",
@@ -348,17 +371,23 @@
"archive_size": "Mida de l'arxiu",
"archive_size_description": "Configureu la mida de l'arxiu de les descàrregues (en GiB)",
"archived": "Arxivat",
"archived_count": "",
"are_these_the_same_person": "Són la mateixa persona?",
"are_you_sure_to_do_this": "Esteu segurs que voleu fer-ho?",
"asset_added_to_album": "Afegit a l'àlbum",
"asset_adding_to_album": "Afegint a l'àlbum...",
"asset_description_updated": "La descripció del recurs s'ha actualitzat",
"asset_filename_is_offline": "L'element {filename} està fora de línia",
"asset_has_unassigned_faces": "L'element té cares no assignades",
"asset_offline": "Element fora de línia",
"asset_offline_description": "Aquest element està fora de línia. L'Immich no pot accedir a la seva ubicació. Si us plau, assegureu-vos que l'actiu està disponible i després torneu la llibreria.",
"asset_skipped": "Saltat",
"asset_uploaded": "Carregat",
"asset_uploading": "S'està carregant...",
"assets": "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_name_count": "S'ha afegit {count, plural, one {# asset} other {# assets}} a {hasName, select, true {<b>{name}</b>} other {new album}}",
"assets_count": "{count, plural, one {Un element} other {# elements}}",
"assets_moved_to_trash_count": "{count, plural, one {Un element mogut} other {# elements moguts}} a la paperera",
"assets_permanently_deleted_count": "{count, plural, one {Un element esborrat} other {# elements esborrats}} permanentment",
@@ -369,11 +398,17 @@
"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",
"back": "Enrere",
"back_close_deselect": "Tornar, tancar o anul·lar la selecció",
"backward": "Enrere",
"birthdate_saved": "Data de naixement guardada amb èxit",
"birthdate_set_description": "La data de naixement s'utilitza per calcular l'edat d'aquesta persona en el moment d'una foto.",
"blurred_background": "Fons difuminat",
"buy": "Comprar llicència",
"build": "Construeix",
"build_image": "Construeix la imatge",
"bulk_delete_duplicates_confirmation": "Esteu segur que voleu suprimir de manera massiva {count, plural, one {# duplicate asset} other {# duplicate asset}}? Això mantindrà el recurs més gran de cada grup i esborrarà permanentment tots els altres duplicats. No podeu desfer aquesta acció!",
"bulk_keep_duplicates_confirmation": "Esteu segur que voleu mantenir {count, plural, one {# duplicate asset} other {# duplicate asset}}? Això resoldrà tots els grups duplicats sense eliminar res.",
"bulk_trash_duplicates_confirmation": "Esteu segur que voleu enviar a les escombraries {count, plural, one {# duplicate asset} other {# duplicate asset}}? Això mantindrà el recurs més gran de cada grup i eliminarà la resta de duplicats.",
"buy": "Comprar Immich",
"camera": "Càmera",
"camera_brand": "Marca de la càmera",
"camera_model": "Model de càmera",
@@ -392,12 +427,16 @@
"change_name": "Canvia el nom",
"change_name_successfully": "Nom canviat amb èxit",
"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_your_password": "Canvia la teva contrasenya",
"changed_visibility_successfully": "Visibilitat canviada amb èxit",
"check_logs": "",
"check_all": "Marqueu-ho tot",
"check_logs": "Comprovar els registres",
"choose_matching_people_to_merge": "Trieu les persones que coincideixin per combinar-les",
"city": "Ciutat",
"clear": "Neteja",
"clear_all": "Neteja-ho tot",
"clear_all_recent_searches": "Esborra totes les cerques recents",
"clear_message": "Neteja el missatge",
"clear_value": "Neteja el valor",
"close": "Tanca",
@@ -411,13 +450,13 @@
"confirm_admin_password": "Confirmeu la contrasenya d'administrador",
"confirm_delete_shared_link": "Esteu segurs que voleu eliminar aquest enllaç compartit?",
"confirm_password": "Confirmació de contrasenya",
"contain": "",
"context": "",
"continue": "",
"contain": "Contingut",
"context": "Context",
"continue": "Continuar",
"copied_image_to_clipboard": "Imatge copiada a porta-retalls.",
"copied_to_clipboard": "Copiada a porta-retalls!",
"copy_error": "Error de còpia",
"copy_file_path": "",
"copy_file_path": "Copia la ruta del fitxer",
"copy_image": "Còpia imatge",
"copy_link": "Còpia l'enllaç",
"copy_link_to_clipboard": "Còpia l'enllaç al porta-retalls",
@@ -438,7 +477,7 @@
"create_user": "Crea un usuari",
"created": "Creat",
"current_device": "Dispositiu actual",
"custom_locale": "",
"custom_locale": "Localització personalitzada",
"custom_locale_description": "Format de dates i números segons la llengua i regió",
"dark": "Fosc",
"date_after": "Data posterior a",
@@ -447,13 +486,14 @@
"date_of_birth_saved": "Data de naixement guardada amb èxit",
"date_range": "Interval de dates",
"day": "Dia",
"default_locale": "",
"deduplicate_all": "Desduplica-ho tot",
"default_locale": "Localització predeterminada",
"default_locale_description": "Format de dates i números segons la configuració del navegador",
"delete": "Esborra",
"delete_album": "Esborra l'àlbum",
"delete_api_key_prompt": "Esteu segurs que voleu eliminar aquesta clau API?",
"delete_duplicates_confirmation": "Esteu segurs que voleu eliminar aquests duplicats permanentment?",
"delete_key": "",
"delete_key": "Suprimeix la clau",
"delete_library": "Suprimeix la llibreria",
"delete_link": "Esborra l'enllaç",
"delete_shared_link": "Odstranit sdílený odkaz",
@@ -463,20 +503,23 @@
"details": "Detalls",
"direction": "Direcció",
"disabled": "Desactivat",
"disallow_edits": "",
"disallow_edits": "No permetre les edicions",
"discover": "Descobreix",
"dismiss_all_errors": "Descarta tots els errors",
"dismiss_error": "Descarta l'error",
"display_options": "Opcions de visualització",
"display_order": "Ordre de visualització",
"display_original_photos": "Mostra les fotografies originals",
"display_original_photos_setting_description": "",
"display_original_photos_setting_description": "Preferiu mostrar la foto original quan visualitzeu un recurs en lloc de miniatures quan el recurs original és compatible amb el web. Això pot provocar una velocitat de visualització de fotos més lenta.",
"do_not_show_again": "No tornis a mostrar aquest missatge",
"done": "Fet",
"download": "Baixar",
"download_settings": "Baixar",
"download_settings_description": "Gestioneu la configuració relacionada amb la descàrrega de recursos",
"downloading": "Baixant",
"downloading_asset_filename": "Descarregant l'element {filename}",
"duplicates": "Duplicats",
"duplicates_description": "Resol cada grup indicant quins, si n'hi ha, són duplicats",
"duration": "Duració",
"durations": {
"days": "",
@@ -507,6 +550,7 @@
"empty": "",
"empty_album": "",
"empty_trash": "Buidar la paperera",
"empty_trash_confirmation": "Esteu segur que voleu buidar la paperera? Això eliminarà tots els recursos a la paperera permanentment d'Immich.\nNo podeu desfer aquesta acció!",
"enable": "Activar",
"enabled": "Activat",
"end_date": "Data final",
@@ -518,28 +562,37 @@
"cannot_navigate_previous_asset": "No es pot navegar a l'element anterior",
"cant_apply_changes": "No es poden aplicar els canvis",
"cant_change_activity": "No es pot {enabled, select, true {desactivar} other {activar}} aquesta activitat",
"cant_change_asset_favorite": "No es pot canviar el favorit per a aquest recurs",
"cant_change_metadata_assets_count": "No es poden canviar les metadades {count, plural, one {de l'element} other {dels # elements}}",
"cant_get_faces": "No es poden obtenir les cares",
"cant_get_number_of_comments": "No es pot obtenir el nombre de comentaris",
"cant_search_people": "No es poden cercar persones",
"cant_search_places": "No es poden cercar llocs",
"cleared_jobs": "Tasques buides per a: {job}",
"error_adding_assets_to_album": "Error afegint elements a l'àlbum",
"error_adding_users_to_album": "Error afegint usuaris a l'àlbum",
"error_deleting_shared_user": "S'ha produït un error en suprimir l'usuari compartit",
"error_downloading": "Error descarregant {filename}",
"error_hiding_buy_button": "S'ha produït un error en amagar el botó de compra",
"error_removing_assets_from_album": "Error eliminant els elements de l'àlbum, consulteu la consola per obtenir més detalls",
"error_selecting_all_assets": "Error seleccionant tots els elements",
"exclusion_pattern_already_exists": "Aquest patró dexclusió ja existeix.",
"failed_job_command": "L'ordre {command} ha fallat per a la tasca: {job}",
"failed_to_create_album": "No s'ha pogut crear l'àlbum",
"failed_to_create_shared_link": "No s'ha pogut crear l'enllaç compartit",
"failed_to_edit_shared_link": "No s'ha pogut editar l'enllaç compartit",
"failed_to_load_asset": "No s'ha pogut carregar l'element",
"failed_to_load_assets": "No s'han pogut carregar els elements",
"failed_to_load_people": "No s'han pogut carregar les persones",
"failed_to_remove_product_key": "No s'ha pogut eliminar la clau del producte",
"failed_to_stack_assets": "No s'han pogut apilar els elements",
"failed_to_unstack_assets": "No s'han pogut desapilar els elements",
"import_path_already_exists": "Aquest camí d'importació ja existeix.",
"incorrect_email_or_password": "Correu electrònic o contrasenya incorrectes",
"paths_validation_failed": "{paths, plural, one {# path} other {# paths}} no ha pogut validar",
"profile_picture_transparent_pixels": "Les fotos de perfil no poden tenir píxels transparents. Per favor, feu zoom in, mogueu la imatge o ambdues.",
"quota_higher_than_disk_size": "Heu establert una quota més gran que la mida de disc",
"repair_unable_to_check_items": "No es pot comprovar {count, select, one {item} other {items}}",
"unable_to_add_album_users": "No es poden afegir usuaris a l'àlbum",
"unable_to_add_assets_to_shared_link": "No s'han pogut afegir els elements a l'enllaç compartit",
"unable_to_add_comment": "No es pot afegir el comentari",
@@ -548,131 +601,186 @@
"unable_to_add_partners": "No es poden afegir companys",
"unable_to_add_remove_archive": "No s'ha pogut {archived, select, true {eliminar l'element de} other {afegir l'element a}} l'arxiu",
"unable_to_add_remove_favorites": "No s'ha pogut {favorite, select, true {afegir l'element als} other {eliminar l'element dels}} preferits",
"unable_to_change_album_user_role": "",
"unable_to_archive_unarchive": "No es pot {archived, select, true {archive} other {unarchive}}",
"unable_to_change_album_user_role": "No es pot canviar el rol d'usuari de l'àlbum",
"unable_to_change_date": "No es pot canviar la data",
"unable_to_change_favorite": "No es pot canviar el favorit per a aquest recurs",
"unable_to_change_location": "No es pot canviar la ubicació",
"unable_to_change_password": "No es pot canviar la contrasenya",
"unable_to_change_visibility": "No es pot canviar la visibilitat de {count, plural, one {# person} other {# people}}",
"unable_to_check_item": "",
"unable_to_check_items": "",
"unable_to_complete_oauth_login": "No es pot completar l'inici de sessió OAuth",
"unable_to_connect": "No pot connectar",
"unable_to_connect_to_server": "No es pot connectar al servidor",
"unable_to_create_admin_account": "",
"unable_to_copy_to_clipboard": "No es pot copiar al porta-retalls, assegureu-vos que esteu accedint a la pàgina mitjançant https",
"unable_to_create_admin_account": "No es pot crear un compte d'administrador",
"unable_to_create_api_key": "No es pot crear una clau d'API nova",
"unable_to_create_library": "No es pot crear la llibreria",
"unable_to_create_user": "No es pot crear l'usuari",
"unable_to_delete_album": "No es pot eliminar l'àlbum",
"unable_to_delete_asset": "",
"unable_to_delete_asset": "No es pot suprimir el recurs",
"unable_to_delete_assets": "S'ha produït un error en suprimir recursos",
"unable_to_delete_exclusion_pattern": "No es pot suprimir el patró d'exclusió",
"unable_to_delete_import_path": "No es pot suprimir la ruta d'importació",
"unable_to_delete_shared_link": "No es pot suprimir l'enllaç compartit",
"unable_to_delete_user": "No es pot eliminar l'usuari",
"unable_to_download_files": "No es poden descarregar fitxers",
"unable_to_edit_exclusion_pattern": "No es pot editar el patró d'exclusió",
"unable_to_edit_import_path": "No es pot editar la ruta d'importació",
"unable_to_empty_trash": "No es pot buidar la paperera",
"unable_to_enter_fullscreen": "No es pot entrar a la pantalla completa",
"unable_to_exit_fullscreen": "No es pot sortir de la pantalla completa",
"unable_to_get_comments_number": "No es pot obtenir el nombre de comentaris",
"unable_to_get_shared_link": "No s'ha pogut obtenir l'enllaç compartit",
"unable_to_hide_person": "No es pot amagar la persona",
"unable_to_link_oauth_account": "No es pot enllaçar el compte OAuth",
"unable_to_load_album": "No es pot carregar l'àlbum",
"unable_to_load_asset_activity": "",
"unable_to_load_items": "",
"unable_to_load_liked_status": "",
"unable_to_play_video": "",
"unable_to_refresh_user": "",
"unable_to_remove_album_users": "",
"unable_to_load_asset_activity": "No es pot carregar l'activitat dels recursos",
"unable_to_load_items": "No es poden carregar els elements",
"unable_to_load_liked_status": "No es pot carregar l'estat de m'agrada",
"unable_to_log_out_all_devices": "No es poden tancar la sessió de tots els dispositius",
"unable_to_log_out_device": "No es pot tancar la sessió del dispositiu",
"unable_to_login_with_oauth": "No es pot iniciar sessió amb OAuth",
"unable_to_play_video": "No es pot reproduir el vídeo",
"unable_to_reassign_assets_existing_person": "No es poden reassignar recursos a {name, select, null {an existing person} other {{name}}}",
"unable_to_reassign_assets_new_person": "No es poden reassignar recursos a una persona nova",
"unable_to_refresh_user": "No es pot actualitzar l'usuari",
"unable_to_remove_album_users": "No es poden eliminar usuaris de l'àlbum",
"unable_to_remove_api_key": "No es pot eliminar la clau de l'API",
"unable_to_remove_assets_from_shared_link": "No es poden eliminar recursos de l'enllaç compartit",
"unable_to_remove_comment": "",
"unable_to_remove_library": "",
"unable_to_remove_library": "No es pot eliminar la biblioteca",
"unable_to_remove_offline_files": "No es poden eliminar els fitxers fora de línia",
"unable_to_remove_partner": "No es pot eliminar company/a",
"unable_to_remove_reaction": "",
"unable_to_remove_reaction": "No es pot eliminar la reacció",
"unable_to_remove_user": "",
"unable_to_repair_items": "",
"unable_to_reset_password": "",
"unable_to_resolve_duplicate": "",
"unable_to_restore_assets": "",
"unable_to_restore_trash": "",
"unable_to_restore_user": "",
"unable_to_save_album": "",
"unable_to_save_name": "",
"unable_to_save_profile": "",
"unable_to_save_settings": "",
"unable_to_scan_libraries": "",
"unable_to_scan_library": "",
"unable_to_set_profile_picture": "",
"unable_to_submit_job": "",
"unable_to_trash_asset": "",
"unable_to_unlink_account": "",
"unable_to_repair_items": "No es poden reparar els elements",
"unable_to_reset_password": "No es pot restablir la contrasenya",
"unable_to_resolve_duplicate": "No es pot resoldre el duplicat",
"unable_to_restore_assets": "No es poden restaurar els recursos",
"unable_to_restore_trash": "No es pot restaurar la paperera",
"unable_to_restore_user": "No es pot restaurar l'usuari",
"unable_to_save_album": "No es pot desar l'àlbum",
"unable_to_save_api_key": "No es pot desar la clau de l'API",
"unable_to_save_date_of_birth": "No es pot desar la data de naixement",
"unable_to_save_name": "No es pot desar el nom",
"unable_to_save_profile": "No es pot desar el perfil",
"unable_to_save_settings": "No es pot desar la configuració",
"unable_to_scan_libraries": "No es poden escanejar les biblioteques",
"unable_to_scan_library": "No es pot escanejar la biblioteca",
"unable_to_set_feature_photo": "No s'ha pogut configurar la foto destacada",
"unable_to_set_profile_picture": "No es pot configurar la foto de perfil",
"unable_to_submit_job": "No es pot enviar la tasca",
"unable_to_trash_asset": "No es pot eliminar el recurs a la paperera",
"unable_to_unlink_account": "No es pot desenllaçar el compte",
"unable_to_update_album_cover": "No es pot actualitzar la portada de l'àlbum",
"unable_to_update_album_info": "No es pot actualitzar la informació de l'àlbum",
"unable_to_update_library": "",
"unable_to_update_location": "",
"unable_to_update_settings": "",
"unable_to_update_user": ""
"unable_to_update_library": "No es pot actualitzar la biblioteca",
"unable_to_update_location": "No es pot actualitzar la ubicació",
"unable_to_update_settings": "No es pot actualitzar la configuració",
"unable_to_update_timeline_display_status": "No es pot actualitzar l'estat de visualització de la cronologia",
"unable_to_update_user": "No es pot actualitzar l'usuari",
"unable_to_upload_file": "No es pot carregar el fitxer"
},
"every_day_at_onepm": "",
"every_night_at_midnight": "",
"every_night_at_twoam": "",
"every_six_hours": "",
"exit_slideshow": "",
"expand_all": "",
"exit_slideshow": "Surt de la presentació de diapositives",
"expand_all": "Ampliar-ho tot",
"expire_after": "Caduca després de",
"expired": "Caducat",
"expires_date": "Caduca el {date}",
"explore": "Explorar",
"export": "Exporta",
"export_as_json": "Exportar com a JSON",
"extension": "Extensió",
"external": "Extern",
"external_libraries": "Llibreries externes",
"face_unassigned": "Sense assignar",
"failed_to_get_people": "",
"favorite": "Preferit",
"favorite_or_unfavorite_photo": "",
"favorite_or_unfavorite_photo": "Foto preferida o no preferida",
"favorites": "Preferits",
"feature": "",
"feature_photo_updated": "",
"feature_photo_updated": "Foto destacada actualitzada",
"featurecollection": "",
"file_name": "Nom de l'arxiu",
"file_name_or_extension": "Nom de l'arxiu o extensió",
"filename": "",
"files": "",
"filetype": "",
"filter_people": "",
"fix_incorrect_match": "",
"force_re-scan_library_files": "",
"forward": "",
"filter_people": "Filtra persones",
"find_them_fast": "Trobeu-los ràpidament pel nom amb la cerca",
"fix_incorrect_match": "Corregiu la coincidència incorrecta",
"force_re-scan_library_files": "Força a tornar a escanejar tots els fitxers de la biblioteca",
"forward": "Endavant",
"general": "General",
"get_help": "",
"getting_started": "",
"go_back": "",
"go_to_search": "",
"go_to_share_page": "",
"group_albums_by": "",
"get_help": "Aconseguir ajuda",
"getting_started": "Començant",
"go_back": "Torna",
"go_to_search": "Vés a cercar",
"go_to_share_page": "Vés a la pàgina de compartir",
"group_albums_by": "Agrupa àlbums per...",
"group_no": "Cap agrupació",
"group_owner": "Agrupar per propietari",
"group_year": "Agrupar per any",
"has_quota": "Quota",
"hide_gallery": "",
"hide_password": "",
"hide_person": "",
"hi_user": "Hola {name} ({email})",
"hide_all_people": "Amaga totes les persones",
"hide_gallery": "Amaga la galeria",
"hide_named_person": "Amaga la persona {name}",
"hide_password": "Amaga la contrasenya",
"hide_person": "Amaga la persona",
"hide_unnamed_people": "Amaga persones sense nom",
"host": "Amfitrió",
"hour": "Hora",
"image": "Imatge",
"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_2_people": "{isVideo, select, true {Video} other {Image}} pres/a {person1} amb {person2} el {date}",
"image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} pres/a amb {person1}, {person2}, i {person3} el {date}",
"image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} pres/a amb {person1}, {person2}, i {additionalCount, number} altres el {date}",
"image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} pres/a a {city}, {country} el {date}",
"image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} pres/a a {city}, {country} amb {person1} 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_4_or_more_people": "{isVideo, select, true {Video} other {Image}} pres/a a {city}, {country} amb {person1}, {person2}, i {additionalCount, number} altres el {date}",
"img": "",
"immich_logo": "",
"immich_web_interface": "Interfície web Immich",
"import_from_json": "Importar des de JSON",
"import_path": "",
"in_archive": "",
"import_path": "Ruta d'importació",
"in_albums": "A {count, plural, one {# album} other {# albums}}",
"in_archive": "En arxiu",
"include_archived": "Incloure arxivats",
"include_shared_albums": "",
"include_shared_albums": "Inclou àlbums compartits",
"include_shared_partner_assets": "Incloure elements dels companys",
"individual_share": "",
"info": "Informació",
"interval": {
"day_at_onepm": "",
"hours": "",
"night_at_midnight": "",
"night_at_twoam": ""
"day_at_onepm": "Cada dia a les 13h",
"hours": "Cada {hours, plural, one {hour} other {{hours, number} hours}}",
"night_at_midnight": "Cada mitjanit",
"night_at_twoam": "Cada nit a les 2 del matí"
},
"invite_people": "",
"invite_people": "Convida gent",
"invite_to_album": "Convida a l'àlbum",
"job_settings_description": "",
"jobs": "Tasques",
"keep": "Mantenir",
"keyboard_shortcuts": "",
"keyboard_shortcuts": "Dreceres de teclat",
"language": "Idioma",
"language_setting_description": "",
"last_seen": "",
"leave": "",
"language_setting_description": "Seleccioneu el vostre idioma",
"last_seen": "Vist per últim cop",
"latest_version": "Última versió",
"latitude": "Latitud",
"leave": "Marxar",
"let_others_respond": "Deixa que els altres responguin",
"level": "",
"level": "Nivell",
"library": "Bibilioteca",
"library_options": "",
"library_options": "Opcions de biblioteca",
"license_activated_title": "La vostra llicència ha estat activada amb èxit",
"license_button_activate": "Activar",
"license_button_buy": "Comprar",
@@ -686,86 +794,114 @@
"license_server_title": "Llicència de servidor",
"license_trial_info_2": "Heu utilitzat l'Immich durant uns",
"license_trial_info_3": "{accountAge, plural, one {# dia} other {# dies}}",
"light": "",
"link_options": "",
"link_to_oauth": "",
"linked_oauth_account": "",
"light": "Llum",
"like_deleted": "M'agrada suprimit",
"link_options": "Opcions d'enllaç",
"link_to_oauth": "Enllaç a OAuth",
"linked_oauth_account": "Compte OAuth enllaçat",
"list": "Llista",
"loading": "Carregant",
"loading_search_results_failed": "",
"loading_search_results_failed": "No s'han pogut carregar els resultats de la cerca",
"log_out": "Tanca la sessió",
"log_out_all_devices": "",
"login_has_been_disabled": "",
"look": "",
"loop_videos": "",
"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_device": "Dispositiu tancat",
"login": "Iniciar sessió",
"login_has_been_disabled": "L'inici de sessió s'ha desactivat.",
"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?",
"longitude": "Longitud",
"look": "Aspecte",
"loop_videos": "Vídeos en bucle",
"loop_videos_description": "Habilita la reproducció en bucle del vídeo en els detalls.",
"make": "Fabricant",
"manage_shared_links": "Spravovat sdílené odkazy",
"manage_sharing_with_partners": "Gestiona la compartició amb els companys",
"manage_the_app_settings": "",
"manage_your_account": "",
"manage_your_api_keys": "",
"manage_your_devices": "",
"manage_your_oauth_connection": "",
"manage_the_app_settings": "Gestioneu la configuració de l'aplicació",
"manage_your_account": "Gestiona el teu compte",
"manage_your_api_keys": "Gestioneu les vostres claus API",
"manage_your_devices": "Gestioneu els vostres dispositius connectats",
"manage_your_oauth_connection": "Gestioneu la vostra connexió OAuth",
"map": "Mapa",
"map_marker_with_image": "",
"map_marker_for_images": "Marcador de mapa per a imatges fetes a {city}, {country}",
"map_marker_with_image": "Marcador de mapa amb imatge",
"map_settings": "Paràmetres de mapa",
"matches": "Coincidències",
"media_type": "Tipus de mitjà",
"memories": "Records",
"memories_setting_description": "",
"memories_setting_description": "Gestiona el que veus als teus records",
"memory": "Record",
"memory_lane_title": "Línia de records {title}",
"menu": "Menú",
"merge": "",
"merge_people": "",
"merge_people_successfully": "",
"merge": "Combinar",
"merge_people": "Combinar persones",
"merge_people_limit": "Només pots combinar fins a 5 cares alhora",
"merge_people_prompt": "Vols combinar aquestes persones? Aquesta acció és irreversible.",
"merge_people_successfully": "Persones combinades amb èxit",
"merged_people_count": "Combinades {count, plural, one {# person} other {# people}}",
"minimize": "Minimitza",
"minute": "Minut",
"missing": "Restants",
"model": "",
"model": "Model",
"month": "Mes",
"more": "Més",
"moved_to_trash": "",
"my_albums": "",
"moved_to_trash": "S'ha mogut a la paperera",
"my_albums": "Els meus àlbums",
"name": "Nom",
"name_or_nickname": "",
"name_or_nickname": "Nom o sobrenom",
"never": "Mai",
"new_api_key": "",
"new_album": "Nou Àlbum",
"new_api_key": "Nova clau de l'API",
"new_password": "Nova contrasenya",
"new_person": "Persona nova",
"new_user_created": "Nou usuari creat",
"newest_first": "",
"new_version_available": "NOVA VERSIÓ DISPONIBLE",
"newest_first": "El més nou primer",
"next": "Següent",
"next_memory": "",
"next_memory": "Següent record",
"no": "No",
"no_albums_message": "Creeu un àlbum per organitzar les vostres fotos i vídeos",
"no_albums_with_name_yet": "Sembla que encara no tens cap àlbum amb aquest nom.",
"no_albums_yet": "Sembla que encara no tens cap àlbum.",
"no_archived_assets_message": "Arxiveu fotos i vídeos per ocultar-los de Fotos",
"no_assets_message": "",
"no_assets_message": "FEU CLIC PER PUJAR LA VOSTRA PRIMERA FOTO",
"no_duplicates_found": "No s'han trobat duplicats.",
"no_exif_info_available": "",
"no_explore_results_message": "",
"no_exif_info_available": "No hi ha informació d'exif disponible",
"no_explore_results_message": "Penja més fotos per explorar la teva col·lecció.",
"no_favorites_message": "Afegiu preferits per trobar les millors fotos i vídeos a l'instant",
"no_libraries_message": "Creeu una llibreria externa per veure les vostres fotos i vídeos",
"no_name": "",
"no_places": "",
"no_results": "",
"no_name": "Sense nom",
"no_places": "No hi ha llocs",
"no_results": "Sense resultats",
"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",
"not_in_any_album": "En cap àlbum",
"note_apply_storage_label_to_previously_uploaded assets": "Nota: per aplicar l'etiqueta d'emmagatzematge als actius penjats anteriorment, executeu el",
"note_unlimited_quota": "Nota: Intruduïu 0 per a quota il·limitada",
"notes": "",
"notification_toggle_setting_description": "",
"notes": "Notes",
"notification_toggle_setting_description": "Activa les notificacions per correu electrònic",
"notifications": "Notificacions",
"notifications_setting_description": "",
"notifications_setting_description": "Gestiona les notificacions",
"oauth": "OAuth",
"offline": "Fora de línia",
"offline_paths": "Rutes fora de línia",
"offline_paths_description": "Aquests resultats poden ser deguts a la supressió manual de fitxers que no formen part d'una biblioteca externa.",
"ok": "D'acord",
"oldest_first": "",
"oldest_first": "El més vell primer",
"onboarding_theme_description": "Trieu un tema de color per a la vostra instància. Podeu canviar-ho més endavant a la vostra configuració.",
"onboarding_welcome_description": "Configurem la vostra instància amb alguns paràmetres habituals.",
"onboarding_welcome_user": "Benvingut, {user}",
"online": "En línia",
"only_favorites": "Només preferits",
"only_refreshes_modified_files": "",
"open_the_search_filters": "",
"only_refreshes_modified_files": "Només actualitza els fitxers modificats",
"open_in_openstreetmap": "Obre a OpenStreetMap",
"open_the_search_filters": "Obriu els filtres de cerca",
"options": "Opcions",
"or": "o",
"organize_your_library": "Organitzeu la llibreria",
"other": "",
"other_devices": "",
"original": "original",
"other": "Altres",
"other_devices": "Altres dispositius",
"other_variables": "Altres variables",
"owned": "Propi",
"owner": "Propietari",
@@ -778,16 +914,16 @@
"password": "Contrasenya",
"password_does_not_match": "La contrasenya no coincideix",
"password_required": "Contrasenya requerida",
"password_reset_success": "",
"password_reset_success": "El restabliment de la contrasenya ha estat correcte",
"past_durations": {
"days": "{days, plural, one {El dia anterior} other {Els # dies anteriors}}",
"hours": "",
"years": "{years, plural, one {L'any passat} other {Els passats # anys}}"
},
"path": "",
"path": "Ruta",
"pattern": "Patró",
"pause": "Pausa",
"pause_memories": "",
"pause_memories": "Pausa els records",
"paused": "En pausa",
"pending": "Pendent",
"people": "Persones",
@@ -802,6 +938,7 @@
"permanently_deleted_assets_count": "{count, plural, one {S'ha eliminat un element} other {S'han eliminat # elements}} permanentment",
"person": "Persona",
"person_hidden": "{name}{hidden, select, true { (ocultat)} other {}}",
"photo_shared_all_users": "Sembla que has compartit les teves fotos amb tots els usuaris o no tens cap usuari amb qui compartir-les.",
"photos": "Fotos",
"photos_and_videos": "Fotos i vídeos",
"photos_count": "{count, plural, one {{count, number} foto} other {{count, number} fotos}}",
@@ -810,36 +947,66 @@
"place": "Lloc",
"places": "Llocs",
"play": "Reprodueix",
"play_memories": "",
"play_motion_photo": "",
"play_or_pause_video": "",
"play_memories": "Reproduir records",
"play_motion_photo": "Reproduir Fotos en Moviment",
"play_or_pause_video": "Reproduir o posar en pausa el vídeo",
"point": "",
"port": "Port",
"preset": "",
"preview": "Previsualització",
"previous": "Anterior",
"previous_memory": "",
"previous_or_next_photo": "",
"primary": "",
"profile_picture_set": "",
"previous_memory": "Memòria anterior",
"previous_or_next_photo": "Foto anterior o següent",
"primary": "Primària",
"profile_image_of_user": "Imatge de perfil de {user}",
"profile_picture_set": "Imatge de perfil configurada.",
"public_album": "Àlbum públic",
"public_share": "",
"purchase_activated_subtitle": "Gràcies per donar suport a Immich i al programari de codi obert",
"purchase_activated_time": "Activat el {date, date}",
"purchase_activated_title": "La teva clau s'ha activat correctament",
"purchase_button_activate": "Activar",
"purchase_button_buy": "Comprar",
"purchase_button_buy_immich": "Compra Immich",
"purchase_button_never_show_again": "No mostrar mai més",
"purchase_button_reminder": "Recordar en 30 dies",
"purchase_button_remove_key": "Elimina la clau",
"purchase_failed_activation": "No s'ha pogut activar! Si us plau, comproveu el vostre correu electrònic per trobar la clau de producte correcta!",
"purchase_individual_description_1": "Per a un particular",
"purchase_input_suggestion": "Tens una clau de producte? Introduïu la clau a continuació",
"purchase_license_subtitle": "Compra Immich per donar suport al desenvolupament continuat del servei",
"purchase_lifetime_description": "Compra de per vida",
"purchase_option_title": "OPCIONS DE COMPRA",
"purchase_panel_info_1": "Crear Immich requereix molt de temps i esforç, tenim enginyers a temps complet treballant-hi per fer-ho tan bo com sigui possible. La nostra missió és que el programari de codi obert i les pràctiques empresarials ètiques es converteixin en una font d'ingressos sostenible per als desenvolupadors i creïn un ecosistema que respecti la privacitat amb alternatives reals als serveis cloud explotadors.",
"purchase_panel_info_2": "Com que estem compromesos a no afegir murs de pagament, aquesta compra no us atorgarà cap funció addicional a Immich. Confiem en usuaris com tu per donar suport al desenvolupament continu d'Immich.",
"purchase_panel_title": "Donar suport al projecte",
"purchase_remove_product_key": "Elimina la clau del producte",
"purchase_remove_product_key_prompt": "Esteu segur que voleu eliminar la clau del producte?",
"purchase_remove_server_product_key": "Elimina la clau de producte del servidor",
"purchase_remove_server_product_key_prompt": "Esteu segur que voleu eliminar la clau de producte del servidor?",
"purchase_server_description_1": "Per a tot el servidor",
"purchase_server_title": "Servidor",
"purchase_settings_server_activated": "La clau de producte del servidor la gestiona l'administrador",
"range": "",
"raw": "",
"reaction_options": "",
"read_changelog": "",
"read_changelog": "Llegeix el registre de canvis",
"reassign": "Reassignar",
"reassing_hint": "Assignar els elements seleccionats a una persona existent",
"recent": "Recent",
"recent_searches": "Cerques recents",
"refresh": "Actualitzar",
"refresh_encoded_videos": "Actualitza vídeos codificats",
"refresh_metadata": "Actualitzar les metadades",
"refresh_thumbnails": "Actualitzar la miniatura",
"refreshed": "Actualitzat",
"refreshes_every_file": "",
"refreshes_every_file": "Actualitza tots els fitxers",
"refreshing_encoded_video": "S'està actualitzant el vídeo codificat",
"refreshing_metadata": "Actualitzant les metadades",
"regenerating_thumbnails": "Regenerant les miniatures",
"remove": "Eliminar",
"remove_assets_title": "Eliminar els elements?",
"remove_custom_date_range": "Elimina l'interval de dates personalitzat",
"remove_from_album": "Treu de l'àlbum",
"remove_from_favorites": "Eliminar dels preferits",
"remove_from_shared_link": "Eliminar de l'enllaç compartit",
@@ -848,17 +1015,18 @@
"removed_api_key": "Eliminada la clau d'API: {name}",
"removed_from_archive": "Eliminat de l'arxiu",
"removed_from_favorites": "Eliminat dels preferits",
"removed_from_favorites_count": "{count, plural {S'han eliminat # elements}, other {S'ha eliminat un element}} dels preferits",
"removed_from_favorites_count": "{count, plural, other {Removed #}} dels preferits",
"repair": "Reparació",
"repair_no_results_message": "",
"repair_no_results_message": "Els fitxers sense seguiment i que falten es mostraran aquí",
"replace_with_upload": "Substituir amb una pujada",
"repository": "Repositori",
"require_password": "",
"require_password": "Requereix contrasenya",
"require_user_to_change_password_on_first_login": "Requerir que l'usuari canviï la contrasenya en el primer inici de sessió",
"reset": "Restablir",
"reset_password": "Restablir contrasenya",
"reset_people_visibility": "Restablir la visibilitat de les persones",
"reset_settings_to_default": "",
"reset_to_default": "Restableix els valors predeterminats",
"resolved_all_duplicates": "Tots els duplicats resolts",
"restore": "Recupera",
"restore_all": "Restaurar-ho tot",
@@ -907,7 +1075,7 @@
"select_photos": "Tria fotografies",
"select_trash_all": "Envia la selecció a la paperera",
"selected": "Seleccionat",
"selected_count": "{count, plural {# seleccionats}, other {Un seleccionat}}",
"selected_count": "",
"send_message": "Envia missatge",
"send_welcome_email": "Envia correu de benvinguda",
"server": "Servidor",
@@ -1045,6 +1213,7 @@
"validate": "Valida",
"variables": "Variables",
"version": "Versió",
"version_announcement_message": "Hola amic, hi ha una nova versió de l'aplicació, si us plau, preneu-vos el temps per visitar les <link>release notes</link> i assegureu-vos que el vostre <code>docker-compose.yml</code> i <code>.env</code> estàn actualitzats per evitar qualsevol configuració incorrecta, especialment si utilitzeu WatchTower o qualsevol mecanisme que gestioni l'actualització automàtica de la vostra aplicació.",
"video": "Vídeo",
"video_hover_setting": "Reprodueix la miniatura en passar el ratolí",
"video_hover_setting_description": "Reprodueix la miniatura quan el ratolí plana sobre l'element. Fins i tot quan estigui deshabilitat, la reproducció s'iniciarà planant sobre el botó de reproducció.",

View File

@@ -410,7 +410,7 @@
"bulk_delete_duplicates_confirmation": "Opravdu chcete hromadně odstranit {count, plural, one {# duplicitní položku} few {# duplicitní položky} other {# duplicitních položek}}? Tím se zachová největší položka z každé skupiny a všechny ostatní duplicity se trvale odstraní. Tuto akci nelze vrátit zpět!",
"bulk_keep_duplicates_confirmation": "Opravdu si chcete ponechat {count, plural, one {# duplicitní položku} few {# duplicitní položky} other {# duplicitních položek}}? Tím se vyřeší všechny duplicitní skupiny, aniž by se cokoli odstranilo.",
"bulk_trash_duplicates_confirmation": "Opravdu chcete hromadně vyhodit {count, plural, one {# duplicitní položku} few {# duplicitní položky} other {# duplicitních položek}}? Tím se zachová největší položka z každé skupiny a všechny ostatní duplikáty se vyhodí.",
"buy": "Zakoupit licenci",
"buy": "Zakoupit Immich",
"camera": "Fotoaparát",
"camera_brand": "Značka fotoaparátu",
"camera_model": "Model fotoaparátu",
@@ -438,6 +438,7 @@
"city": "Město",
"clear": "Vyčistit",
"clear_all": "Vymazat vše",
"clear_all_recent_searches": "Vymazat všechna nedávná vyhledávání",
"clear_message": "Vyčistit zprávu",
"clear_value": "Vyčistit hodnotu",
"close": "Zavřít",
@@ -576,6 +577,7 @@
"error_adding_users_to_album": "Chyba při přidávání uživatelů do alba",
"error_deleting_shared_user": "Chyba při odstraňování sdíleného uživatele",
"error_downloading": "Chyba při stahování {filename}",
"error_hiding_buy_button": "Chyba při skrývání tlačítka koupit",
"error_removing_assets_from_album": "Chyba při odstraňování položek z alba, další podrobnosti najdete v konzoli",
"error_selecting_all_assets": "Chyba při výběru všech položek",
"exclusion_pattern_already_exists": "Tento vzor vyloučení již existuje.",
@@ -586,6 +588,8 @@
"failed_to_get_people": "Nepodařilo se načíst lidi",
"failed_to_load_asset": "Nepodařilo se načíst položku",
"failed_to_load_assets": "Nepodařilo se načíst položky",
"failed_to_load_people": "Chyba načítání osob",
"failed_to_remove_product_key": "Nepodařilo se odebrat klíč produktu",
"failed_to_stack_assets": "Nepodařilo se poskládat položky",
"failed_to_unstack_assets": "Nepodařilo se rozložit položky",
"import_path_already_exists": "Tato cesta importu již existuje.",
@@ -739,7 +743,16 @@
"host": "Hostitel",
"hour": "Hodina",
"image": "Obrázek",
"image_alt_text_date": "v {date}",
"image_alt_text_date": "{isVideo, select, true {Video pořízeno} other {Obrázek pořízen}} {date}",
"image_alt_text_date_1_person": "{isVideo, select, true {Video pořízeno} other {Obrázek pořízen}} {date} uživatelem {person1}",
"image_alt_text_date_2_people": "{isVideo, select, true {Video pořízeno} other {Obrázek pořízen}} {date} uživateli {person1} a {person2}",
"image_alt_text_date_3_people": "{isVideo, select, true {Video pořízeno} other {Obrázek pořízen}} {date} uživateli {person1}, {person2} a {person3}",
"image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video pořízeno} other {Obrázek pořízen}} {date} uživateli {person1}, {person2} a {additionalCount, plural, one {dalším # uživatelem} other {dalšími # uživateli}}",
"image_alt_text_date_place": "{isVideo, select, true {Video pořízeno} other {Obrázek požízen}} {date} v místě {city}, {country}",
"image_alt_text_date_place_1_person": "{isVideo, select, true {Video pořízeno} other {Obrázek požízen}} {date} v místě {city}, {country} uživatelem {person1}",
"image_alt_text_date_place_2_people": "{isVideo, select, true {Video pořízeno} other {Obrázek požízen}} {date} v místě {city}, {country} uživateli {person1} a {person2}",
"image_alt_text_date_place_3_people": "{isVideo, select, true {Video pořízeno} other {Obrázek požízen}} {date} v místě {city}, {country} uživateli {person1}, {person2} a {person3}",
"image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video pořízeno} other {Obrázek požízen}} {date} v místě {city}, {country} uživateli {person1}, {person2} a {additionalCount, plural, one {dalším # uživatelem} other {dalšími # uživateli}}",
"image_alt_text_people": "{count, plural, =1 {a {person1}} =2 {s {person1} a {person2}} =3 {s {person1}, {person2}, a {person3}} other {s {person1}, {person2}, a {others, number} dalšími}}",
"image_alt_text_place": "v {city}, {country}",
"image_taken": "{isVideo, select, true {Video pořízeno} other {Obrázek požízen}}",
@@ -860,6 +873,7 @@
"name": "Jméno",
"name_or_nickname": "Jméno nebo přezdívka",
"never": "Nikdy",
"new_album": "Nové album",
"new_api_key": "Nový API klíč",
"new_password": "Nové heslo",
"new_person": "Nová osoba",
@@ -975,6 +989,38 @@
"profile_picture_set": "Profilový obrázek nastaven.",
"public_album": "Veřejné album",
"public_share": "Veřejné sdílení",
"purchase_account_info": "Podporovatel",
"purchase_activated_subtitle": "Děkujeme vám za podporu aplikace Immich a softwaru s otevřeným zdrojovým kódem",
"purchase_activated_time": "Aktivováno dne {date, date}",
"purchase_activated_title": "Váš klíč byl úspěšně aktivován",
"purchase_button_activate": "Aktivovat",
"purchase_button_buy": "Koupit",
"purchase_button_buy_immich": "Koupit Immich",
"purchase_button_never_show_again": "Nikdy již nezobrazovat",
"purchase_button_reminder": "Připomenout za 30 dní",
"purchase_button_remove_key": "Odstranit klíč",
"purchase_button_select": "Vybrat",
"purchase_failed_activation": "Aktivace se nezdařila! Zkontrolujte prosím svůj e-mail pro správný produktový klíč!",
"purchase_individual_description_1": "Pro jednotlivce",
"purchase_individual_description_2": "Stav podporovatele",
"purchase_individual_title": "Individuální",
"purchase_input_suggestion": "Máte produktový klíč? Zadejte klíč níže",
"purchase_license_subtitle": "Koupit Immich na podporu dalšího rozvoje služby",
"purchase_lifetime_description": "Doživotní platnost",
"purchase_option_title": "MOŽNOSTI NÁKUPU",
"purchase_panel_info_1": "Tvorba aplikace Immich vyžaduje spoustu času a úsilí, a proto na ní pracují vývojáři na plný úvazek, aby byla co nejlepší. Naším cílem je, aby se software s otevřeným zdrojovým kódem a etické obchodní postupy staly udržitelným zdrojem příjmů pro vývojáře a aby vznikl ekosystém respektující soukromí se skutečnými alternativami k ziskuchtivým službám.",
"purchase_panel_info_2": "Protože jsme se zavázali, že nebudeme zavádět paywally, nezískáte tímto nákupem žádné další funkce v aplikaci Immich. Spoléháme na uživatele, jako jste vy, že podpoří neustálý vývoj aplikace.",
"purchase_panel_title": "Podpora projektu",
"purchase_per_server": "Na server",
"purchase_per_user": "Na uživatele",
"purchase_remove_product_key": "Odstranění produktového klíče",
"purchase_remove_product_key_prompt": "Opravdu chcete odebrat produktový klíč?",
"purchase_remove_server_product_key": "Odstranění serverového produktového klíče",
"purchase_remove_server_product_key_prompt": "Opravdu chcete odebrat serverový produktový klíč?",
"purchase_server_description_1": "Pro celý server",
"purchase_server_description_2": "Stav podporovatele",
"purchase_server_title": "Server",
"purchase_settings_server_activated": "Produktový klíč serveru spravuje správce",
"range": "Rozsah",
"raw": "Raw",
"reaction_options": "Možnosti reakce",
@@ -1020,6 +1066,7 @@
"reset_people_visibility": "Obnovit viditelnost lidí",
"reset_settings_to_default": "Obnovit výchozí nastavení",
"reset_to_default": "Obnovit výchozí nastavení",
"resolve_duplicates": "Vyřešit duplicity",
"resolved_all_duplicates": "Vyřešeny všechny duplicity",
"restore": "Obnovit",
"restore_all": "Obnovit vše",
@@ -1064,6 +1111,7 @@
"see_all_people": "Zobrazit všechny lidi",
"select_album_cover": "Vybrat obal alba",
"select_all": "Vybrat vše",
"select_all_duplicates": "Vybrat všechny duplicity",
"select_avatar_color": "Vyberte barvu avatara",
"select_face": "Vybrat obličej",
"select_featured_photo": "Vybrat hlavní fotografii",
@@ -1118,6 +1166,8 @@
"show_person_options": "Zobrazit možnosti osoby",
"show_progress_bar": "Zobrazit ukazatel průběhu",
"show_search_options": "Zobrazit možnosti vyhledávání",
"show_supporter_badge": "Odznak podporovatele",
"show_supporter_badge_description": "Zobrazit odznak podporovatele",
"shuffle": "Náhodný výběr",
"sign_out": "Odhlásit se",
"sign_up": "Zaregistrovat se",
@@ -1191,6 +1241,7 @@
"unnamed_share": "Nejmenované sdílení",
"unsaved_change": "Neuložená změna",
"unselect_all": "Zrušit výběr všech",
"unselect_all_duplicates": "Zrušit výběr všech duplicit",
"unstack": "Zrušit zásobník",
"unstacked_assets_count": "{count, plural, one {Rozložena # položka} few {Rozloženy # položky} other {Rozloženo # položek}}",
"untracked_files": "Nesledované soubory",
@@ -1214,6 +1265,8 @@
"user_license_settings": "Licence",
"user_license_settings_description": "Správa licence",
"user_liked": "Uživateli {user} se {type, select, photo {líbila tato fotka} video {líbilo toto video} asset {líbila tato položka} other {to líbilo}}",
"user_purchase_settings": "Nákup",
"user_purchase_settings_description": "Správa vašeho nákupu",
"user_role_set": "Uživatel {user} nastaven jako {role}",
"user_usage_detail": "Podrobnosti využití uživatelů",
"username": "Uživateleské jméno",

View File

@@ -1,5 +1,5 @@
{
"about": "Über",
"about": "Über Immich",
"account": "Konto",
"account_settings": "Kontoeinstellungen",
"acknowledge": "Bestätigen",
@@ -410,7 +410,7 @@
"bulk_delete_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien}} gemeinsam löschen möchtest? Dabei wird die größte Datei jeder Gruppe behalten und alle anderen Duplikate dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden!",
"bulk_keep_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien}} behalten möchtest? Dies wird alle Duplikat-Gruppen auflösen ohne etwas zu löschen.",
"bulk_trash_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien}} gemeinsam in den Papierkorb verschieben möchtest? Dies wird die größte Datei jeder Gruppe behalten und alle anderen Duplikate in den Papierkorb verschieben.",
"buy": "Lizenz erwerben",
"buy": "Immich erwerben",
"camera": "Kamera",
"camera_brand": "Kamera-Marke",
"camera_model": "Kamera-Modell",
@@ -438,6 +438,7 @@
"city": "Stadt",
"clear": "Leeren",
"clear_all": "Alles leeren",
"clear_all_recent_searches": "Alle letzten Suchvorgänge löschen",
"clear_message": "Nachrichten leeren",
"clear_value": "Wert leeren",
"close": "Schließen",
@@ -576,6 +577,7 @@
"error_adding_users_to_album": "Fehler beim Hinzufügen von Benutzern zum Album",
"error_deleting_shared_user": "Fehler beim Löschen des geteilten Benutzers",
"error_downloading": "Fehler beim Herunterladen von {filename}",
"error_hiding_buy_button": "Fehler beim Ausblenden der Kaufen Schaltfläche",
"error_removing_assets_from_album": "Fehler beim Entfernen von Dateien aus dem Album, siehe Konsole für weitere Details",
"error_selecting_all_assets": "Fehler beim Auswählen aller Dateien",
"exclusion_pattern_already_exists": "Dieses Ausschlussmuster existiert bereits.",
@@ -586,6 +588,8 @@
"failed_to_get_people": "Personen konnten nicht abgerufen werden",
"failed_to_load_asset": "Fehler beim Laden der Datei",
"failed_to_load_assets": "Fehler beim Laden der Dateien",
"failed_to_load_people": "Fehler beim Laden von Personen",
"failed_to_remove_product_key": "Fehler beim Entfernen des Produktschlüssels",
"failed_to_stack_assets": "Dateien konnten nicht gestapelt werden",
"failed_to_unstack_assets": "Dateien konnten nicht entstapelt werden",
"import_path_already_exists": "Dieser Importpfad existiert bereits.",
@@ -739,7 +743,16 @@
"host": "Host",
"hour": "Stunde",
"image": "Bild",
"image_alt_text_date": "am {date}",
"image_alt_text_date": "{isVideo, select, true {Video} other {Bild}} aufgenommen am {date}",
"image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Bild}} aufgenommen mit {person1} am {date}",
"image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Bild}} aufgenommen mit {person1} und {person2} am {date}",
"image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Bild}} aufgenommen mit {person1}, {person2} und {person3} am {date}",
"image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Bild}} aufgenommen mit {person1}, {person2}, und {additionalCount, number} anderen am {date}",
"image_alt_text_date_place": "{isVideo, select, true {Video} other {Bild}} aufgenommen in {city}, {country} am {date}",
"image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Bild}} aufgenommen in {city}, {country} mit {person1} am {date}",
"image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Bild}} aufgenommen in {city}, {country} mit {person1} und {person2} am {date}",
"image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Bild}} aufgenommen in {city}, {country} mit {person1}, {person2}, und {person3} am {date}",
"image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Bild}} aufgenommen in {city}, {country} mit {person1}, {person2}, und {additionalCount, number} anderen am {date}",
"image_alt_text_people": "{count, plural, =1 {mit {person1}} =2 {mit {person1} und {person2}} =3 {mit {person1}, {person2} und {person3}} other {mit {person1}, {person2} und {others, number} anderen}}",
"image_alt_text_place": "in {city}, {country}",
"image_taken": "{isVideo, select, true {Video aufgenommen} other {Bild aufgenommen}}",
@@ -860,6 +873,7 @@
"name": "Name",
"name_or_nickname": "Name oder Nickname",
"never": "Niemals",
"new_album": "Neues Album",
"new_api_key": "Neuer API-Schlüssel",
"new_password": "Neues Passwort",
"new_person": "Neue Person",
@@ -974,6 +988,38 @@
"profile_picture_set": "Profilbild gesetzt.",
"public_album": "Öffentliches Album",
"public_share": "Öffentliche Teilung",
"purchase_account_info": "Unterstützer",
"purchase_activated_subtitle": "Danke für die Unterstützung von Immich und Open-Source Software",
"purchase_activated_time": "Aktiviert am {date, date}",
"purchase_activated_title": "Dein Schlüssel wurde erfolgreich aktiviert",
"purchase_button_activate": "Aktivieren",
"purchase_button_buy": "Kaufen",
"purchase_button_buy_immich": "Immich kaufen",
"purchase_button_never_show_again": "Nicht nochmal anzeigen",
"purchase_button_reminder": "Erinnere mich in 30 Tagen",
"purchase_button_remove_key": "Schlüssel entfernen",
"purchase_button_select": "Auswählen",
"purchase_failed_activation": "Aktivieren fehlgeschlagen! Überprüfe bitte den Produktschlüssel in der E-Mail!",
"purchase_individual_description_1": "Für eine Einzelperson",
"purchase_individual_description_2": "Unterstützer Status",
"purchase_individual_title": "Einzelperson",
"purchase_input_suggestion": "Besitzen Sie bereits einen Produktschlüssel? Bitte geben Sie diesen unten ein",
"purchase_license_subtitle": "Kaufe Immich um eine fortlaufende Entwicklung zu unterstützen",
"purchase_lifetime_description": "Lebenslange Gültigkeit",
"purchase_option_title": "KAUF OPTIONEN",
"purchase_panel_info_1": "Das Entwickeln von Immich ist aufwendig und nimmt viel Zeit in Anspruch, deshalb haben wir ein Team von Vollzeit-Entwickler*innen, welche ihr Bestes geben. Unser Ziel ist es, mit Open-Source Software und ethischen Unternehmenspraktiken eine nachhaltige Einkommensquelle für unsere Entwickler und ein privatsphäre-respektierendes Ökosystem für unsere Nutzenden zu schaffen. Wir wollen eine kompetitive Alternative zu ausbeuterischen Cloud-Diensten erschaffen.",
"purchase_panel_info_2": "Weil wir davon überzeugt sind keine Paywalls zu haben, wird dieser Kauf keine zusätzlichen Funktionen in Immich freischalten. Wir verlassen uns auf Nutzende wie dich, um Entwicklung von Immich zu unterstützen.",
"purchase_panel_title": "Das Projekt unterstützen",
"purchase_per_server": "Pro Server",
"purchase_per_user": "Pro Benutzer",
"purchase_remove_product_key": "Produktschlüssel entfernen",
"purchase_remove_product_key_prompt": "Sicher, dass der Produktschlüssel entfernt werden soll?",
"purchase_remove_server_product_key": "Server Produktschlüssel entfernen",
"purchase_remove_server_product_key_prompt": "Sicher, dass der Server Produktschlüssel entfernt werden soll?",
"purchase_server_description_1": "Für den gesamten Server",
"purchase_server_description_2": "Unterstützer Status",
"purchase_server_title": "Server",
"purchase_settings_server_activated": "Der Server Produktschlüssel wird durch den Administrator verwaltet",
"range": "Reichweite",
"raw": "RAW",
"reaction_options": "Reaktionsmöglichkeiten",
@@ -1019,6 +1065,7 @@
"reset_people_visibility": "Sichtbarkeit von Personen zurücksetzen",
"reset_settings_to_default": "Einstellungen auf Standardwerte zurücksetzen",
"reset_to_default": "Auf Standard zurücksetzen",
"resolve_duplicates": "Duplikate entfernen",
"resolved_all_duplicates": "Alle Duplikate aufgelöst",
"restore": "Wiederherstellen",
"restore_all": "Alle wiederherstellen",
@@ -1063,6 +1110,7 @@
"see_all_people": "Alle Personen anzeigen",
"select_album_cover": "Album-Cover auswählen",
"select_all": "Alles auswählen",
"select_all_duplicates": "Alle Duplikate auswählen",
"select_avatar_color": "Avatar-Farbe auswählen",
"select_face": "Gesicht auswählen",
"select_featured_photo": "Anzeigebild auswählen",
@@ -1117,6 +1165,8 @@
"show_person_options": "Personen-Optionen anzeigen",
"show_progress_bar": "Fortschrittsbalken anzeigen",
"show_search_options": "Suchoptionen anzeigen",
"show_supporter_badge": "Unterstützer Abzeichen",
"show_supporter_badge_description": "Zeige Unterstützer Abzeichen",
"shuffle": "Durchmischen",
"sign_out": "Abmelden",
"sign_up": "Registrieren",
@@ -1190,6 +1240,7 @@
"unnamed_share": "Unbenannte Teilung",
"unsaved_change": "Ungespeicherte Änderung",
"unselect_all": "Alles abwählen",
"unselect_all_duplicates": "Alle Duplikate abwählen",
"unstack": "Entstapeln",
"unstacked_assets_count": "{count, plural, one {# Datei} other {# Dateien}} entstapelt",
"untracked_files": "Unverfolgte Dateien",
@@ -1213,6 +1264,8 @@
"user_license_settings": "Lizenz",
"user_license_settings_description": "Verwalte deine Lizenz",
"user_liked": "{type, select, photo {Dieses Foto} video {Dieses Video} asset {Diese Datei} other {Dies}} gefällt {user}",
"user_purchase_settings": "Kauf",
"user_purchase_settings_description": "Kauf verwalten",
"user_role_set": "{user} als {role} festlegen",
"user_usage_detail": "Nutzungsdetails der Nutzer",
"username": "Nutzername",

View File

@@ -25,7 +25,7 @@
"add_to_shared_album": "Add to shared album",
"added_to_archive": "Added to archive",
"added_to_favorites": "Added to favorites",
"added_to_favorites_count": "Added {count} to favorites",
"added_to_favorites_count": "Added {count, number} to favorites",
"admin": {
"add_exclusion_pattern_description": "Add exclusion patterns. Globbing using *, **, and ? is supported. To ignore all files in any directory named \"Raw\", use \"**/Raw/**\". To ignore all files ending in \".tif\", use \"**/*.tif\". To ignore an absolute path, use \"/path/to/ignore/**\".",
"authentication_settings": "Authentication Settings",
@@ -429,6 +429,7 @@
"city": "City",
"clear": "Clear",
"clear_all": "Clear all",
"clear_all_recent_searches": "Clear all recent searches",
"clear_message": "Clear message",
"clear_value": "Clear value",
"close": "Close",
@@ -557,6 +558,7 @@
"error_adding_users_to_album": "Error adding users to album",
"error_deleting_shared_user": "Error deleting shared user",
"error_downloading": "Error downloading {filename}",
"error_hiding_buy_button": "Error hiding buy button",
"error_removing_assets_from_album": "Error removing assets from album, check console for more details",
"error_selecting_all_assets": "Error selecting all assets",
"exclusion_pattern_already_exists": "This exclusion pattern already exists.",
@@ -568,6 +570,7 @@
"failed_to_load_asset": "Failed to load asset",
"failed_to_load_assets": "Failed to load assets",
"failed_to_load_people": "Failed to load people",
"failed_to_remove_product_key": "Failed to remove product key",
"failed_to_stack_assets": "Failed to stack assets",
"failed_to_unstack_assets": "Failed to un-stack assets",
"import_path_already_exists": "This import path already exists.",
@@ -709,10 +712,16 @@
"host": "Host",
"hour": "Hour",
"image": "Image",
"image_alt_text_date": "on {date}",
"image_alt_text_people": "{count, plural, =1 {with {person1}} =2 {with {person1} and {person2}} =3 {with {person1}, {person2}, and {person3}} other {with {person1}, {person2}, and {others, number} others}}",
"image_alt_text_place": "in {city}, {country}",
"image_taken": "{isVideo, select, true {Video taken} other {Image taken}}",
"image_alt_text_date": "{isVideo, select, true {Video} other {Image}} taken on {date}",
"image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} taken with {person1} on {date}",
"image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} taken with {person1} and {person2} on {date}",
"image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} taken with {person1}, {person2}, and {person3} on {date}",
"image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} taken with {person1}, {person2}, and {additionalCount, number} others on {date}",
"image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} taken in {city}, {country} on {date}",
"image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} taken in {city}, {country} with {person1} on {date}",
"image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} taken in {city}, {country} with {person1} and {person2} on {date}",
"image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} taken in {city}, {country} with {person1}, {person2}, and {person3} on {date}",
"image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} taken in {city}, {country} with {person1}, {person2}, and {additionalCount, number} others on {date}",
"immich_logo": "Immich Logo",
"immich_web_interface": "Immich Web Interface",
"import_from_json": "Import from JSON",
@@ -803,6 +812,7 @@
"name": "Name",
"name_or_nickname": "Name or nickname",
"never": "Never",
"new_album": "New Album",
"new_api_key": "New API Key",
"new_password": "New password",
"new_person": "New person",
@@ -916,7 +926,7 @@
"public_share": "Public Share",
"purchase_account_info": "Supporter",
"purchase_activated_subtitle": "Thank you for supporting Immich and open-source software",
"purchase_activated_time": "Activated on {date}",
"purchase_activated_time": "Activated on {date, date}",
"purchase_activated_title": "Your key has been successfully activated",
"purchase_button_activate": "Activate",
"purchase_button_buy": "Buy",
@@ -925,7 +935,7 @@
"purchase_button_reminder": "Remind me in 30 days",
"purchase_button_remove_key": "Remove key",
"purchase_button_select": "Select",
"purchase_failed_activation": "Failed to activate! Please check your email for the the correct product key!",
"purchase_failed_activation": "Failed to activate! Please check your email for the correct product key!",
"purchase_individual_description_1": "For an individual",
"purchase_individual_description_2": "Supporter status",
"purchase_individual_title": "Individual",
@@ -938,6 +948,10 @@
"purchase_panel_title": "Support the project",
"purchase_per_server": "Per server",
"purchase_per_user": "Per user",
"purchase_remove_product_key": "Remove Product Key",
"purchase_remove_product_key_prompt": "Are you sure you want to remove the product key?",
"purchase_remove_server_product_key": "Remove Server product key",
"purchase_remove_server_product_key_prompt": "Are you sure you want to remove the Server product key?",
"purchase_server_description_1": "For the whole server",
"purchase_server_description_2": "Supporter status",
"purchase_server_title": "Server",
@@ -984,6 +998,7 @@
"reset_password": "Reset password",
"reset_people_visibility": "Reset people visibility",
"reset_to_default": "Reset to default",
"resolve_duplicates": "Resolve duplicates",
"resolved_all_duplicates": "Resolved all duplicates",
"restore": "Restore",
"restore_all": "Restore all",
@@ -1028,6 +1043,7 @@
"see_all_people": "See all people",
"select_album_cover": "Select album cover",
"select_all": "Select all",
"select_all_duplicates": "Select all duplicates",
"select_avatar_color": "Select avatar color",
"select_face": "Select face",
"select_featured_photo": "Select featured photo",
@@ -1135,7 +1151,7 @@
"total_usage": "Total usage",
"trash": "Trash",
"trash_all": "Trash All",
"trash_count": "Trash {count}",
"trash_count": "Trash {count, number}",
"trash_delete_asset": "Trash/Delete Asset",
"trash_no_results_message": "Trashed photos and videos will show up here.",
"trashed_items_will_be_permanently_deleted_after": "Trashed items will be permanently deleted after {days, plural, one {# day} other {# days}}.",
@@ -1153,6 +1169,7 @@
"unnamed_share": "Unnamed Share",
"unsaved_change": "Unsaved change",
"unselect_all": "Unselect all",
"unselect_all_duplicates": "Unselect all duplicates",
"unstack": "Un-stack",
"unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}",
"untracked_files": "Untracked files",
@@ -1162,7 +1179,7 @@
"upload": "Upload",
"upload_concurrency": "Upload concurrency",
"upload_errors": "Upload completed with {count, plural, one {# error} other {# errors}}, refresh the page to see new upload assets.",
"upload_progress": "Remaining {remaining} - Processed {processed}/{total}",
"upload_progress": "Remaining {remaining, number} - Processed {processed, number}/{total, number}",
"upload_skipped_duplicates": "Skipped {count, plural, one {# duplicate asset} other {# duplicate assets}}",
"upload_status_duplicates": "Duplicates",
"upload_status_errors": "Errors",

View File

@@ -98,7 +98,7 @@
"machine_learning_clip_model_description": "El nombre de un modelo CLIP listado <link>aquí</link>. Tenga en cuenta que debe volver a ejecutar el trabajo 'Smart Search' para todas las imágenes al cambiar un modelo.",
"machine_learning_duplicate_detection": "Detección duplicados",
"machine_learning_duplicate_detection_enabled": "Habilitar detección de duplicados",
"machine_learning_duplicate_detection_enabled_description": "Si está deshabilitado, se seguirán deduplicando assets exactamente idénticos.",
"machine_learning_duplicate_detection_enabled_description": "Si está deshabilitado, los activos exactamente idénticos seguirán siendo eliminados.",
"machine_learning_duplicate_detection_setting_description": "Utilice incrustaciones de CLIP para encontrar posibles duplicados",
"machine_learning_enabled": "Habilitar aprendizaje automático",
"machine_learning_enabled_description": "Si está deshabilitada, todas las funciones de ML se deshabilitarán independientemente de la configuración a continuación.",
@@ -410,7 +410,7 @@
"bulk_delete_duplicates_confirmation": "¿Estás seguro de que deseas eliminar de forma masiva {count, plural, one {# duplicate asset} other {# duplicate assets}}? Esto mantendrá el activo más grande de cada grupo y eliminará permanentemente todos los demás duplicados. ¡Esta acción no se puede deshacer!",
"bulk_keep_duplicates_confirmation": "¿Estas seguro de que desea mantener {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto resolverá todos los grupos duplicados sin borrar nada.",
"bulk_trash_duplicates_confirmation": "¿Estas seguro de que desea eliminar masivamente {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto mantendrá el archivo más grande de cada grupo y eliminará todos los demás duplicados.",
"buy": "Comprar licencia",
"buy": "Comprar Immich",
"camera": "Cámara",
"camera_brand": "Fabricante de cámara",
"camera_model": "Modelo de cámara",
@@ -438,6 +438,7 @@
"city": "Ciudad",
"clear": "Limpiar",
"clear_all": "Limpiar todo",
"clear_all_recent_searches": "Borrar búsquedas recientes",
"clear_message": "Limpiar mensaje",
"clear_value": "Limpiar valor",
"close": "Cerrar",
@@ -576,6 +577,7 @@
"error_adding_users_to_album": "Error al añadir usuarios al álbum",
"error_deleting_shared_user": "Error al eliminar usuario compartido",
"error_downloading": "Error al descargar {filename}",
"error_hiding_buy_button": "Error al ocultar el botón de compra",
"error_removing_assets_from_album": "Error al eliminar archivos del álbum; consulte la consola para obtener más detalles",
"error_selecting_all_assets": "Error al seleccionar todos los archivos",
"exclusion_pattern_already_exists": "Este patrón de exclusión ya existe.",
@@ -587,6 +589,7 @@
"failed_to_load_asset": "Error al cargar el elemento",
"failed_to_load_assets": "Error al cargar los elementos",
"failed_to_load_people": "Error al cargar a los usuarios",
"failed_to_remove_product_key": "No se pudo eliminar la clave del producto",
"failed_to_stack_assets": "No se pudieron agrupar los archivos",
"failed_to_unstack_assets": "Error al desagrupar los archivos",
"import_path_already_exists": "Esta ruta de importación ya existe.",
@@ -740,7 +743,16 @@
"host": "Host",
"hour": "Hora",
"image": "Imagen",
"image_alt_text_date": "El {date}",
"image_alt_text_date": "{isVideo, select, true {Video} other {Image}} tomada el {date}",
"image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} tomada con {person1} el {date}",
"image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} tomada con {person1} y {person2} el {date}",
"image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} tomada con {person1}, {person2}, y {person3} el {date}",
"image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} tomada con {person1}, {person2}, y {additionalCount, number} más el {date}",
"image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} tomada en {city}, {country} el {date}",
"image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} tomada en {city}, {country} con {person1} el {date}",
"image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} tomada en {city}, {country} con {person1} y {person2} el {date}",
"image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} tomada en {city}, {country} con {person1}, {person2}, y {person3} el {date}",
"image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} tomada en {city}, {country} con {person1}, {person2}, y {additionalCount, number} más el {date}",
"image_alt_text_people": "{count, plural, =1 {with {person1}} =2 {with {person1} and {person2}} =3 {with {person1}, {person2}, and {person3}} other {with {person1}, {person2}, y {others, number} others}}",
"image_alt_text_place": "En {city}, {country}",
"image_taken": "{isVideo, select, true {Video taken} other {Image taken}}",
@@ -861,6 +873,7 @@
"name": "Nombre",
"name_or_nickname": "Nombre o apodo",
"never": "nunca",
"new_album": "Nuevo álbum",
"new_api_key": "Nueva clave API",
"new_password": "Nueva contraseña",
"new_person": "Nueva persona",
@@ -975,6 +988,38 @@
"profile_picture_set": "Conjunto de imágenes de perfil.",
"public_album": "Álbum público",
"public_share": "Compartir públicamente",
"purchase_account_info": "Soporte",
"purchase_activated_subtitle": "Gracias por apoyar a Immich y al software de código abierto",
"purchase_activated_time": "Activado el {date, date}",
"purchase_activated_title": "Su clave ha sido activada correctamente",
"purchase_button_activate": "Activar",
"purchase_button_buy": "Comprar",
"purchase_button_buy_immich": "Comprar Immich",
"purchase_button_never_show_again": "No volver a mostrar",
"purchase_button_reminder": "Recuérdamelo en 30 días",
"purchase_button_remove_key": "Quitar clave",
"purchase_button_select": "Seleccionar",
"purchase_failed_activation": "¡Error al activar! ¡Por favor, revisa tu correo electrónico para obtener la clave del producto correcta!",
"purchase_individual_description_1": "Para un usuario",
"purchase_individual_description_2": "Estado de soporte",
"purchase_individual_title": "Individual",
"purchase_input_suggestion": "¿Tiene una clave de producto? Introdúzcala a continuación",
"purchase_license_subtitle": "Compre Immich para apoyar el desarrollo continuo del servicio",
"purchase_lifetime_description": "Compra de por vida",
"purchase_option_title": "OPCIONES DE COMPRA",
"purchase_panel_info_1": "Desarrollar Immich requiere mucho tiempo y esfuerzo, y contamos con ingenieros a tiempo completo que trabajan en él para que sea lo mejor posible. Nuestra misión es que el software de código abierto y las prácticas comerciales éticas se conviertan en una fuente de ingresos sostenibles para los desarrolladores y crear un ecosistema que respete la privacidad con alternativas reales a los servicios en la nube de pago.",
"purchase_panel_info_2": "Como nos comprometemos a no añadir pagos, esta compra no le otorgará ninguna característica adicional en Immich. Confiamos en que los usuarios como usted apoyen el desarrollo continuo de Immich.",
"purchase_panel_title": "Apoya el proyecto",
"purchase_per_server": "Por servidor",
"purchase_per_user": "Por usuario",
"purchase_remove_product_key": "Eliminar clave de producto",
"purchase_remove_product_key_prompt": "¿Está seguro de que desea eliminar la clave del producto?",
"purchase_remove_server_product_key": "Eliminar la clave de producto del servidor",
"purchase_remove_server_product_key_prompt": "¿Está seguro de que desea eliminar la clave de producto del servidor?",
"purchase_server_description_1": "Para todo el servidor",
"purchase_server_description_2": "Estado del soporte",
"purchase_server_title": "Servidor",
"purchase_settings_server_activated": "La clave del producto del servidor la administra el administrador",
"range": "",
"raw": "",
"reaction_options": "Opciones de reacción",
@@ -1020,6 +1065,7 @@
"reset_people_visibility": "Restablecer la visibilidad de las personas",
"reset_settings_to_default": "",
"reset_to_default": "Restablecer los valores predeterminados",
"resolve_duplicates": "Resolver duplicados",
"resolved_all_duplicates": "Todos los duplicados resueltos",
"restore": "Restaurar",
"restore_all": "Restaurar todo",
@@ -1064,6 +1110,7 @@
"see_all_people": "Ver todas las personas",
"select_album_cover": "Seleccionar portada del álbum",
"select_all": "Seleccionar todo",
"select_all_duplicates": "Seleccionar todos los duplicados",
"select_avatar_color": "Seleccionar color del avatar",
"select_face": "Seleccionar cara",
"select_featured_photo": "Seleccionar foto principal",
@@ -1118,6 +1165,8 @@
"show_person_options": "Mostrar opciones de la persona",
"show_progress_bar": "Mostrar barra de progreso",
"show_search_options": "Mostrar opciones de búsqueda",
"show_supporter_badge": "Insignia de colaborador",
"show_supporter_badge_description": "Mostrar una insignia de colaborador",
"shuffle": "Modo aleatorio",
"sign_out": "Salir",
"sign_up": "Registrarse",
@@ -1191,6 +1240,7 @@
"unnamed_share": "Compartido sin nombre",
"unsaved_change": "Cambio no guardado",
"unselect_all": "Limpiar selección",
"unselect_all_duplicates": "Deseleccionar todos los duplicados",
"unstack": "Desapilar",
"unstacked_assets_count": "Sin apilar {count, plural, one {# asset} other {# assets}}",
"untracked_files": "Archivos no monitorizados",
@@ -1214,6 +1264,8 @@
"user_license_settings": "Licencia",
"user_license_settings_description": "Gestionar tu licencia",
"user_liked": "{user} le gustó {type, select, photo {this photo} video {this video} asset {this asset} other {it}}",
"user_purchase_settings": "Compra",
"user_purchase_settings_description": "Gestiona tu compra",
"user_role_set": "Carbiar {user} a {role}",
"user_usage_detail": "Detalle del uso del usuario",
"username": "Nombre de usuario",

View File

@@ -314,7 +314,7 @@
"user_delete_immediately_checkbox": "Mise en file d'attente d'un utilisateur et de médias en vue d'une suppression immédiate",
"user_management": "Gestion des utilisateurs",
"user_password_has_been_reset": "Le mot de passe de l'utilisateur a été réinitialisé:",
"user_password_reset_description": "Veuillez saisir un mot de passe temporaire à l'utilisateur et informez le qu'il devra le changer à sa première connexion.",
"user_password_reset_description": "Veuillez saisir un mot de passe temporaire à l'utilisateur et informez-le qu'il devra le changer à sa première connexion.",
"user_restore_description": "Le compte de <b>{user}</b> sera restauré.",
"user_restore_scheduled_removal": "Restaurer l'utilisateur - suppression programmée le {date, date, long}",
"user_settings": "Paramètres utilisateur",
@@ -410,7 +410,7 @@
"bulk_delete_duplicates_confirmation": "Êtes-vous sûr de vouloir supprimer {count, plural, one {# doublon} other {# doublons}}? Cette opération conservera le plus grand média de chaque groupe et supprimera définitivement tous les autres doublons. Vous ne pouvez pas annuler cette action!",
"bulk_keep_duplicates_confirmation": "Êtes-vous sûr de vouloir conserver {count, plural, one {# doublon} other {# doublons}}? Cela résoudra tous les groupes de doublons sans rien supprimer.",
"bulk_trash_duplicates_confirmation": "Êtes-vous sûr de vouloir mettre à la corbeille {count, plural, one {# doublon} other {# doublons}}? Cette opération permet de conserver le plus grand média de chaque groupe et de mettre à la corbeille tous les autres doublons.",
"buy": "Acheter une licence",
"buy": "Acheter Immich",
"camera": "Appareil photo",
"camera_brand": "Marque d'appareil",
"camera_model": "Modèle d'appareil",
@@ -426,7 +426,7 @@
"change_date": "Changer la date",
"change_expiration_time": "Modifier le délai d'expiration",
"change_location": "Changer la localisation",
"change_name": "Changer le nom",
"change_name": "Modifier/Définir le nom",
"change_name_successfully": "Nouveau nom enregistré",
"change_password": "Modifier le mot de passe",
"change_password_description": "C'est la première fois que vous vous connectez ou une demande a été faite pour changer votre mot de passe. Veuillez entrer le nouveau mot de passe ci-dessous.",
@@ -438,6 +438,7 @@
"city": "Ville",
"clear": "Effacer",
"clear_all": "Effacer tout",
"clear_all_recent_searches": "Supprimer les recherches récentes",
"clear_message": "Effacer le message",
"clear_value": "Effacer la valeur",
"close": "Fermer",
@@ -576,6 +577,7 @@
"error_adding_users_to_album": "Erreur lors de l'ajout d'utilisateurs à l'album",
"error_deleting_shared_user": "Erreur lors de la suppression l'utilisateur partagé",
"error_downloading": "Erreur lors du téléchargement de {filename}",
"error_hiding_buy_button": "Impossible de masquer le bouton d'achat",
"error_removing_assets_from_album": "Erreur lors de la suppression des médias de l'album, vérifier la console pour plus de détails",
"error_selecting_all_assets": "Erreur lors de la sélection de tous les médias",
"exclusion_pattern_already_exists": "Ce modèle d'exclusion existe déjà.",
@@ -584,8 +586,10 @@
"failed_to_create_shared_link": "Impossible de créer le lien partagé",
"failed_to_edit_shared_link": "Impossible de modifier le lien partagé",
"failed_to_get_people": "Impossible d'obtenir les personnes",
"failed_to_load_asset": "Échec du chargement du média",
"failed_to_load_assets": "Échec du chargement des médias",
"failed_to_load_asset": "Impossible de charger le média",
"failed_to_load_assets": "Impossible de charger les médias",
"failed_to_load_people": "Impossible de charger les personnes",
"failed_to_remove_product_key": "Échec de suppression de la clé du produit",
"failed_to_stack_assets": "Impossible d'empiler les médias",
"failed_to_unstack_assets": "Impossible de dépiler les médias",
"import_path_already_exists": "Ce chemin d'import existe déjà.",
@@ -739,7 +743,16 @@
"host": "Hôte",
"hour": "Heure",
"image": "Image",
"image_alt_text_date": "à la {date}",
"image_alt_text_date": "{isVideo, select, true {Video} other {Image}} prise le {date}",
"image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} prise avec {person1} le {date}",
"image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} prise avec {person1} et {person2} le {date}",
"image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} prise avec {person1}, {person2}, et {person3} le {date}",
"image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} prise avec {person1}, {person2} et {additionalCount, number} autres personnes le {date}",
"image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} prise à {city}, {country} le {date}",
"image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} prise à {city}, {country} avec {person1} le {date}",
"image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} prise à {city}, {country} avec {person1} et {person2} le {date}",
"image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} prise à {city}, {country} avec {person1}, {person2}, et {person3} le {date}",
"image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} prise à {city}, {country} avec {person1}, {person2} et {additionalCount, number} autres personnes le {date}",
"image_alt_text_people": "{count, plural, =1 {with {person1}} =2 {with {person1} and {person2}} =3 {with {person1}, {person2}, and {person3}} other {with {person1}, {person2}, and {others, number} others}}",
"image_alt_text_place": "à {city}, {country}",
"image_taken": "{isVideo, select, true {Video prise} other {Image prise}}",
@@ -974,6 +987,38 @@
"profile_picture_set": "Photo de profil définie.",
"public_album": "Album public",
"public_share": "Partage public",
"purchase_account_info": "Contributeur",
"purchase_activated_subtitle": "Merci d'avoir apporté votre soutien à Immich et les logiciels open source",
"purchase_activated_time": "Activé le {date, date}",
"purchase_activated_title": "Votre clé a été activée avec succès",
"purchase_button_activate": "Activer",
"purchase_button_buy": "Acheter",
"purchase_button_buy_immich": "Acheter Immich",
"purchase_button_never_show_again": "Ne plus l'afficher",
"purchase_button_reminder": "Me le rappeler dans 30 jours",
"purchase_button_remove_key": "Supprimer la clé",
"purchase_button_select": "Sélectionner",
"purchase_failed_activation": "Erreur à l'activation. Merci de vérifier votre courriel pour confirmer la clé du produit!",
"purchase_individual_description_1": "Pour un utilisateur",
"purchase_individual_description_2": "Statut de contributeur",
"purchase_individual_title": "Utilisateur",
"purchase_input_suggestion": "Si vous avez déjà une clé de produit, renseignez-la ci-dessous",
"purchase_license_subtitle": "Acheter Immich pour soutenir le développement de ce service",
"purchase_lifetime_description": "Achat à vie",
"purchase_option_title": "OPTIONS D'ACHAT",
"purchase_panel_info_1": "Développer Immich nécessite du temps et de l'énergie, et nous avons des ingénieurs qui travaillent à plein temps pour en faire le meilleur produit possible. Notre mission est de générer, pour les logiciels open source et les pratiques de travail éthique, une source de revenus suffisante pour les développeurs et de créer un écosystème respectueux de la vie privée grâce a des alternatives crédibles aux services cloud peu scrupuleux.",
"purchase_panel_info_2": "Comme nous sommes engagés à ne pas ajouter de fonctionnalités payantes, cet achat ne vous donnera pas accès à des éléments supplémentaires dans Immich. Nous dépendons d'utilisateurs comme vous pour soutenir le développement actif d'Immich.",
"purchase_panel_title": "Soutenir le projet",
"purchase_per_server": "Par serveur",
"purchase_per_user": "Par utilisateur",
"purchase_remove_product_key": "Supprimer la clé du produit",
"purchase_remove_product_key_prompt": "Êtes-vous sûr de vouloir supprimer la clé du produit?",
"purchase_remove_server_product_key": "Supprimer la clé du produit pour le Serveur",
"purchase_remove_server_product_key_prompt": "Êtes-vous sûr de vouloir supprimer la clé du produit pour le serveur?",
"purchase_server_description_1": "Pour l'ensemble du serveur",
"purchase_server_description_2": "Statut de contributeur",
"purchase_server_title": "Serveur",
"purchase_settings_server_activated": "La clé du produit pour le Serveur est gérée par l'administrateur",
"range": "",
"raw": "",
"reaction_options": "Options de réaction",
@@ -1019,6 +1064,7 @@
"reset_people_visibility": "Réinitialiser la visibilité des personnes",
"reset_settings_to_default": "",
"reset_to_default": "Rétablir les valeurs par défaut",
"resolve_duplicates": "Traiter les doublons",
"resolved_all_duplicates": "Résolution de tous les doublons",
"restore": "Restaurer",
"restore_all": "Tout restaurer",
@@ -1063,9 +1109,10 @@
"see_all_people": "Voir toutes les personnes",
"select_album_cover": "Sélectionner la couverture d'album",
"select_all": "Tout sélectionner",
"select_all_duplicates": "Sélectionner tous les doublons",
"select_avatar_color": "Sélectionner la couleur de l'avatar",
"select_face": "Sélectionner le visage",
"select_featured_photo": "Sélectionner la photo de la personne",
"select_featured_photo": "Sélectionner la photo de profil de cette personne",
"select_from_computer": "Sélectionner à partir de l'ordinateur",
"select_keep_all": "Choisir de tout garder",
"select_library_owner": "Sélectionner le propriétaire de la bibliothèque",
@@ -1117,6 +1164,8 @@
"show_person_options": "Afficher les options de personnes",
"show_progress_bar": "Afficher la barre de progression",
"show_search_options": "Afficher les options de recherche",
"show_supporter_badge": "Badge de contributeur",
"show_supporter_badge_description": "Afficher le badge de contributeur",
"shuffle": "Mélanger",
"sign_out": "Déconnexion",
"sign_up": "S'enregistrer",
@@ -1190,6 +1239,7 @@
"unnamed_share": "Partage sans nom",
"unsaved_change": "Modification non enregistrée",
"unselect_all": "Annuler la sélection",
"unselect_all_duplicates": "Désélectionner tous les doublons",
"unstack": "Désempiler",
"unstacked_assets_count": "{count, plural, one {# média dépilé} other {# médias dépilés}}",
"untracked_files": "Fichiers non suivis",
@@ -1213,6 +1263,8 @@
"user_license_settings": "Licence",
"user_license_settings_description": "Gérer votre licence",
"user_liked": "{user} a aimé {type, select, photo {cette photo} video {cette vidéo} asset {ce média} other {ceci}}",
"user_purchase_settings": "Achat",
"user_purchase_settings_description": "Gérer votre achat",
"user_role_set": "Définir {user} comme {role}",
"user_usage_detail": "Détail de l'utilisation des utilisateurs",
"username": "Nom d'utilisateur",

View File

@@ -249,6 +249,7 @@
"transcoding_acceleration_vaapi": "VAAPI",
"transcoding_accepted_audio_codecs": "קודקים מקובלים של שמע",
"transcoding_accepted_audio_codecs_description": "בחר אילו קודקים של שמע אינם צריכים לעבור המרת קידוד. משמש רק עבור פוליסות המרת קידוד מסוימות.",
"transcoding_accepted_containers": "מכולות מקובלות",
"transcoding_accepted_video_codecs": "קודקים מקובלים של סרטונים",
"transcoding_accepted_video_codecs_description": "בחר אילו קודקים של סרטונים אינם צריכים לעבור המרת קידוד. משמש רק עבור פוליסות המרת קידוד מסוימות.",
"transcoding_advanced_options_description": "אפשרויות שרוב המשתמשים לא צריכים לשנות",
@@ -408,7 +409,7 @@
"bulk_delete_duplicates_confirmation": "האם את/ה בטוח/ה שברצונך למחוק בכמות גדולה {count, plural, one {נכס # כפול} other {# נכסים כפולים}}? זה ישמור על הנכס הכי גדול של כל קבוצה וימחק לצמיתות את כל שאר הכפילויות. את/ה לא יכול/ה לבטל את הפעולה הזו!",
"bulk_keep_duplicates_confirmation": "האם את/ה בטוח/ה שברצונך להשאיר {count, plural, one {נכס # כפול} other {# נכסים כפולים}}? זה יפתור את כל הקבוצות הכפולות מבלי למחוק דבר.",
"bulk_trash_duplicates_confirmation": "האם את/ה בטוח/ה שברצונך להעביר לאשפה בכמות גדולה {count, plural, one {נכס # כפול} other {# נכסים כפולים}}? זה ישמור על הנכס הגדול ביותר של כל קבוצה ויעביר לאשפה את כל שאר הכפילויות.",
"buy": "רכוש רישיון",
"buy": "רכוש את Immich",
"camera": "מצלמה",
"camera_brand": "מותג המצלמה",
"camera_model": "דגם המצלמה",
@@ -436,6 +437,7 @@
"city": "עיר",
"clear": "נקה",
"clear_all": "נקה הכל",
"clear_all_recent_searches": "נקה את כל החיפושים האחרונים",
"clear_message": "נקה הודעה",
"clear_value": "נקה ערך",
"close": "סגור",
@@ -574,6 +576,7 @@
"error_adding_users_to_album": "שגיאה בהוספת משתמשים לאלבום",
"error_deleting_shared_user": "שגיאה במחיקת משתמש משותף",
"error_downloading": "שגיאה בהורדת {filename}",
"error_hiding_buy_button": "שגיאה בהסתרת לחצן 'קנה'",
"error_removing_assets_from_album": "שגיאה בהסרת נכסים מאלבום, בדוק את המסוף לפרטים נוספים",
"error_selecting_all_assets": "שגיאה בבחירת כל הנכסים",
"exclusion_pattern_already_exists": "דפוס החרגה זה כבר קיים.",
@@ -585,6 +588,7 @@
"failed_to_load_asset": "טעינת נכס נכשלה",
"failed_to_load_assets": "טעינת נכסים נכשלה",
"failed_to_load_people": "נכשל באחזור אנשים",
"failed_to_remove_product_key": "הסרת מפתח מוצר נכשלה",
"failed_to_stack_assets": "יצירת ערימת נכסים נכשלה",
"failed_to_unstack_assets": "ביטול ערימת נכסים נכשל",
"import_path_already_exists": "נתיב הייבוא הזה כבר קיים.",
@@ -738,7 +742,16 @@
"host": "מארח",
"hour": "שעה",
"image": "תמונה",
"image_alt_text_date": "ב {date}",
"image_alt_text_date": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} ב-{date}",
"image_alt_text_date_1_person": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} עם {person1} ב-{date}",
"image_alt_text_date_2_people": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} עם {person1} ו-{person2} ב-{date}",
"image_alt_text_date_3_people": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} עם {person1}, {person2}, ו-{person3} ב-{date}",
"image_alt_text_date_4_or_more_people": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} עם {person1}, {person2}, ו-{additionalCount, number} אחרים ב-{date}",
"image_alt_text_date_place": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} ב-{city}, {country} ב-{date}",
"image_alt_text_date_place_1_person": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} ב-{city}, {country} עם {person1} ב-{date}",
"image_alt_text_date_place_2_people": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} ב-{city}, {country} עם {person1} ו-{person2} ב-{date}",
"image_alt_text_date_place_3_people": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} ב-{city}, {country} עם {person1}, {person2}, ו-{person3} ב-{date}",
"image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} ב-{city}, {country} עם {person1}, {person2}, ו-{additionalCount, number} אחרים ב-{date}",
"image_alt_text_people": "{count, plural, =1 {עם {person1}} =2 {עם {person1} ו{person2}} =3 {עם {person1}, {person2}, ו{person3}} other {עם {person1}, {person2}, ו{others, number} אחרים}}",
"image_alt_text_place": "ב{city}, {country}",
"image_taken": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}}",
@@ -772,6 +785,7 @@
"language_setting_description": "בחר/י את השפה המועדפת עליך",
"last_seen": "נראה לאחרונה",
"latest_version": "גרסה עדכנית ביותר",
"latitude": "קו רוחב",
"leave": "לעזוב",
"let_others_respond": "אפשר לאחרים להגיב",
"level": "רמה",
@@ -818,6 +832,7 @@
"login_has_been_disabled": "הכניסה הושבתה.",
"logout_all_device_confirmation": "את/ה בטוח/ה שברצונך להתנתק מכל המכשירים?",
"logout_this_device_confirmation": "את/ה בטוח/ה שברצונך להתנתק מהמכשיר הזה?",
"longitude": "קו אורך",
"look": "מראה",
"loop_videos": "הפעלה חוזרת של סרטונים",
"loop_videos_description": "אפשר הפעלה חוזרת אוטומטית של סרטון במציג הפרטים.",
@@ -971,6 +986,38 @@
"profile_picture_set": "תמונת פרופיל נבחרה.",
"public_album": "אלבום ציבורי",
"public_share": "שיתוף ציבורי",
"purchase_account_info": "תומך",
"purchase_activated_subtitle": "תודה לך על התמיכה ב-Immich ובתוכנות קוד-פתוח",
"purchase_activated_time": "הופעל ב-{date, date}",
"purchase_activated_title": "המפתח שלך הופעל בהצלחה",
"purchase_button_activate": "הפעל",
"purchase_button_buy": "קנה",
"purchase_button_buy_immich": "קנה Immich",
"purchase_button_never_show_again": "לעולם אל תראה שוב",
"purchase_button_reminder": "הזכר לי בעוד 30 יום",
"purchase_button_remove_key": "הסר מפתח",
"purchase_button_select": "בחר",
"purchase_failed_activation": "ההפעלה נכשלה! נא לבדוק את הדוא\"ל שלך עבור מפתח המוצר הנכון!",
"purchase_individual_description_1": "ליחיד",
"purchase_individual_description_2": "מעמד תומך",
"purchase_individual_title": "יחיד",
"purchase_input_suggestion": "יש לך מפתח מוצר? הכנס את המפתח למטה",
"purchase_license_subtitle": "קנה את Immich כדי לתמוך בפיתוח המתמשך של השירות",
"purchase_lifetime_description": "רכישה לכל החיים",
"purchase_option_title": "אפשרויות רכישה",
"purchase_panel_info_1": "בניית Immich לוקחת הרבה זמן ומאמץ, ויש לנו מהנדסים במשרה מלאה שעובדים על זה כדי לעשות את זה הכי טוב שאנחנו יכולים. המשימה שלנו היא שתוכנות קוד-פתוח ושיטות עסקיות אתיות יהיו מקור הכנסה בר-קיימא למפתחים וליצור מערכת אקולוגית שמכבדת פרטיות עם חלופות אמיתיות לשירותי ענן נצלנים.",
"purchase_panel_info_2": "מכיוון שאנחנו מחויבים לא להוסיף חומות תשלום, הרכישה הזאת לא תקנה לך תכונות נוספות כלשהן ב-Immich. אנחנו סומכים על משתמשים כמוך שיתמכו בפיתוח המתמשך של Immich.",
"purchase_panel_title": "תמוך בפרויקט",
"purchase_per_server": "עבור שרת",
"purchase_per_user": "עבור משתמש",
"purchase_remove_product_key": "הסר מפתח מוצר",
"purchase_remove_product_key_prompt": "האם את/ה בטוח/ה שאת/ה רוצה להסיר את מפתח המוצר?",
"purchase_remove_server_product_key": "הסר מפתח מוצר של שרת",
"purchase_remove_server_product_key_prompt": "האם את/ה בטוח/ה שאת/ה רוצה להסיר את מפתח המוצר של השרת?",
"purchase_server_description_1": "עבור כל השרת",
"purchase_server_description_2": "מעמד תומך",
"purchase_server_title": "שרת",
"purchase_settings_server_activated": "מפתח המוצר של השרת מנוהל על ידי מנהל המערכת",
"range": "",
"raw": "",
"reaction_options": "אפשרויות הגבה",
@@ -1016,6 +1063,7 @@
"reset_people_visibility": "אפס את נראות האנשים",
"reset_settings_to_default": "",
"reset_to_default": "אפס לברירת מחדל",
"resolve_duplicates": "פתור כפילויות",
"resolved_all_duplicates": "כל הכפילויות נפתרו",
"restore": "שחזר",
"restore_all": "שחזר הכל",
@@ -1060,6 +1108,7 @@
"see_all_people": "ראה את כל האנשים",
"select_album_cover": "בחר עטיפת אלבום",
"select_all": "בחר הכל",
"select_all_duplicates": "בחר את כל הכפילויות",
"select_avatar_color": "בחר צבע תמונת פרופיל",
"select_face": "בחר פנים",
"select_featured_photo": "בחר תמונה מייצגת",
@@ -1114,6 +1163,8 @@
"show_person_options": "הצג אפשרויות אדם",
"show_progress_bar": "הצג סרגל התקדמות",
"show_search_options": "הצג אפשרויות חיפוש",
"show_supporter_badge": "תג תומך",
"show_supporter_badge_description": "הצג תג תומך",
"shuffle": "ערבוב",
"sign_out": "יציאה מהמערכת",
"sign_up": "הרשמה",
@@ -1187,6 +1238,7 @@
"unnamed_share": "שיתוף ללא שם",
"unsaved_change": "שינוי לא נשמר",
"unselect_all": "בטל בחירה בהכל",
"unselect_all_duplicates": "בטל בחירת כל הכפילויות",
"unstack": "בטל ערימה",
"unstacked_assets_count": "{count, plural, one {נכס # הוסר} other {# נכסים הוסרו}} מערימה",
"untracked_files": "קבצים ללא מעקב",
@@ -1210,6 +1262,8 @@
"user_license_settings": "רישיון",
"user_license_settings_description": "נהל את הרישיון שלך",
"user_liked": "{user} אהב את {type, select, photo {התמונה הזאת} video {הסרטון הזה} asset {הנכס הזה} other {זה}}",
"user_purchase_settings": "רכישה",
"user_purchase_settings_description": "נהל את הרכישה שלך",
"user_role_set": "הגדר את {user} בתור {role}",
"user_usage_detail": "פרטי השימוש של המשתמש",
"username": "שם משתמש",

View File

@@ -256,6 +256,7 @@
"transcoding_audio_codec": "Audio kodek",
"transcoding_audio_codec_description": "Az Opus a legjobb minőségű opció (jobb minőség ugyanannyi helyet foglalva), de kevésbé kompatibilis a régi eszközökkel vagy szoftverekkel.",
"transcoding_bitrate_description": "A maximum bitrátát meghaladó vagy nem megfelelő formátumú videókat",
"transcoding_codecs_learn_more": "Hogy többet tudjon meg az itt felhasznált kifejezésekről, látogassa meg az FFmpeg dokumentációt a <h264-link>H.264 kodekhez</h264-link>, a <hevc-link>HEVC kodekhez</hevc-link> és a <vp9-link>VP9 kodekhez</vp9-link>.",
"transcoding_constant_quality_mode": "Állandó minőségi mód",
"transcoding_constant_quality_mode_description": "Az ICQ jobb, mint a CQP, viszont nem minden hardver támogatja. A rendszer az itt beállított módszert részesíti előnyben. A NVENC ignorálja a beállítást, mivel nem támogatja az ICQ-t.",
"transcoding_constant_rate_factor": "",

View File

@@ -249,6 +249,7 @@
"transcoding_acceleration_vaapi": "VAAPI",
"transcoding_accepted_audio_codecs": "Codifiche audio accettate",
"transcoding_accepted_audio_codecs_description": "Seleziona quali codifiche audio non devono essere trascodificate. Solo usato per alcune politiche di trascodifica.",
"transcoding_accepted_containers_description": "Seleziona quali formati non hanno bisogno di essere remuxati in MP4. Usato solo per certe politiche di transcodifica.",
"transcoding_accepted_video_codecs": "Codifiche video accettate",
"transcoding_accepted_video_codecs_description": "Seleziona quali codifiche video non devono essere trascodificate. Usato solo per alcune politiche di trascodifica.",
"transcoding_advanced_options_description": "Impostazioni che la maggior parte degli utenti non dovrebbero cambiare",
@@ -436,6 +437,7 @@
"city": "Città",
"clear": "Pulisci",
"clear_all": "Pulisci tutto",
"clear_all_recent_searches": "Rimuovi tutte le ricerche recenti",
"clear_message": "Pulisci messaggio",
"clear_value": "Pulisci valore",
"close": "Chiudi",
@@ -584,6 +586,8 @@
"failed_to_get_people": "Impossibile ottenere le persone",
"failed_to_load_asset": "Errore durante il caricamento dell'asset",
"failed_to_load_assets": "Errore durante il caricamento degli assets",
"failed_to_load_people": "Caricamento delle persone fallito",
"failed_to_remove_product_key": "Rimozione del codice del prodotto fallita",
"failed_to_stack_assets": "Errore durante il raggruppamento degli assets",
"failed_to_unstack_assets": "Errore durante la separazione degli assets",
"import_path_already_exists": "Questo percorso di importazione già esiste.",
@@ -738,6 +742,14 @@
"hour": "Ora",
"image": "Immagine",
"image_alt_text_date": "il {date}",
"image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} scattata con {person1} il giorno {date}",
"image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} scattata con {person1} e {person2} il giorno {date}",
"image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} scattata con {person1}, {person2}, e {person3} il giorno {date}",
"image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} scattata con {person1}, {person2}, e altre {additionalCount, number} persone il giorno {date}",
"image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} scattata a {city}, {country} il giorno {date}",
"image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} scattata a {city}, {country} con {person1} il giorno {date}",
"image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} scattata a {city}, {country} con {person1} e {person2} il giorno {date}",
"image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} scattata a {city}, {country} con {person1}, {person2}, e {person3} il giorno {date}",
"image_alt_text_people": "{count, plural, =1 {con {person1}} =2 {con {person1} e {person2}} =3 {con {person1}, {person2} e {person3}} other {con {person1}, {person2} e {others, number} altri}}",
"image_alt_text_place": "a {city}, {country}",
"image_taken": "{isVideo, select, true {Video registrato} other {Immagine scattata}}",

View File

@@ -127,6 +127,8 @@
"manage_log_settings": "ログ設定を管理します",
"map_dark_style": "ダークモード",
"map_enable_description": "地図表示を有効にします",
"map_gps_settings": "地図・GPS設定",
"map_gps_settings_description": "地図とGPS(逆ジオコーディング)の設定を管理します",
"map_light_style": "ライトモード",
"map_manage_reverse_geocoding_settings": "<link>逆ジオコーディング</link>の設定を管理します",
"map_reverse_geocoding": "逆ジオコーディング",
@@ -245,6 +247,8 @@
"transcoding_acceleration_vaapi": "VA-API",
"transcoding_accepted_audio_codecs": "容認する音声コーデック",
"transcoding_accepted_audio_codecs_description": "トランスコードする必要のない音声コーデックを選択します。特定のトランスコードポリシーにのみ使用されます。",
"transcoding_accepted_containers": "容認するコンテナ",
"transcoding_accepted_containers_description": "MP4に再多重化する必要がないコンテナを選択します。特定のトランスコードポリシーにのみ使用されます。",
"transcoding_accepted_video_codecs": "容認する動画コーデック",
"transcoding_accepted_video_codecs_description": "トランスコードする必要のない動画コーデックを選択します。特定のトランスコードポリシーにのみ使用されます。",
"transcoding_advanced_options_description": "ほとんどのユーザーは変更する必要のないオプション",
@@ -379,7 +383,7 @@
"assets": "アセット",
"assets_added_count": "{count, plural, one {#個} other {#個}}のアセットを追加しました",
"assets_added_to_album_count": "{count, plural, one {#個} other {#個}}のアセットをアルバムに追加しました",
"assets_added_to_name_count": "{count, plural, one {#個} other {#個}}のアセットを{name}に追加しました",
"assets_added_to_name_count": "{count, plural, one {#個} other {#個}}のアセットを{hasName, select, true {<b>{name}</b>} other {新しいアルバム}}に追加しました",
"assets_count": "{count, plural, one {#個} other {#個}}のアセット",
"assets_moved_to_trash_count": "{count, plural, one {#個} other {#個}}のアセットをごみ箱に移動しました",
"assets_permanently_deleted_count": "{count, plural, one {#個} other {#個}}のアセットを完全に削除しました",
@@ -400,6 +404,7 @@
"bulk_delete_duplicates_confirmation": "本当に {count, plural, one {#個} other {#個}}の重複したアセットを一括削除しますか?これにより各重複中の最大のアセットが保持され、他の全ての重複が削除されます。この操作を元に戻すことはできません!",
"bulk_keep_duplicates_confirmation": "本当に{count, plural, one {#個} other {#個}}の重複アセットを保持しますか?これにより何も削除されずに重複グループが解決されます。",
"bulk_trash_duplicates_confirmation": "本当に{count, plural, one {#個} other {#個}}の重複したアセットを一括でごみ箱に移動しますか?これにより各重複中の最大のアセットが保持され、他の全ての重複はごみ箱に移動されます。",
"buy": "Immichを購入",
"camera": "カメラブランド",
"camera_brand": "カメラブランド",
"camera_model": "カメラモデル",
@@ -425,6 +430,7 @@
"city": "市町村",
"clear": "クリア",
"clear_all": "全てクリア",
"clear_all_recent_searches": "全ての最近の検索をクリア",
"clear_message": "メッセージをクリア",
"clear_value": "値をクリア",
"close": "閉じる",
@@ -480,24 +486,27 @@
"default_locale_description": "ブラウザのロケールに基づいて日付と数値をフォーマットします",
"delete": "削除",
"delete_album": "アルバムを削除",
"delete_api_key_prompt": "本当にこのAPI キーを削除しますか?",
"delete_duplicates_confirmation": "本当にこれらの重複を完全に削除しますか?",
"delete_key": "",
"delete_library": "",
"delete_link": "",
"delete_key": "キーを削除",
"delete_library": "ライブラリを削除",
"delete_link": "リンクを削除",
"delete_shared_link": "共有リンクを消す",
"delete_user": "",
"deleted_shared_link": "",
"delete_user": "ユーザーを削除",
"deleted_shared_link": "共有リンクを削除",
"description": "概要欄",
"details": "詳細",
"direction": "",
"disallow_edits": "",
"discover": "",
"dismiss_all_errors": "",
"dismiss_error": "",
"direction": "方向",
"disabled": "無効",
"disallow_edits": "編集を許可しない",
"discover": "探索",
"dismiss_all_errors": "全てのエラーを無視",
"dismiss_error": "エラーを無視",
"display_options": "表示オプション",
"display_order": "表示順",
"display_original_photos": "オリジナルの写真を表示",
"display_original_photos_setting_description": "オリジナルのアセットが Web 互換である場合は、アセットを表示するときにサムネイルではなく元の写真を優先して表示します。これにより写真の表示速度が遅くなる可能性があります。",
"do_not_show_again": "このメッセージを再び表示しない",
"done": "完了",
"download": "ダウンロード",
"download_settings": "ダウンロード",
@@ -506,7 +515,7 @@
"downloading_asset_filename": "アセット {filename} をダウンロード中",
"drop_files_to_upload": "ファイルをドロップしてアップロード",
"duplicates": "重複",
"duration": "",
"duration": "間隔",
"durations": {
"days": "",
"hours": "",
@@ -514,7 +523,8 @@
"months": "",
"years": ""
},
"edit_album": "",
"edit": "編集",
"edit_album": "アルバムを編集",
"edit_avatar": "アバターを編集",
"edit_date": "日付を編集",
"edit_date_and_time": "日時を編集",
@@ -558,6 +568,7 @@
"error_adding_users_to_album": "ユーザーをアルバムに追加中のエラー",
"error_deleting_shared_user": "共有ユーザを削除中のエラー",
"error_downloading": "{filename}をダウンロード中にエラー",
"error_hiding_buy_button": "購入ボタン非表示のエラー",
"error_removing_assets_from_album": "アルバムからアセットを削除中のエラー、詳細についてはコンソールを確認してください",
"error_selecting_all_assets": "全アセット選択のエラー",
"exclusion_pattern_already_exists": "この除外パターンは既に存在します。",
@@ -568,6 +579,8 @@
"failed_to_get_people": "人物を取得できませんでした",
"failed_to_load_asset": "アセットを読み込めませんでした",
"failed_to_load_assets": "アセットを読み込めませんでした",
"failed_to_load_people": "人物を読み込めませんでした",
"failed_to_remove_product_key": "プロダクトキーを削除できませんでした",
"import_path_already_exists": "このインポートパスは既に存在します。",
"incorrect_email_or_password": "メールアドレスまたはパスワードが間違っています",
"paths_validation_failed": "{paths, plural, one {#個} other {#個}}のパスの検証に失敗しました",
@@ -694,7 +707,7 @@
"filetype": "ファイルタイプ",
"filter_people": "人物を絞り込み",
"find_them_fast": "名前で検索して素早く発見",
"fix_incorrect_match": "",
"fix_incorrect_match": "間違った一致を修正",
"force_re-scan_library_files": "強制的に全てのライブラリのファイルを再スキャン",
"forward": "前へ",
"general": "一般",
@@ -718,13 +731,14 @@
"host": "ホスト",
"hour": "時間",
"image": "写真",
"image_alt_text_date": "{date} に撮影",
"image_alt_text_date": "{isVideo, select, true {動画} other {写真}}は{date} に撮影",
"image_alt_text_place": "{country} {city}で撮影",
"image_taken": "{isVideo, select, true {動画は} other {写真は}}",
"img": "",
"immich_logo": "Immich ロゴ",
"immich_web_interface": "Immich Webインターフェース",
"import_from_json": "JSONからインポート",
"import_path": "",
"import_path": "インポートパス",
"in_archive": "アーカイブ済み",
"include_archived": "アーカイブ済みを含める",
"include_shared_albums": "共有アルバムを含める",
@@ -732,27 +746,29 @@
"individual_share": "",
"info": "情報",
"interval": {
"day_at_onepm": "",
"hours": "",
"night_at_midnight": "",
"night_at_twoam": ""
"day_at_onepm": "毎日午後1時",
"hours": "{hours, plural, one {1時間} other {{hours, number}時間}}ごと",
"night_at_midnight": "毎晩真夜中に",
"night_at_twoam": "毎晩午前2時"
},
"invite_people": "",
"invite_people": "人々を招待",
"invite_to_album": "アルバムに招待",
"items_count": "{count, plural, one {#個} other {#個}}の項目",
"job_settings_description": "",
"jobs": "ジョブ",
"keep": "",
"keep": "保持",
"keep_all": "全て保持",
"keyboard_shortcuts": "キーボードショートカット",
"language": "言語",
"language_setting_description": "優先言語を選択してください",
"last_seen": "最新の活動",
"latest_version": "最新バージョン",
"latitude": "緯度",
"leave": "",
"let_others_respond": "他のユーザーの返信を許可する",
"level": "レベル",
"library": "ライブラリ",
"library_options": "",
"library_options": "ライブラリ設定",
"light": "",
"like_deleted": "いいねが削除されました",
"link_options": "リンクのオプション",
@@ -769,6 +785,7 @@
"login_has_been_disabled": "ログインは無効化されています。",
"logout_all_device_confirmation": "本当に全てのデバイスからログアウトしますか?",
"logout_this_device_confirmation": "本当にこのデバイスからログアウトしますか?",
"longitude": "経度",
"look": "見た目",
"loop_videos": "動画をループ",
"loop_videos_description": "有効にすると詳細表示で自動的に動画がループします。",
@@ -798,7 +815,7 @@
"merged_people_count": "{count, plural, one {#人} other {#人}}の人物をマージしました",
"minimize": "最小化",
"minute": "分",
"missing": "",
"missing": "行方不明",
"model": "モデル",
"month": "月",
"more": "もっと表示",
@@ -916,6 +933,31 @@
"profile_picture_set": "プロフィール画像が設定されました。",
"public_album": "公開アルバム",
"public_share": "公開共有",
"purchase_account_info": "サポーター",
"purchase_activated_subtitle": "Immich とオープンソース ソフトウェアを支援していただきありがとうございます",
"purchase_activated_time": "{date, date}にアクティベート",
"purchase_activated_title": "キーは正常にアクティベートされました",
"purchase_button_activate": "アクティベート",
"purchase_button_buy": "購入",
"purchase_button_buy_immich": "Immichを購入",
"purchase_button_never_show_again": "二度と表示しない",
"purchase_button_reminder": "30日後に通知する",
"purchase_button_remove_key": "キーを削除",
"purchase_button_select": "選択",
"purchase_failed_activation": "アクティベートに失敗しました! メールで正しいプロダクトキーを確認してください!",
"purchase_individual_description_1": "個人向け",
"purchase_individual_title": "個人",
"purchase_input_suggestion": "プロダクトキーをお持ちですか? 下に入力してください",
"purchase_license_subtitle": "Immich を購入してサービスの継続的な開発を支援してください",
"purchase_lifetime_description": "生涯の購入",
"purchase_option_title": "購入オプション",
"purchase_panel_title": "プロジェクトを支援",
"purchase_per_server": "サーバーごと",
"purchase_per_user": "ユーザーごと",
"purchase_remove_product_key": "プロダクトキーを削除",
"purchase_remove_product_key_prompt": "本当にプロダクトキーを削除しますか?",
"purchase_remove_server_product_key": "サーバープロダクトキーを削除",
"purchase_remove_server_product_key_prompt": "本当にサーバープロダクトキーを削除しますか?",
"range": "",
"raw": "",
"reaction_options": "",

View File

@@ -35,7 +35,7 @@
"background_task_job": "백그라운드 작업",
"check_all": "모두 확인",
"cleared_jobs": "{job} 작업 중단됨",
"config_set_by_file": "구성 파일의 설정 적용되었습니다.",
"config_set_by_file": "업로드한 설정 파일이 현재 설정 적용니다.",
"confirm_delete_library": "{library} 라이브러리를 삭제하시겠습니까?",
"confirm_delete_library_assets": "이 라이브러리를 삭제하시겠습니까? Immich에서 항목 {count, plural, one {#개} other {#개}}가 삭제되며 되돌릴 수 없습니다. 원본 파일은 삭제되지 않습니다.",
"confirm_email_below": "계속 진행하려면 아래에 \"{email}\" 입력",
@@ -249,7 +249,8 @@
"transcoding_acceleration_vaapi": "VAAPI",
"transcoding_accepted_audio_codecs": "허용된 오디오 코덱",
"transcoding_accepted_audio_codecs_description": "트랜스코딩하지 않을 오디오 코덱을 선택합니다. 이 설정은 특정 트랜스코딩 정책에만 적용됩니다.",
"transcoding_accepted_containers": "Accepted containers",
"transcoding_accepted_containers": "허용된 컨테이너",
"transcoding_accepted_containers_description": "MP4로 변경하지 않을 동영상 컨테이너(확장자)를 선택합니다. 이 설정은 특정 트랜스코딩 정책에만 적용됩니다.",
"transcoding_accepted_video_codecs": "허용된 동영상 코덱",
"transcoding_accepted_video_codecs_description": "트랜스코딩하지 않을 동영상 코덱을 선택합니다. 이 설정은 특정 트랜스코딩 정책에만 적용됩니다.",
"transcoding_advanced_options_description": "대부분의 사용자가 변경할 필요가 없는 옵션",
@@ -409,7 +410,7 @@
"bulk_delete_duplicates_confirmation": "비슷한 항목 {count, plural, one {#개} other {#개}}를 삭제하시겠습니까? 크기가 가장 큰 항목을 제외한 나머지 항목들이 영구적으로 삭제됩니다. 이 작업은 되돌릴 수 없습니다!",
"bulk_keep_duplicates_confirmation": "비슷한 항목 {count, plural, one {#개} other {#개}}를 유지하시겠습니까? 파일을 삭제하지 않고 확인된 것으로 판단합니다.",
"bulk_trash_duplicates_confirmation": "비슷한 항목 {count, plural, one {#개} other {#개}}를 휴지통으로 이동하시겠습니까? 크기가 가장 큰 항목을 제외한 나머지 항목들이 모두 휴지통으로 이동됩니다.",
"buy": "라이선스",
"buy": "Immich",
"camera": "카메라",
"camera_brand": "카메라 제조사",
"camera_model": "카메라 모델",
@@ -437,6 +438,7 @@
"city": "도시",
"clear": "지우기",
"clear_all": "모두 지우기",
"clear_all_recent_searches": "검색 기록 전체 삭제",
"clear_message": "메시지 지우기",
"clear_value": "값 지우기",
"close": "닫기",
@@ -575,6 +577,7 @@
"error_adding_users_to_album": "앨범에 사용자를 추가하는 중 문제가 발생했습니다.",
"error_deleting_shared_user": "공유한 사용자를 제거하는 중 문제가 발생했습니다.",
"error_downloading": "{filename} 다운로드 중 문제가 발생했습니다.",
"error_hiding_buy_button": "구매 버튼을 숨기는 중 문제가 발생했습니다.",
"error_removing_assets_from_album": "앨범에서 항목을 제거하는 중 문제가 발생했습니다. 콘솔에서 세부 정보를 확인하세요.",
"error_selecting_all_assets": "모든 항목을 선택하는 중 문제가 발생했습니다.",
"exclusion_pattern_already_exists": "이 제외 규칙은 이미 존재합니다.",
@@ -585,6 +588,8 @@
"failed_to_get_people": "인물을 불러오지 못했습니다.",
"failed_to_load_asset": "항목을 불러오지 못했습니다.",
"failed_to_load_assets": "항목을 불러오지 못했습니다.",
"failed_to_load_people": "인물을 불러오지 못했습니다.",
"failed_to_remove_product_key": "제품 키를 제거하지 못했습니다.",
"failed_to_stack_assets": "스택을 만들지 못했습니다.",
"failed_to_unstack_assets": "스택을 해제하지 못했습니다.",
"import_path_already_exists": "이 가져올 경로는 이미 존재합니다.",
@@ -738,7 +743,7 @@
"host": "호스트",
"hour": "시간",
"image": "이미지",
"image_alt_text_date": "{date}에 촬영",
"image_alt_text_date": "{date}에 촬영된 {isVideo, select, true {동영상} other {사진}}",
"image_alt_text_people": "{count, plural, =1 {{person1}님과 함께,} =2 {{person1} 및 {person2}님과 함께,} =3 {{person1}, {person2} 및 {person3}님과 함께,} other {{person1}, {person2}, 및 {others, number}명과 함께,}}",
"image_alt_text_place": "{country}, {city}에서",
"image_taken": "{isVideo, select, true {동영상} other {사진}},",
@@ -772,6 +777,7 @@
"language_setting_description": "선호하는 언어 선택",
"last_seen": "최근 활동",
"latest_version": "최신 버전",
"latitude": "위도",
"leave": "나가기",
"let_others_respond": "다른 사용자의 반응 허용",
"level": "레벨",
@@ -801,6 +807,7 @@
"login_has_been_disabled": "로그인이 비활성화되었습니다.",
"logout_all_device_confirmation": "모든 기기에서 로그아웃하시겠습니까?",
"logout_this_device_confirmation": "이 기기에서 로그아웃하시겠습니까?",
"longitude": "경도",
"look": "보기",
"loop_videos": "동영상 반복",
"loop_videos_description": "상세 보기에서 동영상을 자동으로 반복 재생합니다.",
@@ -821,7 +828,7 @@
"memories": "추억",
"memories_setting_description": "추억 표시 설정 관리",
"memory": "추억",
"memory_lane_title": "기억 {title}",
"memory_lane_title": "{title} 추억",
"menu": "메뉴",
"merge": "병합",
"merge_people": "인물 병합",
@@ -955,6 +962,38 @@
"profile_picture_set": "프로필 사진이 설정되었습니다.",
"public_album": "공개 앨범",
"public_share": "모든 사용자와 공유",
"purchase_account_info": "서포터",
"purchase_activated_subtitle": "Immich와 오픈 소스 소프트웨어를 지원해주셔서 감사합니다.",
"purchase_activated_time": "{date, date}에 활성화됨",
"purchase_activated_title": "제품 키가 성공적으로 활성화되었습니다.",
"purchase_button_activate": "활성화",
"purchase_button_buy": "구매",
"purchase_button_buy_immich": "Immich 구매",
"purchase_button_never_show_again": "다시 보지 않기",
"purchase_button_reminder": "30일 후에 다시 알림",
"purchase_button_remove_key": "제품 키 제거",
"purchase_button_select": "선택",
"purchase_failed_activation": "활성화하지 못했습니다. 이메일로 전송된 키를 정확히 입력했는지 확인하세요!",
"purchase_individual_description_1": "개인 사용자용",
"purchase_individual_description_2": "서포터 현황",
"purchase_individual_title": "개인",
"purchase_input_suggestion": "제품 키를 보유 중인가요? 아래에 제품 키를 입력하세요.",
"purchase_license_subtitle": "Immich를 구매하여 지속적인 개발에 도움을 주세요.",
"purchase_lifetime_description": "일회성 구매",
"purchase_option_title": "구매 옵션",
"purchase_panel_info_1": "Immich를 개발하는 데는 많은 시간과 노력이 필요합니다. 우리는 좋은 앱을 만들기 위해 풀 타임 개발자와 함께하고 있으며, 최종적으로 오픈 소스 소프트웨어와 비즈니스 행동 윤리가 개발자에게 지속 가능한 수입원을 제공하고 착취적인 클라우드 서비스를 대체할 수 있는 개인 정보 보호 생태계를 구축하는 것을 원합니다.",
"purchase_panel_info_2": "유료 기능을 추가하지 않기로 약속했기에, 이 구매는 어떠한 추가 기능도 제공하지 않습니다. 우리는 Immich의 지속적인 개발을 지원하는 사용자 여러분에게 의존하고 있습니다.",
"purchase_panel_title": "프로젝트 지원",
"purchase_per_server": "서버당",
"purchase_per_user": "사용자당",
"purchase_remove_product_key": "제품 키 제거",
"purchase_remove_product_key_prompt": "제품 키를 제거하시겠습니까?",
"purchase_remove_server_product_key": "서버 제품 키 제거",
"purchase_remove_server_product_key_prompt": "서버 제품 키를 제거하시겠습니까?",
"purchase_server_description_1": "전체 서버용",
"purchase_server_description_2": "서포터 현황",
"purchase_server_title": "서버",
"purchase_settings_server_activated": "서버 제품 키는 관리자가 관리합니다.",
"range": "",
"raw": "",
"reaction_options": "반응 옵션",
@@ -1098,6 +1137,8 @@
"show_person_options": "인물 옵션 표시",
"show_progress_bar": "진행 표시줄 표시",
"show_search_options": "검색 옵션 표시",
"show_supporter_badge": "서포터 배지",
"show_supporter_badge_description": "서포터 배지 표시",
"shuffle": "셔플",
"sign_out": "로그아웃",
"sign_up": "로그인",
@@ -1192,6 +1233,8 @@
"user": "사용자",
"user_id": "사용자 ID",
"user_liked": "{user}님이 {type, select, photo {이 사진을} video {이 동영상을} asset {이 항목을} other {이 항목을}} 좋아합니다.",
"user_purchase_settings": "결제",
"user_purchase_settings_description": "구매한 항목 관리",
"user_role_set": "{user}님에게 {role} 역할을 설정했습니다.",
"user_usage_detail": "사용자 사용량 상세",
"username": "계정명",

View File

@@ -410,7 +410,7 @@
"bulk_delete_duplicates_confirmation": "Weet je zeker dat je {count, plural, one {# duplicate asset} other {# duplicate assets}} in bulk wilt verwijderen? Dit zal de grootste asset van elke groep behouden en alle andere duplicaten permanent verwijderen. Je kunt deze actie niet ongedaan maken!",
"bulk_keep_duplicates_confirmation": "Weet je zeker dat je {count, plural, one {# duplicate asset} other {# duplicate assets}} wilt behouden? Dit zal alle groepen met duplicaten oplossen zonder iets te verwijderen.",
"bulk_trash_duplicates_confirmation": "Weet je zeker dat je {count, plural, one {# duplicate asset} other {# duplicate assets}} in bulk naar de prullenbak wilt verplaatsen? Dit zal de grootste asset van elke groep behouden en alle andere duplicaten naar de prullenbak verplaatsen.",
"buy": "Licentie kopen",
"buy": "Koop Immich",
"camera": "Camera",
"camera_brand": "Cameramerk",
"camera_model": "Cameramodel",
@@ -438,6 +438,7 @@
"city": "Stad",
"clear": "Wissen",
"clear_all": "Alles wissen",
"clear_all_recent_searches": "Wis alle recente zoekopdrachten",
"clear_message": "Bericht wissen",
"clear_value": "Waarde wissen",
"close": "Sluiten",
@@ -576,6 +577,7 @@
"error_adding_users_to_album": "Fout bij toevoegen gebruikers aan album",
"error_deleting_shared_user": "Fout bij verwijderen gedeelde gebruiker",
"error_downloading": "Fout bij downloaden {filename}",
"error_hiding_buy_button": "Fout bij het verbergen van de koop knop",
"error_removing_assets_from_album": "Fout bij verwijderen van assets uit album, controleer de console voor meer details",
"error_selecting_all_assets": "Fout bij selecteren van alle assets",
"exclusion_pattern_already_exists": "Dit uitsluitingspatroon bestaat al.",
@@ -586,6 +588,8 @@
"failed_to_get_people": "Fout bij ophalen van mensen",
"failed_to_load_asset": "Kan asset niet laden",
"failed_to_load_assets": "Kan assets niet laden",
"failed_to_load_people": "Kan mensen niet laden",
"failed_to_remove_product_key": "Er is een fout opgetreden bij het verwijderen van de product sleutel",
"failed_to_stack_assets": "Fout bij stapelen van assets",
"failed_to_unstack_assets": "Fout bij ontstapelen van assets",
"import_path_already_exists": "Dit import-pad bestaat al.",
@@ -739,7 +743,16 @@
"host": "Host",
"hour": "Uur",
"image": "Afbeelding",
"image_alt_text_date": "op {date}",
"image_alt_text_date": "{isVideo, select, true {Video} other {Image}} genomen op {date}",
"image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} genomen met {person1} op {date}",
"image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} genomen met {person1} en {person2} op {date}",
"image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} genomen met {person1}, {person2}, en {person3} op {date}",
"image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} genomen met {person1}, {person2}, en {additionalCount, number} anderen op {date}",
"image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} genomen in {city}, {country} op {date}",
"image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} genomen in {city}, {country} met {person1} op {date}",
"image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} genomen in {city}, {country} met {person1} en {person2} op {date}",
"image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} genomen in {city}, {country} met {person1}, {person2}, en {person3} op {date}",
"image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} genomen in {city}, {country} met {person1}, {person2}, en {additionalCount, number} anderen op {date}",
"image_alt_text_people": "{count, plural, =1 {met {person1}} =2 {met {person1} en {person2}} =3 {met {person1}, {person2} en {person3}} other {met {person1}, {person2} en {others, number} anderen}}",
"image_alt_text_place": "in {city}, {country}",
"image_taken": "{isVideo, select, true {Video gemaakt} other {Afbeelding genomen}}",
@@ -860,6 +873,7 @@
"name": "Naam",
"name_or_nickname": "Naam of gebruikersnaam",
"never": "Nooit",
"new_album": "Nieuw album",
"new_api_key": "Nieuwe API sleutel",
"new_password": "Nieuw wachtwoord",
"new_person": "Nieuw persoon",
@@ -975,6 +989,38 @@
"profile_picture_set": "Profielfoto ingesteld.",
"public_album": "Openbaar album",
"public_share": "Publieke deellink",
"purchase_account_info": "Supporter",
"purchase_activated_subtitle": "Bedankt voor het ondersteunen van Immich en open-source software",
"purchase_activated_time": "Geactiveerd op {date, date}",
"purchase_activated_title": "Je sleutel is succesvol geactiveerd",
"purchase_button_activate": "Activeren",
"purchase_button_buy": "Kopen",
"purchase_button_buy_immich": "Koop Immich",
"purchase_button_never_show_again": "Nooit meer tonen",
"purchase_button_reminder": "Herinner mij over 30 dagen",
"purchase_button_remove_key": "Sleutel verwijderen",
"purchase_button_select": "Selecteren",
"purchase_failed_activation": "Activeren mislukt! Controleer je e-mail voor de juiste productsleutel!",
"purchase_individual_description_1": "Voor een gebruiker",
"purchase_individual_description_2": "Supporter badge",
"purchase_individual_title": "Gebruiker",
"purchase_input_suggestion": "Heb je een productsleutel? Voer de sleutel hieronder in",
"purchase_license_subtitle": "Koop Immich om de verdere ontwikkeling van de service te ondersteunen",
"purchase_lifetime_description": "Levenslange aankoop",
"purchase_option_title": "AANKOOP MOGELIJKHEDEN",
"purchase_panel_info_1": "Het bouwen van Immich kost veel tijd en moeite, en we hebben fulltime engineers die eraan werken om het zo goed mogelijk te maken. Onze missie is om open-source software en ethische bedrijfspraktijken een duurzame inkomstenbron te laten worden voor ontwikkelaars en een ecosysteem te creëren dat de privacy respecteert met echte alternatieven voor uitbuitende cloudservices.",
"purchase_panel_info_2": "Omdat we ons inzetten om geen paywalls toe te voegen, krijg je met deze aankoop geen extra functies in Immich. We vertrouwen op gebruikers zoals jij om de verdere ontwikkeling van Immich te ondersteunen.",
"purchase_panel_title": "Steun het project",
"purchase_per_server": "Per server",
"purchase_per_user": "Per gebruiker",
"purchase_remove_product_key": "Verwijder product sleutel",
"purchase_remove_product_key_prompt": "Weet je zeker dat je de product sleutel wilt verwijderen?",
"purchase_remove_server_product_key": "Verwijder server product sleutel",
"purchase_remove_server_product_key_prompt": "Weet je zeker dat je de server product sleutel wilt verwijderen?",
"purchase_server_description_1": "Voor de volledige server",
"purchase_server_description_2": "Supporter badge",
"purchase_server_title": "Server",
"purchase_settings_server_activated": "De productcode van de server wordt beheerd door de beheerder",
"range": "",
"raw": "",
"reaction_options": "Reactie opties",
@@ -1020,6 +1066,7 @@
"reset_people_visibility": "Zichtbaarheid mensen resetten",
"reset_settings_to_default": "",
"reset_to_default": "Resetten naar standaard",
"resolve_duplicates": "Duplicaten oplossen",
"resolved_all_duplicates": "Alle duplicaten verwerkt",
"restore": "Herstellen",
"restore_all": "Herstel alle",
@@ -1064,6 +1111,7 @@
"see_all_people": "Bekijk alle mensen",
"select_album_cover": "Selecteer album cover",
"select_all": "Alles selecteren",
"select_all_duplicates": "Selecteer alle duplicaten",
"select_avatar_color": "Selecteer avatarkleur",
"select_face": "Selecteer gezicht",
"select_featured_photo": "Selecteer uitgelichte foto",
@@ -1118,6 +1166,8 @@
"show_person_options": "Toon persoonopties",
"show_progress_bar": "Toon voortgangsbalk",
"show_search_options": "Zoekopties weergeven",
"show_supporter_badge": "Supporter badge",
"show_supporter_badge_description": "Toon een supporterbadge",
"shuffle": "Willekeurig",
"sign_out": "Uitloggen",
"sign_up": "Registreren",
@@ -1191,6 +1241,7 @@
"unnamed_share": "Naamloze deellink",
"unsaved_change": "Niet-opgeslagen wijziging",
"unselect_all": "Alles deselecteren",
"unselect_all_duplicates": "Deselecteer alle duplicaten",
"unstack": "Ontstapelen",
"unstacked_assets_count": "{count, plural, one {# asset} other {# assets}} ontstapeld",
"untracked_files": "Niet bijgehouden bestanden",
@@ -1214,6 +1265,8 @@
"user_license_settings": "Licentie",
"user_license_settings_description": "Beheer je licentie",
"user_liked": "{user} heeft {type, select, photo {deze foto} video {deze video} asset {deze asset} other {dit}} geliket",
"user_purchase_settings": "Kopen",
"user_purchase_settings_description": "Beheer je aankoop",
"user_role_set": "{user} instellen als {role}",
"user_usage_detail": "Gedetailleerd gebruik van gebruikers",
"username": "Gebruikersnaam",

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