1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

Merge branch 'main' into main

This commit is contained in:
Alex 2024-07-08 22:46:30 -05:00 committed by GitHub
commit 4bf2ded729
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
88 changed files with 4159 additions and 2972 deletions

203
cli/package-lock.json generated
View file

@ -10,6 +10,7 @@
"license": "GNU Affero General Public License version 3",
"dependencies": {
"fast-glob": "^3.3.2",
"fastq": "^1.17.1",
"lodash-es": "^4.17.21"
},
"bin": {
@ -34,11 +35,12 @@
"eslint-plugin-unicorn": "^54.0.0",
"mock-fs": "^5.2.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-organize-imports": "^4.0.0",
"typescript": "^5.3.3",
"vite": "^5.0.12",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.2.2",
"vitest-fetch-mock": "^0.2.2",
"yaml": "^2.3.1"
},
"engines": {
@ -1177,17 +1179,17 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.14.1.tgz",
"integrity": "sha512-aAJd6bIf2vvQRjUG3ZkNXkmBpN+J7Wd0mfQiiVCJMu9Z5GcZZdcc0j8XwN/BM97Fl7e3SkTXODSk4VehUv7CGw==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.15.0.tgz",
"integrity": "sha512-uiNHpyjZtFrLwLDpHnzaDlP3Tt6sGMqTCiqmxaN4n4RP0EfYZDODJyddiFDF44Hjwxr5xAcaYxVKm9QKQFJFLA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.14.1",
"@typescript-eslint/type-utils": "7.14.1",
"@typescript-eslint/utils": "7.14.1",
"@typescript-eslint/visitor-keys": "7.14.1",
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/type-utils": "7.15.0",
"@typescript-eslint/utils": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@ -1211,16 +1213,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.14.1.tgz",
"integrity": "sha512-8lKUOebNLcR0D7RvlcloOacTOWzOqemWEWkKSVpMZVF/XVcwjPR+3MD08QzbW9TCGJ+DwIc6zUSGZ9vd8cO1IA==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.15.0.tgz",
"integrity": "sha512-k9fYuQNnypLFcqORNClRykkGOMOj+pV6V91R4GO/l1FDGwpqmSwoOQrOHo3cGaH63e+D3ZiCAOsuS/D2c99j/A==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "7.14.1",
"@typescript-eslint/types": "7.14.1",
"@typescript-eslint/typescript-estree": "7.14.1",
"@typescript-eslint/visitor-keys": "7.14.1",
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/typescript-estree": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"debug": "^4.3.4"
},
"engines": {
@ -1240,14 +1242,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.14.1.tgz",
"integrity": "sha512-gPrFSsoYcsffYXTOZ+hT7fyJr95rdVe4kGVX1ps/dJ+DfmlnjFN/GcMxXcVkeHDKqsq6uAcVaQaIi3cFffmAbA==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.15.0.tgz",
"integrity": "sha512-Q/1yrF/XbxOTvttNVPihxh1b9fxamjEoz2Os/Pe38OHwxC24CyCqXxGTOdpb4lt6HYtqw9HetA/Rf6gDGaMPlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.14.1",
"@typescript-eslint/visitor-keys": "7.14.1"
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@ -1258,14 +1260,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.14.1.tgz",
"integrity": "sha512-/MzmgNd3nnbDbOi3LfasXWWe292+iuo+umJ0bCCMCPc1jLO/z2BQmWUUUXvXLbrQey/JgzdF/OV+I5bzEGwJkQ==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.15.0.tgz",
"integrity": "sha512-SkgriaeV6PDvpA6253PDVep0qCqgbO1IOBiycjnXsszNTVQe5flN5wR5jiczoEoDEnAqYFSFFc9al9BSGVltkg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "7.14.1",
"@typescript-eslint/utils": "7.14.1",
"@typescript-eslint/typescript-estree": "7.15.0",
"@typescript-eslint/utils": "7.15.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@ -1286,9 +1288,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.14.1.tgz",
"integrity": "sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.15.0.tgz",
"integrity": "sha512-aV1+B1+ySXbQH0pLK0rx66I3IkiZNidYobyfn0WFsdGhSXw+P3YOqeTq5GED458SfB24tg+ux3S+9g118hjlTw==",
"dev": true,
"license": "MIT",
"engines": {
@ -1300,14 +1302,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.14.1.tgz",
"integrity": "sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.15.0.tgz",
"integrity": "sha512-gjyB/rHAopL/XxfmYThQbXbzRMGhZzGw6KpcMbfe8Q3nNQKStpxnUKeXb0KiN/fFDR42Z43szs6rY7eHk0zdGQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "7.14.1",
"@typescript-eslint/visitor-keys": "7.14.1",
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@ -1329,16 +1331,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.14.1.tgz",
"integrity": "sha512-CMmVVELns3nak3cpJhZosDkm63n+DwBlDX8g0k4QUa9BMnF+lH2lr3d130M1Zt1xxmB3LLk3NV7KQCq86ZBBhQ==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.15.0.tgz",
"integrity": "sha512-hfDMDqaqOqsUVGiEPSMLR/AjTSCsmJwjpKkYQRo1FNbmW4tBwBspYDwO9eh7sKSTwMQgBw9/T4DHudPaqshRWA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.14.1",
"@typescript-eslint/types": "7.14.1",
"@typescript-eslint/typescript-estree": "7.14.1"
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/typescript-estree": "7.15.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@ -1352,13 +1354,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.14.1.tgz",
"integrity": "sha512-Crb+F75U1JAEtBeQGxSKwI60hZmmzaqA3z9sYsVm8X7W5cwLEm5bRe0/uXS6+MR/y8CVpKSR/ontIAIEPFcEkA==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.15.0.tgz",
"integrity": "sha512-Hqgy/ETgpt2L5xueA/zHHIl4fJI2O4XUE9l4+OIfbJIRSnTJb/QscncdqqZzofQegIJugRIF57OJea1khw2SDw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.14.1",
"@typescript-eslint/types": "7.15.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@ -1853,6 +1855,15 @@
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cross-fetch": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz",
"integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==",
"dev": true,
"dependencies": {
"node-fetch": "^2.6.12"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -3146,6 +3157,26 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dev": true,
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-releases": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
@ -3369,10 +3400,11 @@
}
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"dev": true
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
@ -3406,9 +3438,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"version": "8.4.39",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
"dev": true,
"funding": [
{
@ -3424,9 +3456,10 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
"picocolors": "^1.0.1",
"source-map-js": "^1.2.0"
},
"engines": {
@ -3471,21 +3504,22 @@
}
},
"node_modules/prettier-plugin-organize-imports": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz",
"integrity": "sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.0.0.tgz",
"integrity": "sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@volar/vue-language-plugin-pug": "^1.0.4",
"@volar/vue-typescript": "^1.0.4",
"@vue/language-plugin-pug": "^2.0.24",
"prettier": ">=2.0",
"typescript": ">=2.9"
"typescript": ">=2.9",
"vue-tsc": "^2.0.24"
},
"peerDependenciesMeta": {
"@volar/vue-language-plugin-pug": {
"@vue/language-plugin-pug": {
"optional": true
},
"@volar/vue-typescript": {
"vue-tsc": {
"optional": true
}
}
@ -4168,6 +4202,12 @@
"node": ">=8.0"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"dev": true
},
"node_modules/ts-api-utils": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
@ -4240,9 +4280,9 @@
}
},
"node_modules/typescript": {
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz",
"integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==",
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@ -4315,14 +4355,14 @@
}
},
"node_modules/vite": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.2.tgz",
"integrity": "sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==",
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz",
"integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.38",
"postcss": "^8.4.39",
"rollup": "^4.13.0"
},
"bin": {
@ -4476,6 +4516,37 @@
}
}
},
"node_modules/vitest-fetch-mock": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/vitest-fetch-mock/-/vitest-fetch-mock-0.2.2.tgz",
"integrity": "sha512-XmH6QgTSjCWrqXoPREIdbj40T7i1xnGmAsTAgfckoO75W1IEHKR8hcPCQ7SO16RsdW1t85oUm6pcQRLeBgjVYQ==",
"dev": true,
"dependencies": {
"cross-fetch": "^3.0.6"
},
"engines": {
"node": ">=14.14.0"
},
"peerDependencies": {
"vitest": ">=0.16.0"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"dev": true
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dev": true,
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View file

@ -31,11 +31,12 @@
"eslint-plugin-unicorn": "^54.0.0",
"mock-fs": "^5.2.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-organize-imports": "^4.0.0",
"typescript": "^5.3.3",
"vite": "^5.0.12",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.2.2",
"vitest-fetch-mock": "^0.2.2",
"yaml": "^2.3.1"
},
"scripts": {
@ -59,6 +60,7 @@
},
"dependencies": {
"fast-glob": "^3.3.2",
"fastq": "^1.17.1",
"lodash-es": "^4.17.21"
},
"volta": {

View file

@ -1,10 +1,18 @@
import { platform } from 'node:os';
import { UploadOptionsDto, getAlbumName } from 'src/commands/asset';
import { describe, expect, it } from 'vitest';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { describe, expect, it, vi } from 'vitest';
describe('Unit function tests', () => {
import { Action, checkBulkUpload, defaults, Reason } from '@immich/sdk';
import createFetchMock from 'vitest-fetch-mock';
import { checkForDuplicates, getAlbumName, uploadFiles, UploadOptionsDto } from './asset';
vi.mock('@immich/sdk');
describe('getAlbumName', () => {
it('should return a non-undefined value', () => {
if (platform() === 'win32') {
if (os.platform() === 'win32') {
// This is meaningless for Unix systems.
expect(getAlbumName(String.raw`D:\test\Filename.txt`, {} as UploadOptionsDto)).toBe('test');
}
@ -17,3 +25,177 @@ describe('Unit function tests', () => {
);
});
});
describe('uploadFiles', () => {
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-'));
const testFilePath = path.join(testDir, 'test.png');
const testFileData = 'test';
const baseUrl = 'http://example.com';
const apiKey = 'key';
const retry = 3;
const fetchMocker = createFetchMock(vi);
beforeEach(() => {
// Create a test file
fs.writeFileSync(testFilePath, testFileData);
// Defaults
vi.mocked(defaults).baseUrl = baseUrl;
vi.mocked(defaults).headers = { 'x-api-key': apiKey };
fetchMocker.enableMocks();
fetchMocker.resetMocks();
});
it('returns new assets when upload file is successful', async () => {
fetchMocker.doMockIf(new RegExp(`${baseUrl}/assets$`), () => {
return {
status: 200,
body: JSON.stringify({ id: 'fc5621b1-86f6-44a1-9905-403e607df9f5', status: 'created' }),
};
});
await expect(uploadFiles([testFilePath], { concurrency: 1 })).resolves.toEqual([
{
filepath: testFilePath,
id: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
},
]);
});
it('returns new assets when upload file retry is successful', async () => {
let counter = 0;
fetchMocker.doMockIf(new RegExp(`${baseUrl}/assets$`), () => {
counter++;
if (counter < retry) {
throw new Error('Network error');
}
return {
status: 200,
body: JSON.stringify({ id: 'fc5621b1-86f6-44a1-9905-403e607df9f5', status: 'created' }),
};
});
await expect(uploadFiles([testFilePath], { concurrency: 1 })).resolves.toEqual([
{
filepath: testFilePath,
id: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
},
]);
});
it('returns new assets when upload file retry is failed', async () => {
fetchMocker.doMockIf(new RegExp(`${baseUrl}/assets$`), () => {
throw new Error('Network error');
});
await expect(uploadFiles([testFilePath], { concurrency: 1 })).resolves.toEqual([]);
});
});
describe('checkForDuplicates', () => {
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-'));
const testFilePath = path.join(testDir, 'test.png');
const testFileData = 'test';
const testFileChecksum = 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'; // SHA1
const retry = 3;
beforeEach(() => {
// Create a test file
fs.writeFileSync(testFilePath, testFileData);
});
it('checks duplicates', async () => {
vi.mocked(checkBulkUpload).mockResolvedValue({
results: [
{
action: Action.Accept,
id: testFilePath,
},
],
});
await checkForDuplicates([testFilePath], { concurrency: 1 });
expect(checkBulkUpload).toHaveBeenCalledWith({
assetBulkUploadCheckDto: {
assets: [
{
checksum: testFileChecksum,
id: testFilePath,
},
],
},
});
});
it('returns duplicates when check duplicates is rejected', async () => {
vi.mocked(checkBulkUpload).mockResolvedValue({
results: [
{
action: Action.Reject,
id: testFilePath,
assetId: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
reason: Reason.Duplicate,
},
],
});
await expect(checkForDuplicates([testFilePath], { concurrency: 1 })).resolves.toEqual({
duplicates: [
{
filepath: testFilePath,
id: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
},
],
newFiles: [],
});
});
it('returns new assets when check duplicates is accepted', async () => {
vi.mocked(checkBulkUpload).mockResolvedValue({
results: [
{
action: Action.Accept,
id: testFilePath,
},
],
});
await expect(checkForDuplicates([testFilePath], { concurrency: 1 })).resolves.toEqual({
duplicates: [],
newFiles: [testFilePath],
});
});
it('returns results when check duplicates retry is successful', async () => {
let mocked = vi.mocked(checkBulkUpload);
for (let i = 1; i < retry; i++) {
mocked = mocked.mockRejectedValueOnce(new Error('Network error'));
}
mocked.mockResolvedValue({
results: [
{
action: Action.Accept,
id: testFilePath,
},
],
});
await expect(checkForDuplicates([testFilePath], { concurrency: 1 })).resolves.toEqual({
duplicates: [],
newFiles: [testFilePath],
});
});
it('returns results when check duplicates retry is failed', async () => {
vi.mocked(checkBulkUpload).mockRejectedValue(new Error('Network error'));
await expect(checkForDuplicates([testFilePath], { concurrency: 1 })).resolves.toEqual({
duplicates: [],
newFiles: [],
});
});
});

View file

@ -16,6 +16,7 @@ import { chunk } from 'lodash-es';
import { Stats, createReadStream } from 'node:fs';
import { stat, unlink } from 'node:fs/promises';
import path, { basename } from 'node:path';
import { Queue } from 'src/queue';
import { BaseOptions, authenticate, crawl, sha1 } from 'src/utils';
const s = (count: number) => (count === 1 ? '' : 's');
@ -83,7 +84,7 @@ const scan = async (pathsToCrawl: string[], options: UploadOptionsDto) => {
return files;
};
const checkForDuplicates = async (files: string[], { concurrency, skipHash }: UploadOptionsDto) => {
export const checkForDuplicates = async (files: string[], { concurrency, skipHash }: UploadOptionsDto) => {
if (skipHash) {
console.log('Skipping hash check, assuming all files are new');
return { newFiles: files, duplicates: [] };
@ -99,32 +100,50 @@ const checkForDuplicates = async (files: string[], { concurrency, skipHash }: Up
const newFiles: string[] = [];
const duplicates: Asset[] = [];
try {
// TODO refactor into a queue
for (const items of chunk(files, concurrency)) {
const dto = await Promise.all(items.map(async (filepath) => ({ id: filepath, checksum: await sha1(filepath) })));
const { results } = await checkBulkUpload({ assetBulkUploadCheckDto: { assets: dto } });
for (const { id: filepath, assetId, action } of results as AssetBulkUploadCheckResults) {
const queue = new Queue<string[], AssetBulkUploadCheckResults>(
async (filepaths: string[]) => {
const dto = await Promise.all(
filepaths.map(async (filepath) => ({ id: filepath, checksum: await sha1(filepath) })),
);
const response = await checkBulkUpload({ assetBulkUploadCheckDto: { assets: dto } });
const results = response.results as AssetBulkUploadCheckResults;
for (const { id: filepath, assetId, action } of results) {
if (action === Action.Accept) {
newFiles.push(filepath);
} else {
// rejects are always duplicates
duplicates.push({ id: assetId as string, filepath });
}
progressBar.increment();
}
}
} finally {
progressBar.stop();
progressBar.increment(filepaths.length);
return results;
},
{ concurrency, retry: 3 },
);
for (const items of chunk(files, concurrency)) {
await queue.push(items);
}
await queue.drained();
progressBar.stop();
console.log(`Found ${newFiles.length} new files and ${duplicates.length} duplicate${s(duplicates.length)}`);
// Report failures
const failedTasks = queue.tasks.filter((task) => task.status === 'failed');
if (failedTasks.length > 0) {
console.log(`Failed to verify ${failedTasks.length} file${s(failedTasks.length)}:`);
for (const task of failedTasks) {
console.log(`- ${task.data} - ${task.error}`);
}
}
return { newFiles, duplicates };
};
const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptionsDto): Promise<Asset[]> => {
export const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptionsDto): Promise<Asset[]> => {
if (files.length === 0) {
console.log('All assets were already uploaded, nothing to do.');
return [];
@ -158,15 +177,15 @@ const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptio
const newAssets: Asset[] = [];
try {
for (const items of chunk(files, concurrency)) {
await Promise.all(
items.map(async (filepath) => {
const stats = statsMap.get(filepath) as Stats;
const queue = new Queue<string, AssetMediaResponseDto>(
async (filepath: string) => {
const stats = statsMap.get(filepath);
if (!stats) {
throw new Error(`Stats not found for ${filepath}`);
}
const response = await uploadFile(filepath, stats);
newAssets.push({ id: response.id, filepath });
if (response.status === AssetMediaStatus.Duplicate) {
duplicateCount++;
duplicateSize += stats.size ?? 0;
@ -178,17 +197,32 @@ const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptio
uploadProgress.update(successSize, { value_formatted: byteSize(successSize + duplicateSize) });
return response;
}),
},
{ concurrency, retry: 3 },
);
for (const filepath of files) {
await queue.push(filepath);
}
} finally {
await queue.drained();
uploadProgress.stop();
}
console.log(`Successfully uploaded ${successCount} new asset${s(successCount)} (${byteSize(successSize)})`);
if (duplicateCount > 0) {
console.log(`Skipped ${duplicateCount} duplicate asset${s(duplicateCount)} (${byteSize(duplicateSize)})`);
}
// Report failures
const failedTasks = queue.tasks.filter((task) => task.status === 'failed');
if (failedTasks.length > 0) {
console.log(`Failed to upload ${failedTasks.length} asset${s(failedTasks.length)}:`);
for (const task of failedTasks) {
console.log(`- ${task.data} - ${task.error}`);
}
}
return newAssets;
};

131
cli/src/queue.ts Normal file
View file

@ -0,0 +1,131 @@
import * as fastq from 'fastq';
import { uniqueId } from 'lodash-es';
export type Task<T, R> = {
readonly id: string;
status: 'idle' | 'processing' | 'succeeded' | 'failed';
data: T;
error: unknown | undefined;
count: number;
// TODO: Could be useful to adding progress property.
// TODO: Could be useful to adding start_at/end_at/duration properties.
result: undefined | R;
};
export type QueueOptions = {
verbose?: boolean;
concurrency?: number;
retry?: number;
// TODO: Could be useful to adding timeout property for retry.
};
export type ComputedQueueOptions = Required<QueueOptions>;
export const defaultQueueOptions = {
concurrency: 1,
retry: 0,
verbose: false,
};
/**
* An in-memory queue that processes tasks in parallel with a given concurrency.
* @see {@link https://www.npmjs.com/package/fastq}
* @template T - The type of the worker task data.
* @template R - The type of the worker output data.
*/
export class Queue<T, R> {
private readonly queue: fastq.queueAsPromised<string, Task<T, R>>;
private readonly store = new Map<string, Task<T, R>>();
readonly options: ComputedQueueOptions;
readonly worker: (data: T) => Promise<R>;
/**
* Create a new queue.
* @param worker - The worker function that processes the task.
* @param options - The queue options.
*/
constructor(worker: (data: T) => Promise<R>, options?: QueueOptions) {
this.options = { ...defaultQueueOptions, ...options };
this.worker = worker;
this.store = new Map<string, Task<T, R>>();
this.queue = this.buildQueue();
}
get tasks(): Task<T, R>[] {
const tasks: Task<T, R>[] = [];
for (const task of this.store.values()) {
tasks.push(task);
}
return tasks;
}
getTask(id: string): Task<T, R> {
const task = this.store.get(id);
if (!task) {
throw new Error(`Task with id ${id} not found`);
}
return task;
}
/**
* Wait for the queue to be empty.
* @returns Promise<void> - The returned Promise will be resolved when all tasks in the queue have been processed by a worker.
* This promise could be ignored as it will not lead to a `unhandledRejection`.
*/
async drained(): Promise<void> {
await this.queue.drain();
}
/**
* Add a task at the end of the queue.
* @see {@link https://www.npmjs.com/package/fastq}
* @param data
* @returns Promise<void> - A Promise that will be fulfilled (rejected) when the task is completed successfully (unsuccessfully).
* This promise could be ignored as it will not lead to a `unhandledRejection`.
*/
async push(data: T): Promise<Task<T, R>> {
const id = uniqueId();
const task: Task<T, R> = { id, status: 'idle', error: undefined, count: 0, data, result: undefined };
this.store.set(id, task);
return this.queue.push(id);
}
// TODO: Support more function delegation to fastq.
private buildQueue(): fastq.queueAsPromised<string, Task<T, R>> {
return fastq.promise((id: string) => {
const task = this.getTask(id);
return this.work(task);
}, this.options.concurrency);
}
private async work(task: Task<T, R>): Promise<Task<T, R>> {
task.count += 1;
task.error = undefined;
task.status = 'processing';
if (this.options.verbose) {
console.log('[task] processing:', task);
}
try {
task.result = await this.worker(task.data);
task.status = 'succeeded';
if (this.options.verbose) {
console.log('[task] succeeded:', task);
}
return task;
} catch (error) {
task.error = error;
task.status = 'failed';
if (this.options.verbose) {
console.log('[task] failed:', task);
}
if (this.options.retry > 0 && task.count < this.options.retry) {
if (this.options.verbose) {
console.log('[task] retry:', task);
}
return this.work(task);
}
return task;
}
}
}

View file

@ -45,8 +45,6 @@ Regardless of filesystem, it is not recommended to use a network share for your
| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server | api, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server | api |
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | server | microservices |
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
| `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | |
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |

22
docs/package-lock.json generated
View file

@ -12640,9 +12640,10 @@
}
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
@ -12753,9 +12754,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"version": "8.4.39",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
"funding": [
{
"type": "opencollective",
@ -12770,9 +12771,10 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
"picocolors": "^1.0.1",
"source-map-js": "^1.2.0"
},
"engines": {
@ -16374,9 +16376,9 @@
}
},
"node_modules/typescript": {
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz",
"integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==",
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",

View file

@ -0,0 +1,77 @@
import { mdiCalendarToday, mdiLeadPencil, mdiLockOutline, mdiSpeedometerSlow, mdiWeb } from '@mdi/js';
import Layout from '@theme/Layout';
import React from 'react';
import { Item as TimelineItem, Timeline } from '../components/timeline';
const withLanguage = (date: Date) => (language: string) => date.toLocaleDateString(language);
type Item = Omit<TimelineItem, 'done' | 'getDateLabel'> & { date: Date };
const items: Item[] = [
{
icon: mdiLeadPencil,
iconColor: 'gold',
title: 'PostgreSQL NOTIFY is cursed',
description:
'PostgreSQL does everything in a transaction, including NOTIFY. This means using the socket.io postgres-adapter writes to WAL every 5 seconds.',
link: { url: 'https://github.com/immich-app/immich/pull/10801', text: '#10801' },
date: new Date(2024, 6, 3),
},
{
icon: mdiWeb,
iconColor: 'lightskyblue',
title: 'npm scripts are cursed',
description:
'npm scripts make a http call to the npm registry each time they run, which means they are a terrible way to execute a health check.',
link: { url: 'https://github.com/immich-app/immich/issues/10796', text: '#10796' },
date: new Date(2024, 6, 3),
},
{
icon: mdiSpeedometerSlow,
iconColor: 'brown',
title: '50 extra packages are cursed',
description:
'There is a user in the JavaScript community who goes around adding "backwards compatibility" to projects. They do this by adding 50 extra package dependencies to your project, which are maintained by them.',
link: { url: 'https://github.com/immich-app/immich/pull/10690', text: '#10690' },
date: new Date(2024, 5, 28),
},
{
icon: mdiLockOutline,
iconColor: 'gold',
title: 'Long passwords are cursed',
description:
'The bcrypt implementation only uses the first 72 bytes of a string. Any characters after that are ignored.',
// link: GHSA-4p64-9f7h-3432
date: new Date(2024, 5, 25),
},
{
icon: mdiCalendarToday,
iconColor: 'greenyellow',
title: 'JavaScript Date objects are cursed',
description: 'JavaScript date objects are 1 indexed for years and days, but 0 indexed for months.',
link: { url: 'https://github.com/immich-app/immich/pulls/6787', text: '#6787' },
date: new Date(2024, 0, 31),
},
];
export default function CursedKnowledgePage(): JSX.Element {
return (
<Layout title="Cursed Knowledge" description="Things we wish we didn't know">
<section className="my-8">
<h1 className="md:text-6xl text-center mb-10 text-immich-primary dark:text-immich-dark-primary px-2">
Cursed Knowledge
</h1>
<p className="text-center text-xl px-2">
Cursed knowledge we have learned as a result of building Immich that we wish we never knew.
</p>
<div className="flex justify-around mt-8 w-full max-w-full">
<Timeline
items={items
.sort((a, b) => b.date.getTime() - a.date.getTime())
.map((item) => ({ ...item, getDateLabel: withLanguage(item.date) }))}
/>
</div>
</section>
</Layout>
);
}

133
e2e/package-lock.json generated
View file

@ -29,7 +29,7 @@
"pg": "^8.11.3",
"pngjs": "^7.0.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-organize-imports": "^4.0.0",
"socket.io-client": "^4.7.4",
"supertest": "^7.0.0",
"typescript": "^5.3.3",
@ -68,7 +68,7 @@
"eslint-plugin-unicorn": "^54.0.0",
"mock-fs": "^5.2.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-organize-imports": "^4.0.0",
"typescript": "^5.3.3",
"vite": "^5.0.12",
"vite-tsconfig-paths": "^4.3.2",
@ -971,13 +971,13 @@
}
},
"node_modules/@playwright/test": {
"version": "1.45.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.0.tgz",
"integrity": "sha512-TVYsfMlGAaxeUllNkywbwek67Ncf8FRGn8ZlRdO291OL3NjG9oMbfVhyP82HQF0CZLMrYsvesqoUekxdWuF9Qw==",
"version": "1.45.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.1.tgz",
"integrity": "sha512-Wo1bWTzQvGA7LyKGIZc8nFSTFf2TkthGIFBR+QVNilvwouGzFd4PYukZe3rvf5PSqjHi1+1NyKSDZKcQWETzaA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.45.0"
"playwright": "1.45.1"
},
"bin": {
"playwright": "cli.js"
@ -1345,17 +1345,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.14.1.tgz",
"integrity": "sha512-aAJd6bIf2vvQRjUG3ZkNXkmBpN+J7Wd0mfQiiVCJMu9Z5GcZZdcc0j8XwN/BM97Fl7e3SkTXODSk4VehUv7CGw==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.15.0.tgz",
"integrity": "sha512-uiNHpyjZtFrLwLDpHnzaDlP3Tt6sGMqTCiqmxaN4n4RP0EfYZDODJyddiFDF44Hjwxr5xAcaYxVKm9QKQFJFLA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.14.1",
"@typescript-eslint/type-utils": "7.14.1",
"@typescript-eslint/utils": "7.14.1",
"@typescript-eslint/visitor-keys": "7.14.1",
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/type-utils": "7.15.0",
"@typescript-eslint/utils": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@ -1379,16 +1379,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.14.1.tgz",
"integrity": "sha512-8lKUOebNLcR0D7RvlcloOacTOWzOqemWEWkKSVpMZVF/XVcwjPR+3MD08QzbW9TCGJ+DwIc6zUSGZ9vd8cO1IA==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.15.0.tgz",
"integrity": "sha512-k9fYuQNnypLFcqORNClRykkGOMOj+pV6V91R4GO/l1FDGwpqmSwoOQrOHo3cGaH63e+D3ZiCAOsuS/D2c99j/A==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "7.14.1",
"@typescript-eslint/types": "7.14.1",
"@typescript-eslint/typescript-estree": "7.14.1",
"@typescript-eslint/visitor-keys": "7.14.1",
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/typescript-estree": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"debug": "^4.3.4"
},
"engines": {
@ -1408,14 +1408,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.14.1.tgz",
"integrity": "sha512-gPrFSsoYcsffYXTOZ+hT7fyJr95rdVe4kGVX1ps/dJ+DfmlnjFN/GcMxXcVkeHDKqsq6uAcVaQaIi3cFffmAbA==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.15.0.tgz",
"integrity": "sha512-Q/1yrF/XbxOTvttNVPihxh1b9fxamjEoz2Os/Pe38OHwxC24CyCqXxGTOdpb4lt6HYtqw9HetA/Rf6gDGaMPlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.14.1",
"@typescript-eslint/visitor-keys": "7.14.1"
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@ -1426,14 +1426,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.14.1.tgz",
"integrity": "sha512-/MzmgNd3nnbDbOi3LfasXWWe292+iuo+umJ0bCCMCPc1jLO/z2BQmWUUUXvXLbrQey/JgzdF/OV+I5bzEGwJkQ==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.15.0.tgz",
"integrity": "sha512-SkgriaeV6PDvpA6253PDVep0qCqgbO1IOBiycjnXsszNTVQe5flN5wR5jiczoEoDEnAqYFSFFc9al9BSGVltkg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "7.14.1",
"@typescript-eslint/utils": "7.14.1",
"@typescript-eslint/typescript-estree": "7.15.0",
"@typescript-eslint/utils": "7.15.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@ -1454,9 +1454,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.14.1.tgz",
"integrity": "sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.15.0.tgz",
"integrity": "sha512-aV1+B1+ySXbQH0pLK0rx66I3IkiZNidYobyfn0WFsdGhSXw+P3YOqeTq5GED458SfB24tg+ux3S+9g118hjlTw==",
"dev": true,
"license": "MIT",
"engines": {
@ -1468,14 +1468,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.14.1.tgz",
"integrity": "sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.15.0.tgz",
"integrity": "sha512-gjyB/rHAopL/XxfmYThQbXbzRMGhZzGw6KpcMbfe8Q3nNQKStpxnUKeXb0KiN/fFDR42Z43szs6rY7eHk0zdGQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "7.14.1",
"@typescript-eslint/visitor-keys": "7.14.1",
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@ -1523,16 +1523,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.14.1.tgz",
"integrity": "sha512-CMmVVELns3nak3cpJhZosDkm63n+DwBlDX8g0k4QUa9BMnF+lH2lr3d130M1Zt1xxmB3LLk3NV7KQCq86ZBBhQ==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.15.0.tgz",
"integrity": "sha512-hfDMDqaqOqsUVGiEPSMLR/AjTSCsmJwjpKkYQRo1FNbmW4tBwBspYDwO9eh7sKSTwMQgBw9/T4DHudPaqshRWA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.14.1",
"@typescript-eslint/types": "7.14.1",
"@typescript-eslint/typescript-estree": "7.14.1"
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/typescript-estree": "7.15.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@ -1546,13 +1546,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.14.1.tgz",
"integrity": "sha512-Crb+F75U1JAEtBeQGxSKwI60hZmmzaqA3z9sYsVm8X7W5cwLEm5bRe0/uXS6+MR/y8CVpKSR/ontIAIEPFcEkA==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.15.0.tgz",
"integrity": "sha512-Hqgy/ETgpt2L5xueA/zHHIl4fJI2O4XUE9l4+OIfbJIRSnTJb/QscncdqqZzofQegIJugRIF57OJea1khw2SDw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.14.1",
"@typescript-eslint/types": "7.15.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@ -4263,13 +4263,13 @@
}
},
"node_modules/playwright": {
"version": "1.45.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.0.tgz",
"integrity": "sha512-4z3ac3plDfYzGB6r0Q3LF8POPR20Z8D0aXcxbJvmfMgSSq1hkcgvFRXJk9rUq5H/MJ0Ktal869hhOdI/zUTeLA==",
"version": "1.45.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.1.tgz",
"integrity": "sha512-Hjrgae4kpSQBr98nhCj3IScxVeVUixqj+5oyif8TdIn2opTCPEzqAqNMeK42i3cWDCVu9MI+ZsGWw+gVR4ISBg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.45.0"
"playwright-core": "1.45.1"
},
"bin": {
"playwright": "cli.js"
@ -4282,9 +4282,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.45.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.0.tgz",
"integrity": "sha512-lZmHlFQ0VYSpAs43dRq1/nJ9G/6SiTI7VPqidld9TDefL9tX87bTKExWZZUF5PeRyqtXqd8fQi2qmfIedkwsNQ==",
"version": "1.45.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.1.tgz",
"integrity": "sha512-LF4CUUtrUu2TCpDw4mcrAIuYrEjVDfT1cHbJMfwnE2+1b8PZcFzPNgvZCvq2JfQ4aTjRCCHw5EJ2tmr2NSzdPg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@ -4423,21 +4423,22 @@
}
},
"node_modules/prettier-plugin-organize-imports": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz",
"integrity": "sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.0.0.tgz",
"integrity": "sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@volar/vue-language-plugin-pug": "^1.0.4",
"@volar/vue-typescript": "^1.0.4",
"@vue/language-plugin-pug": "^2.0.24",
"prettier": ">=2.0",
"typescript": ">=2.9"
"typescript": ">=2.9",
"vue-tsc": "^2.0.24"
},
"peerDependenciesMeta": {
"@volar/vue-language-plugin-pug": {
"@vue/language-plugin-pug": {
"optional": true
},
"@volar/vue-typescript": {
"vue-tsc": {
"optional": true
}
}
@ -5271,9 +5272,9 @@
}
},
"node_modules/typescript": {
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz",
"integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==",
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {

View file

@ -39,7 +39,7 @@
"pg": "^8.11.3",
"pngjs": "^7.0.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-organize-imports": "^4.0.0",
"socket.io-client": "^4.7.4",
"supertest": "^7.0.0",
"typescript": "^5.3.3",

View file

@ -9,11 +9,30 @@ describe(`immich-admin`, () => {
describe('list-users', () => {
it('should list the admin user', async () => {
const { stdout, stderr, exitCode } = await immichAdmin(['list-users']);
const { stdout, stderr, exitCode } = await immichAdmin(['list-users']).promise;
expect(exitCode).toBe(0);
expect(stderr).toBe('');
expect(stdout).toContain("email: 'admin@immich.cloud'");
expect(stdout).toContain("name: 'Immich Admin'");
});
});
describe('reset-admin-password', () => {
it('should reset admin password', async () => {
const { child, promise } = immichAdmin(['reset-admin-password']);
let data = '';
child.stdout.on('data', (chunk) => {
data += chunk;
if (data.includes('Please choose a new password (optional)')) {
child.stdin.end('\n');
}
});
const { stderr, stdout, exitCode } = await promise;
expect(exitCode).toBe(0);
expect(stderr).toBe('');
expect(stdout).toContain('The admin password has been updated to:');
});
});
});

View file

@ -64,13 +64,13 @@ export const tempDir = tmpdir();
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
export const immichCli = (args: string[]) =>
executeCommand('node', ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args]);
executeCommand('node', ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args]).promise;
export const immichAdmin = (args: string[]) =>
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
const executeCommand = (command: string, args: string[]) => {
let _resolve: (value: CommandResponse) => void;
const deferred = new Promise<CommandResponse>((resolve) => (_resolve = resolve));
const promise = new Promise<CommandResponse>((resolve) => (_resolve = resolve));
const child = spawn(command, args, { stdio: 'pipe' });
let stdout = '';
@ -86,7 +86,7 @@ const executeCommand = (command: string, args: string[]) => {
});
});
return deferred;
return { promise, child };
};
let client: pg.Client | null = null;

View file

@ -14,17 +14,6 @@ final partnerServiceProvider = Provider(
),
);
enum PartnerDirection {
sharedWith("shared-with"),
sharedBy("shared-by");
const PartnerDirection(
this._value,
);
final String _value;
}
class PartnerService {
final ApiService _apiService;
final Isar _db;
@ -34,8 +23,7 @@ class PartnerService {
Future<List<User>?> getPartners(PartnerDirection direction) async {
try {
final userDtos =
await _apiService.partnersApi.getPartners(direction._value);
final userDtos = await _apiService.partnersApi.getPartners(direction);
if (userDtos != null) {
return userDtos.map((u) => User.fromPartnerDto(u)).toList();
}

View file

@ -73,9 +73,9 @@ class UserService {
Future<List<User>?> getUsersFromServer() async {
final List<User>? users = await _getAllUsers();
final List<User>? sharedBy =
await _partnerService.getPartners(PartnerDirection.sharedBy);
await _partnerService.getPartners(PartnerDirection.by);
final List<User>? sharedWith =
await _partnerService.getPartners(PartnerDirection.sharedWith);
await _partnerService.getPartners(PartnerDirection.with_);
if (users == null || sharedBy == null || sharedWith == null) {
_log.warning("Failed to refresh users");

BIN
mobile/openapi/README.md generated

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -3660,11 +3660,7 @@
"required": true,
"in": "query",
"schema": {
"enum": [
"shared-by",
"shared-with"
],
"type": "string"
"$ref": "#/components/schemas/PartnerDirection"
}
}
],
@ -9473,6 +9469,13 @@
],
"type": "object"
},
"PartnerDirection": {
"enum": [
"shared-by",
"shared-with"
],
"type": "string"
},
"PartnerResponseDto": {
"properties": {
"avatarColor": {

View file

@ -32,9 +32,9 @@
}
},
"node_modules/typescript": {
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz",
"integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==",
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {

View file

@ -2128,7 +2128,7 @@ export function unlinkOAuthAccount(opts?: Oazapfts.RequestOpts) {
}));
}
export function getPartners({ direction }: {
direction: "shared-by" | "shared-with";
direction: PartnerDirection;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
@ -3131,6 +3131,10 @@ export enum Type2 {
export enum MemoryType {
OnThisDay = "on_this_day"
}
export enum PartnerDirection {
SharedBy = "shared-by",
SharedWith = "shared-with"
}
export enum PathEntityType {
Asset = "asset",
Person = "person",

View file

@ -1,5 +1,5 @@
# dev build
FROM ghcr.io/immich-app/base-server-dev:20240702@sha256:5d675b67826ac643ee64ecf2ef78adac1e491eef9a845f30818a1c0d1338ecc8 as dev
FROM ghcr.io/immich-app/base-server-dev:20240708@sha256:2a9e3231c34493cb861299d475c84c031e7f04519dbc895bbebb5017d479a3cb as dev
RUN apt-get install --no-install-recommends -yqq tini
WORKDIR /usr/src/app
@ -41,7 +41,7 @@ RUN npm run build
# prod build
FROM ghcr.io/immich-app/base-server-prod:20240702@sha256:419a873052cf2f012ed1977e4a771a38e68ce64ea1c66047cc06232b1a79bafe
FROM ghcr.io/immich-app/base-server-prod:20240708@sha256:0af3a5bb036c9a4b6a5a51becaa6e94fe182f6bc97480d57e8f2e6f994bfa453
WORKDIR /usr/src/app
ENV NODE_ENV=production \
@ -50,7 +50,7 @@ ENV NODE_ENV=production \
COPY --from=prod /usr/src/app/node_modules ./node_modules
COPY --from=prod /usr/src/app/dist ./dist
COPY --from=prod /usr/src/app/bin ./bin
COPY --from=web /usr/src/app/build ./www
COPY --from=web /usr/src/app/build /build/www
COPY server/resources resources
COPY server/package.json server/package-lock.json ./
COPY server/start*.sh ./

3064
server/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -50,7 +50,7 @@
"@opentelemetry/context-async-hooks": "^1.24.0",
"@opentelemetry/exporter-prometheus": "^0.52.0",
"@opentelemetry/sdk-node": "^0.52.0",
"@react-email/components": "^0.0.19",
"@react-email/components": "^0.0.21",
"@socket.io/redis-adapter": "^8.3.0",
"archiver": "^7.0.0",
"async-lock": "^1.4.0",
@ -121,7 +121,7 @@
"eslint-plugin-unicorn": "^54.0.0",
"mock-fs": "^5.2.0",
"prettier": "^3.0.2",
"prettier-plugin-organize-imports": "^3.2.3",
"prettier-plugin-organize-imports": "^4.0.0",
"rimraf": "^5.0.1",
"source-map-support": "^0.5.21",
"sql-formatter": "^15.0.0",

View file

@ -27,13 +27,28 @@ export const WEB_ROOT = process.env.IMMICH_WEB_ROOT || '/usr/src/app/www';
const HOST_SERVER_PORT = process.env.IMMICH_PORT || '2283';
export const DEFAULT_EXTERNAL_DOMAIN = 'http://localhost:' + HOST_SERVER_PORT;
const GEODATA_ROOT_PATH = process.env.IMMICH_REVERSE_GEOCODING_ROOT || '/usr/src/resources';
export const citiesFile = 'cities500.txt';
export const geodataDatePath = join(GEODATA_ROOT_PATH, 'geodata-date.txt');
export const geodataAdmin1Path = join(GEODATA_ROOT_PATH, 'admin1CodesASCII.txt');
export const geodataAdmin2Path = join(GEODATA_ROOT_PATH, 'admin2Codes.txt');
export const geodataCities500Path = join(GEODATA_ROOT_PATH, citiesFile);
const buildFolder = process.env.IMMICH_BUILD_DATA || '/build';
const folders = {
geodata: join(buildFolder, 'geodata'),
web: join(buildFolder, 'www'),
};
export const resourcePaths = {
lockFile: join(buildFolder, 'build-lock.json'),
geodata: {
dateFile: join(folders.geodata, 'geodata-date.txt'),
admin1: join(folders.geodata, 'admin1CodesASCII.txt'),
admin2: join(folders.geodata, 'admin2Codes.txt'),
cities500: join(folders.geodata, citiesFile),
},
web: {
root: folders.web,
indexHtml: join(folders.web, 'index.html'),
},
};
export const MOBILE_REDIRECT = 'app.immich:/';
export const LOGIN_URL = '/auth/login?autoLaunch=0';

View file

@ -1,7 +1,7 @@
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';
import { ApiQuery, ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto';
import { PartnerResponseDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
import { PartnerDirection } from 'src/interfaces/partner.interface';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { PartnerService } from 'src/services/partner.service';
@ -16,8 +16,8 @@ export class PartnerController {
@ApiQuery({ name: 'direction', type: 'string', enum: PartnerDirection, required: true })
@Authenticated()
// TODO: remove 'direction' and convert to full query dto
getPartners(@Auth() auth: AuthDto, @Query('direction') direction: PartnerDirection): Promise<PartnerResponseDto[]> {
return this.service.getAll(auth, direction);
getPartners(@Auth() auth: AuthDto, @Query() dto: PartnerSearchDto): Promise<PartnerResponseDto[]> {
return this.service.search(auth, dto);
}
@Post(':id')

View file

@ -1,11 +1,19 @@
import { IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty } from 'class-validator';
import { UserResponseDto } from 'src/dtos/user.dto';
import { PartnerDirection } from 'src/interfaces/partner.interface';
export class UpdatePartnerDto {
@IsNotEmpty()
inTimeline!: boolean;
}
export class PartnerSearchDto {
@IsEnum(PartnerDirection)
@ApiProperty({ enum: PartnerDirection, enumName: 'PartnerDirection' })
direction!: PartnerDirection;
}
export class PartnerResponseDto extends UserResponseDto {
inTimeline?: boolean;
}

View file

@ -140,7 +140,7 @@ export class UserAdminResponseDto extends UserResponseDto {
}
export function mapUserAdmin(entity: UserEntity): UserAdminResponseDto {
const license = entity.metadata.find(
const license = entity.metadata?.find(
(item): item is UserMetadataEntity<UserMetadataKey.LICENSE> => item.key === UserMetadataKey.LICENSE,
)?.value;
return {

View file

@ -4,7 +4,7 @@ import { getName } from 'i18n-iso-countries';
import { createReadStream, existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import readLine from 'node:readline';
import { citiesFile, geodataAdmin1Path, geodataAdmin2Path, geodataCities500Path, geodataDatePath } from 'src/constants';
import { citiesFile, resourcePaths } from 'src/constants';
import { AssetEntity } from 'src/entities/asset.entity';
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
@ -37,7 +37,7 @@ export class MapRepository implements IMapRepository {
async init(): Promise<void> {
this.logger.log('Initializing metadata repository');
const geodataDate = await readFile(geodataDatePath, 'utf8');
const geodataDate = await readFile(resourcePaths.geodata.dateFile, 'utf8');
// TODO move to service init
const geocodingMetadata = await this.metadataRepository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE);
@ -150,8 +150,8 @@ export class MapRepository implements IMapRepository {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
const admin1 = await this.loadAdmin(geodataAdmin1Path);
const admin2 = await this.loadAdmin(geodataAdmin2Path);
const admin1 = await this.loadAdmin(resourcePaths.geodata.admin1);
const admin2 = await this.loadAdmin(resourcePaths.geodata.admin2);
try {
await queryRunner.startTransaction();
@ -221,7 +221,7 @@ export class MapRepository implements IMapRepository {
admin1Name: admin1Map.get(`${lineSplit[8]}.${lineSplit[10]}`),
admin2Name: admin2Map.get(`${lineSplit[8]}.${lineSplit[10]}.${lineSplit[11]}`),
}),
geodataCities500Path,
resourcePaths.geodata.cities500,
{ entityFilter: (lineSplit) => lineSplit[7] != 'PPLX' },
);
}

View file

@ -4,6 +4,7 @@ import { exec as execCallback } from 'node:child_process';
import { readFile } from 'node:fs/promises';
import { promisify } from 'node:util';
import sharp from 'sharp';
import { resourcePaths } from 'src/constants';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { GitHubRelease, IServerInfoRepository, ServerBuildVersions } from 'src/interfaces/server-info.interface';
import { Instrumentation } from 'src/utils/instrumentation';
@ -61,9 +62,9 @@ export class ServerInfoRepository implements IServerInfoRepository {
maybeFirstLine('convert --version'),
]);
const lockfile = await readFile('build-lock.json')
const lockfile = await readFile(resourcePaths.lockFile)
.then((buffer) => JSON.parse(buffer.toString()))
.catch(() => this.logger.warn('Failed to read build-lock.json'));
.catch(() => this.logger.warn(`Failed to read ${resourcePaths.lockFile}`));
return {
nodejs: nodejsOutput || process.env.NODE_VERSION || '',

View file

@ -202,7 +202,7 @@ describe(StorageRepository.name, () => {
.filter((entry) => entry[1])
.map(([file]) => file);
expect(actual.sort()).toEqual(expected.sort());
expect(actual.toSorted()).toEqual(expected.toSorted());
});
}
});

View file

@ -2,8 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { Cron, CronExpression, Interval } from '@nestjs/schedule';
import { NextFunction, Request, Response } from 'express';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { ONE_HOUR, WEB_ROOT } from 'src/constants';
import { ONE_HOUR, resourcePaths } from 'src/constants';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { AuthService } from 'src/services/auth.service';
import { JobService } from 'src/services/job.service';
@ -56,9 +55,9 @@ export class ApiService {
ssr(excludePaths: string[]) {
let index = '';
try {
index = readFileSync(join(WEB_ROOT, 'index.html')).toString();
index = readFileSync(resourcePaths.web.indexHtml).toString();
} catch {
this.logger.warn('Unable to open `www/index.html, skipping SSR.');
this.logger.warn(`Unable to open ${resourcePaths.web.indexHtml}, skipping SSR.`);
}
return async (request: Request, res: Response, next: NextFunction) => {

View file

@ -255,13 +255,11 @@ describe(AssetMediaService.name, () => {
}
it('should be sorted (valid)', () => {
// TODO: use toSorted in NodeJS 20.
expect(valid).toEqual([...valid].sort());
expect(valid).toEqual(valid.toSorted());
});
it('should be sorted (invalid)', () => {
// TODO: use toSorted in NodeJS 20.
expect(invalid).toEqual([...invalid].sort());
expect(invalid).toEqual(invalid.toSorted());
});
});
}

View file

@ -21,16 +21,16 @@ describe(PartnerService.name, () => {
expect(sut).toBeDefined();
});
describe('getAll', () => {
describe('search', () => {
it("should return a list of partners with whom I've shared my library", async () => {
partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]);
await expect(sut.getAll(authStub.user1, PartnerDirection.SharedBy)).resolves.toBeDefined();
await expect(sut.search(authStub.user1, { direction: PartnerDirection.SharedBy })).resolves.toBeDefined();
expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
});
it('should return a list of partners who have shared their libraries with me', async () => {
partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]);
await expect(sut.getAll(authStub.user1, PartnerDirection.SharedWith)).resolves.toBeDefined();
await expect(sut.search(authStub.user1, { direction: PartnerDirection.SharedWith })).resolves.toBeDefined();
expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
});
});

View file

@ -1,7 +1,7 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AccessCore, Permission } from 'src/cores/access.core';
import { AuthDto } from 'src/dtos/auth.dto';
import { PartnerResponseDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
import { mapUser } from 'src/dtos/user.dto';
import { PartnerEntity } from 'src/entities/partner.entity';
import { IAccessRepository } from 'src/interfaces/access.interface';
@ -38,7 +38,7 @@ export class PartnerService {
await this.repository.remove(partner);
}
async getAll(auth: AuthDto, direction: PartnerDirection): Promise<PartnerResponseDto[]> {
async search(auth: AuthDto, { direction }: PartnerSearchDto): Promise<PartnerResponseDto[]> {
const partners = await this.repository.getAll(auth.user.id);
const key = direction === PartnerDirection.SharedBy ? 'sharedById' : 'sharedWithId';
return partners

View file

@ -145,7 +145,7 @@ describe('mimeTypes', () => {
it('should be a sorted list', () => {
const keys = Object.keys(mimeTypes.video);
expect(keys).toEqual([...keys].sort());
expect(keys).toEqual(keys.toSorted());
});
it('should contain only video mime types', () => {
@ -171,7 +171,7 @@ describe('mimeTypes', () => {
it('should be a sorted list', () => {
const keys = Object.keys(mimeTypes.sidecar);
expect(keys).toEqual([...keys].sort());
expect(keys).toEqual(keys.toSorted());
});
it('should contain only xml mime types', () => {

View file

@ -5,7 +5,7 @@ import cookieParser from 'cookie-parser';
import { existsSync } from 'node:fs';
import sirv from 'sirv';
import { ApiModule } from 'src/app.module';
import { envName, excludePaths, isDev, serverVersion, WEB_ROOT } from 'src/constants';
import { envName, excludePaths, isDev, resourcePaths, serverVersion } from 'src/constants';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
import { ApiService } from 'src/services/api.service';
@ -38,11 +38,11 @@ async function bootstrap() {
useSwagger(app);
app.setGlobalPrefix('api', { exclude: excludePaths });
if (existsSync(WEB_ROOT)) {
if (existsSync(resourcePaths.web.root)) {
// copied from https://github.com/sveltejs/kit/blob/679b5989fe62e3964b9a73b712d7b41831aa1f07/packages/adapter-node/src/handler.js#L46
// provides serving of precompressed assets and caching of immutable assets
app.use(
sirv(WEB_ROOT, {
sirv(resourcePaths.web.root, {
etag: true,
gzip: true,
brotli: true,

View file

@ -10,6 +10,7 @@
"resolveJsonModule": true,
"target": "es2022",
"moduleResolution": "node16",
"lib": ["dom", "es2023"],
"sourceMap": true,
"outDir": "./dist",
"incremental": true,

141
web/package-lock.json generated
View file

@ -55,7 +55,7 @@
"factory.ts": "^1.4.1",
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-organize-imports": "^4.0.0",
"prettier-plugin-sort-json": "^4.0.0",
"prettier-plugin-svelte": "^3.2.1",
"rollup-plugin-visualizer": "^5.12.0",
@ -1921,9 +1921,9 @@
}
},
"node_modules/@sveltejs/kit": {
"version": "2.5.17",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.17.tgz",
"integrity": "sha512-wiADwq7VreR3ctOyxilAZOfPz3Jiy2IIp2C8gfafhTdQaVuGIHllfqQm8dXZKADymKr3uShxzgLZFT+a+CM4kA==",
"version": "2.5.18",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.18.tgz",
"integrity": "sha512-+g06hvpVAnH7b4CDjhnTDgFWBKBiQJpuSmQeGYOuzbO3SC3tdYjRNlDCrafvDtKbGiT2uxY5Dn9qdEUGVZdWOQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@ -2369,17 +2369,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.14.1.tgz",
"integrity": "sha512-aAJd6bIf2vvQRjUG3ZkNXkmBpN+J7Wd0mfQiiVCJMu9Z5GcZZdcc0j8XwN/BM97Fl7e3SkTXODSk4VehUv7CGw==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.15.0.tgz",
"integrity": "sha512-uiNHpyjZtFrLwLDpHnzaDlP3Tt6sGMqTCiqmxaN4n4RP0EfYZDODJyddiFDF44Hjwxr5xAcaYxVKm9QKQFJFLA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.14.1",
"@typescript-eslint/type-utils": "7.14.1",
"@typescript-eslint/utils": "7.14.1",
"@typescript-eslint/visitor-keys": "7.14.1",
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/type-utils": "7.15.0",
"@typescript-eslint/utils": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@ -2403,16 +2403,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.14.1.tgz",
"integrity": "sha512-8lKUOebNLcR0D7RvlcloOacTOWzOqemWEWkKSVpMZVF/XVcwjPR+3MD08QzbW9TCGJ+DwIc6zUSGZ9vd8cO1IA==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.15.0.tgz",
"integrity": "sha512-k9fYuQNnypLFcqORNClRykkGOMOj+pV6V91R4GO/l1FDGwpqmSwoOQrOHo3cGaH63e+D3ZiCAOsuS/D2c99j/A==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "7.14.1",
"@typescript-eslint/types": "7.14.1",
"@typescript-eslint/typescript-estree": "7.14.1",
"@typescript-eslint/visitor-keys": "7.14.1",
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/typescript-estree": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"debug": "^4.3.4"
},
"engines": {
@ -2432,14 +2432,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.14.1.tgz",
"integrity": "sha512-gPrFSsoYcsffYXTOZ+hT7fyJr95rdVe4kGVX1ps/dJ+DfmlnjFN/GcMxXcVkeHDKqsq6uAcVaQaIi3cFffmAbA==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.15.0.tgz",
"integrity": "sha512-Q/1yrF/XbxOTvttNVPihxh1b9fxamjEoz2Os/Pe38OHwxC24CyCqXxGTOdpb4lt6HYtqw9HetA/Rf6gDGaMPlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.14.1",
"@typescript-eslint/visitor-keys": "7.14.1"
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@ -2450,14 +2450,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.14.1.tgz",
"integrity": "sha512-/MzmgNd3nnbDbOi3LfasXWWe292+iuo+umJ0bCCMCPc1jLO/z2BQmWUUUXvXLbrQey/JgzdF/OV+I5bzEGwJkQ==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.15.0.tgz",
"integrity": "sha512-SkgriaeV6PDvpA6253PDVep0qCqgbO1IOBiycjnXsszNTVQe5flN5wR5jiczoEoDEnAqYFSFFc9al9BSGVltkg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "7.14.1",
"@typescript-eslint/utils": "7.14.1",
"@typescript-eslint/typescript-estree": "7.15.0",
"@typescript-eslint/utils": "7.15.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@ -2478,9 +2478,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.14.1.tgz",
"integrity": "sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.15.0.tgz",
"integrity": "sha512-aV1+B1+ySXbQH0pLK0rx66I3IkiZNidYobyfn0WFsdGhSXw+P3YOqeTq5GED458SfB24tg+ux3S+9g118hjlTw==",
"dev": true,
"license": "MIT",
"engines": {
@ -2492,14 +2492,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.14.1.tgz",
"integrity": "sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.15.0.tgz",
"integrity": "sha512-gjyB/rHAopL/XxfmYThQbXbzRMGhZzGw6KpcMbfe8Q3nNQKStpxnUKeXb0KiN/fFDR42Z43szs6rY7eHk0zdGQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "7.14.1",
"@typescript-eslint/visitor-keys": "7.14.1",
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@ -2560,16 +2560,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.14.1.tgz",
"integrity": "sha512-CMmVVELns3nak3cpJhZosDkm63n+DwBlDX8g0k4QUa9BMnF+lH2lr3d130M1Zt1xxmB3LLk3NV7KQCq86ZBBhQ==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.15.0.tgz",
"integrity": "sha512-hfDMDqaqOqsUVGiEPSMLR/AjTSCsmJwjpKkYQRo1FNbmW4tBwBspYDwO9eh7sKSTwMQgBw9/T4DHudPaqshRWA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.14.1",
"@typescript-eslint/types": "7.14.1",
"@typescript-eslint/typescript-estree": "7.14.1"
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/typescript-estree": "7.15.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@ -2583,13 +2583,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.14.1.tgz",
"integrity": "sha512-Crb+F75U1JAEtBeQGxSKwI60hZmmzaqA3z9sYsVm8X7W5cwLEm5bRe0/uXS6+MR/y8CVpKSR/ontIAIEPFcEkA==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.15.0.tgz",
"integrity": "sha512-Hqgy/ETgpt2L5xueA/zHHIl4fJI2O4XUE9l4+OIfbJIRSnTJb/QscncdqqZzofQegIJugRIF57OJea1khw2SDw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.14.1",
"@typescript-eslint/types": "7.15.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@ -6359,10 +6359,11 @@
}
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"dev": true
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
@ -6424,9 +6425,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"version": "8.4.39",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
"dev": true,
"funding": [
{
@ -6442,9 +6443,10 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
"picocolors": "^1.0.1",
"source-map-js": "^1.2.0"
},
"engines": {
@ -6629,21 +6631,22 @@
}
},
"node_modules/prettier-plugin-organize-imports": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz",
"integrity": "sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.0.0.tgz",
"integrity": "sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@volar/vue-language-plugin-pug": "^1.0.4",
"@volar/vue-typescript": "^1.0.4",
"@vue/language-plugin-pug": "^2.0.24",
"prettier": ">=2.0",
"typescript": ">=2.9"
"typescript": ">=2.9",
"vue-tsc": "^2.0.24"
},
"peerDependenciesMeta": {
"@volar/vue-language-plugin-pug": {
"@vue/language-plugin-pug": {
"optional": true
},
"@volar/vue-typescript": {
"vue-tsc": {
"optional": true
}
}
@ -8625,9 +8628,9 @@
}
},
"node_modules/typescript": {
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz",
"integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==",
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@ -8762,14 +8765,14 @@
}
},
"node_modules/vite": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.2.tgz",
"integrity": "sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==",
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz",
"integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.38",
"postcss": "^8.4.39",
"rollup": "^4.13.0"
},
"bin": {

View file

@ -47,7 +47,7 @@
"factory.ts": "^1.4.1",
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-organize-imports": "^4.0.0",
"prettier-plugin-sort-json": "^4.0.0",
"prettier-plugin-svelte": "^3.2.1",
"rollup-plugin-visualizer": "^5.12.0",

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { focusTrap } from '$lib/actions/focus-trap';
export let show: boolean;
</script>
<button type="button" on:click={() => (show = true)}>Open</button>
{#if show}
<div use:focusTrap>
<div>
<span>text</span>
<button data-testid="one" type="button" on:click={() => (show = false)}>Close</button>
</div>
<input data-testid="two" disabled />
<input data-testid="three" />
</div>
{/if}

View file

@ -0,0 +1,40 @@
import FocusTrapTest from '$lib/actions/__test__/focus-trap-test.svelte';
import { render, screen } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
import { tick } from 'svelte';
describe('focusTrap action', () => {
const user = userEvent.setup();
it('sets focus to the first focusable element', () => {
render(FocusTrapTest, { show: true });
expect(document.activeElement).toEqual(screen.getByTestId('one'));
});
it('supports backward focus wrapping', async () => {
render(FocusTrapTest, { show: true });
await user.keyboard('{Shift>}{Tab}{/Shift}');
expect(document.activeElement).toEqual(screen.getByTestId('three'));
});
it('supports forward focus wrapping', async () => {
render(FocusTrapTest, { show: true });
screen.getByTestId('three').focus();
await user.keyboard('{Tab}');
expect(document.activeElement).toEqual(screen.getByTestId('one'));
});
it('restores focus to the triggering element', async () => {
render(FocusTrapTest, { show: false });
const openButton = screen.getByText('Open');
openButton.focus();
openButton.click();
await tick();
expect(document.activeElement).toEqual(screen.getByTestId('one'));
screen.getByText('Close').click();
await tick();
expect(document.activeElement).toEqual(openButton);
});
});

View file

@ -0,0 +1,55 @@
import { shortcuts } from '$lib/actions/shortcut';
const selectors =
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
export function focusTrap(container: HTMLElement) {
const triggerElement = document.activeElement;
const focusableElement = container.querySelector<HTMLElement>(selectors);
focusableElement?.focus();
const getFocusableElements = (): [HTMLElement | null, HTMLElement | null] => {
const focusableElements = container.querySelectorAll<HTMLElement>(selectors);
return [
focusableElements.item(0), //
focusableElements.item(focusableElements.length - 1),
];
};
const { destroy: destroyShortcuts } = shortcuts(container, [
{
ignoreInputFields: false,
preventDefault: false,
shortcut: { key: 'Tab' },
onShortcut: (event) => {
const [firstElement, lastElement] = getFocusableElements();
if (document.activeElement === lastElement) {
event.preventDefault();
firstElement?.focus();
}
},
},
{
ignoreInputFields: false,
preventDefault: false,
shortcut: { key: 'Tab', shift: true },
onShortcut: (event) => {
const [firstElement, lastElement] = getFocusableElements();
if (document.activeElement === firstElement) {
event.preventDefault();
lastElement?.focus();
}
},
},
]);
return {
destroy() {
destroyShortcuts?.();
if (triggerElement instanceof HTMLElement) {
triggerElement.focus();
}
},
};
}

View file

@ -1,7 +1,6 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
import FocusTrap from '$lib/components/shared-components/focus-trap.svelte';
import { AssetAction, ProjectionType } from '$lib/constants';
import { updateNumberOfComments } from '$lib/stores/activity.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
@ -61,6 +60,7 @@
import { websocketEvents } from '$lib/stores/websocket';
import { canCopyImagesToClipboard } from 'copy-image-clipboard';
import { t } from 'svelte-i18n';
import { focusTrap } from '$lib/actions/focus-trap';
export let assetStore: AssetStore | null = null;
export let asset: AssetResponseDto;
@ -553,10 +553,10 @@
<svelte:document bind:fullscreenElement />
<FocusTrap>
<section
id="immich-asset-viewer"
class="fixed left-0 top-0 z-[1001] grid h-screen w-screen grid-cols-4 grid-rows-[64px_1fr] overflow-hidden bg-black"
use:focusTrap
>
<!-- Top navigation bar -->
{#if $slideshowState === SlideshowState.None}
@ -788,11 +788,7 @@
{/if}
{#if isShowDeleteConfirmation}
<DeleteAssetDialog
size={1}
on:cancel={() => (isShowDeleteConfirmation = false)}
on:confirm={() => deleteAsset()}
/>
<DeleteAssetDialog size={1} on:cancel={() => (isShowDeleteConfirmation = false)} on:confirm={() => deleteAsset()} />
{/if}
{#if isShowProfileImageCrop}
@ -803,7 +799,6 @@
<CreateSharedLinkModal assetIds={[asset.id]} onClose={() => (isShowShareModal = false)} />
{/if}
</section>
</FocusTrap>
<style>
#immich-asset-viewer {

View file

@ -123,7 +123,7 @@
<img
style="display:none"
src={imageLoaderUrl}
alt={getAltText(asset)}
alt={$getAltText(asset)}
on:load={() => ((imageLoaded = true), (assetFileUrl = imageLoaderUrl))}
on:error={() => (imageError = imageLoaded = true)}
/>
@ -136,7 +136,7 @@
{#if $slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground}
<img
src={assetFileUrl}
alt={getAltText(asset)}
alt={$getAltText(asset)}
class="absolute top-0 left-0 -z-10 object-cover h-full w-full blur-lg"
draggable="false"
/>
@ -144,7 +144,7 @@
<img
bind:this={$photoViewer}
src={assetFileUrl}
alt={getAltText(asset)}
alt={$getAltText(asset)}
class="h-full w-full {$slideshowState === SlideshowState.None
? 'object-contain'
: slideshowLookCssMapping[$slideshowLook]}"

View file

@ -19,7 +19,7 @@
export let hidden = false;
export let border = false;
export let preload = true;
export let eyeColor: 'black' | 'white' = 'white';
export let hiddenIconClass = 'text-white';
let complete = false;
let img: HTMLImageElement;
@ -54,7 +54,7 @@
{#if hidden}
<div class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform">
<Icon {title} path={mdiEyeOffOutline} size="2em" class="text-{eyeColor}" />
<Icon {title} path={mdiEyeOffOutline} size="2em" class={hiddenIconClass} />
</div>
{/if}

View file

@ -186,7 +186,7 @@
{#if asset.resized}
<ImageThumbnail
url={getAssetThumbnailUrl({ id: asset.id, size: AssetMediaSize.Thumbnail, checksum: asset.checksum })}
altText={getAltText(asset)}
altText={$getAltText(asset)}
widthStyle="{width}px"
heightStyle="{height}px"
thumbhash={asset.thumbhash}

View file

@ -0,0 +1,106 @@
import { sdkMock } from '$lib/__mocks__/sdk.mock';
import ManagePeopleVisibility from '$lib/components/faces-page/manage-people-visibility.svelte';
import type { PersonResponseDto } from '@immich/sdk';
import { personFactory } from '@test-data/factories/person-factory';
import { render } from '@testing-library/svelte';
import { tick } from 'svelte';
describe('ManagePeopleVisibility Component', () => {
let personVisible: PersonResponseDto;
let personHidden: PersonResponseDto;
let personWithoutName: PersonResponseDto;
beforeAll(() => {
// Prevents errors from `img.decode()` in ImageThumbnail
Object.defineProperty(HTMLImageElement.prototype, 'decode', {
value: vi.fn(),
});
});
beforeEach(() => {
personVisible = personFactory.build({ isHidden: false });
personHidden = personFactory.build({ isHidden: true });
personWithoutName = personFactory.build({ isHidden: false, name: undefined });
sdkMock.updatePeople.mockResolvedValue([]);
});
afterEach(() => {
vi.resetAllMocks();
});
it('does not update people when no changes are made', () => {
const { getByText } = render(ManagePeopleVisibility, {
props: {
people: [personVisible, personHidden, personWithoutName],
onClose: vi.fn(),
},
});
const saveButton = getByText('done');
saveButton.click();
expect(sdkMock.updatePeople).not.toHaveBeenCalled();
});
it('hides unnamed people on first button press', () => {
const { getByText, getByTitle } = render(ManagePeopleVisibility, {
props: {
people: [personVisible, personHidden, personWithoutName],
onClose: vi.fn(),
},
});
getByTitle('hide_unnamed_people').click();
getByText('done').click();
expect(sdkMock.updatePeople).toHaveBeenCalledWith({
peopleUpdateDto: {
people: [{ id: personWithoutName.id, isHidden: true }],
},
});
});
it('hides all people on second button press', async () => {
const { getByText, getByTitle } = render(ManagePeopleVisibility, {
props: {
people: [personVisible, personHidden, personWithoutName],
onClose: vi.fn(),
},
});
getByTitle('hide_unnamed_people').click();
await tick();
getByTitle('hide_all_people').click();
getByText('done').click();
expect(sdkMock.updatePeople).toHaveBeenCalledWith({
peopleUpdateDto: {
people: expect.arrayContaining([
{ id: personVisible.id, isHidden: true },
{ id: personWithoutName.id, isHidden: true },
]),
},
});
});
it('shows all people on third button press', async () => {
const { getByText, getByTitle } = render(ManagePeopleVisibility, {
props: {
people: [personVisible, personHidden, personWithoutName],
onClose: vi.fn(),
},
});
getByTitle('hide_unnamed_people').click();
await tick();
getByTitle('hide_all_people').click();
await tick();
getByTitle('show_all_people').click();
getByText('done').click();
expect(sdkMock.updatePeople).toHaveBeenCalledWith({
peopleUpdateDto: {
people: [{ id: personHidden.id, isHidden: false }],
},
});
});
});

View file

@ -0,0 +1,168 @@
<script lang="ts" context="module">
const enum ToggleVisibility {
HIDE_ALL = 'hide-all',
HIDE_UNNANEMD = 'hide-unnamed',
SHOW_ALL = 'show-all',
}
</script>
<script lang="ts">
import { shortcut } from '$lib/actions/shortcut';
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
import Button from '$lib/components/elements/buttons/button.svelte';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import {
notificationController,
NotificationType,
} from '$lib/components/shared-components/notification/notification';
import { locale } from '$lib/stores/preferences.store';
import { getPeopleThumbnailUrl } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import { updatePeople, type PersonResponseDto } from '@immich/sdk';
import { mdiClose, mdiEye, mdiEyeOff, mdiEyeSettings, mdiRestart } from '@mdi/js';
import { t } from 'svelte-i18n';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
export let people: PersonResponseDto[];
export let onClose: () => void;
export let titleId: string | undefined = undefined;
let toggleVisibility = ToggleVisibility.SHOW_ALL;
let showLoadingSpinner = false;
$: personIsHidden = getPersonIsHidden(people);
$: toggleButton = toggleButtonOptions[getNextVisibility(toggleVisibility)];
const getPersonIsHidden = (people: PersonResponseDto[]) => {
const personIsHidden: Record<string, boolean> = {};
for (const person of people) {
personIsHidden[person.id] = person.isHidden;
}
return personIsHidden;
};
$: toggleButtonOptions = ((): Record<ToggleVisibility, { icon: string; label: string }> => {
return {
[ToggleVisibility.HIDE_ALL]: { icon: mdiEyeOff, label: $t('hide_all_people') },
[ToggleVisibility.HIDE_UNNANEMD]: { icon: mdiEyeSettings, label: $t('hide_unnamed_people') },
[ToggleVisibility.SHOW_ALL]: { icon: mdiEye, label: $t('show_all_people') },
};
})();
const getNextVisibility = (toggleVisibility: ToggleVisibility) => {
if (toggleVisibility === ToggleVisibility.SHOW_ALL) {
return ToggleVisibility.HIDE_UNNANEMD;
} else if (toggleVisibility === ToggleVisibility.HIDE_UNNANEMD) {
return ToggleVisibility.HIDE_ALL;
} else {
return ToggleVisibility.SHOW_ALL;
}
};
const handleToggleVisibility = () => {
toggleVisibility = getNextVisibility(toggleVisibility);
for (const person of people) {
if (toggleVisibility === ToggleVisibility.HIDE_ALL) {
personIsHidden[person.id] = true;
} else if (toggleVisibility === ToggleVisibility.SHOW_ALL) {
personIsHidden[person.id] = false;
} else if (toggleVisibility === ToggleVisibility.HIDE_UNNANEMD && !person.name) {
personIsHidden[person.id] = true;
}
}
};
const handleResetVisibility = () => (personIsHidden = getPersonIsHidden(people));
const handleSaveVisibility = async () => {
showLoadingSpinner = true;
const changed = people
.filter((person) => person.isHidden !== personIsHidden[person.id])
.map((person) => ({ id: person.id, isHidden: personIsHidden[person.id] }));
try {
if (changed.length > 0) {
const results = await updatePeople({ peopleUpdateDto: { people: changed } });
const successCount = results.filter(({ success }) => success).length;
const failCount = results.length - successCount;
if (failCount > 0) {
notificationController.show({
type: NotificationType.Error,
message: $t('errors.unable_to_change_visibility', { values: { count: failCount } }),
});
}
notificationController.show({
type: NotificationType.Info,
message: $t('visibility_changed', { values: { count: successCount } }),
});
}
for (const person of people) {
person.isHidden = personIsHidden[person.id];
}
people = people;
onClose();
} catch (error) {
handleError(error, $t('errors.unable_to_change_visibility', { values: { count: changed.length } }));
} finally {
showLoadingSpinner = false;
}
};
</script>
<svelte:window use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: onClose }} />
<div
class="fixed top-0 z-10 flex h-16 w-full items-center justify-between border-b bg-white p-1 dark:border-immich-dark-gray dark:bg-black dark:text-immich-dark-fg md:p-8"
>
<div class="flex items-center">
<CircleIconButton title={$t('close')} icon={mdiClose} on:click={onClose} />
<div class="flex gap-2 items-center">
<p id={titleId} class="ml-2">{$t('show_and_hide_people')}</p>
<p class="text-sm text-gray-400 dark:text-gray-600">({people.length.toLocaleString($locale)})</p>
</div>
</div>
<div class="flex items-center justify-end">
<div class="flex items-center md:mr-4">
<CircleIconButton title={$t('reset_people_visibility')} icon={mdiRestart} on:click={handleResetVisibility} />
<CircleIconButton title={toggleButton.label} icon={toggleButton.icon} on:click={handleToggleVisibility} />
</div>
{#if !showLoadingSpinner}
<Button on:click={handleSaveVisibility} size="sm" rounded="lg">{$t('done')}</Button>
{:else}
<LoadingSpinner />
{/if}
</div>
</div>
<div class="flex flex-wrap gap-1 bg-immich-bg p-2 pb-8 dark:bg-immich-dark-bg md:px-8 mt-16">
<div class="w-full grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-9 gap-1">
{#each people as person, index (person.id)}
{@const hidden = personIsHidden[person.id]}
<button
type="button"
class="group relative"
on:click={() => (personIsHidden[person.id] = !hidden)}
aria-pressed={hidden}
aria-label={person.name ? $t('hide_named_person', { values: { name: person.name } }) : $t('hide_person')}
>
<ImageThumbnail
preload={index < 20}
{hidden}
shadow
url={getPeopleThumbnailUrl(person)}
altText={person.name}
widthStyle="100%"
hiddenIconClass="text-white group-hover:text-black transition-colors"
/>
{#if person.name}
<span class="absolute bottom-2 left-0 w-full select-text px-1 text-center font-medium text-white">
{person.name}
</span>
{/if}
</button>
{/each}
</div>
</div>

View file

@ -1,81 +0,0 @@
<script lang="ts" context="module">
export enum ToggleVisibilty {
HIDE_ALL = 'hide-all',
HIDE_UNNANEMD = 'hide-unnamed',
VIEW_ALL = 'view-all',
}
</script>
<script lang="ts">
import { fly } from 'svelte/transition';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import { quintOut } from 'svelte/easing';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { mdiClose, mdiEye, mdiEyeOff, mdiEyeSettings, mdiRestart } from '@mdi/js';
import { locale } from '$lib/stores/preferences.store';
import Button from '$lib/components/elements/buttons/button.svelte';
import { t } from 'svelte-i18n';
export let showLoadingSpinner: boolean;
export let toggleVisibility: ToggleVisibilty = ToggleVisibilty.VIEW_ALL;
export let screenHeight: number;
export let countTotalPeople: number;
export let onClose: () => void;
export let onReset: () => void;
export let onChange: (toggleVisibility: ToggleVisibilty) => void;
export let onDone: () => void;
const getNextVisibility = (toggleVisibility: ToggleVisibilty) => {
if (toggleVisibility === ToggleVisibilty.VIEW_ALL) {
return ToggleVisibilty.HIDE_UNNANEMD;
} else if (toggleVisibility === ToggleVisibilty.HIDE_UNNANEMD) {
return ToggleVisibilty.HIDE_ALL;
} else {
return ToggleVisibilty.VIEW_ALL;
}
};
const toggleIconOptions: Record<ToggleVisibilty, string> = {
[ToggleVisibilty.HIDE_ALL]: mdiEyeOff,
[ToggleVisibilty.HIDE_UNNANEMD]: mdiEyeSettings,
[ToggleVisibilty.VIEW_ALL]: mdiEye,
};
$: toggleIcon = toggleIconOptions[toggleVisibility];
</script>
<section
transition:fly={{ y: screenHeight, duration: 150, easing: quintOut, opacity: 1 }}
class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
>
<div
class="fixed top-0 z-10 flex h-16 w-full items-center justify-between border-b bg-white p-1 dark:border-immich-dark-gray dark:bg-black dark:text-immich-dark-fg md:p-8"
>
<div class="flex items-center">
<CircleIconButton title={$t('close')} icon={mdiClose} on:click={onClose} />
<div class="flex gap-2 items-center">
<p class="ml-2">{$t('show_and_hide_people')}</p>
<p class="text-sm text-gray-400 dark:text-gray-600">({countTotalPeople.toLocaleString($locale)})</p>
</div>
</div>
<div class="flex items-center justify-end">
<div class="flex items-center md:mr-8">
<CircleIconButton title={$t('reset_people_visibility')} icon={mdiRestart} on:click={onReset} />
<CircleIconButton
title={$t('toggle_visibility')}
icon={toggleIcon}
on:click={() => onChange(getNextVisibility(toggleVisibility))}
/>
</div>
{#if !showLoadingSpinner}
<Button on:click={onDone} size="sm" rounded="lg">{$t('done')}</Button>
{:else}
<LoadingSpinner />
{/if}
</div>
</div>
<div class="flex flex-wrap gap-1 bg-immich-bg p-2 pb-8 dark:bg-immich-dark-bg md:px-8 mt-16">
<slot />
</div>
</section>

View file

@ -76,7 +76,7 @@
<img
class="h-full w-full rounded-xl object-cover"
src={getAssetThumbnailUrl(memory.assets[0].id)}
alt={`Memory Lane ${getAltText(memory.assets[0])}`}
alt={`Memory Lane ${$getAltText(memory.assets[0])}`}
draggable="false"
/>
<p class="absolute bottom-2 left-4 z-10 text-lg text-white">

View file

@ -1,64 +0,0 @@
<script lang="ts">
import { shortcuts } from '$lib/actions/shortcut';
import { onMount, onDestroy } from 'svelte';
let container: HTMLElement;
let triggerElement: HTMLElement;
onMount(() => {
triggerElement = document.activeElement as HTMLElement;
const focusableElements = getFocusableElements();
focusableElements[0]?.focus();
});
onDestroy(() => {
triggerElement?.focus();
});
const getFocusableElements = () => {
return Array.from(
container.querySelectorAll(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
),
) as HTMLElement[];
};
const trapFocus = (direction: 'forward' | 'backward', event: KeyboardEvent) => {
const focusableElements = getFocusableElements();
const elementCount = focusableElements.length;
const firstElement = focusableElements[0];
const lastElement = focusableElements.at(elementCount - 1);
if (document.activeElement === lastElement && direction === 'forward') {
event.preventDefault();
firstElement?.focus();
} else if (document.activeElement === firstElement && direction === 'backward') {
event.preventDefault();
lastElement?.focus();
}
};
</script>
<div
bind:this={container}
use:shortcuts={[
{
ignoreInputFields: false,
shortcut: { key: 'Tab' },
onShortcut: (event) => {
trapFocus('forward', event);
},
preventDefault: false,
},
{
ignoreInputFields: false,
shortcut: { key: 'Tab', shift: true },
onShortcut: (event) => {
trapFocus('backward', event);
},
preventDefault: false,
},
]}
>
<slot />
</div>

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { clickOutside } from '$lib/actions/click-outside';
import { focusTrap } from '$lib/actions/focus-trap';
import { fade } from 'svelte/transition';
import FocusTrap from '$lib/components/shared-components/focus-trap.svelte';
import ModalHeader from '$lib/components/shared-components/modal-header.svelte';
import { generateId } from '$lib/utils/generate-id';
@ -52,8 +52,8 @@
on:keydown={(event) => {
event.stopPropagation();
}}
use:focusTrap
>
<FocusTrap>
<div
class="z-[9999] max-w-[95vw] max-h-[min(95dvh,56rem)] {modalWidth} overflow-y-auto rounded-3xl bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg immich-scrollbar"
use:clickOutside={{ onOutclick: onClose, onEscape: onClose }}
@ -75,5 +75,4 @@
</div>
{/if}
</div>
</FocusTrap>
</section>

View file

@ -1,8 +1,8 @@
<script lang="ts">
import { focusTrap } from '$lib/actions/focus-trap';
import Button from '$lib/components/elements/buttons/button.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import FocusTrap from '$lib/components/shared-components/focus-trap.svelte';
import { AppRoute } from '$lib/constants';
import { preferences, user } from '$lib/stores/user.store';
import { handleError } from '$lib/utils/handle-error';
@ -42,12 +42,12 @@
};
</script>
<FocusTrap>
<div
in:fade={{ duration: 100 }}
out:fade={{ duration: 100 }}
id="account-info-panel"
class="absolute right-[25px] top-[75px] z-[100] w-[360px] rounded-3xl bg-gray-200 shadow-lg dark:border dark:border-immich-dark-gray dark:bg-immich-dark-gray"
use:focusTrap
>
<div
class="mx-4 mt-4 flex flex-col items-center justify-center gap-4 rounded-3xl bg-white p-4 dark:bg-immich-dark-primary/10"
@ -94,7 +94,6 @@
>
</div>
</div>
</FocusTrap>
{#if isShowSelectAvatar}
<AvatarSelector

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { searchUsers, getPartners, type UserResponseDto } from '@immich/sdk';
import { searchUsers, getPartners, type UserResponseDto, PartnerDirection } from '@immich/sdk';
import { createEventDispatcher, onMount } from 'svelte';
import Button from '../elements/buttons/button.svelte';
import UserAvatar from '../shared-components/user-avatar.svelte';
@ -21,7 +21,7 @@
users = users.filter((_user) => _user.id !== user.id);
// exclude partners from the list of users available for selection
const partners = await getPartners({ direction: 'shared-by' });
const partners = await getPartners({ direction: PartnerDirection.SharedBy });
const partnerIds = new Set(partners.map((partner) => partner.id));
availableUsers = users.filter((user) => !partnerIds.has(user.id));
});

View file

@ -2,6 +2,7 @@
import {
createPartner,
getPartners,
PartnerDirection,
removePartner,
updatePartner,
type PartnerResponseDto,
@ -40,8 +41,8 @@
partners = [];
const [sharedBy, sharedWith] = await Promise.all([
getPartners({ direction: 'shared-by' }),
getPartners({ direction: 'shared-with' }),
getPartners({ direction: PartnerDirection.SharedBy }),
getPartners({ direction: PartnerDirection.SharedWith }),
]);
for (const candidate of sharedBy) {

View file

@ -32,7 +32,7 @@
<!-- THUMBNAIL-->
<img
src={getAssetThumbnailUrl(asset.id)}
alt={getAltText(asset)}
alt={$getAltText(asset)}
title={assetData}
class="h-60 object-cover rounded-t-xl w-full"
draggable="false"

View file

@ -680,7 +680,7 @@
"every_night_at_midnight": "",
"every_night_at_twoam": "",
"every_six_hours": "",
"exif": "Exif",
"exif": "Exif (صيغة ملف صوري قابل للتبادل)",
"exit_slideshow": "خروج من العرض التقديمي",
"expand_all": "توسيع الكل",
"expire_after": "تنتهي بعد",
@ -728,6 +728,10 @@
"host": "المضيف",
"hour": "ساعة",
"image": "صورة",
"image_alt_text_date": "في {date}",
"image_alt_text_people": "{count, plural, =1 {مع {person1}} =2 {مع {person1} و {person2}} =3 {مع {person1} و {person2} و {person3}} other {مع {person1} و {person2} و {others, number} آخرين}}",
"image_alt_text_place": "في {city}, {country}",
"image_taken": "{isVideo, select, true {تم التقاط الفيديو} other {تم التقاط الصورة}}",
"img": "",
"immich_logo": "شعار immich",
"immich_web_interface": "واجهة ويب immich",

View file

@ -7,6 +7,7 @@
"actions": "Accions",
"active": "Activar",
"activity": "Activitat",
"activity_changed": "L'activitat està {enabled, select, true {enabled} other {disabled}}",
"add": "Agregar",
"add_a_description": "Afegeix una descripció",
"add_a_location": "Afegeix una ubicació",
@ -22,18 +23,21 @@
"add_to": "Afegeix a...",
"add_to_album": "Afegeix a l'àlbum",
"add_to_shared_album": "Afegeix a l'àlbum compartit",
"added_to_archive": "Afegit a l'arxivat",
"added_to_favorites": "Afegit a preferits",
"added_to_favorites_count": "{count} afegits a preferits",
"admin": {
"add_exclusion_pattern_description": "Afegeix patrons d'eclusió. És permès de l'ús de *, **, i ? (globbing). Per a ignorar els fitxers de qualsevol directori anomenat \"Raw\" introduïu \"**/Raw/**\". Per a ignorar els fitxers acabats en \".tif\" introduïu \"**/*.tif\". Per a ignorar un camí absolut, utilitzeu \"/camí/a/ignorar/**\".",
"authentication_settings": "Arrenjaments d'autenticació",
"authentication_settings_description": "Gestiona la contrasenya, OAuth, i altres arrenjaments d'autenticació",
"authentication_settings_disable_all": "Estàs segur que vols desactivar tots els mètodes d'inici de sessió? L'inici de sessió quedarà completament desactivat.",
"authentication_settings_reenable": "Per a tornar a habilitar, empra una <link>Comanda de Servidor</link>.",
"background_task_job": "Tasques en segon pla",
"check_all": "Marca-ho tot",
"cleared_jobs": "Treballs esborrats per a: {job}",
"config_set_by_file": "La configuració està definida per un fitxer de configuració",
"confirm_delete_library": "Esteu segur que voleu eliminar la biblioteca {library}?",
"confirm_delete_library_assets": "Esteu segur que voleu eliminar aquesta biblioteca? Això eliminarà els {count} elements de l'Immich i serà irrevesible. Els fitxers romandran al disc.",
"confirm_delete_library_assets": "Estàs segur que vols esborrar aquesta biblioteca? Això esborrarà {count, plural, one {# contained asset} other {all # contained assets}} d'Immich i no es podrà desfer. Els fitxers romandran al disc.",
"confirm_email_below": "Per a confirmar, escriviu \"{email}\" a sota",
"confirm_reprocess_all_faces": "Esteu segur que voleu reprocessar totes les cares? Això també esborrarà la gent que heu anomenat.",
"confirm_user_password_reset": "Esteu segur que voleu reinicialitzar la contrasenya de l'usuari {user}?",
@ -74,6 +78,7 @@
"jobs_failed": "{jobCount, plural, other {# fallides}}",
"library_created": "Bilbioteca creada: {library}",
"library_cron_expression": "Expressió cron",
"library_cron_expression_description": "Estableix l'interval d'escaneig utilitzant el format cron. Per a més informació, consulta per exemple, <link>Crontab Guru</link>",
"library_cron_expression_presets": "Expressions cron predeterminades",
"library_deleted": "Bilbioteca eliminada",
"library_import_path_description": "Especifiqueu una carpeta a importar. Aquesta carpeta, incloses les seves subcarpetes, serà escaneja per a cercar imatges i videos.",
@ -90,6 +95,7 @@
"logging_level_description": "Quan està habilitat, quin nivell de registre emprar.",
"logging_settings": "Registre",
"machine_learning_clip_model": "Model de CLIP",
"machine_learning_clip_model_description": "El nom d'un model CLIP que apareix a <link>aquí</link>. Tingues en compte que has de tornar a executar l'Smart Search' per a totes les imatges quan es canvia de model.",
"machine_learning_duplicate_detection": "Detecció de duplicats",
"machine_learning_duplicate_detection_enabled": "Activa detecció de duplicats",
"machine_learning_duplicate_detection_enabled_description": "Si es deshabilitat, els elements exactament idèntics encara es desduplicaran.",
@ -105,11 +111,11 @@
"machine_learning_max_detection_distance": "Distància màxima de detecció",
"machine_learning_max_detection_distance_description": "Diferència màxima entre dues imatges per a considerar-les duplicades, en un rang d'entre 0.001-0.1. Com més elevat el valor més detecció de duplicats, però pot resultar en falsos positius.",
"machine_learning_max_recognition_distance": "Màxima diferència de reconeixement",
"machine_learning_max_recognition_distance_description": "",
"machine_learning_max_recognition_distance_description": "La distància màxima entre dues cares per considerar-les la mateixa persona, que oscil·la entre 0-2. Reduir aquest valor pot evitar etiquetar dues persones com la mateixa, mentre que augmentar-lo pot evitar etiquetar la mateixa persona com dues diferents. Tingues en compte que és més fàcil fusionar dues persones que dividir-ne una en dues, així que, si és possible, és millor optar per un llindar més baix.",
"machine_learning_min_detection_score": "Puntuació mínima de detecció",
"machine_learning_min_detection_score_description": "",
"machine_learning_min_detection_score_description": "La puntuació mínima de confiança per detectar una cara és de 0 a 1. Valors més baixos detectaran més cares, però poden donar lloc a falsos positius.",
"machine_learning_min_recognized_faces": "Nombre mínim de cares reconegudes",
"machine_learning_min_recognized_faces_description": "",
"machine_learning_min_recognized_faces_description": "El nombre mínim de cares reconegudes per crear una persona. Augmentar aquest valor fa que el reconeixement facial sigui més precís, però augmenta la possibilitat que una cara no sigui assignada a una persona.",
"machine_learning_settings": "Configuració d'aprenentatge automàtic",
"machine_learning_settings_description": "Gestiona funcions i configuració d'aprenentatge automàtic",
"machine_learning_smart_search": "Cerca Intel·ligent",
@ -176,21 +182,42 @@
"oauth_storage_quota_claim_description": "",
"oauth_storage_quota_default": "Quota d'emmagatzemament predeterminada (GiB)",
"oauth_storage_quota_default_description": "",
"offline_paths": "Rutes sense connexió",
"offline_paths_description": "Aquests resultats poden ser deguts a l'eliminació manual de fitxers que no formen part d'una biblioteca externa.",
"password_enable_description": "Inicia sessió amb correu electrònic i contrasenya",
"password_settings": "Inici de sessió amb contrasenya",
"password_settings_description": "Gestiona la configuració de l'inici de sessió amb contrasenya",
"server_external_domain_settings": "",
"server_external_domain_settings_description": "",
"paths_validated_successfully": "Tots els camins han estat validats amb èxit",
"quota_size_gib": "Tamany de la quota (GiB)",
"refreshing_all_libraries": "Actualitzant totes les biblioteques",
"registration": "Registre d'administrador",
"registration_description": "Com que ets el primer usuari del sistema, seràs designat com a administrador i seràs responsable de les tasques administratives. També seràs l'encarregat de crear usuaris addicionals.",
"removing_offline_files": "Eliminant fitxers fora de línia",
"repair_all": "Reparar tot",
"repair_matched_items": "Coincidència {count, plural, one {# item} other {# items}}",
"repaired_items": "Corregit {count, plural, one {# item} other {# items}}",
"require_password_change_on_login": "Requerir que l'usuari canviï la contrasenya en el primer inici de sessió",
"reset_settings_to_default": "Restablir configuracions per defecte",
"reset_settings_to_recent_saved": "Restaurar la configuració guardada més recent",
"scanning_library_for_changed_files": "Escanejant biblioteca per trobar fitxers modificats",
"scanning_library_for_new_files": "Escanejant biblioteca per trobar fitxers nous",
"send_welcome_email": "Enviar correu electrònic de benvinguda",
"server_external_domain_settings": "Domini extern",
"server_external_domain_settings_description": "Domini per enllaços públics compartits, incloent http(s)://",
"server_settings": "Configuració del servidor",
"server_settings_description": "",
"server_welcome_message": "",
"server_welcome_message_description": "",
"sidecar_job_description": "",
"slideshow_duration_description": "",
"smart_search_job_description": "",
"storage_template_enable_description": "",
"storage_template_hash_verification_enabled": "",
"storage_template_hash_verification_enabled_description": "",
"server_settings_description": "Gestionar la configuració del servidor",
"server_welcome_message": "Missatge de benvinguda",
"server_welcome_message_description": "Missatge que es mostra a la pàgina d'inici de sessió.",
"sidecar_job": "Metadades auxiliars",
"sidecar_job_description": "Descobrir o sincronitzar metadades auxiliars des del sistema de fitxers",
"slideshow_duration_description": "Segons per mostrar cada imatge",
"smart_search_job_description": "Executar aprenentatge automàtic sobre els recursos per donar suport a la cerca intel·ligent",
"storage_template_date_time_description": "La data de creació del recurs s'utilitza per a la informació de la data i l'hora",
"storage_template_date_time_sample": "Temps d'exemple: {date}",
"storage_template_enable_description": "Habilitar el motor de plantilles d'emmagatzematge",
"storage_template_hash_verification_enabled": "Verificació Hash habilitada",
"storage_template_hash_verification_enabled_description": "Activa la verificació de hash. No la desactivis a menys que estiguis segur de les implicacions",
"storage_template_migration": "Migració de plantilles d'emmagatzematge",
"storage_template_migration_job": "",
"storage_template_settings": "",
"storage_template_settings_description": "",
@ -572,8 +599,8 @@
"login_has_been_disabled": "",
"look": "",
"loop_videos": "",
"loop_videos_description": "Habilita la reproducció en bucle del vídeo en els detalls",
"make": "Make",
"loop_videos_description": "Habilita la reproducció en bucle del vídeo en els detalls.",
"make": "Fer",
"manage_shared_links": "Spravovat sdílené odkazy",
"manage_sharing_with_partners": "",
"manage_the_app_settings": "",

View file

@ -601,7 +601,7 @@
"unable_to_change_favorite": "Nelze změnit oblíbení položky",
"unable_to_change_location": "Nelze změnit polohu",
"unable_to_change_password": "Nelze změnit heslo",
"unable_to_change_visibility": "Nelze změnit viditelnost pro {count, plural, one {# osobu} few {# osoby} other {# lidí}}",
"unable_to_change_visibility": "Nelze změnit viditelnost u {count, plural, one {# osoby} few {# osob} other {# lidí}}",
"unable_to_check_item": "Nelze zkontrolovat položku",
"unable_to_check_items": "Nelze zkontrolovat položky",
"unable_to_complete_oauth_login": "Nelze dokončit OAuth přihlášení",
@ -729,6 +729,10 @@
"host": "Hostitel",
"hour": "Hodina",
"image": "Obrázek",
"image_alt_text_date": "v {date}",
"image_alt_text_people": "{count, plural, =1 {a {person1}} =2 {s {person1} a {person2}} =3 {s {person1}, {person2}, a {person3}} other {s {person1}, {person2}, a {others, number} dalšími}}",
"image_alt_text_place": "v {city}, {country}",
"image_taken": "{isVideo, select, true {Video pořízeno} other {Obrázek požízen}}",
"img": "Img",
"immich_logo": "Immich Logo",
"immich_web_interface": "Webové rozhraní Immich",
@ -1189,7 +1193,7 @@
"view_previous_asset": "Zobrazit předchozí položku",
"view_stack": "Zobrazit zásobník",
"viewer": "Prohlížeč",
"visibility_changed": "Viditelnost změněna pro {count, plural, one {# osobu} few {# osoby} other {# lidí}}",
"visibility_changed": "Viditelnost změněna u {count, plural, one {# osoby} few {# osob} other {# lidí}}",
"waiting": "Čekající",
"warning": "Upozornění",
"week": "Týden",

View file

@ -729,6 +729,10 @@
"host": "Host",
"hour": "Stunde",
"image": "Bild",
"image_alt_text_date": "am {date}",
"image_alt_text_people": "{count, plural, =1 {mit {person1}} =2 {mit {person1} und {person2}} =3 {mit {person1}, {person2} und {person3}} other {mit {person1}, {person2} und {others, number} anderen}}",
"image_alt_text_place": "in {city}, {country}",
"image_taken": "{isVideo, select, true {Video aufgenommen} other {Bild aufgenommen}}",
"img": "Img",
"immich_logo": "Immich-Logo",
"immich_web_interface": "Immich Webschnittstelle",
@ -928,7 +932,7 @@
"previous_memory": "Vorherige Erinnerung",
"previous_or_next_photo": "Vorheriges oder nächstes Foto",
"primary": "Primär",
"profile_image_of_user": "Profilbild von {title}",
"profile_image_of_user": "Profilbild von {user}",
"profile_picture_set": "Profilbild gesetzt.",
"public_album": "Öffentliches Album",
"public_share": "Öffentliche Teilung",

View file

@ -692,12 +692,19 @@
"group_year": "Group by year",
"has_quota": "Has quota",
"hi_user": "Hi {name} ({email})",
"hide_all_people": "Hide all people",
"hide_gallery": "Hide gallery",
"hide_named_person": "Hide person {name}",
"hide_password": "Hide password",
"hide_person": "Hide person",
"hide_unnamed_people": "Hide unnamed people",
"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}}",
"immich_logo": "Immich Logo",
"immich_web_interface": "Immich Web Interface",
"import_from_json": "Import from JSON",
@ -1018,6 +1025,7 @@
"sharing_sidebar_description": "Display a link to Sharing in the sidebar",
"shift_to_permanent_delete": "press ⇧ to permanently delete asset",
"show_album_options": "Show album options",
"show_all_people": "Show all people",
"show_and_hide_people": "Show & hide people",
"show_file_location": "Show file location",
"show_gallery": "Show gallery",
@ -1080,7 +1088,6 @@
"to_trash": "Trash",
"toggle_settings": "Toggle settings",
"toggle_theme": "Toggle theme",
"toggle_visibility": "Toggle visibility",
"total_usage": "Total usage",
"trash": "Trash",
"trash_all": "Trash All",

View file

@ -729,6 +729,8 @@
"host": "Host",
"hour": "Hora",
"image": "Imagen",
"image_alt_text_date": "El {date}",
"image_alt_text_place": "En {city}, {country}",
"img": "",
"immich_logo": "Logo de Immich",
"immich_web_interface": "Interfaz Web de Immich",
@ -928,7 +930,7 @@
"previous_memory": "Recuerdo anterior",
"previous_or_next_photo": "Foto anterior o siguiente",
"primary": "Básico",
"profile_image_of_user": "Foto de perfil de {title}",
"profile_image_of_user": "Foto de perfil de {user}",
"profile_picture_set": "Conjunto de imágenes de perfil.",
"public_album": "Álbum público",
"public_share": "Compartir públicamente",

View file

@ -52,21 +52,21 @@
"force_delete_user_warning": "هشدار: این عمل باعث حذف فوری کاربر و تمام فایل‌ها می‌شود. این عمل قابل بازگشت نیست و فایل‌ها قابل بازیابی نیستند.",
"forcing_refresh_library_files": "بروزرسانی اجباری تمام فایل‌های کتابخانه",
"image_format_description": "فرمت WebP فایل‌های کوچکتری نسبت به JPEG ایجاد می‌کند، اما زمان کدگذاری آن کندتر است.",
"image_prefer_embedded_preview": "",
"image_prefer_embedded_preview": "ترجیحات پیش‌نمایش تعبیه‌شده",
"image_prefer_embedded_preview_setting_description": "استفاده از پیش‌نمایش داخلی در عکس‌های RAW به عنوان ورودی پردازش تصویر هنگامی که در دسترس باشد. این می‌تواند رنگ‌های دقیق‌تری را برای برخی تصاویر تولید کند، اما کیفیت پیش‌نمایش به دوربین بستگی دارد و ممکن است تصویر آثار فشرده‌سازی بیشتری داشته باشد.",
"image_prefer_wide_gamut": "",
"image_prefer_wide_gamut": "ترجیحات گستره رنگی وسیع",
"image_prefer_wide_gamut_setting_description": "برای تصاویر کوچک از فضای رنگی Display P3 استفاده کنید. این کار باعث حفظ زنده بودن رنگ‌ها در تصاویر با گستره رنگی وسیع می‌شود، اما ممکن است تصاویر در دستگاه‌های قدیمی با نسخه‌های قدیمی مرورگر به شکل متفاوتی نمایش داده شوند. تصاویر با فضای رنگی sRGB به همان حالت sRGB نگه داشته می‌شوند تا از تغییرات رنگی جلوگیری شود.",
"image_preview_format": "فرمت نمایش",
"image_preview_resolution": "",
"image_preview_resolution_description": "",
"image_quality": "",
"image_quality_description": "",
"image_settings": "",
"image_settings_description": "",
"image_thumbnail_format": "",
"image_thumbnail_resolution": "",
"image_thumbnail_resolution_description": "",
"job_concurrency": "",
"image_preview_resolution": "وضوح پیش نمایش",
"image_preview_resolution_description": "از این فرمت برای مشاهده یک عکس و همچنین برای یادگیری ماشین استفاده می‌شود. وضوح بالاتر می‌تواند جزئیات بیشتری را حفظ کند، اما زمان بیشتری برای رمزگذاری نیاز دارد، حجم فایل‌ها را بزرگتر می‌کند و ممکن است باعث کاهش پاسخگویی برنامه شود.",
"image_quality": "کیفیت",
"image_quality_description": "کیفیت تصویر از 1 تا 100. هرچه بالاتر باشد، کیفیت بهتر است اما فایل‌های بزرگ‌تری تولید می‌کند. این گزینه بر روی تصاویر پیش‌نمایش و بندانگشتی تأثیر می‌گذارد.",
"image_settings": "تنظیمات عکس",
"image_settings_description": "مدیریت کیفیت و وضوح تصاویر تولید شده",
"image_thumbnail_format": "قالب تصویر بندانگشتی",
"image_thumbnail_resolution": "وضوح تصویر بندانگشتی",
"image_thumbnail_resolution_description": "از این فرمت برای مشاهده گروهی عکس‌ها (مانند صفحه اصلی، نمایش آلبوم و غیره) استفاده می‌شود. وضوح بالاتر می‌تواند جزئیات بیشتری را حفظ کند، اما زمان بیشتری برای رمزگذاری نیاز دارد، حجم فایل‌ها را بزرگتر می‌کند و ممکن است باعث کاهش پاسخگویی برنامه شود.",
"job_concurrency": "همزمانی {job}",
"job_not_concurrency_safe": "این کار ایمنی همزمانی را تضمین نمی‌کند.",
"job_settings": "تنظیمات کار",
"job_settings_description": "مدیریت همزمانی کار",
@ -76,60 +76,61 @@
"library_created": "کتابخانه ایجاد شده: {library}",
"library_cron_expression": "عبارت کرون",
"library_cron_expression_description": "تنظیم فاصله زمانی اسکن با استفاده از فرمت کرون. برای اطلاعات بیشتر لطفا به مثال‌های <link>Crontab Guru</link> مراجعه کنید",
"library_cron_expression_presets": "",
"library_deleted": "",
"library_import_path_description": "",
"library_scanning": "",
"library_scanning_description": "",
"library_scanning_enable_description": "",
"library_settings": "",
"library_settings_description": "",
"library_tasks_description": "",
"library_watching_enable_description": "",
"library_watching_settings": "",
"library_watching_settings_description": "",
"logging_enable_description": "",
"logging_level_description": "",
"logging_settings": "",
"machine_learning_clip_model": "",
"machine_learning_clip_model_description": "",
"machine_learning_duplicate_detection": "",
"machine_learning_duplicate_detection_enabled": "",
"library_cron_expression_presets": "پیش‌تنظیمات عبارت Cron",
"library_deleted": "کتابخانه حذف شد",
"library_import_path_description": "یک پوشه برای وارد کردن مشخص کنید. این پوشه، به همراه زیرپوشه‌ها، برای یافتن تصاویر و ویدیوها اسکن خواهد شد.",
"library_scanning": "اسکن دوره ای",
"library_scanning_description": "تنظیم اسکن دوره‌ای کتابخانه",
"library_scanning_enable_description": "فعال کردن اسکن دوره‌ای کتابخانه",
"library_settings": "کتابخانه خارجی",
"library_settings_description": "مدیریت تنظیمات کتابخانه خارجی",
"library_tasks_description": "انجام وظایف کتابخانه",
"library_watching_enable_description": "نظارت بر تغییرات فایل در کتابخانه‌های خارجی",
"library_watching_settings": "نظارت بر کتابخانه (آزمایشی)",
"library_watching_settings_description": "نظارت خودکار بر فایل‌های تغییر یافته",
"logging_enable_description": "فعال سازی ورود",
"logging_level_description": "وقتی فعال باشد، از چه سطح گزارش استفاده شود.",
"logging_settings": "گزارشات",
"machine_learning_clip_model": "مدل CLIP",
"machine_learning_clip_model_description": "نام یک مدل CLIP که در <link>اینجا</link> فهرست شده است. توجه داشته باشید که پس از تغییر مدل، باید کار 'جستجوی هوشمند' را برای همه تصاویر دوباره اجرا کنید.",
"machine_learning_duplicate_detection": "تشخیص تکراری ها",
"machine_learning_duplicate_detection_enabled": "فعال سازی تشخیص تکراری ها",
"machine_learning_duplicate_detection_enabled_description": "",
"machine_learning_duplicate_detection_setting_description": "",
"machine_learning_enabled": "",
"machine_learning_enabled_description": "",
"machine_learning_facial_recognition": "",
"machine_learning_facial_recognition_description": "",
"machine_learning_facial_recognition_model": "",
"machine_learning_facial_recognition_model_description": "",
"machine_learning_facial_recognition_setting": "",
"machine_learning_facial_recognition_setting_description": "",
"machine_learning_max_detection_distance": "",
"machine_learning_max_detection_distance_description": "",
"machine_learning_max_recognition_distance": "",
"machine_learning_max_recognition_distance_description": "",
"machine_learning_min_detection_score": "",
"machine_learning_min_detection_score_description": "",
"machine_learning_min_recognized_faces": "",
"machine_learning_min_recognized_faces_description": "",
"machine_learning_settings": "",
"machine_learning_settings_description": "",
"machine_learning_smart_search": "",
"machine_learning_smart_search_description": "",
"machine_learning_smart_search_enabled": "",
"machine_learning_smart_search_enabled_description": "",
"machine_learning_url_description": "",
"manage_concurrency": "",
"manage_log_settings": "",
"map_dark_style": "",
"map_enable_description": "",
"map_light_style": "",
"map_reverse_geocoding": "",
"map_reverse_geocoding_enable_description": "",
"map_reverse_geocoding_settings": "",
"map_settings": "",
"map_settings_description": "",
"machine_learning_duplicate_detection_setting_description": "از تعبیه‌های CLIP برای یافتن تکراری‌های احتمالی استفاده کنید",
"machine_learning_enabled": "فعال سازی یادگیری ماشین",
"machine_learning_enabled_description": "اگر غیرفعال شود، تمامی ویژگی‌های یادگیری ماشین بدون توجه به تنظیمات زیر غیرفعال خواهند شد.",
"machine_learning_facial_recognition": "تشخیص چهره",
"machine_learning_facial_recognition_description": "تشخیص، شناسایی و گروه‌بندی چهره‌ها در تصاویر",
"machine_learning_facial_recognition_model": "مدل تشخیص چهره",
"machine_learning_facial_recognition_model_description": "مدل‌ها به ترتیب اندازه،نزولی فهرست شده‌اند. مدل‌های بزرگتر کندتر هستند و از حافظه بیشتری استفاده می‌کنند، اما نتایج بهتری تولید می‌کنند. توجه داشته باشید که پس از تغییر مدل، باید کار تشخیص چهره را برای تمامی تصاویر دوباره اجرا کنید.",
"machine_learning_facial_recognition_setting": "فعال سازی تشخیص چهره",
"machine_learning_facial_recognition_setting_description": "اگر غیرفعال شود، تصاویر برای تشخیص چهره کدگذاری نخواهند شد و بخش افراد در صفحه کاوش پر نخواهد شد.",
"machine_learning_max_detection_distance": "حداکثر فاصله تشخیص",
"machine_learning_max_detection_distance_description": "حداکثر فاصله بین دو تصویر برای در نظر گرفتن آنها به عنوان تکراری، در محدوده 0.001 تا 0.1 است. مقادیر بالاتر تکراری‌های بیشتری را شناسایی می‌کند، اما ممکن است منجر به تشخیص‌های اشتباه شود.",
"machine_learning_max_recognition_distance": "حداکثر فاصله تشخیص",
"machine_learning_max_recognition_distance_description": "حداکثر فاصله بین دو چهره برای در نظر گرفتن آنها به عنوان یک شخص، در محدوده 0 تا 2 است. کاهش این مقدار می‌تواند از برچسب زدن دو نفر به عنوان یک شخص جلوگیری کند، در حالی که افزایش آن می‌تواند از برچسب زدن یک نفر به عنوان دو شخص مختلف جلوگیری کند. توجه داشته باشید که ادغام دو نفر به عنوان یک نفر آسان‌تر از تقسیم یک نفر به دو نفر است، بنابراین در صورت امکان مبنای یک آستانه کمتر را انتخاب کنید.",
"machine_learning_min_detection_score": "حداقل امتیاز تشخیص",
"machine_learning_min_detection_score_description": "حداقل امتیاز اعتماد برای تشخیص یک چهره، در بازه 0 تا 1 قرار دارد. مقادیر کمتر باعث تشخیص بیشتر چهره می‌شود، اما ممکن است منجر به تشخیص‌های اشتباه شود.",
"machine_learning_min_recognized_faces": "حداقل چهره های شناخته شده",
"machine_learning_min_recognized_faces_description": "حداقل تعداد چهره‌های تشخیص داده شده برای ایجاد یک شخص. افزایش این مقدار باعث دقیق‌تر شدن تشخیص چهره می‌شود، اما همزمان باعث افزایش احتمال این می‌شود که یک چهره به یک شخص نسبت داده نشود.",
"machine_learning_settings": "تنظیمات یادگیری ماشین",
"machine_learning_settings_description": "مدیریت ویژگی‌ها و تنظیمات یادگیری ماشین",
"machine_learning_smart_search": "جستجوی هوشمند",
"machine_learning_smart_search_description": "جستجوی تصاویر با استفاده از تعبیه‌های CLIP به صورت معنایی",
"machine_learning_smart_search_enabled": "فعال سازی جستجوی هوشمند",
"machine_learning_smart_search_enabled_description": "اگر غیرفعال باشد، تصاویر برای جستجوی هوشمند رمزگذاری نخواهند شد.",
"machine_learning_url_description": "آدرسی اینترنتی سرور یادگیری ماشین",
"manage_concurrency": "مدیریت همزمانی",
"manage_log_settings": "مدیریت تنظیمات گزارش",
"map_dark_style": "حالت تیره",
"map_enable_description": "فعال سازی ویژگی های نقشه",
"map_light_style": "حالت روشن",
"map_manage_reverse_geocoding_settings": "مدیریت تنظیمات <link>کدگذاری مکانی معکوس </link>",
"map_reverse_geocoding": "ژئوکدینگ معکوس",
"map_reverse_geocoding_enable_description": "فعال سازی ژئوکدینگ معکوس",
"map_reverse_geocoding_settings": "تنظیمات ژئوکدینگ معکوس",
"map_settings": "تنظیمات نقشه و مکانهای روی نقشه",
"map_settings_description": "مدیریت تنظیمات نقشه",
"map_style_description": "",
"metadata_extraction_job": "",
"metadata_extraction_job_description": "",

View file

@ -43,7 +43,7 @@
"crontab_guru": "Crontab Guru",
"disable_login": "Poista kirjautuminen käytöstä",
"disabled": "Ei käytössä",
"duplicate_detection_job_description": "Suorittaa koneoppimisen, jolla havaitaan samankaltaisia kuvia. Tukeutuu Smart Search:iin",
"duplicate_detection_job_description": "Tunnista samankaltaiset kuvat käyttäen koneoppimista. Tukeutuu Smart Search:iin",
"exclusion_pattern_description": "Poissulkevat määritteet mahdollistavat tiettyjen tiedostojen ja kansioiden jättämisen pois kirjastoasi skannatessa. Tästä on hyötyä jos kansiot sisältävät tiedostoja mitä et halua tuoda, kuten RAW-tiedostot.",
"external_library_created_at": "Ulkoinen kirjasto (luotu {date})",
"external_library_management": "Ulkoisen kirjaston hallinta",
@ -179,7 +179,7 @@
"oauth_storage_label_claim": "Tallennustilan nimikkeen valtuutusväittämä (claim)",
"oauth_storage_label_claim_description": "Määriä käyttäjän tallennustilan nimike tämän väittämän arvoksi automaattisesti.",
"oauth_storage_quota_claim": "Tallennustilan kiintiön väittämä (claim)",
"oauth_storage_quota_claim_description": "Aseta käyttäjän tallennustilakiintiön arvo tähän väittämään.",
"oauth_storage_quota_claim_description": "Aseta automaattisesti käyttäjien tallennustilan määrä tähän arvoon.",
"oauth_storage_quota_default": "Tallennustilan oletuskiintiö (Gt)",
"oauth_storage_quota_default_description": "Käytettävä kiintiön määrä gigatavuissa, käytetään kun väittämää ei ole annettu (0 rajoittamaton kiintiö).",
"offline_paths": "Offline-tilan polut",
@ -190,6 +190,8 @@
"paths_validated_successfully": "Kaikki polut validoitu",
"quota_size_gib": "Kiintiön koko (Gt)",
"refreshing_all_libraries": "Virkistetään kaikki kirjastot",
"registration": "Pääkäyttäjän rekisteröinti",
"registration_description": "Pääkäyttäjänä olet vastuussa järjestelmän hallinnallisista tehtävistä ja uusien käyttäjien luomisesta.",
"removing_offline_files": "Poistetaan Offline-tiedostot",
"repair_all": "Korjaa kaikki",
"repair_matched_items": "Löytyi {count, plural, one {# osuma} other {# osumaa}}",
@ -210,11 +212,18 @@
"sidecar_job_description": "Havaitse tai synkronoi tiedostojen kylkiäismetadatat",
"slideshow_duration_description": "Montako sekuntia kuvaa näytetään",
"smart_search_job_description": "Aja aineiston koneoppiminen, jolla tuet älykästä hakua",
"storage_template_date_time_description": "Kohteen luontipäivän aikaleimaa käytetään datetime tietoa varten",
"storage_template_date_time_sample": "Esimerkki päivämäärä {date}",
"storage_template_enable_description": "Ota käyttöön tallennustilan mallit",
"storage_template_hash_verification_enabled": "Tarkistussumman varmennus epäonnistui",
"storage_template_hash_verification_enabled": "Tarkistussumman varmennus käytössä",
"storage_template_hash_verification_enabled_description": "Ottaa käyttöön varmistussummien laskennan. Älä poista käytöstä jollet ole aivan varma seurauksista",
"storage_template_migration": "Tallennustilan mallien migraatio",
"storage_template_migration_job": "Tallennustilan migrointityö",
"storage_template_migration_description": "Käytä nykyistä <link>{template}:a</link> aikaisemmin lähetettyihin kohteisiin",
"storage_template_migration_info": "Malli vaikuttaa vain uusiin kohteisiin. Käyttääksesi mallia jo olemassa oleviin kohteisiin, aja <link>{job}</link>.",
"storage_template_migration_job": "Tallennustilan mallin muutostyö",
"storage_template_more_details": "Saadaksesi lisätietoa tästä ominaisuudesta, katso <template-link>Tallennustilan Mallit</template-link> sekä <implications-link>mihin se vaikuttaa</implications-link>",
"storage_template_onboarding_description": "Kun tämä ominaisuus on käytössä, se järjestää tiedostot automaattisesti käyttäjän määrittämän mallin perusteella. Vakausongelmien vuoksi ominaisuus on oletuksena poistettu käytöstä. Lisätietoja on <link>dokumentaatiossa</link>.",
"storage_template_path_length": "Arvioitu tiedostopolun pituusrajoitus: <b>{length, number}</b>/{limit, number}",
"storage_template_settings": "Tallennustilan malli",
"storage_template_settings_description": "Hallitse palvelimelle ladatun aineiston kansiorakennetta ja tiedostonimiä",
"system_settings": "Järjestelmäasetukset",
@ -239,7 +248,7 @@
"transcoding_advanced_options_description": "Asetukset, joita useimpien käyttäjien ei tulisi muuttaa",
"transcoding_audio_codec": "Äänikoodekki",
"transcoding_audio_codec_description": "Opus on paras laadultaan, mutta ei välttämättä ole yhteensopiva vanhempien laitteiden tai sovellusten kanssa.",
"transcoding_bitrate_description": "",
"transcoding_bitrate_description": "Videot, jotka ylittävät enimmäisbittinopeuden tai eivät ole hyväksytyssä muodossa",
"transcoding_constant_quality_mode": "Tasaisen laadun tyyppi",
"transcoding_constant_quality_mode_description": "ICQ on parempi kuin CQP, mutta jotkut laitteistokiihdyttimet eivät tue sitä. Tätä asetusta käytetään oletuksena laatuun pohjautuvissa muunnoksissa, paitsi NVENC mikä ei tue ICQ:ta.",
"transcoding_constant_rate_factor": "",
@ -307,29 +316,42 @@
"admin_password": "Ylläpitäjän salasana",
"administration": "Ylläpito",
"advanced": "Edistyneet",
"age_months": "Ikä {months, plural, one {# kuukausi} other {# kuukautta}}",
"age_year_months": "Ikä 1 vuosi, {months, plural, one {# kuukausi} other {# kuukautta}}",
"age_years": "{years, plural, other {Ikä #v}}",
"album_added": "Albumi lisätty",
"album_added_notification_setting_description": "Saa sähköpostia kun sinut lisätään jaettuun albumiin",
"album_cover_updated": "Albumin kansikuva päivitetty",
"album_delete_confirmation": "Haluatko varmasti poistaa albumin {album}?\nJos albumi on jaettu, muut eivät pääse siihen enää.",
"album_info_updated": "Albumin tiedot päivitetty",
"album_name": "Albumin nimi",
"album_options": "Albumin asetukset",
"album_share_no_users": "Näyttää että olet jakanut tämän albumin kaikkien kanssa, tai sinulla ei ole käyttäjiä joille jakaa.",
"album_updated": "Albumi päivitetty",
"album_updated_setting_description": "Saa sähköpostia kun jaetussa albumissa on uutta sisältöä",
"album_user_removed": "{user} poistettu",
"albums": "Albumit",
"albums_count": "{count, plural, one {{count, number} Albumi} other {{count, number} Albumit}}",
"albums_count": "{count, plural, one {{count, number} albumi} other {{count, number} albumia}}",
"all": "Kaikki",
"all_albums": "Kaikki albumit",
"all_people": "Kaikki henkilöt",
"all_videos": "Kaikki videot",
"allow_dark_mode": "Salli tumma tila",
"allow_edits": "Salli muutokset",
"api_key": "API-avain",
"api_keys": "API-avaimet",
"app_settings": "Sovellusasetukset",
"appears_in": "Versiosta 1.106.4 lähtien käytetään vain aineiston sivupalkissa kuvaamaan termiä \"esiintyy [albumeissa]\"",
"archive": "Arkistoi",
"archive": "Arkisto",
"archive_or_unarchive_photo": "Arkistoi kuva tai palauta arkistosta",
"archive_size": "Arkiston koko",
"archive_size_description": "Määritä arkiston koko latauksissa (Gt)",
"archived": "Arkistoitu",
"archived_count": "{count, plural, other {Arkistoitu #}}",
"are_these_the_same_person": "Ovatko he sama henkilö?",
"are_you_sure_to_do_this": "Haluatko varmasti tehdä tämän?",
"asset_added_to_album": "Lisätty albumiin",
"asset_adding_to_album": "Lisätään albumiin...",
"asset_offline": "Aineisto offline-tilassa",
"asset_skipped": "Ohitettu",
"asset_uploaded": "Lähetetty",
@ -433,6 +455,7 @@
"date_after": "Päivä jälkeen",
"date_and_time": "Päivämäärä ja aika",
"date_before": "Päivä ennen",
"date_of_birth_saved": "Syntymäaika tallennettu",
"date_range": "Päivämäärän rajaus",
"day": "Päivä",
"deduplicate_all": "Poista kaikkien kaksoiskappaleet",
@ -525,12 +548,16 @@
"error_downloading": "Tiedostoa {filename} ei voitu ladata",
"error_removing_assets_from_album": "Medioiden poisto epäonnistui. Katso konsolista lisätietoja",
"error_selecting_all_assets": "Kaikkia medioita ei voitu valita",
"exclusion_pattern_already_exists": "Tämä poissulkemismalli on jo olemassa.",
"failed_job_command": "Komento {command} työlle {job} epäonnistui",
"failed_to_create_album": "Albumin luonti epäonnistui",
"failed_to_create_shared_link": "Jaetun linkin luonti epäonnistui",
"failed_to_edit_shared_link": "Jaetun linkin muokkaus epäonnistui",
"failed_to_get_people": "Henkilöiden haku epäonnistui",
"failed_to_load_asset": "Kohteen lataus epäonnistui",
"failed_to_load_assets": "Kohteiden lataus epäonnistui",
"failed_to_stack_assets": "Medioiden pinoaminen epäonnistui",
"failed_to_unstack_assets": "Medioiden pinoamisen purku epäonnistui",
"import_path_already_exists": "Tämä tuontipolku on jo olemassa.",
"incorrect_email_or_password": "Väärä sähköpostiosoite tai salasana",
"paths_validation_failed": "{paths, plural, one {# polun} other {# polun}} validointi epäonnistui",
@ -588,7 +615,8 @@
"unable_to_update_location": "Sijainnin päivitys epäonnistui",
"unable_to_update_settings": "Asetusten päivitys epäonnistui",
"unable_to_update_timeline_display_status": "Aikajanalla näyttämisen asetusta ei voitu tallettaa",
"unable_to_update_user": "Käyttäjän muokkaus epäonnistui"
"unable_to_update_user": "Käyttäjän muokkaus epäonnistui",
"unable_to_upload_file": "Tiedostoa ei voitu ladata"
},
"every_day_at_onepm": "",
"every_night_at_midnight": "",
@ -601,6 +629,8 @@
"expired": "Voimassaolo päättynyt",
"expires_date": "Vanhenee {date}",
"explore": "Tutki",
"export": "Vie",
"export_as_json": "Vie JSON-muodossa",
"extension": "",
"external_libraries": "",
"failed_to_get_people": "",
@ -608,48 +638,54 @@
"favorite_or_unfavorite_photo": "",
"favorites": "Suosikit",
"feature": "",
"feature_photo_updated": "",
"feature_photo_updated": "Kansikuva ladattu",
"featurecollection": "",
"file_name": "",
"file_name_or_extension": "",
"filename": "",
"filename": "Tiedostonimi",
"files": "",
"filetype": "",
"filetype": "Tiedostotyyppi",
"filter_people": "",
"fix_incorrect_match": "",
"force_re-scan_library_files": "",
"forward": "",
"forward": "Eteenpäin",
"general": "",
"get_help": "",
"getting_started": "",
"go_back": "",
"go_back": "Palaa",
"go_to_search": "",
"go_to_share_page": "",
"group_albums_by": "",
"group_no": "Ei ryhmitystä",
"group_owner": "Ryhmitä omistajan mukaan",
"group_year": "Ryhmitä vuoden mukaan",
"has_quota": "",
"hi_user": "Hei {name} ({email})",
"hide_gallery": "",
"hide_password": "",
"hide_person": "",
"host": "",
"hour": "",
"hour": "Tunti",
"image": "Kuva",
"img": "",
"immich_logo": "",
"import_path": "",
"in_archive": "",
"in_albums": "{count, plural, one {# Albumissa} other {# albumissa}}",
"in_archive": "Arkistossa",
"include_archived": "Sisällytä arkistoidut",
"include_shared_albums": "",
"include_shared_partner_assets": "",
"individual_share": "",
"info": "",
"info": "Lisätietoja",
"interval": {
"day_at_onepm": "",
"hours": "",
"night_at_midnight": "",
"night_at_twoam": ""
},
"invite_people": "",
"invite_people": "Kutsu ihmisiä",
"invite_to_album": "Kutsu albumiin",
"items_count": "{count, plural, one {# kpl} other {# kpl}}",
"job_settings_description": "",
"jobs": "Taustatehtävät",
"keep": "Säilytä",
@ -659,7 +695,7 @@
"language_setting_description": "Valitse suosimasi kieli",
"last_seen": "Viimeksi nähty",
"latest_version": "Viimeisin versio",
"leave": "",
"leave": "Lähde",
"let_others_respond": "Anna muiden vastata",
"level": "Taso",
"library": "Kirjasto",
@ -668,8 +704,8 @@
"link_options": "",
"link_to_oauth": "",
"linked_oauth_account": "",
"list": "",
"loading": "",
"list": "Lista",
"loading": "Ladataan",
"loading_search_results_failed": "",
"log_out": "Kirjaudu ulos",
"log_out_all_devices": "Kirjaudu ulos kaikilta laitteilta",
@ -683,81 +719,91 @@
"make": "Valmistaja",
"manage_shared_links": "Hallitse jaettuja linkkejä",
"manage_sharing_with_partners": "",
"manage_the_app_settings": "",
"manage_your_account": "",
"manage_the_app_settings": "Hallitse sovelluksen asetuksia",
"manage_your_account": "Hallitse tiliäsi",
"manage_your_api_keys": "",
"manage_your_devices": "",
"manage_your_oauth_connection": "",
"map": "",
"map": "Kartta",
"map_marker_with_image": "",
"map_settings": "Kartta-asetukset",
"media_type": "",
"media_type": "Median tyyppi",
"memories": "",
"memories_setting_description": "",
"menu": "",
"merge": "",
"merge_people": "",
"merge_people_successfully": "",
"minimize": "",
"minute": "",
"missing": "",
"memory": "Muisto",
"menu": "Valikko",
"merge": "Yhdistä",
"merge_people": "Yhdistä henkilöt",
"merge_people_successfully": "Henkilöt yhdistetty",
"merged_people_count": "{count, plural, one {# Henkilö} other {# henkilöä}} yhdistetty",
"minimize": "PIenennä",
"minute": "Minuutti",
"missing": "Puuttuu",
"model": "Malli",
"month": "Kuukauden mukaan",
"more": "",
"moved_to_trash": "",
"my_albums": "",
"more": "Enemmän",
"moved_to_trash": "Siirretty roskakoriin",
"my_albums": "Albumini",
"name": "Nimi",
"name_or_nickname": "",
"name_or_nickname": "Nimi tai lempinimi",
"never": "ei koskaan",
"new_api_key": "",
"new_api_key": "Uusi API Key",
"new_password": "Uusi salasana",
"new_person": "",
"new_user_created": "",
"newest_first": "",
"new_person": "Uusi henkilö",
"new_user_created": "Uusi käyttäjä lisätty",
"new_version_available": "UUSI VERSIO SAATAVILLA",
"newest_first": "Uusin ensin",
"next": "Seuraava",
"next_memory": "",
"no": "",
"no_albums_message": "",
"next_memory": "Seuraava muisto",
"no": "Ei",
"no_albums_message": "Luo albumi pitääksesi kuvat ja videot järjestyksessä",
"no_albums_with_name_yet": "Näyttää siltä, ettei sinulla ole yhtään tämän nimistä albumia.",
"no_albums_yet": "Näyttää siltä, ettei sinulla ole vielä yhtään albumia.",
"no_archived_assets_message": "",
"no_assets_message": "",
"no_exif_info_available": "",
"no_assets_message": "NAPAUTA LATAAKSESI ENSIMMÄISEN KUVASI",
"no_exif_info_available": "EXIF-tietoa ei saatavilla",
"no_explore_results_message": "",
"no_favorites_message": "",
"no_favorites_message": "Lisää suosikkeja löytääksesi nopeasti parhaat kuvasi ja videosi",
"no_libraries_message": "",
"no_name": "",
"no_name": "Ei nimeä",
"no_places": "",
"no_results": "",
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"no_shared_albums_message": "Luo albumi, jotta voit jakaa kuvia ja videoita toisille",
"not_in_any_album": "Ei yhdessäkään albumissa",
"notes": "Muistiinpanot",
"notification_toggle_setting_description": "",
"notifications": "Ilmoitukset",
"notifications_setting_description": "",
"oauth": "",
"oauth": "OAuth",
"offline": "",
"ok": "",
"oldest_first": "",
"online": "",
"ok": "Ok",
"oldest_first": "Vanhin ensin",
"onboarding_welcome_user": "Tervetuloa {user}",
"online": "Online",
"only_favorites": "",
"only_refreshes_modified_files": "",
"open_the_search_filters": "",
"options": "Vaihtoehdot",
"organize_your_library": "",
"other": "",
"other_devices": "",
"or": "tai",
"organize_your_library": "Järjestele kirjastosi",
"original": "alkuperäinen",
"other": "Muut",
"other_devices": "Toiset laitteet",
"other_variables": "",
"owned": "Omistettu",
"owner": "Omistaja",
"partner_sharing": "",
"partners": "",
"partner": "Kumppani",
"partner_can_access": "{partner} voi päästä",
"partner_sharing": "Kumppanijako",
"partners": "Kumppanit",
"password": "Salasana",
"password_does_not_match": "",
"password_required": "",
"password_reset_success": "",
"password_does_not_match": "Salasanat eivät täsmää",
"password_required": "Salasana vaaditaan",
"password_reset_success": "Salasanan nollaus onnistui",
"past_durations": {
"days": "",
"hours": "",
"years": ""
"days": "{years, plural, one {Viimeisin päivä} other {Viimeiset # päivää}}",
"hours": "{years, plural, one {Viimeisin tunti} other {Viimeiset # tuntia}}",
"years": "{years, plural, one {Viimeisin vuosi} other {Viimeiset # vuotta}}"
},
"path": "Polku",
"pattern": "",
@ -771,7 +817,8 @@
"permanent_deletion_warning": "",
"permanent_deletion_warning_setting_description": "",
"permanently_delete": "Poista pysyvästi",
"permanently_deleted_asset": "",
"permanently_deleted_asset": "Media poistettu pysyvästi",
"permanently_deleted_assets_count": "{count, plural, one {# media} other {# mediaa}} poistettu pysyvästi",
"person": "Henkilö",
"person_hidden": "{name}{hidden, select, true { (piilotettu)} other {}}",
"photo_shared_all_users": "Näyttää että olet jakanut kuvasi kaikkien käyttäjien kanssa, tai sinulla ei ole käyttäjää kenelle jakaa.",
@ -794,6 +841,7 @@
"previous_memory": "Edellinen muisto",
"previous_or_next_photo": "Edellinen tai seuraava kuva",
"primary": "Ensisijainen",
"profile_image_of_user": "Käyttäjän {user} profiilikuva",
"profile_picture_set": "Profiilikuva asetettu.",
"public_album": "Julkinen albumi",
"public_share": "Julkinen jako",
@ -832,7 +880,8 @@
"rename": "Nimeä uudelleen",
"repair": "Korjaa",
"repair_no_results_message": "",
"replace_with_upload": "Korvaa ladatulla",
"replace_with_upload": "Lähetä korvaava tiedosto",
"repository": "Tietovarasto",
"require_password": "Vaadi salasana",
"require_user_to_change_password_on_first_login": "Vaadi käyttäjää vaihtamaan salasana seuraavalla kirjautumiskerralla",
"reset": "Nollaa",
@ -873,7 +922,7 @@
"search_no_people_named": "Ei \"{name}\" nimisiä henkilöitä",
"search_people": "Etsi ihmisiä",
"search_places": "Etsi paikkoja",
"search_state": "",
"search_state": "Etsi tilaa...",
"search_timezone": "Etsi aikavyöhyke...",
"search_type": "Etsinnän tyyppi",
"search_your_photos": "Etsi kuvia",

View file

@ -928,7 +928,7 @@
"previous_memory": "Souvenir précédent",
"previous_or_next_photo": "Photo précédente ou suivante",
"primary": "Primaire",
"profile_image_of_user": "Image de profil de {title}",
"profile_image_of_user": "Image de profil de {user}",
"profile_picture_set": "Photo de profil définie.",
"public_album": "Album public",
"public_share": "Partage public",

View file

@ -2,192 +2,192 @@
"about": "אודות",
"account": "חשבון",
"account_settings": "הגדרות חשבון",
"acknowledge": "אשר",
"acknowledge": "הבנתי",
"action": "פעולה",
"actions": "פעולות",
"active": "פעיל",
"activity": "פעילות",
"activity_changed": "פעילות היא {enabled, select, true {enabled} other {disabled}}",
"activity_changed": "הפעילות {enabled, select, true {מופעלת} other {מושבתת}}",
"add": "הוסף",
"add_a_description": "הוסף תיאור",
"add_a_location": "הוסף מיקום",
"add_a_name": "הוסף שם",
"add_a_title": "הוסף כותרת",
"add_exclusion_pattern": "הוסף דפוס החרגה",
"add_import_path": "הוספת נתיב יבוא",
"add_import_path": "הוסף נתיב יבוא",
"add_location": "הוסף מיקום",
"add_more_users": "הוסף עוד משתמשים",
"add_partner": "הוסף שותף",
"add_path": "הוסף נתיב",
"add_photos": "הוסף תמונות",
"add_to": "הוספה אל..",
"add_to_album": "הוספה אל אלבום",
"add_to_shared_album": "הוספה אל אלבום משותף",
"add_to": "הוסף ל..",
"add_to_album": "הוסף לאלבום",
"add_to_shared_album": "הוסף לאלבום משותף",
"added_to_archive": "נוסף לארכיון",
"added_to_favorites": "נוסף למועדפים",
"added_to_favorites_count": "{count} נוספו למועדפים",
"admin": {
"add_exclusion_pattern_description": "הוסף דפוסי החרגה. נתמכת התאמת דפוסים באמצעות *, ** ו-?. כדי להתעלם מכל הקבצים בתיקיה כלשהי בשם \"Raw\", השתמש ב \"**/Raw/**\". כדי להתעלם מכל הקבצים המסתיימים ב \"tif.\", השתמש ב \"tif.*/**\". כדי להתעלם מנתיב מוחלט, השתמש ב \"**/path/to/ignore\".",
"add_exclusion_pattern_description": "הוסף דפוסי החרגה. נתמכת התאמת דפוסים באמצעות *, ** ו-?. כדי להתעלם מכל הקבצים בתיקיה כלשהי בשם \"Raw\", השתמש ב \"**/Raw/**\". כדי להתעלם מכל הקבצים המסתיימים ב \"tif.\", השתמש ב \"tif.*/**\". כדי להתעלם מנתיב מוחלט, השתמש ב \"**/נתיב/להתעלמות\".",
"authentication_settings": "הגדרות אימות",
"authentication_settings_description": "נהל הגדרות סיסמה, OAuth, ואימות אחר",
"authentication_settings_disable_all": "האם את/ה בטוח/ה שברצונך להשבית את כל שיטות ההתחברות? כניסה למערכת תהיה מושבתת לחלוטין.",
"authentication_settings_reenable": "בשביל להפעיל מחדש, השתמש ב <link>Server Command</link>.",
"authentication_settings_reenable": "כדי לאפשר מחדש, השתמש ב<link>פקודת שרת</link>.",
"background_task_job": "משימות רקע",
"check_all": "סמן הכל",
"cleared_jobs": "נוקו משימות עבור: {job}",
"config_set_by_file": הגדרות תצורה מוגדרים על ידי קובץ תצורה",
"confirm_delete_library": "האם אתה בטוח שברצונך למחוק את הספרייה {library}?",
"confirm_delete_library_assets": "האם אתה בטוח שברצונך למחוק את הספרייה הזו? פעולה זו תמחק את כל {count} הנכסים הכלולים מ-Immich, פעולה זו אינה ניתנת לביטול. הקבצים יישארו בדיסק.",
"confirm_email_below": "כדי לאשר הזן \"{email}\"",
"confirm_reprocess_all_faces": "האם את/ה בטוח/ה שברצונך לעבד מחדש את כל הפרצופים? זה גם ינקה אנשים עם שמות.",
"config_set_by_file": תצורה מוגדרת כעת על ידי קובץ תצורה",
"confirm_delete_library": "האם את/ה בטוח שברצונך למחוק את הספרייה {library}?",
"confirm_delete_library_assets": "האם את/ה בטוח שברצונך למחוק את הספרייה הזו? זה ימחק את {count, plural, one {נכס # המוכל} other {כל # הנכסים המוכלים}} מ-Immich ואינו ניתן לביטול. קבצים יישארו בדיסק.",
"confirm_email_below": "כדי לאשר, יש להקליד \"{email}\" למטה",
"confirm_reprocess_all_faces": "האם את/ה בטוח/ה שברצונך לעבד מחדש את כל הפרצופים? זה גם ינקה אנשים בעלי שם.",
"confirm_user_password_reset": "האם את/ה בטוח/ה שברצונך לאפס את הסיסמה של המשתמש {user}?",
"crontab_guru": "Crontab Guru",
"disable_login": "השבת כניסה",
"disabled": "מושבת",
"duplicate_detection_job_description": פעל למידת מכונה על נכסים כדי לזהות תמונות דומות. נשען על חיפוש חכם",
"exclusion_pattern_description": "דפוסי אי הכללה מאפשרים לך להתעלם מקבצים ומתיקיות בעת סריקת הספרייה שלך. זה שימושי אם יש לך תיקיות המכילות קבצים שאינך רוצה לייבא, לדוגמא קובצי RAW.",
"external_library_created_at": "ספרייה חיצונית (נוצרה בתאריך {date})",
"duplicate_detection_job_description": רץ למידת מכונה על נכסים כדי לזהות תמונות דומות. נשען על חיפוש חכם",
"exclusion_pattern_description": "דפוסי החרגה מאפשרים לך להתעלם מקבצים ומתיקיות בעת סריקת הספרייה שלך. זה שימושי אם יש לך תיקיות המכילות קבצים שאינך רוצה לייבא, כגון קובצי RAW.",
"external_library_created_at": "ספרייה חיצונית (נוצרה ב-{date})",
"external_library_management": "ניהול ספרייה חיצונית",
"face_detection": "איתור פנים",
"face_detection_description": "מזהה את הפנים בנכסים באמצעות למידת מכונה. עבור סרטונים, רק התמונה הממוזערת משמשת עבור איתור פנים. האפשרות \"הכל\" (להפעיל הכל מחדש) מעבד את כל הנכסים מחדש. האפשרות \"חסרים\" מעבד נכסים שעדיין לא עובדו. פרצופים שזוהו יעמדו בתור לזיהוי פנים לאחר השלמת איתור הפנים, לאחר מכן ישויכו לאנשים קיימים או חדשים בהתאמה.",
"facial_recognition_job_description": "משייך פרצופים שזוהו לתוך אנשים בהתאמה. שלב זה פועל לאחר השלמת איתור פנים. האפשרות \"הכל\" (להפעיל הכל מחדש) מקבץ את כל הפרצופים. האפשרות \"חסרים\" מוסיפה לתור פנים שלא הוקצה להם אדם.",
"failed_job_command": "הפקודה {command} נכשלה עבור העבודה: {job}",
"force_delete_user_warning": "אזהרה: פעולה זו תסיר מיד את המשתמש ואת כל הנכסים. לא ניתן לבטל פעולה זו ולא ניתן לשחזר את הקבצים.",
"face_detection_description": "אתר את הפנים בנכסים באמצעות למידת מכונה. עבור סרטונים, רק התמונה הממוזערת נלקחת בחשבון. \"הכל\" מעבד (מחדש) את כל הנכסים. \"חסרים\" מוסיף לתור נכסים שלא עובדו עדיין. לאחר שאיתור הפנים הושלם, פנים שאותרו יעמדו בתור לזיהוי פנים המשייך אותן לאנשים קיימים או חדשים.",
"facial_recognition_job_description": "קבץ פרצופים שאותרו לאנשים. שלב זה מורץ לאחר השלמת איתור פנים. \"הכל\" מקבץ (מחדש) את כל הפרצופים. \"חסרים\" מוסיף לתור פנים שלא הוקצה להם אדם.",
"failed_job_command": "הפקודה {command} נכשלה עבור המשימה: {job}",
"force_delete_user_warning": "אזהרה: פעולה זו תסיר מיד את המשתמש ואת כל הנכסים. לא ניתן לבטל פעולה זו והקבצים לא ניתנים לשחזור.",
"forcing_refresh_library_files": "כפה רענון של כל קבצי הספרייה",
"image_format_description": "WebP מספק קבצים קטנים יותר מ JPEG אך איטי יותר לקידוד.",
"image_prefer_embedded_preview": "העדף תמונה מוטמעת",
"image_prefer_embedded_preview_setting_description": "השתמש בתצוגות מקדימות מוטמעות בתמונות RAW כקלט לעיבוד תמונה כאשר זמינות. זה יכול להפיק צבעים מדויקים יותר עבור תמונות מסוימות, אבל איכות התצוגה המקדימה תלויה במצלמה וייתכן שלתמונה יהיו יותר פריטי דחיסה.",
"image_prefer_wide_gamut": "העדפה לסולם צבעים רחב",
"image_format_description": "WebP מפיק קבצים קטנים יותר מ JPEG, אך הוא איטי יותר לקידוד.",
"image_prefer_embedded_preview": "העדף תצוגה מקדימה מוטמעת",
"image_prefer_embedded_preview_setting_description": "השתמש בתצוגות מקדימות מוטמעות בתמונות RAW כקלט לעיבוד תמונה כאשר זמינות. זה יכול להפיק צבעים מדויקים יותר עבור תמונות מסוימות, אבל האיכות של התצוגה המקדימה היא תלוית מצלמה ולתמונה עשויים להיות יותר פגמי דחיסה.",
"image_prefer_wide_gamut": "העדף סולם צבעים רחב",
"image_prefer_wide_gamut_setting_description": "השתמש ב-Display P3 לתמונות ממוזערות. זה משמר טוב יותר את החיוניות של תמונות עם מרחבי צבע רחבים, אבל תמונות עשויות להופיע אחרת במכשירים ישנים עם גרסת דפדפן ישנה. תמונות sRGB נשמרות כ-sRGB כדי למנוע שינויי צבע.",
"image_preview_format": "פורמט תצוגה מקדימה",
"image_preview_resolution": "רזולוציית תצוגה מקדימה",
"image_preview_resolution_description": "משמש בעת צפייה בתמונה בודדת ועבור למידת מכונה. רזולוציות גבוהות יותר יכולות לשמר פרטים רבים יותר, אך לוקחות זמן רב יותר לקידוד, יש להן גדלי קבצים גדולים יותר ויכולות להפחית את התגובתיות של האפליקציה.",
"image_preview_resolution_description": "משמש בעת צפייה בתמונה בודדת ועבור למידת מכונה. רזולוציות גבוהות יותר יכולות לשמר פירוט רב יותר אך לוקחות יותר זמן לקידוד, יש להן גדלי קבצים גדולים יותר, ויכולות להפחית את התגובתיות של האפליקציה.",
"image_quality": "איכות",
"image_quality_description": "איכות תמונה מ-1-100. ערך גבוה יותר משפר איכות אך מייצר קבצים גדולים יותר, אופציה זאת משפיעה על תצוגה מקדימה ותמונות ממוזערות.",
"image_quality_description": "איכות תמונה מ-1-100. גבוה יותר עדיף לאיכות אך מייצר קבצים גדולים יותר, אפשרות זו משפיעה על התצוגה המקדימה ותמונות ממוזערות.",
"image_settings": "הגדרות תמונה",
"image_settings_description": "נהל את האיכות והרזולוציה של תמונות שייווצרו",
"image_settings_description": "נהל את האיכות והרזולוציה של תמונות שנוצרו",
"image_thumbnail_format": "פורמט תמונה ממוזערת",
"image_thumbnail_resolution": "רזולוציית תמונה ממוזערת",
"image_thumbnail_resolution_description": "משמש בעת צפייה בקבוצות של תמונות (ציר זמן ראשי, תצוגת אלבום וכו'). רזולוציות גבוהות יותר יכולות להציג פרטים רבים יותר, אך ייקח זמן רב יותר לקידוד, יש להן גדלי קבצים גדולים יותר ויכולות להפחית את המהירות של האפליקציה.",
"job_concurrency": "מקבילות {job}",
"job_not_concurrency_safe": "עבודה זו אינה בטוחה במקביל.",
"job_settings": "הגדרות של JOBS",
"job_settings_description": "ניהול מקביליות של JOBS",
"job_status": "סטטוס Job",
"jobs_delayed": "{jobCount} עיכוב",
"jobs_failed": "{jobCount} נכשל",
"library_created": "ספרייה נוצרה: {library}",
"library_cron_expression": "ביטוי זמן עם Cron Expression",
"library_cron_expression_description": "הגדר את מרווח הסריקה באמצעות פורמט cron. למידע נוסף, עיין ב <link>Crontab Guru</link>",
"library_cron_expression_presets": "תבנית מוכנה של Cron expression",
"image_thumbnail_resolution_description": "משמש בעת צפייה בקבוצות של תמונות (ציר זמן ראשי, תצוגת אלבום וכו'). רזולוציות גבוהות יותר יכולות לשמר פירוט רב יותר אך לוקחות יותר זמן לקידוד, יש להן גדלי קבצים גדולים יותר, ויכולות להפחית את התגובתיות של האפליקציה.",
"job_concurrency": "בו-זמניות של {job}",
"job_not_concurrency_safe": "משימה זו אינה בטוחה במקביל.",
"job_settings": "הגדרות משימה",
"job_settings_description": "ניהול בו-זמניות של משימה",
"job_status": "מצב משימה",
"jobs_delayed": "{jobCount, plural, other {# בעיכוב}}",
"jobs_failed": "{jobCount, plural, other {# נכשלו}}",
"library_created": "נוצרה ספרייה: {library}",
"library_cron_expression": "ביטוי cron",
"library_cron_expression_description": "הגדר את מרווח הסריקה באמצעות פורמט ה-cron. למידע נוסף אנא פנה למשל אל <link>Crontab Guru</link>",
"library_cron_expression_presets": "הגדרות ביטוי cron קבועות מראש",
"library_deleted": "ספרייה נמחקה",
"library_import_path_description": "ציין תיקיה לייבוא. תיקייה זו, כולל תיקיות משנה, תיסרק לאיתור תמונות וסרטונים.",
"library_import_path_description": "ציין תיקיה לייבוא. תיקייה זו, כולל תיקיות משנה, תיסרק עבור תמונות וסרטונים.",
"library_scanning": "סריקה מחזורית",
"library_scanning_description": "הגדר את הסריקה המחזורית של הספרייה",
"library_scanning_enable_description": "אפשר את הסריקה המחזורית של הספרייה",
"library_scanning_description": "הגדר סריקת ספרייה מחזורית",
"library_scanning_enable_description": "אפשר סריקת ספרייה מחזורית",
"library_settings": "ספרייה חיצונית",
"library_settings_description": "נהל את הגדרות הספרייה החיצונית",
"library_settings_description": "נהל הגדרות ספרייה חיצונית",
"library_tasks_description": "ביצוע משימות ספרייה",
"library_watching_enable_description": "סרוק את הספרייה החיצונית בזמן אמת עבור שינוים בקבצים",
"library_watching_settings": "סריקת ספרייה בזמן אמת (ניסיוני)",
"library_watching_settings_description": "סרוק באופן אוטומטי עבור קבצים שהשתנו",
"logging_enable_description": "הפעל יומני מערכת",
"library_watching_enable_description": "עקוב אחר שינויי קובץ בספריות חיצוניות",
"library_watching_settings": "צפייה בספרייה (ניסיוני)",
"library_watching_settings_description": "צפה אוטומטית לאתר קבצים שהשתנו",
"logging_enable_description": "אפשר רישום ביומן",
"logging_level_description": "כאשר פועל, באיזה רמת יומן לתעד.",
"logging_settings": "תיעוד ביומן",
"logging_settings": "רישום ביומן",
"machine_learning_clip_model": "מודל CLIP",
"machine_learning_clip_model_description": "שמו של מודל ה CLIP המופיע <link>כאן</link>. שים לב שעליך להפעיל מחדש את העבודה 'חיפוש חכם (Smart Search)' עבור כל הנכסים בעת שינוי מודל CLIP.",
"machine_learning_duplicate_detection": "זיהוי כפילויות",
"machine_learning_duplicate_detection_enabled": "אפשר זיהוי כפילויות בתמונות",
"machine_learning_duplicate_detection_enabled_description": "אם לא פעיל, קבצים זהים בדיוק עדיין לא ישוכפלו.",
"machine_learning_duplicate_detection_setting_description": "השתמש בהטמעות של CLIP למציאת תמונות דומות",
"machine_learning_clip_model_description": "שמו של מודל CLIP רשום <link>כאן</link>. שימ/י לב שעליך להפעיל מחדש את המשימה 'חיפוש חכם' עבור כל התמונות בעת שינוי מודל.",
"machine_learning_duplicate_detection": "איתור כפילויות",
"machine_learning_duplicate_detection_enabled": "אפשר איתור כפילויות",
"machine_learning_duplicate_detection_enabled_description": "אם מושבת, נכסים זהים בדיוק עדיין יהיו מבוטלי שכפול.",
"machine_learning_duplicate_detection_setting_description": "השתמש בהטמעות של CLIP כדי למצוא כפילויות אפשריות",
"machine_learning_enabled": "אפשר למידת מכונה",
"machine_learning_enabled_description": "אם כבוי, כל אפשרויות למידת המכונה יהיו מושבתות ללא קשר להגדרות המופיעות בקטע זה.",
"machine_learning_enabled_description": "אם מושבת, כל תכונות למידת מכונה יהיו מושבתות ללא קשר להגדרות שלהלן.",
"machine_learning_facial_recognition": "זיהוי פנים",
"machine_learning_facial_recognition_description": "זהה סווג והקצה פנים מתוך תמונות",
"machine_learning_facial_recognition_description": "אתר, זהה וקבץ פנים בתמונות",
"machine_learning_facial_recognition_model": "מודל זיהוי פנים",
"machine_learning_facial_recognition_model_description": "הדגמים מפורטים בסדר גודל יורד. דגמים גדולים יותר הם איטיים יותר ומשתמשים ביותר זיכרון, אך מניבים תוצאות טובות יותר. שים לב שעליך להפעיל מחדש את עבודת זיהוי הפנים עבור כל התמונות בעת שינוי מודל.",
"machine_learning_facial_recognition_model_description": "הדגמים מפורטים בסדר גודל יורד. דגמים גדולים יותר הם איטיים יותר ומשתמשים ביותר זיכרון, אך מניבים תוצאות טובות יותר. שים לב שעליך להפעיל מחדש את משימת זיהוי הפנים עבור כל התמונות בעת שינוי מודל.",
"machine_learning_facial_recognition_setting": "אפשר זיהוי פנים",
"machine_learning_facial_recognition_setting_description": "אם מושבת, התמונות לא יקודדו לזיהוי פנים ולא יאכלסו את הקטע אנשים בקטע \"אני רוצה לראות\".",
"machine_learning_max_detection_distance": "מרחק זיהוי מקסימלי",
"machine_learning_max_detection_distance_description": "מרחק מקסימלי בין שתי תמונות כדי לראות בהן כפילויות, נע בין 0.001-0.1. ערכים גבוהים יותר יאתרו כפילויות נוספות, אך עלולים לגרום לתוצאות חיוביות שגויות.",
"machine_learning_max_recognition_distance": "מרחק זיהוי מקסימלי",
"machine_learning_max_recognition_distance_description": "מרחק מירבי בין שני פנים שייחשב לאותו אדם, נע בין 0-2. הורדת הערך הזה יכולה למנוע תיוג של שני אנשים כאותו אדם, בעוד שהעלאתו יכולה למנוע תיוג של אותו אדם כשני אנשים שונים. שימו לב שקל יותר למזג שני אנשים מאשר לפצל אדם אחד לשניים, אז כדאי שתטעו בצד של סף נמוך יותר כשאפשר.",
"machine_learning_min_detection_score": "ציון זיהוי מינימלי",
"machine_learning_min_detection_score_description": "ציון ביטחון מינימלי לזיהוי פנים מ-0-1. ערכים נמוכים יותר יזהו יותר פרצופים אך עלולים לגרום לתוצאות חיוביות שגויות.",
"machine_learning_min_recognized_faces": "מינימום פרצופים לאדם",
"machine_learning_min_recognized_faces_description": "המספר המינימלי של פרצופים מזוהים ליצירת אדם. הגדלת זה הופכת את זיהוי הפנים למדויק יותר אך ייתכן ולא תראו את האדם בקטע \"אנשים\" אם אין לו את המינימום פרצופים המוגדר.",
"machine_learning_facial_recognition_setting_description": "אם מושבת, תמונות לא יקודדו לזיהוי פנים ולא יאכלסו את קטע האנשים בעמוד ה\"חקור\".",
"machine_learning_max_detection_distance": "מרחק איתור מרבי",
"machine_learning_max_detection_distance_description": "מרחק מרבי בין שתי תמונות להערכתן ככפילות, נע בין 0.001-0.1. ערכים גבוהים יותר יאתרו כפילויות נוספות, אך עלולים לגרום לתוצאות חיוביות שגויות.",
"machine_learning_max_recognition_distance": "מרחק זיהוי מרבי",
"machine_learning_max_recognition_distance_description": "מרחק מרבי בין שני פנים שייחשב לאותו אדם, נע בין 0-2. הורדת ערך זה יכולה למנוע תיוג של שני אנשים כאותו אדם, בעוד שהעלאתו יכולה למנוע תיוג של אותו אדם כשני אנשים שונים. שים לב שקל יותר למזג שני אנשים מאשר לפצל אדם אחד לשניים, אז כדאי לטעות בצד של סף נמוך יותר כשאפשר.",
"machine_learning_min_detection_score": "ציון איתור מינימלי",
"machine_learning_min_detection_score_description": "ציון ביטחון מינימלי לאיתור פנים מ-0-1. ערכים נמוכים יותר יאתרו יותר פנים אך עלולים לגרום לתוצאות חיוביות שגויות.",
"machine_learning_min_recognized_faces": "מינימום פנים מזוהים",
"machine_learning_min_recognized_faces_description": "המספר המינימלי של פנים מזוהים ליצירת אדם. הגדלת ערך זה הופכת את זיהוי הפנים למדויק יותר בעלות של הגברת הסיכוי שלא יוקצו פנים לאדם.",
"machine_learning_settings": "הגדרות למידת מכונה",
"machine_learning_settings_description": "נהל את התכונות וההגדרות של למידת המכונה",
"machine_learning_smart_search": "חיפוש חכם",
"machine_learning_smart_search_description": "חפש תמונות באופן סמנטי באמצעות הטמעות של CLIP",
"machine_learning_smart_search_enabled": "אפשר חיפוש חכם",
"machine_learning_smart_search_enabled_description": "אם מושבת, תמונות לא יקודדו לחיפוש חכם.",
"machine_learning_url_description": "כתובת ה URL של שרת עבור למידת מכונה",
"manage_concurrency": "נהל מקבילות",
"machine_learning_url_description": "כתובת האתר של שרת למידת מכונה",
"manage_concurrency": "נהל בו-זמניות",
"manage_log_settings": "נהל הגדרות רישום ביומן",
"map_dark_style": "עיצוב כהה",
"map_enable_description": "אפשר אפשרויות מפה",
"map_enable_description": "אפשר תכונות מפה",
"map_light_style": "עיצוב בהיר",
"map_manage_reverse_geocoding_settings": "Manage <link>Reverse Geocoding</link> settings",
"map_manage_reverse_geocoding_settings": "נהל הגדרות <link>קידוד גאוגרפי הפוך</link>",
"map_reverse_geocoding": "קידוד גיאוגרפי הפוך",
"map_reverse_geocoding_enable_description": "אפשר קידוד גיאוגרפי הפוך",
"map_reverse_geocoding_settings": "הגדרות קידוד גיאוגרפי הפוך",
"map_settings": "הגדרות מפה ו-GPS",
"map_settings_description": "נהל את הגדרות המפה",
"map_style_description": "כתובת אתר לעיצוב מפה בשם style.json",
"metadata_extraction_job": "חלץ מטא-דאטא (EXIF)",
"map_style_description": "כתובת אתר לערכת נושא של מפה style.json",
"metadata_extraction_job": "חלץ מטא-נתונים",
"metadata_extraction_job_description": "חלץ מידע מטא נתונים מכל נכס, כגון GPS ורזולוציה",
"migration_job": "מיזוג",
"migration_job": "העברה",
"migration_job_description": "העבר תמונות ממוזערות של נכסים ופנים למבנה התיקיות העדכני ביותר",
"no_paths_added": "לא נוספו נתיבים",
"no_pattern_added": "לא נוספה תבנית",
"note_apply_storage_label_previous_assets": "הערה: כדי להחיל את תווית האחסון על נכסים שהועלו בעבר, הפעל את",
"note_apply_storage_label_previous_assets": "הערה: כדי להחיל את תווית האחסון על נכסים שהועלו בעבר, הרץ את",
"note_cannot_be_changed_later": "הערה: אי אפשר לשנות את זה מאוחר יותר!",
"note_unlimited_quota": "הערה: הזן 0 עבור מכסת אחסון בלתי מוגבלת",
"notification_email_from_address": "מכתובת",
"notification_email_from_address_description": "כתובת דואר אלקטרוני של השולח, לדוגמה: \"שרת תמונות ה-Immich שלי <noreply@immich.app>\"",
"notification_email_from_address_description": "כתובת דואר אלקטרוני של השולח, לדוגמה: \"Immich שרת תמונות <noreply@immich.app>\"",
"notification_email_host_description": "כתובת מארח של שרת הדוא\"ל (למשל smtp.immich.app)",
"notification_email_ignore_certificate_errors": "התעלם משגיאות אישור",
"notification_email_ignore_certificate_errors_description": "התעלם משגיאות אימות אישור TLS (לא מומלץ)",
"notification_email_ignore_certificate_errors": "התעלם משגיאות תעודה",
"notification_email_ignore_certificate_errors_description": "התעלם משגיאות אימות תעודת TLS (לא מומלץ)",
"notification_email_password_description": "סיסמה לשימוש בעת אימות עם שרת הדואר האלקטרוני",
"notification_email_port_description": "יציאה של שרת האימייל (למשל 25, 465 או 587)",
"notification_email_sent_test_email_button": "שלח מייל בדיקה ושמור",
"notification_email_setting_description": "הגדרות לשליחת הודעות אימייל",
"notification_email_test_email": "שלח מייל בדיקה",
"notification_email_test_email_failed": "נכשל בשליחת מייל בדיקה, בדוק את הערכים שלך",
"notification_email_test_email_sent": "מייל בדיקה נשלח אל {email}. בדוק את תיבת המייל שלך.",
"notification_email_port_description": "יציאה של שרת הדוא\"ל (למשל 25, 465, או 587)",
"notification_email_sent_test_email_button": "שלח דוא\"ל בדיקה ושמור",
"notification_email_setting_description": "הגדרות לשליחת התראות דוא\"ל",
"notification_email_test_email": "שלח דוא\"ל בדיקה",
"notification_email_test_email_failed": "נכשל בשליחת דוא\"ל בדיקה, בדוק את הערכים שלך",
"notification_email_test_email_sent": "דוא\"ל בדיקה נשלח אל {email}. נא לבדוק את תיבת הדואר הנכנס שלך.",
"notification_email_username_description": "שם משתמש לשימוש בעת אימות עם שרת הדוא\"ל",
"notification_enable_email_notifications": "אפשר הודעות דוא\"ל",
"notification_enable_email_notifications": "אפשר התראות דוא\"ל",
"notification_settings": "הגדרות התראות",
"notification_settings_description": "נהל את הגדרות ההתראות, כולל התראות אימייל",
"notification_settings_description": "נהל הגדרות התראות, כולל דוא\"ל",
"oauth_auto_launch": "הפעלה אוטומטית",
"oauth_auto_launch_description": "התחל את זרימת ההתחברות של OAuth באופן אוטומטי עם הניווט לדף ההתחברות של Immich",
"oauth_auto_launch_description": "התחל את זרימת ההתחברות של OAuth באופן אוטומטי עם הניווט לדף ההתחברות",
"oauth_auto_register": "רישום אוטומטי",
"oauth_auto_register_description": "רשום באופן אוטומטי משתמש חדש לאחר הרשמה עם OAuth",
"oauth_button_text": "טקסט של הכפתור",
"oauth_client_id": "Client ID",
"oauth_client_secret": "Client Secret",
"oauth_auto_register_description": "רשום אוטומטית משתמשים חדשים לאחר כניסה עם OAuth",
"oauth_button_text": "טקסט לחצן",
"oauth_client_id": "מזהה לקוח",
"oauth_client_secret": "סוד לקוח",
"oauth_enable_description": "התחבר עם OAuth",
"oauth_issuer_url": "Issuer URL",
"oauth_mobile_redirect_uri": "Mobile redirect URI",
"oauth_mobile_redirect_uri_override": "Mobile redirect URI override",
"oauth_mobile_redirect_uri_override_description": "אפשר כאשר 'app.immich:/' הוא לא כתובת URI חוקית.",
"oauth_scope": "Scope",
"oauth_issuer_url": "כתובת אתר המנפיק",
"oauth_mobile_redirect_uri": "URI להפניה מחדש בנייד",
"oauth_mobile_redirect_uri_override": "עקיפת URI להפניה מחדש בנייד",
"oauth_mobile_redirect_uri_override_description": "אפשר כאשר 'app.immich:/' היא כתובת להפניה מחדש לא חוקית.",
"oauth_scope": "רמת הרשאה",
"oauth_settings": "OAuth",
"oauth_settings_description": "נהל הגדרות התחברות עם OAuth",
"oauth_settings_more_details": "למידע נוסף אודות תכונה זו, בדוק את <link>התיעוד</link>.",
"oauth_signing_algorithm": "מנגנון חתימה",
"oauth_storage_label_claim": "לירוש שם תווית אחסון",
"oauth_storage_label_claim_description": "הגדר אוטומטית את תווית האחסון של המשתמש לערך של הערך הזה.",
"oauth_storage_quota_claim": "לירוש את מכסת האחסון",
"oauth_storage_quota_claim_description": "הגדר אוטומטית את מכסת האחסון של המשתמש לערך של הערך הזה.",
"oauth_signing_algorithm": "אלגוריתם חתימה",
"oauth_storage_label_claim": "דרישת תווית אחסון",
"oauth_storage_label_claim_description": "הגדר אוטומטית את תווית האחסון של המשתמש לערך של דרישה זו.",
"oauth_storage_quota_claim": "דרישת מכסת אחסון",
"oauth_storage_quota_claim_description": "הגדר אוטומטית את מכסת האחסון של המשתמש לערך של דרישה זו.",
"oauth_storage_quota_default": "מכסת אחסון ברירת מחדל (GiB)",
"oauth_storage_quota_default_description": "מכסה ב-GiB לשימוש כאשר לא מסופק ערך (הזן 0 עבור מכסה בלתי מוגבלת).",
"oauth_storage_quota_default_description": "מכסה ב-GiB לשימוש כאשר לא מסופקת דרישה (הזן 0 עבור מכסה בלתי מוגבלת).",
"offline_paths": "נתיבים לא מקוונים",
"offline_paths_description": "תוצאות אלו עשויות להיות עקב מחיקה ידנית של קבצים שאינם חלק מספרייה חיצונית.",
"password_enable_description": "התחבר עם דוא\"ל וסיסמה",
"password_settings": "סיסמת התחברות",
"password_settings_description": "נהל את הגדרות הכניסה לסיסמה",
"password_settings_description": "נהל הגדרות סיסמת התחברות",
"paths_validated_successfully": "כל הנתיבים אומתו בהצלחה",
"quota_size_gib": "גודל מכסת האחסון (GiB)",
"refreshing_all_libraries": "מרענן את כל הספריות",
@ -195,19 +195,19 @@
"registration_description": "מכיוון שאתה המשתמש הראשון במערכת, אתה תוקצה כמנהל ואתה אחראי על משימות ניהול, ומשתמשים נוספים ייווצרו על ידך.",
"removing_offline_files": "הסרת קבצים לא מקוונים",
"repair_all": "תקן הכל",
"repair_matched_items": "Matched {count, plural, one {# item} other {# items}}",
"repaired_items": "Repaired {count, plural, one {# item} other {# items}}",
"repair_matched_items": "{count, plural, one {פריט # תואם} other {# פריטים תואמים}}",
"repaired_items": "{count, plural, one {פריט # תוקן} other {# פריטים תוקנו}}",
"require_password_change_on_login": "דרוש מהמשתמש לשנות סיסמה בכניסה הראשונה",
"reset_settings_to_default": "אפס את ההגדרות לברירת המחדל",
"reset_settings_to_recent_saved": "אפס את ההגדרות להגדרות שנשמרו לאחרונה",
"reset_settings_to_default": "אפס הגדרות לברירת המחדל",
"reset_settings_to_recent_saved": "אפס הגדרות להגדרות שנשמרו לאחרונה",
"scanning_library_for_changed_files": "סורק ספרייה לאיתור קבצים שהשתנו",
"scanning_library_for_new_files": "סורק ספרייה לאיתור קבצים חדשים",
"send_welcome_email": "שלח מייל ברוכים הבאים",
"send_welcome_email": "שלח דוא\"ל קבלת פנים",
"server_external_domain_settings": "דומיין חיצוני",
"server_external_domain_settings_description": "דומיין עבור שיתוף חיצוני, כולל http(s)://",
"server_external_domain_settings_description": "דומיין עבור קישורים משותפים ציבוריים, כולל http(s)://",
"server_settings": "הגדרות שרת",
"server_settings_description": "נהל הגדרות שרת",
"server_welcome_message": "הודעת ברוכים הבאים",
"server_welcome_message": "הודעת פתיחה",
"server_welcome_message_description": "הודעה שמוצגת במסך ההתחברות.",
"sidecar_job": "מטא נתונים של Sidecar",
"sidecar_job_description": "גלה או סנכרן מטא-נתונים צדדיים ממערכת הקבצים",
@ -216,12 +216,12 @@
"storage_template_date_time_description": "חותמת הזמן של נכסים משמש עבור תאריך ושעה",
"storage_template_date_time_sample": "זמן לדוגמא {date}",
"storage_template_enable_description": "הפעל מנוע תבנית אחסון",
"storage_template_hash_verification_enabled": "אימות Hash נכשל",
"storage_template_hash_verification_enabled": "אימות גיבוב מופעל",
"storage_template_hash_verification_enabled_description": "מאפשר אימות hash, אל תשבית זאת אלא אם כן אתה בטוח בהשלכות",
"storage_template_migration": "העברת תבנית אחסון",
"storage_template_migration_description": "החל את ה <link>{template}</link> על נכסים שהועלו",
"storage_template_migration_info": "שינויים בתבנית יחולו רק על נכסים חדשים. כדי להחיל את התבנית גם על נכסים שהועלו בעבר, הפעל את <link>{job}</link>.",
"storage_template_migration_job": "עבודת העברת אחסון",
"storage_template_migration_job": "משימת העברת תבנית אחסון",
"storage_template_more_details": "לפרטים נוספים אודות תכונה זו, עיין ב<template-link>תבנית האחסון</template-link> ו<implications-link>השלכותיה</implications-link>",
"storage_template_onboarding_description": "כאשר מופעלת, תכונה זו תארגן אוטומטית קבצים בהתבסס על תבנית שהמשתמש הגדיר. עקב בעיות יציבות התכונה כבויה כברירת מחדל. לפרטים נוספים, בבקשה לראות את ה<link>תיעוד</link>.",
"storage_template_path_length": "מגבלת אורך נתיב משוערת: <b>{length, number}</b>/{limit, number}",
@ -260,13 +260,13 @@
"transcoding_hardware_acceleration": "Hardware Acceleration - האצת חומרה",
"transcoding_hardware_acceleration_description": "ניסיוני; הרבה יותר מהיר, אבל תהיה באיכות נמוכה יותר באותו קצב סיביות",
"transcoding_hardware_decoding": "Hardware decoding - פענוח חומרה",
"transcoding_hardware_decoding_setting_description": "חל רק על NVENC ו-RKMPP. מאפשר האצה מקצה לקצה במקום רק להאיץ קידוד. ייתכן שלא יפעל בכל הסרטונים.",
"transcoding_hardware_decoding_setting_description": "חל רק על NVENC, QSV ו-RKMPP. מאפשר האצה מקצה לקצה במקום רק להאיץ קידוד. ייתכן שלא יפעל על כל הסרטונים.",
"transcoding_hevc_codec": "קידוד HEVC",
"transcoding_max_b_frames": "B-frames מקסימלי",
"transcoding_max_b_frames_description": "ערכים גבוהים יותר משפרים את יעילות הדחיסה, אך מאטים את הקידוד. ייתכן שלא יהיה תואם להאצת חומרה במכשירים ישנים יותר. 0 משבית את B-frames, בעוד ש-1 מגדיר ערך זה באופן אוטומטי.",
"transcoding_max_bitrate": "bitrate מקסימלי",
"transcoding_max_bitrate": "קצב סיביות מרבי",
"transcoding_max_bitrate_description": "קביעת קצב סיביות מקסימלי יכולה להפוך את גדלי הקבצים לצפויים יותר בעלות קלה לאיכות. ב-720p, ערכים טיפוסיים הם 2600k עבור VP9 או HEVC, או 4500k עבור H.264. מושבת אם מוגדר ל-0.",
"transcoding_max_keyframe_interval": "מרווח keyframe מקסימלי",
"transcoding_max_keyframe_interval": "מרווח תמונת מפתח מרבי",
"transcoding_max_keyframe_interval_description": "מגדיר את מרחק הפריימים המרבי בין פריימים מפתח. ערכים נמוכים מחמירים את יעילות הדחיסה, אך משפרים את זמני החיפוש ועשויים לשפר את האיכות בסצנות עם תנועה מהירה. 0 מגדיר ערך זה באופן אוטומטי.",
"transcoding_optimal_description": "סרטונים גבוהים מרזולוציית היעד או לא בפורמט מקובל",
"transcoding_preferred_hardware_device": "מכשיר חומרה מועדף",
@ -284,13 +284,13 @@
"transcoding_temporal_aq_description": "חל רק על NVENC. מגביר את האיכות של סצנות עם פרטים גבוהים, בתנועה נמוכה. ייתכן שלא יהיה תואם למכשירים ישנים יותר.",
"transcoding_threads": "תהליכים במקביל",
"transcoding_threads_description": "ערכים גבוהים יותר מובילים לקידוד מהיר יותר, אך מגבירים את העומס על השרת. ערך זה לא צריך להיות יותר ממספר ליבות המעבד. ממקסם את הניצול אם מוגדר ל-0.",
"transcoding_tone_mapping": "Tone-mapping",
"transcoding_tone_mapping": "מיפוי גוונים",
"transcoding_tone_mapping_description": "ניסיונות לשמר את המראה של סרטוני HDR כשהם מומרים ל-SDR. כל אלגוריתם עושה פשרות שונות עבור צבע, פירוט ובהירות. Hable שומר על פרטים, Mobius שומר על צבע, ו Reinhard שומר על בהירות.",
"transcoding_tone_mapping_npl": "Tone-mapping NPL",
"transcoding_tone_mapping_npl": "בהירות שיא נומינלית למיפוי גוונים",
"transcoding_tone_mapping_npl_description": "הצבעים יותאמו כך שיראו נורמליים לתצוגה של בהירות זו. באופן מנוגד לאינטואיציה, ערכים נמוכים מגבירים את בהירות הווידאו ולהיפך מכיוון שהוא מפצה על בהירות התצוגה. 0 מגדיר ערך זה באופן אוטומטי.",
"transcoding_transcode_policy": "מדיניות Transcode",
"transcoding_transcode_policy_description": "מדיניות לגבי מתי יש להמיר סרטון. סרטוני HDR תמיד יקודדו (למעט אם ההמרה מושבתת).",
"transcoding_two_pass_encoding": "Two-pass encoding",
"transcoding_two_pass_encoding": "קידוד בשני מעברים",
"transcoding_two_pass_encoding_setting_description": "המרת קוד בשני מעברים כדי לייצר סרטונים מקודדים טובים יותר. כאשר קצב סיביות מקסימלי מופעל (נדרש כדי שהוא יעבוד עם H.264 ו-HEVC), מצב זה משתמש בטווח קצב סיביות המבוסס על קצב הסיביות המקסימלי ומתעלם מ-CRF. עבור VP9, ניתן להשתמש ב-CRF אם קצב סיביות מקסימלי מושבת.",
"transcoding_video_codec": "וידאו Codec",
"transcoding_video_codec_description": "ל-VP9 יש יעילות גבוהה ותאימות לאינטרנט, אבל לוקח יותר זמן להמיר את הקידוד עבורו. HEVC מתפקד באופן דומה, אך בעל תאימות אינטרנט נמוכה יותר. H.264 תואם באופן נרחב ומהיר לקידוד, אך מייצר קבצים גדולים בהרבה. AV1 הוא ה-codec היעיל ביותר אך לוקה בתמיכה במכשירים ישנים יותר.",
@ -301,8 +301,9 @@
"trash_settings_description": "נהל את הגדרות סל המחזור",
"untracked_files": "קבצים ללא מעקב",
"untracked_files_description": "האפליקציה לא עוקבת אחר קבצים אלה. הם יכולים להיות תוצאות של העברות כושלות, העלאות עם הפרעה באמצע או שנותרו מאחור בגלל באג",
"user_delete_delay": "החשבון והנכסים של <b>{user}</b> יתוזמנו למחיקה לצמיתות בעוד {delay, plural, one {יום #} other {# ימים}}.",
"user_delete_delay_settings": "עיכוב מחיקה",
"user_delete_delay_settings_description": "מספר הימים לאחר ההסרה עד מחיקה לצמיתות של החשבון והנכסים של המשתמש. עבודת מחיקת המשתמש פועלת בחצות כדי לבדוק אם יש משתמשים שמוכנים למחיקה. שינויים בהגדרה זו יוערכו בביצוע הבא.",
"user_delete_delay_settings_description": "מספר הימים לאחר ההסרה עד מחיקה לצמיתות של החשבון והנכסים של המשתמש. משימת מחיקת המשתמש פועלת בחצות כדי לבדוק אם יש משתמשים שמוכנים למחיקה. שינויים בהגדרה זו יוערכו בביצוע הבא.",
"user_delete_immediately": "החשבון והנכסים של <b>{user}</b> יעמדו בתור למחיקה לצמיתות <b>באופן מיידי</b>.",
"user_delete_immediately_checkbox": "שים משתמש ונכסים בתור למחיקה מיידית",
"user_management": "ניהול משתמשים",
@ -323,6 +324,9 @@
"admin_password": "סיסמת מנהל",
"administration": "ניהול",
"advanced": "מתקדם",
"age_months": "גיל {months, plural, one {חודש #} other {# חודשים}}",
"age_year_months": "גיל שנה 1, {months, plural, one {חודש #} other {# חודשים}}",
"age_years": "{years, plural, other {גיל #}}",
"album_added": "אלבום התווסף",
"album_added_notification_setting_description": "קבל הודעת דוא\"ל כאשר אתה מתווסף לאלבום משותף",
"album_cover_updated": "עטיפת האלבום עודכנה",
@ -334,22 +338,22 @@
"album_options": "אפשרויות האלבום",
"album_remove_user": "להסיר משתמש?",
"album_remove_user_confirmation": "האם את/ה בטוחה שברצונך להסיר את {user}?",
"album_share_no_users": "נראה שאת/ה שיתפת את האלבום הזה עם כל המשתמשים או שאין לך אף משתמש לשתף עם.",
"album_share_no_users": "נראה שאת/ה שיתפת את האלבום הזה עם כל המשתמשים או שאין לך אף משתמש לשתף איתו.",
"album_updated": "אלבום עודכן",
"album_updated_setting_description": "קבל הודעת דוא\"ל כאשר לאלבום משותף יש נכסים חדשים",
"album_user_left": "עזב את {album}",
"album_user_removed": "{user} הוסר",
"album_with_link_access": "תן לכל אחד עם הקישור לראות תמונות ואנשים באלבום הזה.",
"album_with_link_access": "אפשר לכל אחד עם הקישור לראות תמונות ואנשים באלבום הזה.",
"albums": "אלבומים",
"albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albums}}",
"albums_count": "{count, plural, one {אלבום {count, number}} other {{count, number} אלבומים}}",
"all": "הכל",
"all_albums": "כל האלבומים",
"all_people": "כל האנשים",
"all_videos": "כל הסרטונים",
"allow_dark_mode": "אפשר מצב כהה",
"allow_edits": "אפשר עריכות",
"allow_public_user_to_download": "התר למשתמש ציבורי להוריד",
"allow_public_user_to_upload": "התר למשתמש ציבורי להעלות",
"allow_public_user_to_download": "אפשר למשתמש ציבורי להוריד",
"allow_public_user_to_upload": "אפשר למשתמש ציבורי להעלות",
"api_key": "מפתח API",
"api_key_description": "הערך הזה יוצג רק פעם אחת. אנא וודא/י שהעתקת אותו לפני סגירת החלון.",
"api_key_empty": "מפתח ה API שלך לא אמור להיות ריק",
@ -361,6 +365,7 @@
"archive_size": "גודל הארכיון",
"archive_size_description": "הגדר את גודל הארכיון להורדות (ב-GiB)",
"archived": "בארכיון",
"archived_count": "{count, plural, other {# הועברו לארכיון}}",
"are_these_the_same_person": "האם אלה אותו האדם?",
"are_you_sure_to_do_this": "האם את/ה בטוח/ה שברצונך לעשות את זה?",
"asset_added_to_album": "נוסף לאלבום",
@ -370,19 +375,23 @@
"asset_has_unassigned_faces": "לנכס יש פרצופים שלא הוקצו",
"asset_hashing": "מגבב...",
"asset_offline": "נכס לא זמין",
"asset_offline_description": "הנכס הזה אינו מקוון. Immich לא יכול לגשת למיקום הקובץ שלו. נא לוודא שהנכס זמין ואז לסרוק מחדש את הספרייה.",
"asset_offline_description": "הנכס הזה אינו מקוון. Immich לא יכול לגשת למיקום הקובץ שלו. נא לוודא שהנכס זמין ואז סרוק מחדש את הספרייה.",
"asset_skipped": "דולג",
"asset_uploaded": "הועלה",
"asset_uploading": "מעלה...",
"assets": "נכסים",
"assets_added_count": "{count, plural, one {נוסף נכס #} other {נוספו # נכסים}}",
"assets_added_to_album_count": "{count, plural, one {נוסף נכס #} other {נוספו # נכסים}} לאלבום",
"assets_added_to_name_count": "{count, plural, one {נוסף נכס #} other {נוספו # נכסים}} אל {name}",
"assets_count": "{count, plural, one {# נכס} other {# נכסים}}",
"assets_moved_to_trash": "Moved {count, plural, one {# asset} other {# assets}} to trash",
"assets_moved_to_trash_count": "{count, plural, one {נכס הועבר} other {נכסים הועברו}} לאשפה",
"assets_permanently_deleted_count": "{count, plural, one {נכס נמחק} other {נכסים נמחקו}} לצמיתות",
"assets_removed_count": "{count, plural, one {נכס הוסר} other {נכסים הוסרו}}",
"assets_moved_to_trash_count": "{count, plural, one {נכס # הועבר} other {# נכסים הועברו}} לאשפה",
"assets_permanently_deleted_count": "{count, plural, one {נכס # נמחק} other {# נכסים נמחקו}} לצמיתות",
"assets_removed_count": "{count, plural, one {נכס # הוסר} other {# נכסים הוסרו}}",
"assets_restore_confirmation": "האם את/ה בטוח שברצונך לשחזר את כל הנכסים שבאשפה? את/ה לא יכול/ה לבטל את הפעולה הזו!",
"assets_restored_count": "{count, plural, one {נכס שוחזר} other {נכסים שוחזרו}}",
"assets_trashed_count": "{count, plural, one {נכס הועבר} other {נכסים הועברו}} לאשפה",
"assets_restored_count": "{count, plural, one {נכס # שוחזר} other {# נכסים שוחזרו}}",
"assets_trashed_count": "{count, plural, one {נכס # הושלך} other {# נכסים הושלכו}} לאשפה",
"assets_were_part_of_album_count": "{count, plural, one {נכס היה} other {נכסים היו}} כבר חלק מהאלבום",
"authorized_devices": "מכשירים מורשים",
"back": "אחורה",
"back_close_deselect": "חזור, סגור, או בטל בחירה",
@ -390,11 +399,11 @@
"birthdate_saved": "תאריך לידה נשמר בהצלחה",
"birthdate_set_description": "תאריך לידה משמש לחישוב הגיל של האדם הזה בזמן תצלום.",
"blurred_background": "רקע מטושטש",
"build": "בנה",
"build_image": "בנה תמונה",
"bulk_delete_duplicates_confirmation": "האם אתה בטוח שברצונך למחוק {count} נכסים כפולים? זה ישמור על הנכס עם המשקל הגדול ביותר של כל קבוצת כפילויות וימחק לצמיתות את כל שאר הכפילויות. אתה לא יכול לבטל את הפעולה הזו!",
"bulk_keep_duplicates_confirmation": "האם אתה בטוח שברצונך לשמור {count} נכסים כפולים? זה יסמן את כל הקבוצות הכפולות כלא רלוונטיות מבלי למחוק דבר.",
"bulk_trash_duplicates_confirmation": "האם אתה בטוח שברצונך להעביר לאשפה {count} נכסים כפולים בכמות גדולה? פעולה זו תשמור על הנכס עם המשקל הגדול ביותר של כל קבוצת כפילויות ותשלח לאשפה את כל שאר הכפילויות.",
"build": "Build",
"build_image": "Build Image",
"bulk_delete_duplicates_confirmation": "האם את/ה בטוח/ה שברצונך למחוק בכמות גדולה {count, plural, one {נכס # כפול} other {# נכסים כפולים}}? זה ישמור על הנכס הכי גדול של כל קבוצה וימחק לצמיתות את כל שאר הכפילויות. את/ה לא יכול לבטל את הפעולה הזו!",
"bulk_keep_duplicates_confirmation": "האם את/ה בטוח/ה שברצונך להשאיר {count, plural, one {נכס # כפול} other {# נכסים כפולים}}? זה יפתור את כל הקבוצות הכפולות מבלי למחוק דבר.",
"bulk_trash_duplicates_confirmation": "האם את/ה בטוח/ה שברצונך להעביר לאשפה בכמות גדולה {count, plural, one {נכס # כפול} other {# נכסים כפולים}}? זה ישמור על הנכס הגדול ביותר של כל קבוצה ויעביר לאשפה את כל שאר הכפילויות.",
"camera": "מצלמה",
"camera_brand": "מותג המצלמה",
"camera_model": "מודל המצלמה",
@ -412,7 +421,7 @@
"change_location": "שנה מיקום",
"change_name": "שנה שם",
"change_name_successfully": "השם הוחלף בהצלחה",
"change_password": "החלף סיסמה",
"change_password": "שינוי סיסמה",
"change_password_description": "זאת או הפעם הראשונה שאת/ה מתחבר/ת למערכת או שנעשתה בקשה לשינוי הסיסמה שלך. נא להזין את הסיסמה החדשה למטה.",
"change_your_password": "החלף את הסיסמה שלך",
"changed_visibility_successfully": "החשיפה השתנתה בהצלחה",
@ -472,7 +481,7 @@
"date_of_birth_saved": "תאריך לידה נשמר בהצלחה",
"date_range": "טווח תאריכים",
"day": "יום",
"deduplicate_all": "ביטול כפילויות של הכל",
"deduplicate_all": "מחק כפילויות של הכל",
"default_locale": "מיקום ברירת מחדל",
"default_locale_description": "הצג תאריכים ומספרים על סמך המיקום של הדפדפן שלך",
"delete": "מחק",
@ -537,18 +546,25 @@
"empty": "",
"empty_album": "אלבום ריק",
"empty_trash": "רוקן סל מחזור",
"empty_trash_confirmation": "האם את/ה בטוח/ה שברצונך לרוקן את האשפה? זה יסיר לצמיתות את כל הנכסים באשפה מImmich.\nאת/ה לא יכול/ה לבטל פעולה זו!",
"enable": "הפעל",
"enabled": "מופעל",
"end_date": "תאריך סיום",
"error": "שגיאה",
"error_loading_image": "שגיאה בעת טעינת התמונה",
"error_title": "שגיאה - משהו השתבש",
"errors": {
"cannot_navigate_next_asset": "לא ניתן לנווט לנכס הבא",
"cannot_navigate_previous_asset": "לא ניתן לנווט לנכס הקודם",
"cant_apply_changes": "לא ניתן להחיל שינויים",
"cant_change_activity": "לא ניתן {enabled, select, true {להשבית} other {לאפשר}} פעילות",
"cant_change_asset_favorite": "לא ניתן לשנות עדיפות עבור נכס",
"cant_change_metadata_assets_count": "לא ניתן לשנות את המטא-נתונים של {count, plural, one {נכס #} other {# נכסים}}",
"cant_get_faces": "לא ניתן לקבל פרצופים",
"cant_get_number_of_comments": "לא ניתן לקבל מספר של תגובות",
"cant_search_people": "לא ניתן לחפש אנשים",
"cant_search_places": "לא ניתן לחפש מקומות",
"cleared_jobs": "Jobs נוקו עבור: {job}",
"cleared_jobs": "משימות נוקו עבור: {job}",
"error_adding_assets_to_album": "שגיאה בהוספת נכסים לאלבום",
"error_adding_users_to_album": "שגיאה בהוספת משתמשים לאלבום",
"error_deleting_shared_user": "שגיאה במחיקת משתמש משותף",
@ -556,7 +572,7 @@
"error_removing_assets_from_album": "שגיאה בהסרת נכסים מאלבום, בדוק בלוח הבקרה לפרטים נוספים",
"error_selecting_all_assets": "שגיאה בבחירת כל הנכסים",
"exclusion_pattern_already_exists": "דפוס החרגה זה כבר קיים.",
"failed_job_command": "הפקודה {command} נכשלה עבור ה {job} :Job",
"failed_job_command": "הפקודה {command} נכשלה עבור המשימה: {job}",
"failed_to_create_album": "יצירת אלבום נכשלה",
"failed_to_create_shared_link": "יצירת קישור משותף נכשלה",
"failed_to_edit_shared_link": "עריכת קישור משותף נכשלה",
@ -567,7 +583,7 @@
"failed_to_unstack_assets": "ביטול ערימת נכסים נכשל",
"import_path_already_exists": "נתיב הייבוא הזה כבר קיים.",
"incorrect_email_or_password": "דוא\"ל או סיסמה שגויים",
"paths_validation_failed": "{paths, plural, one {# path} other {# paths}} אימות נכשל",
"paths_validation_failed": "{paths, plural, one {נתיב # נכשל} other {# נתיבים נכשלו}} אימות",
"profile_picture_transparent_pixels": "תמונות פרופיל אינן יכולות לכלול פיקסלים שקופים. נא להגדיל ו/או להזיז את התמונה.",
"quota_higher_than_disk_size": "אתה מגדיר מכסת אחסון גבוהה יותר מגודל הדיסק",
"repair_unable_to_check_items": "לא ניתן לסמן {count, select, one {item} other {items}}",
@ -577,11 +593,15 @@
"unable_to_add_exclusion_pattern": "לא ניתן להוסיף דפוס אי הכללה",
"unable_to_add_import_path": "לא ניתן להוסיף נתיב ייבוא",
"unable_to_add_partners": "לא ניתן להוסיף שותפים",
"unable_to_add_remove_archive": "לא ניתן {archived, select, true {להסיר נכס מ} other {להוסיף נכס אל}}ארכיון",
"unable_to_add_remove_favorites": "לא ניתן {favorite, select, true {להוסיף נכס ל} other {להסיר נכס מ}} מועדפים",
"unable_to_archive_unarchive": "לא ניתן {archived, select, true {להעביר לארכיון} other {להוציא מארכיון}}",
"unable_to_change_album_user_role": "לא ניתן לשנות את התפקיד של משתמש האלבום",
"unable_to_change_date": "לא ניתן לשנות תאריך",
"unable_to_change_favorite": "לא ניתן לשנות עדיפות עבור נכס",
"unable_to_change_location": "לא ניתן לשנות מיקום",
"unable_to_change_password": "לא ניתן לשנות סיסמה",
"unable_to_change_visibility": "לא ניתן לשנות את הנראות עבור {count, plural, one {אדם #} other {# אנשים}}",
"unable_to_check_item": "",
"unable_to_check_items": "",
"unable_to_complete_oauth_login": "לא ניתן להשלים התחברות OAuth",
@ -617,6 +637,7 @@
"unable_to_log_out_device": "לא ניתן לנתק מכשיר",
"unable_to_login_with_oauth": "לא ניתן להתחבר באמצעות OAuth",
"unable_to_play_video": "לא ניתן להפעיל את הסרטון",
"unable_to_reassign_assets_existing_person": "לא ניתן להקצות נכסים ל{name, select, null {אדם קיים} other {{name}}}",
"unable_to_reassign_assets_new_person": "לא ניתן להקצות מחדש נכסים לאדם חדש",
"unable_to_refresh_user": "לא ניתן לרענן את המשתמש",
"unable_to_remove_album_users": "לא ניתן להסיר משתמשים מהאלבום",
@ -641,10 +662,10 @@
"unable_to_save_profile": "לא ניתן לשמור פרופיל",
"unable_to_save_settings": "לא ניתן לשמור הגדרות",
"unable_to_scan_libraries": "לא ניתן לסרוק ספריות",
"unable_to_scan_library": "לא ניתן לסרוק ספריה",
"unable_to_scan_library": "לא ניתן לסרוק ספרייה",
"unable_to_set_feature_photo": "לא ניתן לבחור תמונה ראשית",
"unable_to_set_profile_picture": "לא ניתן לבחור תמונת פרופיל",
"unable_to_submit_job": "לא ניתן לשלוח JOB",
"unable_to_submit_job": "לא ניתן לשלוח משימה",
"unable_to_trash_asset": "לא ניתן להעביר נכס לסל המחזור",
"unable_to_unlink_account": "לא ניתן לבטל קישור חשבון",
"unable_to_update_album_cover": "לא ניתן לעדכן עטיפת אלבום",
@ -660,6 +681,7 @@
"every_night_at_midnight": "",
"every_night_at_twoam": "",
"every_six_hours": "",
"exif": "Exif",
"exit_slideshow": "צא ממצב מצגת",
"expand_all": "הרחב הכל",
"expire_after": "פג לאחר",
@ -707,12 +729,14 @@
"host": "מארח",
"hour": "שעה",
"image": "תמונה",
"image_alt_text_date": "ב {date}",
"image_alt_text_place": "ב {city}, {country}",
"img": "",
"immich_logo": "הלוגו של Immich",
"immich_web_interface": "ממשק משתמש של Immich בדפדפן",
"import_from_json": "ייבוא מ-JSON",
"import_path": "נתיב ייבוא",
"in_albums": "In {count, plural, one {# album} other {# albums}}",
"in_albums": "ב{count, plural, one {אלבום #} other {# אלבומים}}",
"in_archive": "בארכיון",
"include_archived": "כלול \"בארכיון\"",
"include_shared_albums": "כלול \"אלבומים משותפים\"",
@ -721,14 +745,15 @@
"info": "מידע",
"interval": {
"day_at_onepm": "כל יום ב 13:00 בצהריים",
"hours": "Every {hours, plural, one {hour} other {{hours, number} hours}}",
"hours": "כל {hours, plural, one {שעה} other {{hours, number} שעות}}",
"night_at_midnight": "כל יום בחצות",
"night_at_twoam": "כל יום ב 02:00 לפנות בוקר"
},
"invite_people": "להזמין אנשים",
"invite_to_album": "הזמנה לאלבום",
"items_count": "{count, plural, one {# פריט} other {# פריטים}}",
"job_settings_description": "",
"jobs": "Jobs",
"jobs": "משימות",
"keep": "שמור",
"keep_all": "שמור הכל",
"keyboard_shortcuts": "קיצורי מקלדת",
@ -760,7 +785,7 @@
"look": "הסתכל",
"loop_videos": "וידיאו בלופ",
"loop_videos_description": "הפעל לולאה אוטומטית של סרטון במציג הנכסים.",
"make": "צור",
"make": "תוצרת",
"manage_shared_links": "נהל קישורים משותפים",
"manage_sharing_with_partners": "נהל שיתוף שותפים",
"manage_the_app_settings": "נהל הגדרות אפליקציה",
@ -783,6 +808,7 @@
"merge_people_limit": "ניתן למזג עד 5 פרצופים בכל פעם",
"merge_people_prompt": "האם אתה רוצה למזג את האנשים האלה? פעולה זו היא בלתי הפיכה.",
"merge_people_successfully": "מיזוג אנשים בוצע בהצלחה",
"merged_people_count": "{count, plural, one {אדם # מוזג} other {# אנשים מוזגו}}",
"minimize": "מזער",
"minute": "דקה",
"missing": "חסר",
@ -860,9 +886,9 @@
"password_required": "סיסמה נדרשת",
"password_reset_success": "איפוס הסיסמה הצליח",
"past_durations": {
"days": "Past {days, plural, one {day} other {{days, number} days}}",
"hours": "Past {hours, plural, one {hour} other {{hours, number} hours}}",
"years": "Past {years, plural, one {year} other {{years, number} years}}"
"days": "{days, plural, one {יום אחרון} other {# ימים אחרונים}}",
"hours": "{hours, plural, one {שעה אחרונה} other {# שעות אחרונות}}",
"years": "{years, plural, one {שנה אחרונה} other {# שנים אחרונות}}"
},
"path": "נתיב",
"pattern": "תבנית",
@ -871,18 +897,23 @@
"paused": "הושה",
"pending": "ממתין",
"people": "אנשים",
"people_edits_count": "{count, plural, one {אדם # נערך} other {# אנשים נערכו}}",
"people_sidebar_description": "הצג קישור לאנשים בסרגל הצד",
"perform_library_tasks": "",
"permanent_deletion_warning": "אזהרת מחיקה לצמיתות",
"permanent_deletion_warning_setting_description": "הצג אזהרה בעת מחיקת נכסים לצמיתות",
"permanently_delete": "מחק לצמיתות",
"permanently_delete_assets_count": "מחק לצמיתות {count, plural, one {נכס} other {נכסים}}",
"permanently_delete_assets_prompt": "האם את/ה בטוח/ה שברצונך למחוק לצמיתות {count, plural, one {נכס זה?} other {<b>#</b> נכסים אלה?}} זה גם יסיר {count, plural, one {את זה} other {אותם}} מאלבומים.",
"permanently_deleted_asset": "נכס נמחק לצמיתות",
"permanently_deleted_assets": "Permanently deleted {count, plural, one {# asset} other {# assets}}",
"permanently_deleted_assets_count": "{count, plural, one {# נכס נמחק} other {# נכסים נמחקו}} לצמיתות",
"person": "אדם",
"person_hidden": "{name}{hidden, select, true { (מוסתר)} other {}}",
"photo_shared_all_users": "נראה שאת/ה שיתפת את התמונות שלך עם כל המשתמשים או שאין לך אף משתמש לשתף עם.",
"photos": "תמונות",
"photos_and_videos": "תמונות & סרטונים",
"photos_count": "{count, plural, one {{count, number} Photo} other {{count, number} Photos}}",
"photos_count": "{count, plural, one {תמונה {count, number}} other {{count, number} תמונות}}",
"photos_from_previous_years": "תמונות משנים קודמות",
"pick_a_location": "בחר מיקום",
"place": "מקום",
@ -899,7 +930,7 @@
"previous_memory": "זיכרון קודם",
"previous_or_next_photo": "התמונה הקודמת או הבאה",
"primary": "ראשי",
"profile_image_of_user": "תמונת פרופיל של {title}",
"profile_image_of_user": "תמונת פרופיל של {user}",
"profile_picture_set": "תמונת פרופיל נבחרה.",
"public_album": "אלבום ציבורי",
"public_share": "שיתוף ציבורי",
@ -908,6 +939,8 @@
"reaction_options": "אפשרויות תגובה (לב/אהבתי)",
"read_changelog": "קרא את יומן השינויים",
"reassign": "הקצה מחדש",
"reassigned_assets_to_existing_person": "{count, plural, one {נכס #} other {# נכסים}} הוקצו מחדש אל {name, select, null {אדם קיים} other {{name}}}",
"reassigned_assets_to_new_person": "{count, plural, one {נכס #} other {# נכסים}} הוקצו מחדש לאדם חדש",
"reassing_hint": "הקצה נכסים שנבחרו לאדם קיים",
"recent": "לאחרונה",
"recent_searches": "חיפושים אחרונים",
@ -921,6 +954,8 @@
"refreshing_metadata": "מרענן מטא-נתונים",
"regenerating_thumbnails": "מחדש תמונות ממוזערות",
"remove": "הסרה",
"remove_assets_album_confirmation": "האם את/ה בטוח/ה שברצונך להסיר {count, plural, one {נכס #} other {# נכסים}} מהאלבום?",
"remove_assets_shared_link_confirmation": "האם את/ה בטוח/ה שברצונך להסיר {count, plural, one {נכס #} other {# נכסים}} מהקישור המשותף הזה?",
"remove_assets_title": "הסר נכסים?",
"remove_custom_date_range": "הסר טווח תאריכים מותאם",
"remove_from_album": "הסרה מאלבום",
@ -931,6 +966,7 @@
"removed_api_key": "מפתח API הוסר: {name}",
"removed_from_archive": "הוסר מארכיון",
"removed_from_favorites": "הוסר ממועדפים",
"removed_from_favorites_count": "{count, plural, other {הוסרו #}} מהמועדפים",
"rename": "שנה שם",
"repair": "תיקון",
"repair_no_results_message": "קבצים חסרי מעקב וחסרים יופיעו כאן",
@ -995,6 +1031,7 @@
"select_photos": "בחר תמונות",
"select_trash_all": "בחר מחק הכל",
"selected": "נבחר",
"selected_count": "{count, plural, other {# נבחרו}}",
"send_message": "שלח הודעה",
"send_welcome_email": "שלח מייל ברוכים הבאים",
"server": "שרת",
@ -1015,7 +1052,7 @@
"shared_by_you": "משותף על ידך",
"shared_from_partner": "תמונות מאת {partner}",
"shared_links": "קישורים משותפים",
"shared_photos_and_videos_count": "{assetCount} תמונות וסרטונים משותפים.",
"shared_photos_and_videos_count": "{assetCount, plural, other {# תמונות וסרטונים משותפים.}}",
"shared_with_partner": "משותף עם {partner}",
"sharing": "שיתוף",
"sharing_enter_password": "אנא הזן את הסיסמה כדי לצפות בדף זה.",
@ -1052,6 +1089,7 @@
"source": "מקור",
"stack": "קבץ",
"stack_selected_photos": "קבץ תמונות נבחרות",
"stacked_assets_count": "{count, plural, one {נכס # נערם} other {# נכסים נערמו}}",
"stacktrace": "Stacktrace",
"start": "התחל",
"start_date": "תאריך התחלה",
@ -1090,31 +1128,34 @@
"trash_count": "אשפה {count}",
"trash_delete_asset": "העבר לאשפה/מחק נכס",
"trash_no_results_message": "תמונות וסרטונים שהועברו לאשפה יופיעו כאן.",
"trashed_items_will_be_permanently_deleted_after": "Trashed items will be permanently deleted after {days, plural, one {# day} other {# days}}.",
"trashed_items_will_be_permanently_deleted_after": "פריטים באשפה ימחקו לצמיתות לאחר {days, plural, one {יום #} other {# ימים}}.",
"type": "סוג",
"unarchive": "הוצא מארכיון",
"unarchived": "הוצא מהארכיון",
"unarchived_count": "{count, plural, other {# הוסרו מהארכיון}}",
"unfavorite": "לא מועדף",
"unhide_person": "בטל הסתרת פנים",
"unknown": "לא ידוע",
"unknown_album": "אלבום לא ידוע",
"unknown_year": "שנה לא ידוע",
"unlimited": "בלתי מוגבל",
"unlink_oauth": "בטל קישור עם Oauth",
"unlink_oauth": "בטל את הקישור של OAuth",
"unlinked_oauth_account": "חשבון OAuth לא מקושר",
"unnamed_album": "אלבום ללא שם",
"unnamed_share": "שיתוף ללא שם",
"unsaved_change": "שינוי לא נשמר",
"unselect_all": "בטל סימון בהכל",
"unstack": "בטל קיבוץ",
"unstacked_assets_count": "{count, plural, one {# נכס הוסר} other {# נכסים הוסרו}} מערימה",
"untracked_files": "קבצים ללא מעקב",
"untracked_files_decription": "האפליקציה לא עוקבת אחר קבצים אלה. הם יכולים להיות תוצאות של העברות כושלות, העלאות שהיו איתם הפרעות או שנותרו מאחור בגלל באג",
"up_next": "הבא אחריו",
"updated_password": "סיסמה מעודכנת",
"upload": "ההעלאה",
"upload_concurrency": "העלאה במקביל",
"upload_errors": "העלאה הושלמה עם {count, plural, one {שגיאה #} other {# שגיאות}}, רענן את הדף כדי לראות נכסי העלאה חדשים.",
"upload_progress": "נותרו {remaining} - טופלו {processed}/{total}",
"upload_skipped_duplicates": "דילג על {count, plural, one {# duplicate asset} other {# duplicate assets}}",
"upload_skipped_duplicates": "דילג על {count, plural, one {נכס כפול #} other {# נכסים כפולים}}",
"upload_status_duplicates": "כפילויות",
"upload_status_errors": "שגיאות",
"upload_status_uploaded": "הועלה",
@ -1124,7 +1165,7 @@
"use_custom_date_range": "השתמש בטווח תאריכים מותאם במקום",
"user": "משתמש",
"user_id": "User ID",
"user_liked": "{user} אהב {type, select, photo {this photo} video {this video} asset {this asset} other {it}}",
"user_liked": "{user} אהב את {type, select, photo {התמונה הזאת} video {הסרטון הזה} asset {הנכס הזה} other {זה}}",
"user_role_set": "הגדר את {user} בתור {role}",
"user_usage_detail": "פרטי השימוש של המשתמש",
"username": "שם משתמש",
@ -1139,7 +1180,7 @@
"video_hover_setting": "הפעל תצוגה מקדימה של הסרטון עם בריחוף",
"video_hover_setting_description": "הפעל תמונה ממוזערת של וידאו כאשר העכבר מרחף מעל הפריט. גם כאשר הוא מושבת, ניתן להתחיל את ההשמעה על ידי ריחוף מעל סמל ההפעלה.",
"videos": "סרטונים",
"videos_count": "{count, plural, one {# Video} other {# Videos}}",
"videos_count": "{count, plural, one {סרטון #} other {# סרטונים}}",
"view": "הצג",
"view_album": "הצג אלבום",
"view_all": "הצג הכל",
@ -1149,12 +1190,14 @@
"view_previous_asset": "הצג את הנכס הקודם",
"view_stack": "הצג ערימה",
"viewer": "מציג",
"visibility_changed": "הנראות השתנתה עבור {count, plural, one {אדם #} other {# אנשים}}",
"waiting": "ממתין",
"warning": "אזהרה",
"week": "שבוע",
"welcome": "ברוכים הבאים",
"welcome_to_immich": "ברוכים הבאים אל immich",
"year": "שנה",
"years_ago": "לפני {years, plural, one {שנה #} other {# שנים}}",
"yes": "כן",
"you_dont_have_any_shared_links": "אין לך קישורים משותפים",
"zoom_image": "התקרב לתמונה"

View file

@ -1,4 +1,5 @@
{
"about": "Az Immich-ről",
"account": "Fiók",
"account_settings": "Fiók Beállítások",
"acknowledge": "Rendben, láttam",
@ -21,6 +22,9 @@
"add_to": "Hozzáadás ide...",
"add_to_album": "Felvétel albumba",
"add_to_shared_album": "Felvétel megosztott albumba",
"added_to_archive": "Hozzáadva az archívumhoz",
"added_to_favorites": "Hozzáadva a kedvencekhez",
"added_to_favorites_count": "{count} hozzáadva a kedvencekhez",
"admin": {
"add_exclusion_pattern_description": "Kizáró minta megadása. Támogatja *, ** és ? dzsókerek használatát. Pl. a \"Raw\" könyvtárban tárolt összes fájl figyelmen kívül hagyásához használható a \"**/Raw/**\". Minden \".tif\" fájl figyelmen kívül hagyásához használható a \"**/*.tif\". Abszolut elérési útvonal figyelmen kívül hagyásához használható a \"/path/to/ignore/**\".",
"authentication_settings": "Hitelesítési beállítások",
@ -30,7 +34,7 @@
"cleared_jobs": "{job} munkák kitörölve",
"config_set_by_file": "A konfigurációt jelenleg egy konfigurációs fájl állítja be",
"confirm_delete_library": "Biztosan ki szeretné törölni a {library} képtárat?",
"confirm_delete_library_assets": "Biztosan ki szeretné törölni a ezt a képtárat? Ez kitöröl {count} benne lévő fájlt és nem vonható vissza. A fájlok a lemezen maradnak.",
"confirm_delete_library_assets": "Biztosan ki szeretné törölni a ezt a képtárat? Ez kitöröl {count, plural, one {#} other {#}} benne lévő fájlt az Immichből és nem vonható vissza. A fájlok a lemezen maradnak.",
"confirm_email_below": "A megerősítéshez írja \"{email}\"-t alább",
"confirm_reprocess_all_faces": "Biztos benne, hogy újra szeretné feldolgozni az összes arcot? Ez a megnevezett személyeket is törli.",
"confirm_user_password_reset": "Biztosan vissza szeretné állítani {user} jelszavát?",
@ -66,8 +70,8 @@
"job_settings": "Feladat beállítások",
"job_settings_description": "Feladatok párhuzamosságának beállítása",
"job_status": "Feladat állapota",
"jobs_delayed": "{jobCount} késik",
"jobs_failed": "{jobCount} sikertelen",
"jobs_delayed": "{jobCount, plural, other {# késik}}",
"jobs_failed": "{jobCount, plural, other {# sikertelen}}",
"library_created": "A(z) {library} képtár elkészült",
"library_cron_expression": "Cron kifejezés",
"library_cron_expression_description": "Átfésülések közötti intervallum beállítása cron formátumban. Több információt találhat például itt: <link>Crontab Guru</link>",
@ -175,13 +179,14 @@
"oauth_storage_quota_default": "Alapértelmezett tárhelykvóta (GiB)",
"oauth_storage_quota_default_description": "Kvóta GiB-ben, amelyet akkor kell használni, ha nem nyújtanak be követelést (adjon meg 0-t a korlátlan kvótához).",
"offline_paths": "Offilne Útvonalak",
"offline_paths_description": "Ezek az eredmények olyan fájlok kézi törlésének tudhatók be, amelyek nem részei külső könyvtárnak.",
"offline_paths_description": "Ezek az eredmények olyan fájlok kézi törlésének tudhatók be, amelyek nem részei külső képtárnak.",
"password_enable_description": "Bejelentkezés emaillel és jelszóval",
"password_settings": "Jelszavas Bejelentkezés",
"password_settings_description": "Jelszavas bejelentkezés beállítások kezelése",
"paths_validated_successfully": "Összes útvonal sikeresen érvényesítve",
"quota_size_gib": "Kvóta Mérete (GiB)",
"refreshing_all_libraries": "Összes képtár újratöltése",
"registration": "Admin Regisztráció",
"removing_offline_files": "Offline Fájlok eltávolítása",
"repair_all": "Összes Javítása",
"repair_matched_items": "{count, plural, one {# egyezés} other {# egyezés}}",
@ -203,12 +208,12 @@
"slideshow_duration_description": "Az egyes képek megjelenítésének ideje másodpercben",
"smart_search_job_description": "Futtasson gépi tanulást a képi vagyonon az intelligens keresés támogatása érdekében",
"storage_template_enable_description": "Tárolási sablon motor engedélyezése",
"storage_template_hash_verification_enabled": "Hash ellenőrzés sikertelen",
"storage_template_hash_verification_enabled": "Hash ellenőrzés engedélyezve",
"storage_template_hash_verification_enabled_description": "Engedélyezi a hash-ellenőrzést - ne kapcsolja ki, csak ha tisztában van a következményekkel",
"storage_template_migration": "Tárolási sablon migrálása",
"storage_template_migration_description": "A jelenlegi <link>{template}</link> alkalmazása az ezelőtt feltöltött fájlokra",
"storage_template_migration_info": "A megváltozott sablon csak az újonnan feltöltött fájlokra lesz alkalmazva. A fájlok visszamenőleges megváltoztatásához futtatni kell a megfelelő munkát: <link>{job}</link>.",
"storage_template_migration_job": "Tárhely Migrációja",
"storage_template_migration_job": "Tárhely Sablon Migrációja",
"storage_template_settings": "Tárolási sablon",
"storage_template_settings_description": "Kezelje a feltöltött képi vagyontárgyak mappaszerkezetét és fájlnevét",
"system_settings": "Rendszerbeállítások",
@ -283,7 +288,7 @@
"trash_settings_description": "Lomtár beállítások kezelése",
"untracked_files": "Nem kezelt fájlok",
"untracked_files_description": "Ezekkel a fájlokkal semmit nem csinál az alkalmazás. Ez lehetséges pl. meghiúsult mozgatás, megszakított feltöltés miatt, vagy valamilyen alkalmazáshiba következtében",
"user_delete_delay": "<b>{user}</b> felhasználói fiókja és képi vagyona véglegesen törölve lesz {delay} nap múlva.",
"user_delete_delay": "<b>{user}</b> felhasználói fiókja és képi vagyona véglegesen törölve lesz {delay, plural, one {# nap} other {# nap}} múlva.",
"user_delete_delay_settings": "Törlési késleltetés",
"user_delete_delay_settings_description": "Ennyi nap teljen el az eltávolítás után a felhasználói fiók és ahhoz tartozó elemek végleges törlése között. A törlésért felelős folyamat éjfélkor indul, és megnézi van-e törlésre kész felhasználó. A beállítás változtatása a következő végrehajtás során lép életbe.",
"user_password_has_been_reset": "A felhasználó jelszava megváltoztatásra került:",
@ -331,11 +336,11 @@
"back": "Vissza",
"backward": "Visszafele",
"blurred_background": "Homályos háttér",
"bulk_delete_duplicates_confirmation": "Biztosan törölni akarsz {count} duplikált fájlt? A művelet során minden hasonló fájlcsoportból a legnagyobb méretű fájlt megtartja, minden másik duplikált fájlt kitörli. Ezt a műveletet nem lehet visszavonni!",
"bulk_trash_duplicates_confirmation": "Biztosan kitörölsz {count} duplikált fájlt? Ez a művelet megtartja minden fájlcsoportból a legnagyobb méretű elemet, és kitörli minden másik duplikáltat.",
"camera": "Kamera",
"camera_brand": "Kamera márka",
"camera_model": "Kamera modell",
"bulk_delete_duplicates_confirmation": "Biztosan törölni akarsz {count, plural, one {# duplikált fájlt} other {# duplikált fájlt}}? A művelet során minden hasonló fájlcsoportból a legnagyobb méretű fájlt megtartja, minden másik duplikált fájlt kitörli. Ezt a műveletet nem lehet visszavonni!",
"bulk_trash_duplicates_confirmation": "Biztosan kitörölsz {count, plural, one {# duplikált fájlt} other {# duplikált fájlt}}? Ez a művelet megtartja minden fájlcsoportból a legnagyobb méretű elemet, és kitörli minden másik duplikáltat.",
"camera": "Fényképezőgép",
"camera_brand": "Fényképezőgép márka",
"camera_model": "Fényképezőgép modell",
"cancel": "Mégsem",
"cancel_search": "Keresés visszavonása",
"cannot_merge_people": "Személyek összevonása nem lehetséges",
@ -639,7 +644,7 @@
"minute": "Perc",
"missing": "Hiányzó",
"model": "Modell",
"month": "hónapok szerint",
"month": "Hónap",
"more": "Több",
"moved_to_trash": "Lomtárba mozgatva",
"my_albums": "Albumaim",
@ -660,7 +665,7 @@
"no_exif_info_available": "",
"no_explore_results_message": "Töltsön fel több fényképet, hogy felfedezze a gyűjteményét.",
"no_favorites_message": "Jelöljön meg kedvenceket, hogy gyorsan megtalálhassa legjobb fényképeit és videóit",
"no_libraries_message": "Hozzon létre külső könyvtárat a fényképei és videói megtekintéséhez",
"no_libraries_message": "Hozzon létre külső képtárat a fényképei és videói megtekintéséhez",
"no_name": "Nincs Név",
"no_places": "",
"no_results": "Nincsenek eredmények",
@ -679,7 +684,7 @@
"only_refreshes_modified_files": "Csak a megváltoztatott fájlokat frissíti",
"open_the_search_filters": "",
"options": "Beállítások",
"organize_your_library": "Rendszerezze könyvtárát",
"organize_your_library": "Rendszerezze képtárát",
"other": "Egyéb",
"other_devices": "Egyéb eszközök",
"other_variables": "Egyéb változók",

View file

@ -370,7 +370,7 @@
"asset_description_updated": "Deskripsi aset telah diperbarui",
"asset_filename_is_offline": "Aset {filename} sedang luring",
"asset_has_unassigned_faces": "Aset memiliki wajah yang belum ditetapkan",
"asset_hashing": "Hashing...",
"asset_hashing": "Memilah...",
"asset_offline": "Aset luring",
"asset_offline_description": "Aset ini sedang luring. Immich tidak dapat mengakses lokasi berkasnya. Pastikan aset tersebut tersedia lalu pindai ulang pustaka.",
"asset_skipped": "Dilewati",
@ -706,6 +706,10 @@
"host": "Hos",
"hour": "Jam",
"image": "Gambar",
"image_alt_text_date": "pada {date}",
"image_alt_text_people": "{count, plural, =1 {dengan {person1}} =2 {dengan {person1} dan {person2}} =3 {dengan {person1}, {person2}, dan {person3}} other {dengan {person1}, {person2}, dan {others, number} lainnya}}",
"image_alt_text_place": "di {city}, {country}",
"image_taken": "{isVideo, select, true {Video diambil} other {Gambar diambil}}",
"immich_logo": "Logo Immich",
"immich_web_interface": "Antarmuka Web Immich",
"import_from_json": "Impor dari JSON",
@ -876,7 +880,7 @@
"permanent_deletion_warning_setting_description": "Tampilkan peringatan ketika menghapus aset secara permanen",
"permanently_delete": "Hapus secara permanen",
"permanently_delete_assets_count": "Hapus {count, plural, one {aset} other {aset}} secara permanen",
"permanently_delete_assets_prompt": "Kamu yakin untuk menghapus permanen {count, plural, one {aset ini?} other {sebanyak <b>#</b> aset-aset berikut?}} Ini juga akan menghapus {count, plural, one {ini dari} other {semua dari}} album-albumnya.",
"permanently_delete_assets_prompt": "Apakah Anda yakin untuk menghapus {count, plural, one {aset ini secara permanen?} other {sebanyak <b>#</b> aset-aset berikut secara permanen?}} Ini juga akan menghapus {count, plural, one {ini dari} other {semua dari}} album-albumnya.",
"permanently_deleted_asset": "Aset dihapus secara permanen",
"permanently_deleted_assets": "",
"permanently_deleted_assets_count": "{count, plural, one {# aset} other {# aset}} dihapus secara permanen",
@ -901,7 +905,7 @@
"previous_memory": "Kenangan sebelumnya",
"previous_or_next_photo": "Foto sebelumnya atau berikutnya",
"primary": "Utama",
"profile_image_of_user": "Foto profil dari {title}",
"profile_image_of_user": "Foto profil dari {user}",
"profile_picture_set": "Foto profil ditetapkan.",
"public_album": "Album publik",
"public_share": "Pembagian Publik",

View file

@ -93,7 +93,7 @@
"library_watching_settings_description": "Osserva automaticamente i cambiamenti dei file",
"logging_enable_description": "Attiva il logging",
"logging_level_description": "Quando attivato, che livello di log utilizzare.",
"logging_settings": "Logging",
"logging_settings": "Registro dei Log",
"machine_learning_clip_model": "Modello CLIP",
"machine_learning_clip_model_description": "Il nome del modello CLIP mostrato <link>qui</link>. Bita cge devi rieseguire il processo 'Ricerca Intelligente' per tutte le immagini al cambio del modello.",
"machine_learning_duplicate_detection": "Rilevamento Duplicati",
@ -740,7 +740,7 @@
"include_shared_albums": "Includi album condivisi",
"include_shared_partner_assets": "Includi asset condivisi del compagno",
"individual_share": "Condivisione individuale",
"info": "Informazioni",
"info": "Info",
"interval": {
"day_at_onepm": "Ogni giorno alle 13",
"hours": "Ogni {hours, plural, one {ora} other {{hours, number} ore}}",
@ -927,7 +927,7 @@
"previous_memory": "Ricordo precedente",
"previous_or_next_photo": "Precedente o prossima foto",
"primary": "Primario",
"profile_image_of_user": "Immagine profilo di {title}",
"profile_image_of_user": "Immagine profilo di {user}",
"profile_picture_set": "Foto profilo impostata.",
"public_album": "Album pubblico",
"public_share": "Condivisione Pubblica",

View file

@ -1,4 +1,5 @@
{
"about": "アプリについて",
"account": "アカウント",
"account_settings": "アカウント設定",
"acknowledge": "了解",
@ -6,6 +7,7 @@
"actions": "アクション",
"active": "アクティブ",
"activity": "アクティビティ",
"activity_changed": "アクティビティは{enabled, select, true {有効化} other {無効化}}されました",
"add": "追加",
"add_a_description": "説明を追加",
"add_a_location": "場所を追加",
@ -21,19 +23,23 @@
"add_to": "追加先...",
"add_to_album": "アルバムに追加",
"add_to_shared_album": "共有アルバムに追加",
"added_to_archive": "アーカイブに追加済",
"added_to_favorites": "お気に入りに追加済",
"added_to_favorites_count": "{count} 画像をお気に入りに追加済",
"admin": {
"add_exclusion_pattern_description": "除外パターンを追加します。ワイルドカード「*」「**」「?」を使用できます。すべてのディレクトリで「Raw」と名前が付いたファイルを無視するには、「**/Raw/**」を使用します。また、「.tif」で終わるファイルをすべて無視するには、「**/*.tif」を使用します。さらに、絶対パスを無視するには「/path/to/ignore/**」を使用します。",
"authentication_settings": "認証設定",
"authentication_settings_description": "認証設定の管理パスワード、OAuth、その他",
"authentication_settings_disable_all": "本当に全てのログイン方法を無効にしますか? ログインは完全に無効になります。",
"authentication_settings_reenable": "再び有効にするには、<link>サーバーコマンド</link>を使用してください。",
"background_task_job": "バックグラウンドタスク",
"check_all": "すべてを選択",
"cleared_jobs": "{job}のジョブをクリアしました",
"config_set_by_file": "設定は現在 Config File で設定されている",
"confirm_delete_library": "本当に {library} を削除しますか?",
"confirm_delete_library_assets": "本当にこのライブラリを削除しますか? {count} 個のアセットがすべてImmichから削除され、元に戻すことはできません。ファイルはディスク上に残ります。",
"confirm_delete_library_assets": "本当にこのライブラリを削除しますか? {count, plural, one {#個のアセット} other {all #個のアセット全て}} がImmichから削除され、元に戻すことはできません。ファイルはディスク上に残ります。",
"confirm_email_below": "確認のため、以下に \"{email}\" と入力してください",
"confirm_reprocess_all_faces": "本当にすべての顔を再処理しますか?(名前が付けられた人物も消去されます)",
"confirm_reprocess_all_faces": "本当にすべての顔を再処理しますか? これにより名前が付けられた人物も消去されます。",
"confirm_user_password_reset": "本当に {user} のパスワードをリセットしますか?",
"crontab_guru": "Crontab Guru",
"disable_login": "ログインを無効にする",
@ -41,10 +47,11 @@
"duplicate_detection_job_description": "機械学習を用いて類似画像の検出を行います。(スマートサーチに依存)",
"exclusion_pattern_description": "除外パターンを使用すると、ライブラリをスキャンする際にファイルやフォルダを無視することができます。RAWファイルなど、インポートしたくないファイルを含むフォルダがある場合に便利です。",
"external_library_created_at": "外部ライブラリ(作成日:{date}",
"external_library_management": "外部ライブラリ管理",
"external_library_management": "外部ライブラリ管理",
"face_detection": "顔検出",
"face_detection_description": "機械学習を使用してアセット内の顔を検出します。動画の場合は、サムネイルのみが対象となります。\"All\" はすべてのアセットを(再)処理します。 \"Missing\" はまだ処理されていないアセットをキューに入れます。顔検出の完了後、検出された顔は顔認識のキューへ入れられ、既存または新規の人物にグループ化されます。",
"facial_recognition_job_description": "検出された顔を人物にグループ化します。このステップは顔検出が完了した後に実行されます。 \"All\" はすべての顔を(再)クラスタリングし、 \"Missing\" は人物が割り当てられていない顔をキューに入れます。",
"failed_job_command": "ジョブ {job}のコマンド {command}が失敗しました",
"force_delete_user_warning": "警告:この操作を行うと、ユーザーとすべてのアセットが直ちに削除されます。これは元に戻せず、ファイルも復元できません。",
"forcing_refresh_library_files": "すべてのライブラリファイルを強制更新",
"image_format_description": "WebPはJPEGよりもファイルサイズが小さいですが、エンコードに時間がかかります。",
@ -67,10 +74,11 @@
"job_settings": "ジョブ設定",
"job_settings_description": "ジョブの同時実行を管理します",
"job_status": "ジョブ ステータス",
"jobs_delayed": "{jobCount}件の遅延",
"jobs_failed": "{jobCount}件の失敗",
"jobs_delayed": "{jobCount, plural, other {#件}}の遅延",
"jobs_failed": "{jobCount, plural, other {#件}}の失敗",
"library_created": "作成されたライブラリ:{library}",
"library_cron_expression": "Cron表記",
"library_cron_expression_description": "cron形式を使用してスキャン間隔を設定します。 詳細については、<link>Crontab Guru</link> などを参照してください",
"library_cron_expression_presets": "Cron表記プリセット",
"library_deleted": "ライブラリは削除されました",
"library_import_path_description": "インポートするフォルダを指定します。このフォルダはサブフォルダを含めて、画像と動画のスキャンが行われます。",
@ -84,9 +92,10 @@
"library_watching_settings": "ライブラリ監視(実験的)",
"library_watching_settings_description": "変更されたファイルを自動的に監視",
"logging_enable_description": "ログの有効化",
"logging_level_description": "ログレベルの設定",
"logging_level_description": "有効な場合に使用されるログ レベル。",
"logging_settings": "ログ",
"machine_learning_clip_model": "Clipモデル",
"machine_learning_clip_model_description": "CLIP モデルの名前は<link>ここ</link>にリストされています。モデルを変更した場合は、すべてのイメージに対して「スマート検索」ジョブを再実行する必要があります。",
"machine_learning_duplicate_detection": "重複検出",
"machine_learning_duplicate_detection_enabled": "重複検出の有効化",
"machine_learning_duplicate_detection_enabled_description": "無効にした場合でも、完全に同一アセットの重複は排除されます。",
@ -119,6 +128,7 @@
"map_dark_style": "ダークモード",
"map_enable_description": "マップ機能を有効にします",
"map_light_style": "ライトモード",
"map_manage_reverse_geocoding_settings": "<link>逆ジオコーディング</link>の設定を管理します",
"map_reverse_geocoding": "リバースジオコード",
"map_reverse_geocoding_enable_description": "リバースジオコード(座標より住所を作成)を有効にする",
"map_reverse_geocoding_settings": "リバースジオコード設定",
@ -131,7 +141,7 @@
"migration_job_description": "アセットおよび顔のサムネイルを最新のフォルダ構造に移行します",
"no_paths_added": "パスが追加されていません",
"no_pattern_added": "パターンが追加されていません",
"note_apply_storage_label_previous_assets": "注意: 以前にアップロードされたアセットにストレージラベルを適用するには、以下を実行してください",
"note_apply_storage_label_previous_assets": "注意: 以前にアップロードされたアセットにストレージラベルを適用するには、以下を実行してください",
"note_cannot_be_changed_later": "注意: これを後で変更することはできません!",
"note_unlimited_quota": "注意: 無制限のクォータを設定する場合は0を入力してください",
"notification_email_from_address": "送信メールアドレス",
@ -143,7 +153,8 @@
"notification_email_port_description": "メールサーバーのポート番号を指定します25, 465, 587",
"notification_email_sent_test_email_button": "テストメールを送信して設定を保存",
"notification_email_setting_description": "メール通知の送信設定",
"notification_email_test_email_failed": "メールの送信に失敗しました。設定を確認してください。",
"notification_email_test_email": "テストメールを送信",
"notification_email_test_email_failed": "テストメールの送信に失敗しました。設定を確認してください",
"notification_email_test_email_sent": "{email} へテストメールが送信されました。受信トレイを確認してください。",
"notification_email_username_description": "メールサーバーでの認証時に使用するユーザーネームを設定します",
"notification_enable_email_notifications": "メール通知を有効にします",
@ -164,6 +175,7 @@
"oauth_scope": "スコープ",
"oauth_settings": "OAuth",
"oauth_settings_description": "OAuthログイン設定を管理します",
"oauth_settings_more_details": "この機能の詳細については、<link>ドキュメント</link>を参照してください。",
"oauth_signing_algorithm": "署名アルゴリズム",
"oauth_storage_label_claim": "",
"oauth_storage_label_claim_description": "ユーザーのストレージラベルを、このクレームの値に自動的に設定します。",
@ -171,12 +183,15 @@
"oauth_storage_quota_claim_description": "ユーザーのストレージクォータをこのクレームの値に自動的に設定します。",
"oauth_storage_quota_default": "デフォルトのストレージ割り当て(GiB)",
"oauth_storage_quota_default_description": "クレームが提供されていない場合に使用されるクォータをGiB単位で設定します無制限にする場合は0を入力してください。",
"offline_paths": "オフラインのパス",
"offline_paths_description": "これらの結果は、外部ライブラリに属さないファイルを手動で削除したことによる可能性があります。",
"password_enable_description": "メールアドレスとパスワードでログイン",
"password_settings": "パスワード ログイン",
"password_settings_description": "パスワード ログイン設定を管理します",
"paths_validated_successfully": "すべてのパスが正常に検証されました",
"quota_size_gib": "割り当て容量 (GiB)",
"refreshing_all_libraries": "すべてのライブラリを更新",
"registration_description": "あなたはシステムの最初のユーザーであるため、管理者として割り当てられ、管理タスクを担当し、追加のユーザーはあなたによって作成されます。",
"removing_offline_files": "オフライン ファイルを削除します",
"repair_all": "すべてを修復",
"repair_matched_items": "一致: {count, plural, one {# item} other {# items}}",
@ -197,6 +212,8 @@
"sidecar_job_description": "ファイルシステムからXMPメタデータを検出または同期する",
"slideshow_duration_description": "各画像を表示する秒数",
"smart_search_job_description": "スマート検索をサポートするため、アセットに対して機械学習を実行する",
"storage_template_date_time_description": "アセットの作成タイムスタンプは日時の情報に使用されます",
"storage_template_date_time_sample": "日時の例 {date}",
"storage_template_enable_description": "ストレージ テンプレート エンジンの有効化",
"storage_template_hash_verification_enabled": "ハッシュ検証を有効化",
"storage_template_hash_verification_enabled_description": "ハッシュ検証の有効化(よくわからなければ、有効にしてください)",
@ -209,6 +226,7 @@
"storage_template_path_length": "おおよそのパス長の制限: <b>{length, number}</b>/{limit, number}",
"storage_template_settings": "ストレージ テンプレート",
"storage_template_settings_description": "アップロードしたアセットのフォルダ構造とファイル名を管理します",
"storage_template_user_label": "<code>{label}</code>はユーザーのストレージラベルです",
"system_settings": "システム設定",
"theme_custom_css_settings": "カスタムCSS",
"theme_custom_css_settings_description": "CSS を使って Immich のデザインをカスタマイズできます。",
@ -232,6 +250,7 @@
"transcoding_audio_codec": "音声コーデック",
"transcoding_audio_codec_description": "Opusが最も品質の高い選択ですが、古いデバイスやソフトウェアとの互換性が低下します。",
"transcoding_bitrate_description": "最大ビットレートを超える動画、または容認されていない形式の動画",
"transcoding_codecs_learn_more": "ここで使用される用語について知るには、<h264-link>H.264 コーデック</h264-link>と<hevc-link>HEVC コーデック</hevc-link>、<vp9-link>VP9 コーデック</vp9-link>についてのFFmpegのドキュメントを参照してください。",
"transcoding_constant_quality_mode": "品質固定モード",
"transcoding_constant_quality_mode_description": "ICQはCQPより優れていますが、一部のハードウェアアクセラレーションデバイスはこのモードをサポートしていません。このオプションを設定すると、品質ベースのエンコードを使用する際に指定されたモードが優先されます。ただし、NVEncではICQをサポートしていないため、この設定は無視されます。",
"transcoding_constant_rate_factor": "CRF値 (-crf)",
@ -240,7 +259,7 @@
"transcoding_hardware_acceleration": "ハードウェアアクセラレーション",
"transcoding_hardware_acceleration_description": "より高速ですが、同じビットレートではより低品質になります(実験的)",
"transcoding_hardware_decoding": "ハードウェアデコード",
"transcoding_hardware_decoding_setting_description": "NVEncRKMPP にのみ適用されます。エンコード アクセラレーションだけでなく、エンドツーエンド アクセラレーションを可能にします。すべての動画で動作するとは限りません。",
"transcoding_hardware_decoding_setting_description": "NVEnc、QSV、RKMPP にのみ適用されます。エンコード アクセラレーションだけでなく、エンドツーエンド アクセラレーションを可能にします。すべての動画で動作するとは限りません。",
"transcoding_hevc_codec": "HEVC コーデック",
"transcoding_max_b_frames": "最大Bフレーム",
"transcoding_max_b_frames_description": "値を高くすると圧縮効率が向上しますが、エンコード速度が遅くなります。古いデバイスのハードウェアアクセラレーションでは対応していない場合があります。\"0\" はBフレームを無効にし、\"-1\" はこの値を自動的に設定します。",
@ -267,7 +286,7 @@
"transcoding_tone_mapping": "トーンマッピング",
"transcoding_tone_mapping_description": "HDR動画をSDRに変換する際に見た目を維持しようと試みます。各アルゴリズムは、色、詳細、明るさに対して異なるトレードオフを行います。Hableは詳細を維持し、Mobiusは色を維持し、Reinhardは明るさを維持します。",
"transcoding_tone_mapping_npl": "トーンマッピング NPL",
"transcoding_tone_mapping_npl_description": "",
"transcoding_tone_mapping_npl_description": "この明るさの表示で正常に見えるように色が調整されます。直観に反しますが、値を低くするとディスプレイの明るさが補正されてビデオの明るさが増加し、その逆も同様です。0にするとこの値は自動で設定されます。",
"transcoding_transcode_policy": "トランスコードポリシー",
"transcoding_transcode_policy_description": "動画がトランスコードされるべきかを決めるポリシー。HDRビデオは常にトランスコードされます(トランスコードが無効化されている場合を除く)。",
"transcoding_two_pass_encoding": "Two-passエンコード",
@ -316,14 +335,18 @@
"album_share_no_users": "このアルバムを全てのユーザーと共有したか、共有するユーザーがいないようです。",
"album_updated": "アルバム更新",
"album_updated_setting_description": "共有アルバムに新しいアセットが追加されたとき通知を受け取る",
"album_user_removed": "{user} を削除しました",
"album_with_link_access": "リンクを知っている人は誰でも、このアルバム中の写真と人物を閲覧できるようになります。",
"albums": "アルバム",
"albums_count": "{count, plural, one {{count, number} 件} other {{count, number} 件}}のアルバム",
"all": "すべて",
"all_albums": "全てのアルバム",
"all_people": "全ての人物",
"all_videos": "全ての動画",
"allow_dark_mode": "ダークモードを許可",
"allow_edits": "編集を許可",
"allow_public_user_to_download": "一般ユーザーによるダウンロードを許可",
"allow_public_user_to_upload": "一般ユーザーによるアップロードを許可",
"api_key": "APIキー",
"api_key_description": "この値は一回のみ表示されます。 ウィンドウを閉じる前に必ずコピーしてください。",
"api_key_empty": "APIキー名は空白にできません",
@ -333,6 +356,7 @@
"archive": "アーカイブ",
"archive_or_unarchive_photo": "写真をアーカイブまたはアーカイブ解除",
"archive_size": "アーカイブサイズ",
"archive_size_description": "ダウンロードのアーカイブ サイズを設定(GiB 単位)",
"archived": "",
"are_these_the_same_person": "これらは同じ人物ですか?",
"are_you_sure_to_do_this": "本当にこれを行いますか?",
@ -365,10 +389,10 @@
"backward": "新しい方へ",
"birthdate_saved": "生年月日が正常に保存されました",
"birthdate_set_description": "生年月日は、写真撮影時のこの人物の年齢を計算するために使用されます。",
"blurred_background": "",
"bulk_delete_duplicates_confirmation": "本当に {count} 個の重複したアセットを一括削除しますか?これにより各重複中の最大のアセットが保持され、他の全ての重複が削除されます。この操作を元に戻すことはできません!",
"bulk_keep_duplicates_confirmation": "本当に {count}個の重複アセットを保持しますか?これにより何も削除されずに重複グループが解決されます。",
"bulk_trash_duplicates_confirmation": "本当に{count}個の重複したアセットを一括でごみ箱に移動しますか?これにより各重複中の最大のアセットが保持され、他の全ての重複はごみ箱に移動されます。",
"blurred_background": "ぼやけた背景",
"bulk_delete_duplicates_confirmation": "本当に {count, plural, one {#個} other {#}}の重複したアセットを一括削除しますか?これにより各重複中の最大のアセットが保持され、他の全ての重複が削除されます。この操作を元に戻すことはできません!",
"bulk_keep_duplicates_confirmation": "本当に{count, plural, one {#個} other {#}}の重複アセットを保持しますか?これにより何も削除されずに重複グループが解決されます。",
"bulk_trash_duplicates_confirmation": "本当に{count, plural, one {#個} other {#}}の重複したアセットを一括でごみ箱に移動しますか?これにより各重複中の最大のアセットが保持され、他の全ての重複はごみ箱に移動されます。",
"camera": "カメラブランド",
"camera_brand": "カメラブランド",
"camera_model": "カメラモデル",
@ -432,20 +456,23 @@
"create_new_person_hint": "選択されたアセットを新しい人物に割り当て",
"create_new_user": "新規ユーザーの作成",
"create_user": "ユーザーを作成",
"created": "",
"created": "作成",
"current_device": "現在のデバイス",
"custom_locale": "",
"custom_locale_description": "",
"custom_locale_description": "言語と地域に基づいて日付と数値をフォーマットします",
"dark": "",
"date_after": "",
"date_and_time": "日付と時間",
"date_before": "",
"date_of_birth_saved": "生年月日は正常に保存されました",
"date_range": "日付",
"day": "",
"default_locale": "",
"default_locale_description": "",
"deduplicate_all": "全て重複排除",
"default_locale": "デフォルトのロケール",
"default_locale_description": "ブラウザのロケールに基づいて日付と数値をフォーマットします",
"delete": "削除",
"delete_album": "アルバムを削除",
"delete_duplicates_confirmation": "本当にこれらの重複を完全に削除しますか?",
"delete_key": "",
"delete_library": "",
"delete_link": "",
@ -459,14 +486,17 @@
"discover": "",
"dismiss_all_errors": "",
"dismiss_error": "",
"display_options": "",
"display_order": "",
"display_original_photos": "",
"display_original_photos_setting_description": "",
"display_options": "表示オプション",
"display_order": "表示順",
"display_original_photos": "オリジナルの写真を表示",
"display_original_photos_setting_description": "オリジナルのアセットが Web 互換である場合は、アセットを表示するときにサムネイルではなく元の写真を優先して表示します。これにより写真の表示速度が遅くなる可能性があります。",
"done": "完了",
"download": "ダウンロード",
"download_settings": "ダウンロード",
"download_settings_description": "アセットのダウンロードに関連する設定を管理します",
"downloading": "ダウンロード中",
"downloading_asset_filename": "アセット {filename} をダウンロード中",
"drop_files_to_upload": "ファイルをドロップしてアップロード",
"duplicates": "重複",
"duration": "",
"durations": {
@ -477,46 +507,75 @@
"years": ""
},
"edit_album": "",
"edit_avatar": "",
"edit_date": "",
"edit_date_and_time": "",
"edit_avatar": "アバターを編集",
"edit_date": "日付を編集",
"edit_date_and_time": "日時を編集",
"edit_exclusion_pattern": "",
"edit_faces": "",
"edit_faces": "顔を編集",
"edit_import_path": "",
"edit_import_paths": "",
"edit_key": "",
"edit_key": "キーを編集",
"edit_link": "リンクを編集する",
"edit_location": "位置情報を編集",
"edit_name": "名前を変更",
"edit_people": "",
"edit_title": "",
"edit_user": "",
"edit_people": "人物を編集",
"edit_title": "タイトルを編集",
"edit_user": "ユーザーを編集",
"edited": "",
"editor": "",
"email": "メールアドレス",
"empty": "",
"empty_album": "",
"empty_trash": "コミ箱を空にする",
"empty_trash_confirmation": "本当にゴミ箱を空しますか? これにより、ゴミ箱内のすべてのアセットが Immich から永久に削除されます。\nこの操作を元に戻すことはできません!",
"enable": "",
"enabled": "",
"end_date": "",
"error": "",
"error_loading_image": "",
"end_date": "終了日",
"error": "エラー",
"error_loading_image": "画像の読み込みエラー",
"error_title": "エラー - 問題が発生しました",
"errors": {
"cannot_navigate_next_asset": "次のアセットに移動できません",
"cannot_navigate_previous_asset": "前のアセットに移動できません",
"cant_apply_changes": "変更を適用できません",
"cant_change_activity": "アクティビティを{enabled, select, true {無効化} other {有効化}}できません",
"cant_change_asset_favorite": "アセットのお気に入りを変更できません",
"cant_change_metadata_assets_count": "{count, plural, one {#個} other {#個}}のアセットのメタデータを変更できません",
"cant_get_faces": "顔を取得できません",
"cant_get_number_of_comments": "コメント数を取得できません",
"cant_search_people": "人物を検索できません",
"cant_search_places": "場所を検索できません",
"cleared_jobs": "{job} のジョブをクリアしました",
"error_adding_assets_to_album": "アセットをアルバムに追加中のエラー",
"error_adding_users_to_album": "ユーザーをアルバムに追加中のエラー",
"error_deleting_shared_user": "共有ユーザを削除中のエラー",
"error_downloading": "{filename}をダウンロード中にエラー",
"error_removing_assets_from_album": "アルバムからアセットを削除中のエラー、詳細についてはコンソールを確認してください",
"error_selecting_all_assets": "全アセット選択のエラー",
"failed_job_command": "ジョブ{job} のコマンド{command} が失敗しました",
"failed_to_create_album": "アルバムを作成できませんでした",
"failed_to_create_shared_link": "共有リンクを作成できませんでした",
"failed_to_edit_shared_link": "共有リンクを編集できませんでした",
"failed_to_get_people": "人物を取得できませんでした",
"failed_to_load_asset": "アセットを読み込めませんでした",
"failed_to_load_assets": "アセットを読み込めませんでした",
"unable_to_add_album_users": "",
"unable_to_add_comment": "",
"unable_to_add_partners": "",
"unable_to_add_partners": "パートナーを追加できません",
"unable_to_add_remove_archive": "アーカイブ{archived, select, true {からアセットを削除} other {にアセットを追加}}できません",
"unable_to_archive_unarchive": "{archived, select, true {アーカイブ} other {アーカイブ解除}}できません",
"unable_to_change_album_user_role": "",
"unable_to_change_date": "",
"unable_to_change_date": "日付を変更できません",
"unable_to_change_location": "",
"unable_to_check_item": "",
"unable_to_check_items": "",
"unable_to_create_admin_account": "",
"unable_to_create_admin_account": "管理者アカウントを作成できません",
"unable_to_create_library": "",
"unable_to_create_user": "",
"unable_to_delete_album": "",
"unable_to_delete_asset": "",
"unable_to_delete_user": "",
"unable_to_download_files": "ファイルをダウンロードできません",
"unable_to_empty_trash": "",
"unable_to_enter_fullscreen": "",
"unable_to_exit_fullscreen": "",
@ -526,20 +585,21 @@
"unable_to_load_items": "",
"unable_to_load_liked_status": "",
"unable_to_play_video": "",
"unable_to_refresh_user": "",
"unable_to_refresh_user": "ユーザーを更新できません",
"unable_to_remove_album_users": "",
"unable_to_remove_comment": "",
"unable_to_remove_library": "",
"unable_to_remove_partner": "",
"unable_to_remove_partner": "パートナーを削除できません",
"unable_to_remove_reaction": "",
"unable_to_remove_user": "",
"unable_to_repair_items": "",
"unable_to_reset_password": "",
"unable_to_resolve_duplicate": "",
"unable_to_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_date_of_birth": "生年月日を保存できません",
"unable_to_save_name": "",
"unable_to_save_profile": "",
"unable_to_save_settings": "",
@ -549,22 +609,27 @@
"unable_to_submit_job": "",
"unable_to_trash_asset": "",
"unable_to_unlink_account": "",
"unable_to_update_library": "",
"unable_to_update_location": "",
"unable_to_update_settings": "",
"unable_to_update_user": ""
"unable_to_update_album_info": "アルバム情報を更新できません",
"unable_to_update_library": "ライブラリを更新できません",
"unable_to_update_location": "場所を更新できません",
"unable_to_update_settings": "設定を更新できません",
"unable_to_update_user": "ユーザーを更新できません",
"unable_to_upload_file": "ファイルをアップロードできません"
},
"every_day_at_onepm": "",
"every_night_at_midnight": "",
"every_night_at_twoam": "",
"every_six_hours": "",
"exit_slideshow": "",
"exit_slideshow": "スライドショーを終わる",
"expand_all": "",
"expire_after": "有効期限",
"expired": "有効期限が切れました",
"expires_date": "{date} に失効",
"explore": "探索",
"export": "エクスポート",
"export_as_json": "JSONとしてエクスポート",
"extension": "",
"external_libraries": "",
"external_libraries": "外部ライブラリ",
"failed_to_get_people": "",
"favorite": "お気に入り",
"favorite_or_unfavorite_photo": "",
@ -572,8 +637,8 @@
"feature": "",
"feature_photo_updated": "",
"featurecollection": "",
"file_name": "",
"file_name_or_extension": "",
"file_name": "ファイル名",
"file_name_or_extension": "ファイル名または拡張子",
"filename": "",
"files": "",
"filetype": "",
@ -587,7 +652,10 @@
"go_back": "",
"go_to_search": "",
"go_to_share_page": "",
"group_albums_by": "",
"group_albums_by": "これでアルバムをグループ化…",
"group_no": "グループ化なし",
"group_owner": "所有者でグループ化",
"group_year": "年でグループ化",
"has_quota": "",
"hide_gallery": "",
"hide_password": "",
@ -597,8 +665,9 @@
"image": "写真",
"img": "",
"immich_logo": "",
"import_from_json": "JSONからインポート",
"import_path": "",
"in_archive": "",
"in_archive": "アーカイブ済み",
"include_archived": "アーカイブ済みを含める",
"include_shared_albums": "",
"include_shared_partner_assets": "",
@ -612,84 +681,98 @@
},
"invite_people": "",
"invite_to_album": "アルバムに招待",
"items_count": "{count, plural, one {#個} other {#個}}の項目",
"job_settings_description": "",
"jobs": "",
"jobs": "ジョブ",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
"language_setting_description": "",
"last_seen": "",
"keyboard_shortcuts": "キーボードショートカット",
"language": "言語",
"language_setting_description": "優先言語を選択してください",
"last_seen": "最新の活動",
"latest_version": "最新バージョン",
"leave": "",
"let_others_respond": "他のユーザーの返信を許可する",
"level": "",
"level": "レベル",
"library": "ライブラリ",
"library_options": "",
"light": "",
"link_options": "",
"link_options": "リンクのオプション",
"link_to_oauth": "",
"linked_oauth_account": "",
"list": "",
"loading": "",
"loading_search_results_failed": "",
"list": "リスト",
"loading": "読み込み中",
"loading_search_results_failed": "検索結果を読み込めませんでした",
"log_out": "ログアウト",
"log_out_all_devices": "",
"login_has_been_disabled": "",
"look": "",
"loop_videos": "",
"loop_videos_description": "有効にするとディテールビューで自動で動画がループします",
"log_out_all_devices": "全てのデバイスからログアウト",
"logged_out_all_devices": "全てのデバイスからログアウトしました",
"logged_out_device": "デバイスからログアウトしました",
"login": "ログイン",
"login_has_been_disabled": "ログインは無効化されています。",
"logout_all_device_confirmation": "本当に全てのデバイスからログアウトしますか?",
"logout_this_device_confirmation": "本当にこのデバイスからログアウトしますか?",
"look": "見た目",
"loop_videos": "動画をループ",
"loop_videos_description": "有効にすると詳細表示で自動的に動画がループします。",
"make": "メーカー",
"manage_shared_links": "共有済みのリンクを管理",
"manage_sharing_with_partners": "",
"manage_the_app_settings": "",
"manage_your_account": "",
"manage_your_api_keys": "",
"manage_your_devices": "",
"manage_your_oauth_connection": "",
"manage_sharing_with_partners": "パートナーとの共有を管理します",
"manage_the_app_settings": "アプリの設定を管理します",
"manage_your_account": "あなたのアカウントを管理します",
"manage_your_api_keys": "あなたのAPIキーを管理します",
"manage_your_devices": "ログインデバイスを管理します",
"manage_your_oauth_connection": "OAuth接続を管理します",
"map": "地図",
"map_marker_for_images": "{country} {city}で撮影された写真の地図マーカー",
"map_marker_with_image": "",
"map_settings": "マップの設定",
"media_type": "",
"memories": "",
"memories_setting_description": "",
"menu": "",
"merge": "",
"merge_people": "",
"matches": "マッチ",
"media_type": "メディアタイプ",
"memories": "メモリー",
"memories_setting_description": "メモリーの内容を管理します",
"memory": "メモリー",
"menu": "メニュー",
"merge": "マージ",
"merge_people": "人物をマージ",
"merge_people_limit": "一度に結合できる顔は最大5つまでです",
"merge_people_prompt": "これらの人物を統合しますか? この操作は元に戻せません。",
"merge_people_successfully": "",
"minimize": "",
"minute": "",
"minimize": "最小化",
"minute": "",
"missing": "",
"model": "モデル",
"month": "月",
"more": "",
"moved_to_trash": "",
"my_albums": "",
"more": "もっと表示",
"moved_to_trash": "ゴミ箱に移動しました",
"my_albums": "私のアルバム",
"name": "名前",
"name_or_nickname": "",
"name_or_nickname": "名前またはニックネーム",
"never": "行わない",
"new_api_key": "",
"new_api_key": "新しいAPI キー",
"new_password": "新しいパスワード",
"new_person": "",
"new_user_created": "",
"new_version_available": "新しいバージョンが利用可能",
"newest_first": "",
"next": "次",
"next_memory": "",
"no": "",
"no_albums_message": "",
"no_archived_assets_message": "",
"no_assets_message": "",
"no_albums_message": "アルバムを作成して写真や動画を整理しましょう",
"no_archived_assets_message": "写真や動画をアーカイブして、写真一覧から非表示にします",
"no_assets_message": "クリックして最初の写真をアップロード",
"no_duplicates_found": "重複は見つかりませんでした。",
"no_exif_info_available": "",
"no_explore_results_message": "コレクションを探索するにはさらに写真をアップロードしてください。",
"no_favorites_message": "",
"no_libraries_message": "",
"no_favorites_message": "お気に入りに追加すると最高の写真や動画をすぐに見つけられます",
"no_libraries_message": "あなたの写真やビデオを表示するための外部ライブラリを作成しましょう",
"no_name": "",
"no_places": "",
"no_results": "",
"no_shared_albums_message": "",
"not_in_any_album": "",
"notes": "",
"notification_toggle_setting_description": "",
"notification_toggle_setting_description": "メール通知を有効化",
"notifications": "通知",
"notifications_setting_description": "",
"notifications_setting_description": "通知を管理します",
"oauth": "",
"offline": "",
"ok": "了解",
@ -697,20 +780,23 @@
"online": "",
"only_favorites": "",
"only_refreshes_modified_files": "",
"open_the_search_filters": "",
"open_the_search_filters": "検索フィルタを開く",
"options": "オプション",
"organize_your_library": "",
"organize_your_library": "ライブラリを整理",
"other": "",
"other_devices": "",
"other_variables": "",
"owned": "所有中",
"owner": "オーナー",
"partner_sharing": "",
"partners": "",
"partner": "パートナー",
"partner_can_access_assets": "アーカイブ済みのものと削除済みのものを除いた全ての写真と動画",
"partner_can_access_location": "写真が撮影された場所",
"partner_sharing": "パートナとの共有",
"partners": "パートナー",
"password": "パスワード",
"password_does_not_match": "",
"password_required": "",
"password_reset_success": "",
"password_reset_success": "パスワードのリセットに成功",
"past_durations": {
"days": "",
"hours": "",
@ -722,14 +808,17 @@
"pause_memories": "",
"paused": "",
"pending": "",
"people": "ピープル",
"people_sidebar_description": "",
"people": "人物",
"people_sidebar_description": "人物へのリンクをサイドバーに表示",
"perform_library_tasks": "",
"permanent_deletion_warning": "",
"permanent_deletion_warning_setting_description": "",
"permanent_deletion_warning": "永久削除の警告",
"permanent_deletion_warning_setting_description": "アセットを完全に削除するときに警告を表示する",
"permanently_delete": "",
"permanently_deleted_asset": "",
"photo_shared_all_users": "写真をすべてのユーザーと共有したか、共有するユーザーがいないようです。",
"photos": "写真",
"photos_and_videos": "写真と動画",
"photos_count": "{count, plural, one {{count, number}枚} other {{count, number}枚}}",
"photos_from_previous_years": "",
"pick_a_location": "",
"place": "",
@ -740,8 +829,8 @@
"play_or_pause_video": "",
"point": "",
"port": "",
"preset": "",
"preview": "",
"preset": "プリセット",
"preview": "プレビュー",
"previous": "",
"previous_memory": "",
"previous_or_next_photo": "",
@ -754,7 +843,9 @@
"read_changelog": "",
"recent": "",
"recent_searches": "",
"refresh": "",
"refresh": "更新",
"refresh_metadata": "メタデータを更新",
"refresh_thumbnails": "サムネイルを更新",
"refreshed": "",
"refreshes_every_file": "",
"remove": "",
@ -762,18 +853,20 @@
"remove_from_favorites": "",
"remove_from_shared_link": "",
"remove_offline_files": "",
"removed_from_archive": "アーカイブから削除されました",
"repair": "修復",
"repair_no_results_message": "追跡されていないファイルや存在しないファイルがここに表示されます",
"replace_with_upload": "",
"replace_with_upload": "アップロードして置き換え",
"require_password": "",
"reset": "",
"reset_password": "",
"reset": "リセット",
"reset_password": "パスワードをリセット",
"reset_people_visibility": "",
"reset_settings_to_default": "",
"resolved_all_duplicates": "全ての重複を解決しました",
"restore": "復元",
"restore_user": "",
"retry_upload": "",
"review_duplicates": "",
"retry_upload": "アップロードを再試行",
"review_duplicates": "重複を調査",
"role": "",
"save": "保存",
"saved_profile": "",
@ -784,18 +877,18 @@
"scan_new_library_files": "",
"scan_settings": "",
"search": "検索",
"search_albums": "",
"search_by_context": "",
"search_camera_make": "",
"search_camera_model": "",
"search_city": "",
"search_country": "",
"search_albums": "アルバムを検索",
"search_by_context": "状況で検索",
"search_camera_make": "カメラメーカーを検索…",
"search_camera_model": "カメラのモデルを検索…",
"search_city": "市町村を検索…",
"search_country": "国を検索…",
"search_for_existing_person": "",
"search_people": "",
"search_places": "",
"search_state": "",
"search_timezone": "",
"search_type": "",
"search_type": "検索タイプ",
"search_your_photos": "写真を検索",
"searching_locales": "",
"second": "",
@ -814,25 +907,27 @@
"server_version": "サーバーバージョン",
"set": "",
"set_as_album_cover": "",
"set_as_profile_picture": "",
"set_date_of_birth": "",
"set_as_profile_picture": "プロフィール画像として設定",
"set_date_of_birth": "生年月日を設定",
"set_profile_picture": "",
"set_slideshow_to_fullscreen": "",
"set_slideshow_to_fullscreen": "スライドショーをフルスクリーンにする",
"settings": "設定",
"settings_saved": "",
"share": "共有",
"shared": "共有済み",
"shared_by": "",
"shared_by_you": "",
"shared_from_partner": "{partner} による写真",
"shared_links": "共有リンク",
"sharing": "共有中",
"sharing_sidebar_description": "",
"shared_with_partner": "{partner} と共有しました",
"sharing": "共有",
"sharing_sidebar_description": "共有へのリンクをサイドバーに表示",
"show_album_options": "",
"show_file_location": "",
"show_gallery": "",
"show_hidden_people": "",
"show_in_timeline": "",
"show_in_timeline_setting_description": "",
"show_in_timeline_setting_description": "このユーザーの写真と動画をタイムラインに表示",
"show_keyboard_shortcuts": "",
"show_metadata": "メタデータを見る",
"show_or_hide_info": "",
@ -841,20 +936,25 @@
"show_progress_bar": "",
"show_search_options": "",
"shuffle": "",
"sign_out": "サインアウト",
"sign_up": "",
"size": "",
"skip_to_content": "",
"slideshow": "",
"slideshow_settings": "",
"slideshow": "スライドショー",
"slideshow_settings": "スライドショー設定",
"sort_albums_by": "",
"sort_created": "作成日",
"sort_modified": "変更日",
"sort_recent": "最新の写真",
"stack": "スタック",
"stack_selected_photos": "",
"stacktrace": "",
"start_date": "",
"start_date": "開始日",
"state": "都道府県",
"status": "ステータス",
"stop_motion_photo": "",
"stop_photo_sharing": "写真の共有を無効化しますか?",
"stop_photo_sharing_description": "{partner} はあなたの写真にアクセスできなくなります。",
"storage": "ストレージ",
"storage_label": "",
"submit": "",
@ -864,17 +964,18 @@
"sync": "",
"template": "",
"theme": "テーマ",
"theme_selection": "",
"theme_selection_description": "",
"theme_selection": "テーマ選択",
"theme_selection_description": "ブラウザのシステム設定に基づいてテーマを明色または暗色に自動的に設定します",
"time_based_memories": "",
"timezone": "タイムゾーン",
"to_archive": "アーカイブ",
"toggle_settings": "",
"toggle_theme": "",
"toggle_visibility": "",
"total_usage": "",
"trash": "ゴミ箱",
"trash_all": "",
"trash_no_results_message": "",
"trash_no_results_message": "ゴミ箱に入れられた写真や動画がここに表示されます。",
"type": "",
"unarchive": "アーカイブを解除",
"unarchived": "",
@ -887,26 +988,35 @@
"unlinked_oauth_account": "",
"unselect_all": "",
"unstack": "スタックを解除",
"untracked_files_decription": "これらのファイルはアプリケーションによって追跡されていません。これらは移動の失敗、アップロードの中断、またはバグにより取り残されたものである可能性があります",
"up_next": "",
"updated_password": "",
"upload": "アップロード",
"upload_concurrency": "",
"upload_concurrency": "アップロードの同時実行数",
"upload_errors": "アップロードは{count, plural, one {#個} other {#個}}のエラーで完了しました、新しくアップロードされたアセットを見るにはページを更新してください。",
"upload_progress": "残り {remaining} - {processed}/{total} 処理済み",
"upload_skipped_duplicates": "{count, plural, one {#個} other {#個}}の重複アセットをスキップしました",
"upload_status_duplicates": "重複",
"upload_status_errors": "エラー",
"upload_status_uploaded": "アップロード済",
"upload_success": "アップロード成功、新しくアップロードされたアセットを見るにはページを更新してください。",
"url": "",
"usage": "",
"user": "",
"user_id": "",
"user_id": "ユーザーID",
"user_usage_detail": "",
"username": "",
"users": "",
"utilities": "",
"users": "ユーザー",
"utilities": "ユーティリティ",
"validate": "",
"variables": "",
"version": "",
"version": "バージョン",
"version_announcement_closing": "あなたの友人、Alex",
"video": "動画",
"video_hover_setting_description": "",
"video_hover_setting": "ホバー時にサムネイルで動画を再生",
"video_hover_setting_description": "マウスが項目の上にあるときに動画のサムネイルを再生します。無効時でも再生アイコンにカーソルを合わせると再生を開始できます。",
"videos": "ビデオ",
"videos_count": "{count, plural, one {#個} other {#個}}の動画",
"view_all": "すべて見る",
"view_all_users": "",
"view_links": "",

View file

@ -37,7 +37,7 @@
"cleared_jobs": "작업 초기화: {job}",
"config_set_by_file": "구성 파일의 내용으로 설정됨",
"confirm_delete_library": "{library} 라이브러리를 삭제하시겠습니까?",
"confirm_delete_library_assets": "이 라이브러리를 삭제하시겠습니까? Immich에서 {count, plural, one {#개 항목을} other {모든 항목 #개를}} 삭제하며 되돌릴 수 없습니다. 파일은 디스크에 남아 있습니다.",
"confirm_delete_library_assets": "이 라이브러리를 삭제하시겠습니까? Immich에서 {count, plural, one {#개 항목을} other {항목 #개를 모두}} 삭제하며 되돌릴 수 없습니다. 파일은 디스크에 남아 있습니다.",
"confirm_email_below": "계속 진행하려면 아래에 \"{email}\" 입력",
"confirm_reprocess_all_faces": "모든 얼굴을 다시 처리하시겠습니까? 이름이 지정된 인물도 삭제됩니다.",
"confirm_user_password_reset": "{user}의 비밀번호를 재설정하시겠습니까?",
@ -74,8 +74,8 @@
"job_settings": "작업 설정",
"job_settings_description": "작업 동시성 관리",
"job_status": "작업 상태",
"jobs_delayed": "작업 {jobCount, plural, other {#개}} 지연됨",
"jobs_failed": "작업 {jobCount, plural, other {#개}} 실패",
"jobs_delayed": "작업 {jobCount, plural, other {#개}} 지연됨",
"jobs_failed": "작업 {jobCount, plural, other {#개}} 실패",
"library_created": "라이브러리 {library} 생성됨",
"library_cron_expression": "Cron 표현식",
"library_cron_expression_description": "cron 형식을 사용하여 스캔 주기를 설정합니다. 자세한 예제는 <link>Crontab Guru</link>를 확인하세요",
@ -159,7 +159,7 @@
"notification_email_username_description": "이메일 서버 인증 시 사용할 사용자 이름",
"notification_enable_email_notifications": "이메일 알림 활성화",
"notification_settings": "알림 설정",
"notification_settings_description": "이메일을 포함한 알림 설정 관리",
"notification_settings_description": "이메일 등의 알림 설정 관리",
"oauth_auto_launch": "자동 실행",
"oauth_auto_launch_description": "로그인 페이지로 이동하면 자동으로 OAuth 로그인 시작",
"oauth_auto_register": "자동 등록",
@ -183,7 +183,7 @@
"oauth_storage_quota_claim_description": "사용자의 스토리지 할당을 요청 값으로 자동으로 설정합니다.",
"oauth_storage_quota_default": "기본 스토리지 할당량 (GiB)",
"oauth_storage_quota_default_description": "할당량을 요청하지 않은 경우 사용할 GiB 단위의 기본 할당량 (무제한 할당량을 설정하려면 0 입력).",
"offline_paths": "오프라인 경로",
"offline_paths": "누락된 파일",
"offline_paths_description": "외부 라이브러리가 아닌 서버 내의 파일을 직접 삭제한 경우 발생할 수 있습니다.",
"password_enable_description": "이메일과 비밀번호로 로그인",
"password_settings": "비밀번호 로그인",
@ -193,10 +193,10 @@
"refreshing_all_libraries": "모든 라이브러리 새로고침",
"registration": "관리자 가입",
"registration_description": "시스템의 첫 사용자이므로 관리자로 지정되었으며, 관리 작업 및 사용자 생성을 담당하게 됩니다.",
"removing_offline_files": "오프라인 파일 삭제 중",
"removing_offline_files": "누락된 파일 제거 중",
"repair_all": "모두 수리",
"repair_matched_items": "{count, plural, one {#개} other {#개}} 항목 일치",
"repaired_items": "{count, plural, one {#개} other {#개}} 항목이 수리됨",
"repair_matched_items": "{count, plural, one {#개} other {#개}} 항목 일치",
"repaired_items": "{count, plural, one {#개} other {#개}} 항목을 수리함",
"require_password_change_on_login": "사용자가 처음 로그인할 때 비밀번호 변경 요구",
"reset_settings_to_default": "설정을 기본값으로 복원",
"reset_settings_to_recent_saved": "마지막으로 저장된 설정으로 복원",
@ -211,7 +211,7 @@
"server_welcome_message_description": "로그인 페이지에 표시되는 메시지입니다.",
"sidecar_job": "사이드카 메타데이터",
"sidecar_job_description": "파일 시스템의 사이드카 메타데이터 탐색 및 동기화",
"slideshow_duration_description": "초 단위의 각 사진이 표시되는 시간",
"slideshow_duration_description": "각 사진을 표시할 초 단위의 시간",
"smart_search_job_description": "스마트 검색을 사용하기 위한 기계 학습 실행",
"storage_template_date_time_description": "콘텐츠의 생성 타임스탬프가 날짜 및 시간 정보로 사용됨",
"storage_template_date_time_sample": "시간 형식 예 {date}",
@ -328,7 +328,7 @@
"age_year_months": "생후 1년, {months, plural, one {#개월} other {#개월}}",
"age_years": "{years, plural, other {#}}세",
"album_added": "앨범 추가",
"album_added_notification_setting_description": "공유 앨범에 추가되면 이메일 알림을 받기",
"album_added_notification_setting_description": "공유 앨범에 초대되면 이메일 알림을 받기",
"album_cover_updated": "앨범 커버 업데이트됨",
"album_delete_confirmation": "{album} 앨범을 삭제하시겠습니까?\n이 앨범이 공유된 경우, 다른 사용자가 더 이상 앨범에 접근할 수 없습니다.",
"album_info_updated": "앨범 정보 업데이트됨",
@ -366,16 +366,16 @@
"archive_size_description": "다운로드할 압축 파일의 크기 구성 (GiB 단위)",
"archived": "보관됨",
"archived_count": "보관함으로 {count, plural, other {#개}} 항목 이동됨",
"are_these_the_same_person": "이 인물들은 동일인인가요?",
"are_these_the_same_person": "이들은 동일 인가요?",
"are_you_sure_to_do_this": "정말로 이 작업을 수행하시겠습니까?",
"asset_added_to_album": "앨범에 추가됨",
"asset_adding_to_album": "앨범에 추가 중...",
"asset_description_updated": "항목 설명이 업데이트됨",
"asset_filename_is_offline": "{filename} 콘텐츠 오프라인",
"asset_filename_is_offline": "{filename} 항목 누락됨",
"asset_has_unassigned_faces": "할당되지 않은 얼굴이 콘텐츠에 있음",
"asset_hashing": "해싱...",
"asset_offline": "콘텐츠 오프라인",
"asset_offline_description": "이 콘텐츠는 오프라인 상태이며, Immich가 파일 위치에 접근할 수 없습니다. 콘텐츠가 사용 가능한지 확인한 후 라이브러리를 다시 스캔하세요.",
"asset_offline": "항목 누락됨",
"asset_offline_description": "이 항목은 누락되었으며, 파일 위치에 접근할 수 없습니다. 파일에 접근이 가능한지 확인 후 라이브러리를 다시 스캔하세요.",
"asset_skipped": "건너뜀",
"asset_uploaded": "업로드됨",
"asset_uploading": "업로드 중...",
@ -416,9 +416,9 @@
"cant_get_faces": "얼굴을 가져올 수 없음",
"cant_search_people": "인물을 검색할 수 없음",
"cant_search_places": "장소를 검색할 수 없음",
"change_date": "날짜 수정",
"change_expiration_time": "만료일 수정",
"change_location": "위치 수정",
"change_date": "날짜 변경",
"change_expiration_time": "만료일 변경",
"change_location": "위치 변경",
"change_name": "이름 변경",
"change_name_successfully": "이름이 성공적으로 변경됨",
"change_password": "비밀번호 변경",
@ -498,7 +498,7 @@
"details": "상세 정보",
"direction": "방향",
"disabled": "비활성화",
"disallow_edits": "편집 비허용",
"disallow_edits": "편집 불가",
"discover": "탐색",
"dismiss_all_errors": "모든 오류 무시",
"dismiss_error": "오류 무시",
@ -510,7 +510,7 @@
"done": "완료",
"download": "다운로드",
"download_settings": "다운로드",
"download_settings_description": "콘텐츠 다운로드 관련 설정 관리",
"download_settings_description": "다운로드 설정 관리",
"downloading": "다운로드 중",
"downloading_asset_filename": "{filename} 다운로드 중",
"drop_files_to_upload": "아무 곳에나 파일을 드롭하여 업로드",
@ -536,8 +536,8 @@
"edit_key": "키 편집",
"edit_link": "링크 편집",
"edit_location": "경로 편집",
"edit_name": "이름 편집",
"edit_people": "인물 편집",
"edit_name": "이름 변경",
"edit_people": "인물 수정",
"edit_title": "제목 편집",
"edit_user": "사용자 편집",
"edited": "수정됨",
@ -597,9 +597,9 @@
"unable_to_add_remove_favorites": "항목을 {favorite, select, true {즐겨찾기에 추가할 수 없음} other {즐겨찾기에서 제거할 수 없음}}",
"unable_to_archive_unarchive": "{archived, select, true {보관함으로 항목을 이동할} other {보관함에서 항목을 제거할}} 수 없음",
"unable_to_change_album_user_role": "앨범 사용자의 역할을 변경할 수 없음",
"unable_to_change_date": "날짜를 수정할 수 없음",
"unable_to_change_date": "날짜를 변경할 수 없음",
"unable_to_change_favorite": "항목의 즐겨찾기 상태를 변경할 수 없음",
"unable_to_change_location": "위치를 수정할 수 없음",
"unable_to_change_location": "위치를 변경할 수 없음",
"unable_to_change_password": "비밀번호를 변경할 수 없음",
"unable_to_change_visibility": "인물 {count, plural, one {#명} other {#명}}의 숨김 여부를 변경할 수 없음",
"unable_to_check_item": "",
@ -645,7 +645,7 @@
"unable_to_remove_assets_from_shared_link": "공유 링크에서 항목을 제거할 수 없음",
"unable_to_remove_comment": "",
"unable_to_remove_library": "라이브러리를 제거할 수 없음",
"unable_to_remove_offline_files": "오프라인 파일을 제거할 수 없음",
"unable_to_remove_offline_files": "누락된 파일을 제거할 수 없음",
"unable_to_remove_partner": "파트너를 제거할 수 없음",
"unable_to_remove_reaction": "반응을 삭제할 수 없음",
"unable_to_remove_user": "",
@ -729,6 +729,10 @@
"host": "호스트",
"hour": "시간",
"image": "이미지",
"image_alt_text_date": "{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": "{country}, {city}에서",
"image_taken": "{isVideo, select, true {동영상 촬영됨} other {사진 촬영됨}},",
"img": "",
"immich_logo": "Immich 로고",
"immich_web_interface": "Immich 웹 인터페이스",
@ -798,15 +802,15 @@
"matches": "일치",
"media_type": "미디어 종류",
"memories": "추억",
"memories_setting_description": "추억에 표시하려는 항목 관리",
"memories_setting_description": "추억에 표시는 항목 관리",
"memory": "추억",
"menu": "메뉴",
"merge": "병합",
"merge_people": "인물 병합",
"merge_people_limit": "한 번에 최대 5개의 얼굴만 병합할 수 있음",
"merge_people_prompt": " 인물들을 병합하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
"merge_people_prompt": "선택한 인물들을 병합하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
"merge_people_successfully": "인물을 성공적으로 병합함",
"merged_people_count": "{count, plural, one {#명} other {#명}}의 인물을 병합함",
"merged_people_count": "인물 {count, plural, one {#명} other {#명}}을 병합함",
"minimize": "최소화",
"minute": "분",
"missing": "누락",
@ -848,10 +852,10 @@
"notes": "참고",
"notification_toggle_setting_description": "이메일 알림 활성화",
"notifications": "알림",
"notifications_setting_description": "알림 관리",
"notifications_setting_description": "알림 설정 관리",
"oauth": "OAuth",
"offline": "오프라인",
"offline_paths": "오프라인 경로",
"offline_paths": "누락된 파일",
"offline_paths_description": "외부 라이브러리가 아닌 서버 내의 파일을 직접 삭제한 경우 발생할 수 있습니다.",
"ok": "확인",
"oldest_first": "오래된 순",
@ -960,7 +964,7 @@
"remove_from_album": "앨범에서 제거",
"remove_from_favorites": "즐겨찾기에서 제거",
"remove_from_shared_link": "공유 링크에서 삭제",
"remove_offline_files": "오프라인 파일 삭제",
"remove_offline_files": "누락된 파일 제거",
"remove_user": "사용자 삭제",
"removed_api_key": "API 키 삭제됨: {name}",
"removed_from_archive": "보관함에서 제거됨",

View file

@ -729,6 +729,10 @@
"host": "Host",
"hour": "Uur",
"image": "Afbeelding",
"image_alt_text_date": "op {date}",
"image_alt_text_people": "{count, plural, =1 {met {person1}} =2 {met {person1} en {person2}} =3 {met {person1}, {person2} en {person3}} other {met {person1}, {person2} en {others, number} anderen}}",
"image_alt_text_place": "in {city}, {country}",
"image_taken": "{isVideo, select, true {Video gemaakt} other {Afbeelding genomen}}",
"img": "",
"immich_logo": "Immich logo",
"immich_web_interface": "Immich Web Interface",

View file

@ -1194,7 +1194,7 @@
"welcome": "Witaj",
"welcome_to_immich": "Witamy w immich",
"year": "Rok",
"years_ago": "{years, plural, one {# rok} other {# lat}} temu",
"years_ago": "{years, plural, one {# rok} few {# lata} other {# lat}} temu",
"yes": "Tak",
"you_dont_have_any_shared_links": "Nie masz żadnych udostępnionych linków",
"zoom_image": "Powiększ obraz"

View file

@ -23,12 +23,18 @@
"add_to": "Adicionar a...",
"add_to_album": "Adicionar ao álbum",
"add_to_shared_album": "Adicionar ao álbum compartilhado",
"added_to_archive": "Adicionado ao arquivo",
"added_to_favorites": "Adicionado aos favoritos",
"added_to_favorites_count": "Adicionados {count} aos favoritos",
"admin": {
"add_exclusion_pattern_description": "Adicione padrões de exclusão. Utilizar *, ** ou ? são suportados. Para ignorar todos os arquivos em qualquer diretório chamado \"Raw\", use \"**/Raw/**'. Para ignorar todos os arquivos que finalizam em \".tif\", use \"**/*.tif\". Para ignorar um caminho absoluto, use \"/caminho/para/ignorar/**\".",
"authentication_settings": "Configurações de Autenticação",
"authentication_settings_description": "Gerenciar senhas, OAuth, e outras configurações de autenticação",
"authentication_settings_disable_all": "Tem a certeza que deseja desativar todos os métodos de entrada? Entrar será completamente desativado.",
"authentication_settings_reenable": "Para reativar, use um <link>Comando de servidor</link>.",
"background_task_job": "Tarefas em segundo plano",
"check_all": "Selecionar Tudo",
"cleared_jobs": "Eliminadas as tarefas de: {job}",
"config_set_by_file": "A configuração está atualmente definida por um arquivo de configuração",
"confirm_delete_library": "Você tem certeza que deseja excluir a biblioteca {library} ?",
"confirm_delete_library_assets": "Você tem certeza que deseja excluir esta biblioteca? Isso excluirá todos os {count} ativos contidos no Immich e não poderá ser desfeito. Os arquivos permanecerão no disco.",
@ -45,6 +51,7 @@
"face_detection": "Detecção de faces",
"face_detection_description": "Detecta faces em ativos com inteligência artificial. Para vídeos, apenas a miniatura é considerada. \"Todos\" (re)processa todos os ativos. \"Ausente\" enfileira ativos que ainda não foram processados. As faces detectadas serão enfileiradas para reconhecimento facial após a conclusão da detecção de faces, agrupando-os em pessoas novas ou existentes.",
"facial_recognition_job_description": "Agrupa faces detectados em pessoas. Esta etapa é executada após a conclusão da detecção de faces. \"Todos\" (re)agrupa todos os rostos. \"Ausentes\" enfileira faces que ainda não têm uma pessoa atribuída.",
"failed_job_command": "Comando {command} falhou para a tarefa: {job}",
"force_delete_user_warning": "AVISO: Isso removerá imediatamente o usuário e todos os ativos. Isso não pode ser desfeito e os arquivos não podem ser recuperados.",
"forcing_refresh_library_files": "Forçando a atualização de todos os arquivos da biblioteca",
"image_format_description": "WebP produz arquivos menores que JPEG, mas é mais lento para codificar.",
@ -71,6 +78,7 @@
"jobs_failed": "{jobCount} falhou",
"library_created": "Criado biblioteca: {library}",
"library_cron_expression": "Expressão Cron",
"library_cron_expression_description": "Defina o intervalo de procura utilizando o formato cron. Para mais informações consulte <link>Guru Crontab</link>",
"library_cron_expression_presets": "Predefinições de expressão Cron",
"library_deleted": "Biblioteca excluída",
"library_import_path_description": "Especifique uma pasta para importar. Esta pasta, incluindo subpastas, será escaneada em busca de imagens e vídeos.",
@ -119,6 +127,7 @@
"map_dark_style": "Tema Escuro",
"map_enable_description": "Ativar recursos do mapa",
"map_light_style": "Tema Claro",
"map_manage_reverse_geocoding_settings": "Gerir definições de <link>Geocoding inverso</link>",
"map_reverse_geocoding": "Geocodificação reversa",
"map_reverse_geocoding_enable_description": "Ativar geocodificação reversa",
"map_reverse_geocoding_settings": "Configurações de geocodificação reversa",
@ -165,6 +174,7 @@
"oauth_scope": "Escopo",
"oauth_settings": "OAuth",
"oauth_settings_description": "Gerenciar configurações de login do OAuth",
"oauth_settings_more_details": "Para mais informações sobre esta funcionalidade, veja a <link>documentação</link>.",
"oauth_signing_algorithm": "Algoritmo de assinatura",
"oauth_storage_label_claim": "Reivindicação de rótulo de armazenamento",
"oauth_storage_label_claim_description": "Defina automaticamente o rótulo de armazenamento do usuário para o valor desta declaração.",
@ -180,6 +190,8 @@
"paths_validated_successfully": "Todos os caminhos validados com sucesso",
"quota_size_gib": "Tamanho da cota (GiB)",
"refreshing_all_libraries": "Atualizando todas as bibliotecas",
"registration": "Registo de Admin",
"registration_description": "Como é o primeiro utilizador no sistema, será marcado como administrador, e será responsável pelas tarefas administrativas, sendo que utilizadores adicionais serão criados por si.",
"removing_offline_files": "Removendo arquivos offline",
"repair_all": "Reparar tudo",
"repair_matched_items": "Encontrado {count, plural, one {# item} other {# itens}}",
@ -943,6 +955,7 @@
"videos": "Vídeos",
"videos_count": "{count, plural, one {# Vídeo} other {# Vídeos}}",
"view": "Ver",
"view_album": "Ver Álbum",
"view_all": "Ver tudo",
"view_all_users": "Ver todos usuários",
"view_links": "Ver links",
@ -950,6 +963,7 @@
"view_previous_asset": "Ver ativo anterior",
"viewer": "Visualizar",
"waiting": "Aguardando",
"warning": "Aviso",
"week": "Semana",
"welcome": "Bem-vindo",
"welcome_to_immich": "Bem-vindo ao Immich",

View file

@ -39,8 +39,8 @@
"confirm_delete_library": "Вы действительно хотите удалить библиотеку \"{library}\"?",
"confirm_delete_library_assets": "Вы уверены, что хотите удалить эту библиотеку? В результате из Immich будет удалено {count} файлов, отменить действие будет невозможно. Файлы останутся на диске.",
"confirm_email_below": "Чтобы подтвердить, введите \"{email}\" ниже",
"confirm_reprocess_all_faces": "Вы уверены что хотите повторно определить все лица? Это также удалит имя со всех лиц.",
"confirm_user_password_reset": "Вы уверены что хотите сбросить пароль пользователя {user}?",
"confirm_reprocess_all_faces": "Вы уверены, что хотите повторно определить все лица? Это также удалит имя со всех лиц.",
"confirm_user_password_reset": "Вы уверены, что хотите сбросить пароль пользователя {user}?",
"crontab_guru": "Crontab Guru",
"disable_login": "Отключить вход",
"disabled": "Выключено",
@ -65,7 +65,7 @@
"image_quality": "Качество",
"image_quality_description": "Качество предосмотра фото, от 1 до 100. Чем выше число, тем лучше качество и больше вес изображения.",
"image_settings": "Настройки Изображений",
"image_settings_description": "Управляйте качеством и разрешением создаваемых изображений",
"image_settings_description": "Управление качеством и разрешением создаваемых изображений",
"image_thumbnail_format": "Формат миниатюр",
"image_thumbnail_resolution": "Разрешение миниатюр",
"image_thumbnail_resolution_description": "Используется при просмотре групп фотографий (на временной шкале, при просмотре альбомов и т.д.). Миниатюры с более высоким разрешением сохранят больше деталей, но потребуют больше времени для кодирования, имеют больший вес и могут снизить скорость отклика приложения.",
@ -185,7 +185,7 @@
"oauth_storage_quota_default_description": "Квота в GiB, которая будет использоваться, если настройка не задана (введите 0 для неограниченной квоты).",
"offline_paths": "Недоступные пути",
"offline_paths_description": "Эти результаты могут быть вызваны ручным удалением файлов, которые не являются частью внешней библиотеки.",
"password_enable_description": "Вход используя эллектронный адрес и пароль",
"password_enable_description": "Входить по электронной почте и паролю",
"password_settings": "Настройки входа с паролем",
"password_settings_description": "Управление настройками входа по паролю",
"paths_validated_successfully": "Все пути успешно прошли проверку",
@ -207,7 +207,7 @@
"server_external_domain_settings_description": "Домен для общедоступных ссылок, включая http(s)://",
"server_settings": "Настройки Сервера",
"server_settings_description": "Управление настройками сервера",
"server_welcome_message": "Приветственное Сообщение",
"server_welcome_message": "Приветственное сообщение",
"server_welcome_message_description": "Сообщение, которое отображается на странице входа.",
"sidecar_job": "Метаданные из sidecar-файлов",
"sidecar_job_description": "Обнаружение и синхронизация метаданных из sidecar-файлов",
@ -226,7 +226,7 @@
"storage_template_onboarding_description": "При включении этой функции файлы будут автоматически организованы в соответствии с пользовательским шаблоном. Из-за проблем со стабильностью функция по умолчанию отключена. Дополнительную информацию можно найти в <link>документации</link>.",
"storage_template_path_length": "Примерная длина пути: <b>{length, number}</b>/{limit, number}",
"storage_template_settings": "Шаблон хранилища",
"storage_template_settings_description": "Управляйте структурой папок и именем загружаемого файла",
"storage_template_settings_description": "Управление структурой папок и именем загружаемого файла",
"storage_template_user_label": "<code>{label}</code> - это метка хранилища пользователя",
"system_settings": "Системные настройки",
"theme_custom_css_settings": "Пользовательский CSS",
@ -289,7 +289,7 @@
"transcoding_tone_mapping_npl": "Tone-mapping NPL",
"transcoding_tone_mapping_npl_description": "Цвета будут отрегулированы так, чтобы выглядеть нормально для дисплея такой яркости. Как ни странно, более низкие значения увеличивают яркость видео и наоборот, поскольку компенсируют яркость дисплея. 0 устанавливает это значение автоматически.",
"transcoding_transcode_policy": "Политика перекодирования",
"transcoding_transcode_policy_description": "Правила, определяющие, когда видео должно быть перекодировано. HDR-видео всегда будут перекодироваться (за исключением случаев, когда перекодирование отключено).",
"transcoding_transcode_policy_description": "Правила, определяющие когда видео должно быть перекодировано. HDR-видео всегда будут перекодироваться (за исключением случаев, когда перекодирование отключено).",
"transcoding_two_pass_encoding": "Двухпроходное кодирование",
"transcoding_two_pass_encoding_setting_description": "Перекодируйте видео в два этапа, чтобы получить более качественное кодирование. Если включен режим максимального битрейта (необходимый для работы с H.264 и HEVC), этот режим использует диапазон битрейта, основанный на максимальном битрейте, и игнорирует CRF. Для VP9 можно использовать CRF, если отключен максимальный битрейт.",
"transcoding_video_codec": "Видео Кодек",
@ -306,7 +306,7 @@
"user_delete_delay_settings_description": "Срок, через который происходит окончательное удаление учетной записи пользователя и его ресурсов после удаления учётной записи, в днях. Задача по удалению пользователей выполняется в полночь. Изменения этой настройки будут учтены при следующем запуске задачи.",
"user_delete_immediately": "Аккаунт и ресурсы пользователя <b>{user}</b> будут поставлены в очередь на <b>немедленное</b> окончательное удаление.",
"user_delete_immediately_checkbox": "Поставить пользователя и объекты в очередь для удаления",
"user_management": "Управление Пользователями",
"user_management": "Управление пользователями",
"user_password_has_been_reset": "Пароль пользователя был сброшен:",
"user_password_reset_description": "Пожалуйста, предоставьте временный пароль пользователю и сообщите ему, что при следующем входе в систему пароль нужно будет изменить.",
"user_restore_description": "Аккаунт пользователя <b>{user}</b> будет восстановлен.",
@ -315,7 +315,7 @@
"user_settings_description": "Управление настройками пользователей",
"user_successfully_removed": "Пользователь {email} был успешно удален.",
"version_check_enabled_description": "Включить периодические запросы к GitHub для проверки наличия новых версий",
"version_check_settings": "Проверка Версии",
"version_check_settings": "Проверка версии",
"version_check_settings_description": "Включить/отключить уведомление о новой версии",
"video_conversion_job": "Перекодирование видео",
"video_conversion_job_description": "Перекодируйте видео для более широкой совместимости с браузерами и устройствами"
@ -328,7 +328,7 @@
"age_year_months": "Возраст 1 год, {months, plural, one {# месяц} few {# месяца} many {# месяцев} other {# месяца}}",
"age_years": "{years, plural, other {Возраст #}}",
"album_added": "Альбом добавлен",
"album_added_notification_setting_description": "Получить уведомление на электронную почту, когда вы добавлены к общему альбому",
"album_added_notification_setting_description": "Получить уведомление по электронной почте, когда вы добавлены к общему альбому",
"album_cover_updated": "Обложка альбома обновлена",
"album_delete_confirmation": "Вы уверены, что хотите удалить альбом {album}?\nЕсли этот альбом общий, то другие пользователи не смогут получить к нему доступ.",
"album_info_updated": "Информация об альбоме обновлена",
@ -356,7 +356,7 @@
"allow_public_user_to_upload": "Разрешить публичным пользователям загружать файлы",
"api_key": "API Ключ",
"api_key_description": "Это значение будет показано только один раз. Пожалуйста, убедитесь, что скопировали его перед закрытием окна.",
"api_key_empty": "Ваш API ключ не может быть пустым",
"api_key_empty": "Ваш API ключ не должен быть пустым",
"api_keys": "Ключи API",
"app_settings": "Параметры приложения",
"appears_in": "Появляется в",
@ -510,7 +510,7 @@
"done": "Готово",
"download": "Скачать",
"download_settings": "Скачать",
"download_settings_description": "Управление настройками относящимся к скачиванию объектов",
"download_settings_description": "Управление настройками скачивания объектов",
"downloading": "Загрузка",
"downloading_asset_filename": "Загрузка объекта {filename}",
"drop_files_to_upload": "Перенесите файлы в любое место для загрузки",
@ -729,6 +729,7 @@
"host": "Хост",
"hour": "Час",
"image": "Изображения",
"image_alt_text_place": "в {city}, {country}",
"img": "",
"immich_logo": "Лого Immich",
"immich_web_interface": "Веб интерфейс Immich",
@ -789,7 +790,7 @@
"manage_the_app_settings": "Управление настройками приложения",
"manage_your_account": "Управление учётной записью",
"manage_your_api_keys": "Управление API-ключами",
"manage_your_devices": "Управляйте устройствами, вошедшими в систему",
"manage_your_devices": "Управление устройствами, вошедшими в систему",
"manage_your_oauth_connection": "Настройки подключённого OAuth",
"map": "Карта",
"map_marker_for_images": "Маркер на карте для изображений, сделанных в {city}, {country}",
@ -898,7 +899,7 @@
"people_edits_count": "Изменено {count, plural, one {# человек} few {# человека} many {# людей} other {# человек}}",
"people_sidebar_description": "Отображать ссылку на персоны в боковой панели",
"perform_library_tasks": "",
"permanent_deletion_warning": "Предупреждение о удалении",
"permanent_deletion_warning": "Предупреждение об удалении",
"permanent_deletion_warning_setting_description": "Отображать предупреждение при безвозвратном удалении ресурсов",
"permanently_delete": "Удалить навсегда",
"permanently_delete_assets_count": "Полностью удалить {count, plural, one {ресурс} few {ресурса} many {ресурсов} other {ресурсов}}",
@ -1021,7 +1022,7 @@
"select_all": "Выбрать все",
"select_avatar_color": "Выбрать цвет аватара",
"select_face": "Выбрать лицо",
"select_featured_photo": "Выбрать избранное фото",
"select_featured_photo": "Выбрать фото профиля",
"select_from_computer": "Выберите с компьютера",
"select_keep_all": "Оставить всё выбранное",
"select_library_owner": "Выбрать владельца библиотеки",
@ -1162,11 +1163,11 @@
"usage": "Использование",
"use_custom_date_range": "Использовать пользовательский диапазон дат",
"user": "Пользователь",
"user_id": "ID Пользователя",
"user_id": "ID пользователя",
"user_liked": "{user} понравилось {type, select, photo {это фото} video {это видео} asset {этот ресурс} other {это}}",
"user_role_set": "Установить {user} в качестве {role}",
"user_usage_detail": "Подробно о использовании пользователями",
"username": "Имя Пользователя",
"username": "Имя пользователя",
"users": "Пользователи",
"utilities": "Утилиты",
"validate": "Проверить",

View file

@ -1,4 +1,5 @@
{
"about": "O aplikácií",
"account": "Účet",
"account_settings": "Nastavenia účtu",
"acknowledge": "Potvrdiť",
@ -6,6 +7,7 @@
"actions": "Akcie",
"active": "Aktívny",
"activity": "Aktivita",
"activity_changed": "Aktivita je {enabled, select, true{povolená} other {zakázaná}}",
"add": "Pridať",
"add_a_description": "Pridať popis",
"add_a_location": "Pridať polohu",
@ -21,6 +23,9 @@
"add_to": "Pridať do...",
"add_to_album": "Pridať do albumu",
"add_to_shared_album": "Pridať do zdieľaného albumu",
"added_to_archive": "Pridané do archívu",
"added_to_favorites": "Pridané do obľúbených",
"added_to_favorites_count": "Pridané {count} do obľúbených",
"admin": {
"authentication_settings": "Nastavenia overenia",
"authentication_settings_description": "Spravovať heslo, protokol OAuth a ďalšie nastavenia overenia",

View file

@ -383,14 +383,14 @@
"assets_added_count": "Dodato {count, plural, one {# datoteka} other {# datoteka}}",
"assets_added_to_album_count": "Dodato je {count, plural, one {# datoteka} other {# datoteka}} u album",
"assets_added_to_name_count": "Dodato {count, plural, one {# datoteka} other {# datoteke}} u {name}",
"assets_count": "{count, plural, one {# datoteka} other {# datoteke}}",
"assets_count": "{count, plural, one {# datoteka} few {# datoteke} other {# datoteka}}",
"assets_moved_to_trash": "{count, plural, one {Premeštena # datoteka} few {Premeštene # datoteke} other {Premeštene # datoteka}} u otpad",
"assets_moved_to_trash_count": "Premešteno {count, plural, one {# datoteka} other {# datoteke}} u otpad",
"assets_permanently_deleted_count": "Trajno izbrisano {count, plural, one {# datoteka} other {# datoteke}}",
"assets_removed_count": "Uklonjeno {count, plural, one {# datoteka} other {# datoteke}}",
"assets_moved_to_trash_count": "Premešteno {count, plural, one {# datoteka} few {# datoteke} other {# datoteka}} u otpad",
"assets_permanently_deleted_count": "Trajno izbrisano {count, plural, one {# datoteka} few {# datoteke} other {# datoteka}}",
"assets_removed_count": "Uklonjeno {count, plural, one {# datoteka} few {# datoteke} other {# datoteka}}",
"assets_restore_confirmation": "Da li ste sigurni da želite da vratite sve svoje datoteke koje su u otpadu? Ne možete poništiti ovu radnju!",
"assets_restored_count": "Vraćeno {count, plural, one {# datoteka} other {# datoteke}}",
"assets_trashed_count": "Bacio u otpad {count, plural, one {# datoteku} other {# datoteke}}",
"assets_restored_count": "Vraćeno {count, plural, one {# datoteka} few {# datoteke} other {# datoteka}}",
"assets_trashed_count": "Bačeno u otpad {count, plural, one {# datoteka} few{# datoteke} other {# datoteka}}",
"assets_were_part_of_album_count": "{count, plural, one {Datoteka je} other {Datoteke su}} već deo albuma",
"authorized_devices": "Ovlašćeni uređaji",
"back": "Nazad",
@ -401,7 +401,7 @@
"blurred_background": "Zamućena pozadina",
"build": "Sagradi (Build)",
"build_image": "Sagradi (Build) image",
"bulk_delete_duplicates_confirmation": "Da li ste sigurni da želite grupno da izbrišete {count, plural, one {# dupliran elemenat} other {# dupliranih elemenata}}? Ovo će zadržati najveće sredstvo svake grupe i trajno izbrisati sve druge duplikate. Ne možete poništiti ovu radnju!",
"bulk_delete_duplicates_confirmation": "Da li ste sigurni da želite grupno da izbrišete {count, plural, one {# dupliran elemenat} few {# duplirana elementa} other {# dupliranih elemenata}}? Ovo će zadržati najveće sredstvo svake grupe i trajno izbrisati sve druge duplikate. Ne možete poništiti ovu radnju!",
"bulk_keep_duplicates_confirmation": "Da li ste sigurni da želite da zadržite {count, plural, one {1 dupliranu datoteku} few {# duplirane datoteke} other {# dupliranih datoteka}}? Ovo će rešiti sve duplirane grupe bez brisanja bilo čega.",
"bulk_trash_duplicates_confirmation": "Da li ste sigurni da želite grupno da odbacite {count, plural, one {1 dupliranu datoteku} few {# duplirane datoteke} other {# dupliranih datoteka}}? Ovo će zadržati najveću datoteku svake grupe i odbaciti sve ostale duplikate.",
"camera": "Kamera",

View file

@ -4,9 +4,10 @@
"account_settings": "Kontoinställningar",
"acknowledge": "Bekräfta",
"action": "Åtgärd",
"actions": "Handlingar",
"actions": "Händelser",
"active": "Aktiva",
"activity": "Aktivitet",
"activity_changed": "Aktiviteten är {aktiverad, välj, sant {aktiverad} annat {inaktiverad}}",
"add": "Lägg till",
"add_a_description": "Lägg till en beskrivning",
"add_a_location": "Lägg till en plats",
@ -30,8 +31,10 @@
"authentication_settings": "Autentiseringsinställningar",
"authentication_settings_description": "Hantera lösenord, OAuth, och andra autentiseringsinställningar",
"authentication_settings_disable_all": "Är du säker på att du vill inaktivera alla inloggningsmetoder? Inloggning kommer att helt inaktiveras.",
"authentication_settings_reenable": "För att återaktivera, använd <link>Server Command</link>.",
"background_task_job": "Bakgrundsaktiviteter",
"check_all": "Välj Alla",
"cleared_jobs": "Rensade jobben för:{job}",
"config_set_by_file": "Konfigurationen är satt av en konfigurationsfil",
"confirm_delete_library": "Är du säker på att du vill radera {library} album?",
"confirm_delete_library_assets": "Är du säker på att du vill radera detta album? Samtliga {count} objekt kommer att tas bort från Immich och åtgärden kan inte ångras. Filerna kommer att behållas på hårddisken.",
@ -75,6 +78,7 @@
"jobs_failed": "{jobCount} misslyckades",
"library_created": "Skapat bibliotek: {library}",
"library_cron_expression": "Cron-uttryck",
"library_cron_expression_description": "Ställ in intervallet för skanningen med cron-formatet. För mer information gå till t.ex. <link>Crontab Guru </link>",
"library_cron_expression_presets": "Cron Uttrycksförinställningar",
"library_deleted": "Biblioteket har tagits bort",
"library_import_path_description": "Ange en mapp att importera. Den här mappen, inklusive undermappar, skannas efter bilder och videor.",
@ -90,21 +94,25 @@
"logging_enable_description": "Aktivera loggning",
"logging_level_description": "När aktiverad, vilken loggnivå som ska användas.",
"logging_settings": "Loggning",
"machine_learning_clip_model": "",
"machine_learning_clip_model": "CLIP modell",
"machine_learning_clip_model_description": "Namnet på en CLIP-modell listad <link> här </link>. Observera att du måste köra ett \"Smart Search\" jobb för alla bilder när du ändrar en modell.",
"machine_learning_duplicate_detection": "Dubblettdetektering",
"machine_learning_duplicate_detection_enabled": "Aktivera dubblett detektion",
"machine_learning_duplicate_detection_enabled_description": "Om den inaktiveras kommer exakt identiska tillgångar fortfarande att dedupliceras.",
"machine_learning_duplicate_detection_setting_description": "",
"machine_learning_duplicate_detection_setting_description": "Använd CLIP-inbäddningar för att hitta troliga dubbletter",
"machine_learning_enabled": "Aktivera maskininlärning",
"machine_learning_enabled_description": "Om det är inaktiverat kommer alla ML-funktioner att inaktiveras oavsett inställningarna nedan.",
"machine_learning_facial_recognition": "Ansiktsigenkänning",
"machine_learning_facial_recognition_description": "Upptäck, känna igen och gruppera ansikten i bilder",
"machine_learning_facial_recognition_model": "Ansiktsigenkänningsmodell",
"machine_learning_facial_recognition_model_description": "",
"machine_learning_facial_recognition_setting_description": "",
"machine_learning_max_detection_distance": "",
"machine_learning_max_detection_distance_description": "",
"machine_learning_max_recognition_distance": "",
"machine_learning_max_recognition_distance_description": "",
"machine_learning_min_detection_score": "",
"machine_learning_facial_recognition_model_description": "Modeller är listade i fallande storleksordning. Större modeller är långsammare och använder mer minne, men ger bättre resultat. Observera att du måste köra Face Detection-jobbet för alla bilder när du ändrar en modell.",
"machine_learning_facial_recognition_setting": "Aktivera ansiktsigenkänning",
"machine_learning_facial_recognition_setting_description": "Om avmarkerad kommer bilder inte att kodas till ansiktsigenkänningen vilket innebär att bilder inte kommer att läggas till i listan av igenkända personer på sidan Utforska.",
"machine_learning_max_detection_distance": "Maximal detektions avstånd",
"machine_learning_max_detection_distance_description": "Maximalt avstånd mellan två bilder för att överväga dem dubbletter, från 0,001-0,1. Högre värden kommer att upptäcka fler dubbletter, men kan leda till falsk positivt.",
"machine_learning_max_recognition_distance": "Maximalt igenkänningsavstånd",
"machine_learning_max_recognition_distance_description": "Det maximala avståndet mellan två ansikten för att anses som samma person, från 0-2. Sänkning av denna kan medföra märkning av två personer som samma person, samtidigt som det kan förhindra att märkning av samma person som två olika personer. Observera att det är lättare att slå samman två personer än att dela en person i två, så ligg hellre närmare den lägre tröskel om det är möjligt.",
"machine_learning_min_detection_score": "Minsta detektions poäng",
"machine_learning_min_detection_score_description": "",
"machine_learning_min_recognized_faces": "",
"machine_learning_min_recognized_faces_description": "",
@ -526,7 +534,7 @@
"invite_people": "",
"invite_to_album": "Bjuder in till album",
"job_settings_description": "",
"jobs": "",
"jobs": "Jobb",
"keep": "",
"keyboard_shortcuts": "",
"language": "",
@ -570,7 +578,7 @@
"merge_people_successfully": "",
"minimize": "",
"minute": "",
"missing": "",
"missing": "Saknade",
"model": "Modell",
"month": "Månad",
"more": "",
@ -765,6 +773,7 @@
"stack": "Stapel",
"stack_selected_photos": "",
"stacktrace": "",
"start": "Starta",
"start_date": "Startdatum",
"state": "Stat",
"status": "Status",

View file

@ -729,6 +729,10 @@
"host": "主机",
"hour": "时",
"image": "图片",
"image_alt_text_date": "在{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": "在{country} {city}",
"image_taken": "{isVideo, select, true {选择视频} other {选择图片}}",
"img": "图片",
"immich_logo": "Immich Logo",
"immich_web_interface": "Immich Web接口",
@ -928,7 +932,7 @@
"previous_memory": "上一个",
"previous_or_next_photo": "上一张或下一张照片",
"primary": "首要",
"profile_image_of_user": "{title}的资料图片",
"profile_image_of_user": "{user}的个人资料图片",
"profile_picture_set": "个人资料图片已设置。",
"public_album": "公开相册",
"public_share": "公开共享",

View file

@ -0,0 +1,68 @@
import { getAltText } from '$lib/utils/thumbnail-util';
import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
import { init, register, waitLocale } from 'svelte-i18n';
describe('getAltText', () => {
beforeAll(async () => {
await init({ fallbackLocale: 'en-US' });
register('en-US', () => import('$lib/i18n/en.json'));
await waitLocale('en-US');
});
it('defaults to the description, if available', () => {
const asset = {
exifInfo: { description: 'description' },
} as AssetResponseDto;
getAltText.subscribe((fn) => {
expect(fn(asset)).toEqual('description');
});
});
it('includes the city and country', () => {
const asset = {
exifInfo: { city: 'city', country: 'country' },
localDateTime: '2024-01-01T12:00:00.000Z',
} as AssetResponseDto;
getAltText.subscribe((fn) => {
expect(fn(asset)).toEqual('Image taken in city, country on January 1, 2024');
});
});
// convert the people tests into an it.each
it.each([
[[{ name: 'person' }], 'Image taken with person on January 1, 2024'],
[[{ name: 'person1' }, { name: 'person2' }], 'Image taken with person1 and person2 on January 1, 2024'],
[
[{ name: 'person1' }, { name: 'person2' }, { name: 'person3' }],
'Image taken with person1, person2, and person3 on January 1, 2024',
],
[
[{ name: 'person1' }, { name: 'person2' }, { name: 'person3' }, { name: 'person4' }],
'Image taken with person1, person2, and 2 others on January 1, 2024',
],
])('includes people, correctly formatted', (people, expected) => {
const asset = {
localDateTime: '2024-01-01T12:00:00.000Z',
people,
} as AssetResponseDto;
getAltText.subscribe((fn) => {
expect(fn(asset)).toEqual(expected);
});
});
it('handles videos, location, people, and date', () => {
const asset = {
exifInfo: { city: 'city', country: 'country' },
localDateTime: '2024-01-01T12:00:00.000Z',
people: [{ name: 'person1' }, { name: 'person2' }, { name: 'person3' }, { name: 'person4' }, { name: 'person5' }],
type: AssetTypeEnum.Video,
} as AssetResponseDto;
getAltText.subscribe((fn) => {
expect(fn(asset)).toEqual('Video taken in city, country with person1, person2, and 3 others on January 1, 2024');
});
});
});

View file

@ -1,4 +1,6 @@
import type { AssetResponseDto } from '@immich/sdk';
import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
import { t } from 'svelte-i18n';
import { derived } from 'svelte/store';
import { fromLocalDateTime } from './timeline-util';
/**
@ -35,29 +37,39 @@ export function getThumbnailSize(assetCount: number, viewWidth: number): number
return 300;
}
export function getAltText(asset: AssetResponseDto) {
export const getAltText = derived(t, ($t) => {
return (asset: AssetResponseDto) => {
if (asset.exifInfo?.description) {
return asset.exifInfo.description;
}
let altText = 'Image taken';
if (asset.exifInfo?.city && asset.exifInfo.country) {
altText += ` in ${asset.exifInfo.city}, ${asset.exifInfo.country}`;
let altText = $t('image_taken', { values: { isVideo: asset.type === AssetTypeEnum.Video } });
if (asset.exifInfo?.city && asset.exifInfo?.country) {
const placeText = $t('image_alt_text_place', {
values: { city: asset.exifInfo.city, country: asset.exifInfo.country },
});
altText += ` ${placeText}`;
}
const names = asset.people?.filter((p) => p.name).map((p) => p.name) ?? [];
if (names.length == 1) {
altText += ` with ${names[0]}`;
}
if (names.length > 1 && names.length <= 3) {
altText += ` with ${names.slice(0, -1).join(', ')} and ${names.at(-1)}`;
}
if (names.length > 3) {
altText += ` with ${names.slice(0, 2).join(', ')}, and ${names.length - 2} others`;
if (names.length > 0) {
const namesText = $t('image_alt_text_people', {
values: {
count: names.length,
person1: names[0],
person2: names[1],
person3: names[2],
others: names.length > 3 ? names.length - 2 : 0,
},
});
altText += ` ${namesText}`;
}
const date = fromLocalDateTime(asset.localDateTime).toLocaleString({ dateStyle: 'long' });
altText += ` on ${date}`;
const dateText = $t('image_alt_text_date', { values: { date } });
altText += ` ${dateText}`;
return altText;
}
};
});

View file

@ -1,13 +1,14 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
import Button from '$lib/components/elements/buttons/button.svelte';
import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import ManagePeopleVisibility from '$lib/components/faces-page/manage-people-visibility.svelte';
import MergeSuggestionModal from '$lib/components/faces-page/merge-suggestion-modal.svelte';
import PeopleCard from '$lib/components/faces-page/people-card.svelte';
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
import SetBirthDateModal from '$lib/components/faces-page/set-birth-date-modal.svelte';
import ShowHide, { ToggleVisibilty } from '$lib/components/faces-page/show-hide.svelte';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import {
@ -15,43 +16,29 @@
NotificationType,
} from '$lib/components/shared-components/notification/notification';
import { ActionQueryParameterValue, AppRoute, QueryParameter } from '$lib/constants';
import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
import { locale } from '$lib/stores/preferences.store';
import { websocketEvents } from '$lib/stores/websocket';
import { handlePromiseError } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import { shortcut } from '$lib/actions/shortcut';
import {
getPerson,
mergePerson,
searchPerson,
updatePeople,
updatePerson,
type PeopleUpdateItem,
type PersonResponseDto,
} from '@immich/sdk';
import { clearQueryParam } from '$lib/utils/navigation';
import { getPerson, mergePerson, searchPerson, updatePerson, type PersonResponseDto } from '@immich/sdk';
import { mdiAccountOff, mdiEyeOutline } from '@mdi/js';
import { onMount } from 'svelte';
import type { PageData } from './$types';
import { locale } from '$lib/stores/preferences.store';
import { clearQueryParam } from '$lib/utils/navigation';
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
import { t } from 'svelte-i18n';
import { websocketEvents } from '$lib/stores/websocket';
import { quintOut } from 'svelte/easing';
import { fly } from 'svelte/transition';
import type { PageData } from './$types';
import { focusTrap } from '$lib/actions/focus-trap';
export let data: PageData;
let people = data.people.people;
let countTotalPeople = data.people.total;
let countHiddenPeople = data.people.hidden;
$: people = data.people.people;
$: visiblePeople = people.filter((people) => !people.isHidden);
$: countVisiblePeople = searchName ? searchedPeopleLocal.length : visiblePeople.length;
$: showPeople = searchName ? searchedPeopleLocal : visiblePeople;
let selectHidden = false;
let initialHiddenValues: Record<string, boolean> = {};
let eyeColorMap: Record<string, 'black' | 'white'> = {};
let searchName = '';
let showLoadingSpinner = false;
let toggleVisibility: ToggleVisibilty = ToggleVisibilty.VIEW_ALL;
let showChangeNameModal = false;
let showSetBirthDateModal = false;
let showMergeModal = false;
@ -62,24 +49,9 @@
let edittingPerson: PersonResponseDto | null = null;
let searchedPeopleLocal: PersonResponseDto[] = [];
let handleSearchPeople: (force?: boolean, name?: string) => Promise<void>;
let showPeople: PersonResponseDto[] = [];
let countVisiblePeople: number;
let changeNameInputEl: HTMLInputElement | null;
let innerHeight: number;
for (const person of people) {
initialHiddenValues[person.id] = person.isHidden;
}
$: {
if (searchName) {
showPeople = searchedPeopleLocal;
countVisiblePeople = searchedPeopleLocal.length;
} else {
showPeople = people.filter((person) => !person.isHidden);
countVisiblePeople = countTotalPeople - countHiddenPeople;
}
}
onMount(() => {
const getSearchedPeople = $page.url.searchParams.get(QueryParameter.SEARCHED_PEOPLE);
if (getSearchedPeople) {
@ -87,11 +59,12 @@
handlePromiseError(handleSearchPeople(true, searchName));
}
return websocketEvents.on('on_person_thumbnail', (personId: string) => {
people.map((person) => {
for (const person of people) {
if (person.id === personId) {
person.updatedAt = Date.now().toString();
person.updatedAt = new Date().toISOString();
}
});
}
// trigger reactivity
people = people;
});
@ -105,89 +78,6 @@
}
};
const handleCloseClick = () => {
for (const person of people) {
person.isHidden = initialHiddenValues[person.id];
}
// trigger reactivity
people = people;
// Reset variables used on the "Show & hide people" modal
showLoadingSpinner = false;
selectHidden = false;
toggleVisibility = ToggleVisibilty.VIEW_ALL;
};
const handleResetVisibility = () => {
for (const person of people) {
person.isHidden = initialHiddenValues[person.id];
}
// trigger reactivity
people = people;
};
const handleToggleVisibility = (toggleVisibility: ToggleVisibilty) => {
for (const person of people) {
if (toggleVisibility == ToggleVisibilty.HIDE_ALL) {
person.isHidden = true;
}
if (toggleVisibility == ToggleVisibilty.VIEW_ALL) {
person.isHidden = false;
}
if (toggleVisibility == ToggleVisibilty.HIDE_UNNANEMD && !person.name) {
person.isHidden = true;
}
}
// trigger reactivity
people = people;
};
const handleDoneClick = async () => {
showLoadingSpinner = true;
let changed: PeopleUpdateItem[] = [];
try {
// Check if the visibility for each person has been changed
for (const person of people) {
if (person.isHidden !== initialHiddenValues[person.id]) {
changed.push({ id: person.id, isHidden: person.isHidden });
if (person.isHidden) {
countHiddenPeople++;
} else {
countHiddenPeople--;
}
// Update the initial hidden values
initialHiddenValues[person.id] = person.isHidden;
}
}
if (changed.length > 0) {
const results = await updatePeople({
peopleUpdateDto: { people: changed },
});
const count = results.filter(({ success }) => success).length;
if (results.length - count > 0) {
notificationController.show({
type: NotificationType.Error,
message: $t('errors.unable_to_change_visibility', { values: { count: results.length - count } }),
});
}
notificationController.show({
type: NotificationType.Info,
message: $t('visibility_changed', { values: { count: count } }),
});
}
} catch (error) {
handleError(error, $t('errors.unable_to_change_visibility', { values: { count: changed.length } }));
}
// Reset variables used on the "Show & hide people" modal
showLoadingSpinner = false;
selectHidden = false;
toggleVisibility = ToggleVisibilty.VIEW_ALL;
};
const handleMergeSamePerson = async (response: [PersonResponseDto, PersonResponseDto]) => {
const [personToMerge, personToBeMergedIn] = response;
showMergeModal = false;
@ -205,10 +95,6 @@
people = people.filter((person: PersonResponseDto) => person.id !== personToMerge.id);
people = people.map((person: PersonResponseDto) => (person.id === personToBeMergedIn.id ? mergedPerson : person));
if (personToMerge.isHidden) {
countHiddenPeople--;
}
countTotalPeople--;
notificationController.show({
message: $t('merge_people_successfully'),
type: NotificationType.Info,
@ -273,12 +159,7 @@
return person;
});
for (const person of people) {
initialHiddenValues[person.id] = person.isHidden;
}
showChangeNameModal = false;
countHiddenPeople++;
notificationController.show({
message: $t('changed_visibility_successfully'),
type: NotificationType.Info,
@ -391,7 +272,7 @@
};
</script>
<svelte:window bind:innerHeight use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: handleCloseClick }} />
<svelte:window bind:innerHeight />
{#if showMergeModal}
<MergeSuggestionModal
@ -409,7 +290,7 @@
description={countVisiblePeople === 0 && !searchName ? undefined : `(${countVisiblePeople.toLocaleString($locale)})`}
>
<svelte:fragment slot="buttons">
{#if countTotalPeople > 0}
{#if people.length > 0}
<div class="flex gap-2 items-center justify-center">
<div class="hidden sm:block">
<div class="w-40 lg:w-80 h-10">
@ -494,42 +375,16 @@
/>
{/if}
</UserPageLayout>
{#if selectHidden}
<ShowHide
onDone={handleDoneClick}
onClose={handleCloseClick}
onReset={handleResetVisibility}
onChange={handleToggleVisibility}
bind:showLoadingSpinner
bind:toggleVisibility
{countTotalPeople}
screenHeight={innerHeight}
<section
transition:fly={{ y: innerHeight, duration: 150, easing: quintOut, opacity: 0 }}
class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
role="dialog"
aria-modal="true"
aria-labelledby="manage-visibility-title"
use:focusTrap
>
<div class="w-full grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-9 gap-1">
{#each people as person, index (person.id)}
<button
type="button"
class="relative"
on:click={() => (person.isHidden = !person.isHidden)}
on:mouseenter={() => (eyeColorMap[person.id] = 'black')}
on:mouseleave={() => (eyeColorMap[person.id] = 'white')}
>
<ImageThumbnail
preload={searchName !== '' || index < 20}
bind:hidden={person.isHidden}
shadow
url={getPeopleThumbnailUrl(person)}
altText={person.name}
widthStyle="100%"
bind:eyeColor={eyeColorMap[person.id]}
/>
{#if person.name}
<span class="absolute bottom-2 left-0 w-full select-text px-1 text-center font-medium text-white">
{person.name}
</span>
{/if}
</button>
{/each}
</div>
</ShowHide>
<ManagePeopleVisibility bind:people titleId="manage-visibility-title" onClose={() => (selectHidden = false)} />
</section>
{/if}

View file

@ -1,12 +1,12 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getAllAlbums, getPartners } from '@immich/sdk';
import { PartnerDirection, getAllAlbums, getPartners } from '@immich/sdk';
import type { PageLoad } from './$types';
export const load = (async () => {
await authenticate();
const sharedAlbums = await getAllAlbums({ shared: true });
const partners = await getPartners({ direction: 'shared-with' });
const partners = await getPartners({ direction: PartnerDirection.SharedWith });
const $t = await getFormatter();
return {

View file

@ -337,7 +337,7 @@
<td class=" text-ellipsis px-4 text-sm">
{totalCount[index]}
</td>
<td class=" text-ellipsis px-4 text-sm">{diskUsage[index]} {diskUsageUnit[index]}</td>
<td class=" text-ellipsis px-4 text-sm"> {diskUsage[index]} {ByteUnit[diskUsageUnit[index]]}</td>
{/if}
<td class=" text-ellipsis px-4 text-sm">

View file

@ -0,0 +1,12 @@
import { faker } from '@faker-js/faker';
import type { PersonResponseDto } from '@immich/sdk';
import { Sync } from 'factory.ts';
export const personFactory = Sync.makeFactory<PersonResponseDto>({
birthDate: Sync.each(() => faker.date.past().toISOString()),
id: Sync.each(() => faker.string.uuid()),
isHidden: Sync.each(() => faker.datatype.boolean()),
name: Sync.each(() => faker.person.fullName()),
thumbnailPath: Sync.each(() => faker.system.filePath()),
updatedAt: Sync.each(() => faker.date.recent().toISOString()),
});