mirror of
https://github.com/immich-app/immich.git
synced 2026-03-03 03:57:01 +00:00
Compare commits
150 Commits
v2.5.2
...
refactor/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ebc110603 | ||
|
|
2fb9f84b56 | ||
|
|
434ded92f5 | ||
|
|
bc7a1c838c | ||
|
|
7cb355279e | ||
|
|
ecb09501a5 | ||
|
|
34eb2e1410 | ||
|
|
2d6580acd8 | ||
|
|
9aa3fe82c1 | ||
|
|
66733eb4c0 | ||
|
|
e5156df4f1 | ||
|
|
8ef4e4d452 | ||
|
|
7413356a2f | ||
|
|
5bf4e9595c | ||
|
|
6c0c4b3dda | ||
|
|
206a208410 | ||
|
|
72cef8b94b | ||
|
|
81c93101a0 | ||
|
|
b06c21325e | ||
|
|
730b770e67 | ||
|
|
b85f6f3fce | ||
|
|
81f592ca52 | ||
|
|
a62e8ed179 | ||
|
|
1cf3a80840 | ||
|
|
9f6dbf710c | ||
|
|
f207f99e86 | ||
|
|
0d35231dfd | ||
|
|
675bbf3ac3 | ||
|
|
c45450b6ac | ||
|
|
fea6e8d9f3 | ||
|
|
27ebbab1d9 | ||
|
|
4647ecf2ea | ||
|
|
78c8f1d5a9 | ||
|
|
ec4de54ea2 | ||
|
|
420cd5193b | ||
|
|
7e0356e227 | ||
|
|
913904f418 | ||
|
|
e54678e0d6 | ||
|
|
222c90b7b7 | ||
|
|
1c1a000c78 | ||
|
|
458d5f0f8f | ||
|
|
4c948647fc | ||
|
|
3be8e265cd | ||
|
|
e3c4e0197a | ||
|
|
1ddb8f0667 | ||
|
|
adfb003d03 | ||
|
|
8c8b11f80c | ||
|
|
90d554947f | ||
|
|
caeba5063b | ||
|
|
280174026f | ||
|
|
a9e0fa43fa | ||
|
|
e6e56d75e2 | ||
|
|
0886281dd8 | ||
|
|
32dea76a92 | ||
|
|
6af534fe4c | ||
|
|
71fe9192fd | ||
|
|
7fa6f617f5 | ||
|
|
c3730c8eab | ||
|
|
3462fc434e | ||
|
|
561469b826 | ||
|
|
937bef9a4d | ||
|
|
5f18110e97 | ||
|
|
57485023ae | ||
|
|
8a9b541dd0 | ||
|
|
25be5fc22d | ||
|
|
906c38273f | ||
|
|
10b2bf7970 | ||
|
|
7cf8a9936a | ||
|
|
59c4a49ffd | ||
|
|
00486cbcc8 | ||
|
|
b524d7b6fd | ||
|
|
5b705cb723 | ||
|
|
354dd3cc3c | ||
|
|
57483a1e7f | ||
|
|
bcea64875f | ||
|
|
84e30abe5d | ||
|
|
e3e243fa2b | ||
|
|
b3820c259e | ||
|
|
a356497d96 | ||
|
|
16fe828913 | ||
|
|
211dc3c056 | ||
|
|
ff9052f7f5 | ||
|
|
999ce34251 | ||
|
|
491ed3d927 | ||
|
|
94e86c6e76 | ||
|
|
8581b4f350 | ||
|
|
4835d5f97f | ||
|
|
435565be1b | ||
|
|
94d3039606 | ||
|
|
092ebe01a5 | ||
|
|
37e5968a7a | ||
|
|
cfc5ed5997 | ||
|
|
1b3c0e4f65 | ||
|
|
fd49d7d566 | ||
|
|
ad9f3cfa05 | ||
|
|
9d8efe2685 | ||
|
|
ed4d9abdae | ||
|
|
ac9f6921cc | ||
|
|
f0da875e37 | ||
|
|
b0e1a425b3 | ||
|
|
7211d80e5f | ||
|
|
92c79a7122 | ||
|
|
7580521a76 | ||
|
|
2dd3a764ae | ||
|
|
a42c08ed84 | ||
|
|
3c77c724c5 | ||
|
|
84b2979485 | ||
|
|
e9c2ca008a | ||
|
|
9c098109e0 | ||
|
|
27a2808470 | ||
|
|
0a8a65a45e | ||
|
|
2b6055e830 | ||
|
|
ba2dfa7df6 | ||
|
|
237ea3aedd | ||
|
|
810e9254f3 | ||
|
|
57e0835b46 | ||
|
|
e97030a7ae | ||
|
|
fdf06a91cc | ||
|
|
732303661b | ||
|
|
e9f8521a50 | ||
|
|
6bd60270b4 | ||
|
|
5a6fd7af06 | ||
|
|
6cdebdd3b3 | ||
|
|
9dddccd831 | ||
|
|
440b3b4c6f | ||
|
|
3ea65f8d27 | ||
|
|
38c1f0b1fd | ||
|
|
5212bca3d0 | ||
|
|
2990bde0bb | ||
|
|
af1ecaf5cc | ||
|
|
3870ebc3c6 | ||
|
|
0a9d969b47 | ||
|
|
94965f6d66 | ||
|
|
8872d2c7ae | ||
|
|
23445fdcc1 | ||
|
|
25f2273e24 | ||
|
|
95e8e474b8 | ||
|
|
9f52d864cf | ||
|
|
0273dcb0cf | ||
|
|
1436e2a75f | ||
|
|
855817514c | ||
|
|
d5ad35ea52 | ||
|
|
e63213d774 | ||
|
|
0be1ffade6 | ||
|
|
1a04caee29 | ||
|
|
3ace578fc0 | ||
|
|
25c573bc7a | ||
|
|
10bb83cf75 | ||
|
|
10b53b525d | ||
|
|
8db61d341f |
@@ -26,7 +26,81 @@
|
||||
"vitest.explorer",
|
||||
"ms-playwright.playwright",
|
||||
"ms-azuretools.vscode-docker"
|
||||
]
|
||||
],
|
||||
"settings": {
|
||||
"tasks": {
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Fix Permissions, Install Dependencies",
|
||||
"type": "shell",
|
||||
"command": "[ -f /immich-devcontainer/container-start.sh ] && /immich-devcontainer/container-start.sh || exit 0",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "dedicated",
|
||||
"showReuseMessage": true,
|
||||
"clear": false,
|
||||
"group": "Devcontainer tasks",
|
||||
"close": true
|
||||
},
|
||||
"runOptions": {
|
||||
"runOn": "default"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Immich API Server (Nest)",
|
||||
"dependsOn": ["Fix Permissions, Install Dependencies"],
|
||||
"type": "shell",
|
||||
"command": "[ -f /immich-devcontainer/container-start-backend.sh ] && /immich-devcontainer/container-start-backend.sh || exit 0",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "dedicated",
|
||||
"showReuseMessage": true,
|
||||
"clear": false,
|
||||
"group": "Devcontainer tasks",
|
||||
"close": true
|
||||
},
|
||||
"runOptions": {
|
||||
"runOn": "folderOpen"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Immich Web Server (Vite)",
|
||||
"dependsOn": ["Fix Permissions, Install Dependencies"],
|
||||
"type": "shell",
|
||||
"command": "[ -f /immich-devcontainer/container-start-frontend.sh ] && /immich-devcontainer/container-start-frontend.sh || exit 0",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "dedicated",
|
||||
"showReuseMessage": true,
|
||||
"clear": false,
|
||||
"group": "Devcontainer tasks",
|
||||
"close": true
|
||||
},
|
||||
"runOptions": {
|
||||
"runOn": "folderOpen"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Build Immich CLI",
|
||||
"type": "shell",
|
||||
"command": "pnpm --filter cli build:dev"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
|
||||
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -131,7 +131,7 @@ jobs:
|
||||
- device: rocm
|
||||
suffixes: '-rocm'
|
||||
platforms: linux/amd64
|
||||
runner-mapping: '{"linux/amd64": "mich"}'
|
||||
runner-mapping: '{"linux/amd64": "pokedex-giant"}'
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@0477486d82313fba68f7c82c034120a4b8981297 # multi-runner-build-workflow-v2.1.0
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
36
.github/workflows/test.yml
vendored
36
.github/workflows/test.yml
vendored
@@ -497,14 +497,15 @@ jobs:
|
||||
run: npx playwright install chromium --only-shell
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Docker build
|
||||
run: docker compose build
|
||||
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Run e2e tests (web)
|
||||
env:
|
||||
CI: true
|
||||
run: npx playwright test --project=chromium
|
||||
PLAYWRIGHT_DISABLE_WEBSERVER: true
|
||||
run: npx playwright test --project=web
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Archive web results
|
||||
- name: Archive e2e test (web) results
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
if: success() || failure()
|
||||
with:
|
||||
@@ -513,14 +514,37 @@ jobs:
|
||||
- name: Run ui tests (web)
|
||||
env:
|
||||
CI: true
|
||||
PLAYWRIGHT_DISABLE_WEBSERVER: true
|
||||
run: npx playwright test --project=ui
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Archive ui results
|
||||
- name: Archive ui test (web) results
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
if: success() || failure()
|
||||
with:
|
||||
name: e2e-ui-test-results-${{ matrix.runner }}
|
||||
path: e2e/playwright-report/
|
||||
- name: Run maintenance tests
|
||||
env:
|
||||
CI: true
|
||||
PLAYWRIGHT_DISABLE_WEBSERVER: true
|
||||
run: npx playwright test --project=maintenance
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Archive maintenance tests (web) results
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
if: success() || failure()
|
||||
with:
|
||||
name: e2e-maintenance-isolated-test-results-${{ matrix.runner }}
|
||||
path: e2e/playwright-report/
|
||||
- name: Capture Docker logs
|
||||
if: always()
|
||||
run: docker compose logs --no-color > docker-compose-logs.txt
|
||||
working-directory: ./e2e
|
||||
- name: Archive Docker logs
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
if: always()
|
||||
with:
|
||||
name: docker-compose-logs-${{ matrix.runner }}
|
||||
path: e2e/docker-compose-logs.txt
|
||||
success-check-e2e:
|
||||
name: End-to-End Tests Success
|
||||
needs: [e2e-tests-server-cli, e2e-tests-web]
|
||||
@@ -591,9 +615,9 @@ jobs:
|
||||
- name: Lint with ruff
|
||||
run: |
|
||||
uv run ruff check --output-format=github immich_ml
|
||||
- name: Check black formatting
|
||||
- name: Format with ruff
|
||||
run: |
|
||||
uv run black --check immich_ml
|
||||
uv run ruff format --check immich_ml
|
||||
- name: Run mypy type checking
|
||||
run: |
|
||||
uv run mypy --strict immich_ml/
|
||||
|
||||
80
.vscode/tasks.json
vendored
80
.vscode/tasks.json
vendored
@@ -1,80 +0,0 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Fix Permissions, Install Dependencies",
|
||||
"type": "shell",
|
||||
"command": "[ -f /immich-devcontainer/container-start.sh ] && /immich-devcontainer/container-start.sh || exit 0",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "dedicated",
|
||||
"showReuseMessage": true,
|
||||
"clear": false,
|
||||
"group": "Devcontainer tasks",
|
||||
"close": true
|
||||
},
|
||||
"runOptions": {
|
||||
"runOn": "default"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Immich API Server (Nest)",
|
||||
"dependsOn": ["Fix Permissions, Install Dependencies"],
|
||||
"type": "shell",
|
||||
"command": "[ -f /immich-devcontainer/container-start-backend.sh ] && /immich-devcontainer/container-start-backend.sh || exit 0",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "dedicated",
|
||||
"showReuseMessage": true,
|
||||
"clear": false,
|
||||
"group": "Devcontainer tasks",
|
||||
"close": true
|
||||
},
|
||||
"runOptions": {
|
||||
"runOn": "default"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Immich Web Server (Vite)",
|
||||
"dependsOn": ["Fix Permissions, Install Dependencies"],
|
||||
"type": "shell",
|
||||
"command": "[ -f /immich-devcontainer/container-start-frontend.sh ] && /immich-devcontainer/container-start-frontend.sh || exit 0",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "dedicated",
|
||||
"showReuseMessage": true,
|
||||
"clear": false,
|
||||
"group": "Devcontainer tasks",
|
||||
"close": true
|
||||
},
|
||||
"runOptions": {
|
||||
"runOn": "default"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Immich Server and Web",
|
||||
"dependsOn": ["Immich Web Server (Vite)", "Immich API Server (Nest)"],
|
||||
"runOptions": {
|
||||
"runOn": "folderOpen"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Build Immich CLI",
|
||||
"type": "shell",
|
||||
"command": "pnpm --filter cli build:dev"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -23,9 +23,21 @@ We generally discourage PRs entirely generated by an LLM. For any part generated
|
||||
|
||||
From time to time, we put a feature freeze on parts of the codebase. For us, this means we won't accept most PRs that make changes in that area. Exempted from this are simple bug fixes that require only minor changes. We will close feature PRs that target a feature-frozen area, even if that feature is highly requested and you put a lot of work into it. Please keep that in mind, and if you're ever uncertain if a PR would be accepted, reach out to us first (e.g., in the aforementioned `#contributing` channel). We hate to throw away work. Currently, we have feature freezes on:
|
||||
|
||||
* Sharing/Asset ownership
|
||||
* (External) libraries
|
||||
- Sharing/Asset ownership
|
||||
- (External) libraries
|
||||
|
||||
## Non-code contributions
|
||||
|
||||
If you want to contribute to Immich but you don't feel comfortable programming in our tech stack, there are other ways you can help the team. All our translations are done through [Weblate](https://hosted.weblate.org/projects/immich). These rely entirely on the community; if you speak a language that isn't fully translated yet, submitting translations there is greatly appreciated! If you like helping others, answering Q&A discussions here on GitHub and replying to people on our Discord is also always appreciated.
|
||||
If you want to contribute to Immich but you don't feel comfortable programming in our tech stack, there are other ways you can help the team.
|
||||
|
||||
### Translations
|
||||
|
||||
All our translations are done through [Weblate](https://hosted.weblate.org/projects/immich). These rely entirely on the community; if you speak a language that isn't fully translated yet, submitting translations there is greatly appreciated!
|
||||
|
||||
### Datasets
|
||||
|
||||
Help us improve our [Immich Datasets](https://datasets.immich.app) by submitting photos and videos taken from a variety of devices, including smartphones, DSLRs, and action cameras, as well as photos with unique features, such as panoramas, burst photos, and photo spheres. These datasets will be publically available for anyone to use, do not submit private/sensitive photos.
|
||||
|
||||
### Community support
|
||||
|
||||
If you like helping others, answering Q&A discussions here on GitHub and replying to people on our Discord is also always appreciated.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.6",
|
||||
"description": "Command Line Interface (CLI) for Immich",
|
||||
"type": "module",
|
||||
"exports": "./dist/index.js",
|
||||
@@ -20,7 +20,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^24.10.9",
|
||||
"@types/node": "^24.10.11",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
AssetBulkUploadCheckResult,
|
||||
AssetMediaResponseDto,
|
||||
AssetMediaStatus,
|
||||
Permission,
|
||||
addAssetsToAlbum,
|
||||
checkBulkUpload,
|
||||
createAlbum,
|
||||
@@ -20,13 +21,11 @@ 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, Batcher, authenticate, crawl, sha1 } from 'src/utils';
|
||||
import { BaseOptions, Batcher, authenticate, crawl, requirePermissions, s, sha1 } from 'src/utils';
|
||||
|
||||
const UPLOAD_WATCH_BATCH_SIZE = 100;
|
||||
const UPLOAD_WATCH_DEBOUNCE_TIME_MS = 10_000;
|
||||
|
||||
const s = (count: number) => (count === 1 ? '' : 's');
|
||||
|
||||
// TODO figure out why `id` is missing
|
||||
type AssetBulkUploadCheckResults = Array<AssetBulkUploadCheckResult & { id: string }>;
|
||||
type Asset = { id: string; filepath: string };
|
||||
@@ -136,6 +135,7 @@ export const startWatch = async (
|
||||
|
||||
export const upload = async (paths: string[], baseOptions: BaseOptions, options: UploadOptionsDto) => {
|
||||
await authenticate(baseOptions);
|
||||
await requirePermissions([Permission.AssetUpload]);
|
||||
|
||||
const scanFiles = await scan(paths, options);
|
||||
|
||||
@@ -180,18 +180,49 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas
|
||||
}
|
||||
|
||||
let multiBar: MultiBar | undefined;
|
||||
let totalSize = 0;
|
||||
const statsMap = new Map<string, Stats>();
|
||||
|
||||
// Calculate total size first
|
||||
for (const filepath of files) {
|
||||
const stats = await stat(filepath);
|
||||
statsMap.set(filepath, stats);
|
||||
totalSize += stats.size;
|
||||
}
|
||||
|
||||
if (progress) {
|
||||
multiBar = new MultiBar(
|
||||
{ format: '{message} | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' },
|
||||
{
|
||||
format: '{message} | {bar} | {percentage}% | ETA: {eta_formatted} | {value}/{total}',
|
||||
formatValue: (v: number, options, type) => {
|
||||
// Don't format percentage
|
||||
if (type === 'percentage') {
|
||||
return v.toString();
|
||||
}
|
||||
return byteSize(v).toString();
|
||||
},
|
||||
etaBuffer: 100, // Increase samples for ETA calculation
|
||||
},
|
||||
Presets.shades_classic,
|
||||
);
|
||||
|
||||
// Ensure we restore cursor on interrupt
|
||||
process.on('SIGINT', () => {
|
||||
if (multiBar) {
|
||||
multiBar.stop();
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
} else {
|
||||
console.log(`Received ${files.length} files, hashing...`);
|
||||
console.log(`Received ${files.length} files (${byteSize(totalSize)}), hashing...`);
|
||||
}
|
||||
|
||||
const hashProgressBar = multiBar?.create(files.length, 0, { message: 'Hashing files ' });
|
||||
const checkProgressBar = multiBar?.create(files.length, 0, { message: 'Checking for duplicates' });
|
||||
const hashProgressBar = multiBar?.create(totalSize, 0, {
|
||||
message: 'Hashing files ',
|
||||
});
|
||||
const checkProgressBar = multiBar?.create(totalSize, 0, {
|
||||
message: 'Checking for duplicates',
|
||||
});
|
||||
|
||||
const newFiles: string[] = [];
|
||||
const duplicates: Asset[] = [];
|
||||
@@ -211,7 +242,13 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas
|
||||
}
|
||||
}
|
||||
|
||||
checkProgressBar?.increment(assets.length);
|
||||
// Update progress based on total size of processed files
|
||||
let processedSize = 0;
|
||||
for (const asset of assets) {
|
||||
const stats = statsMap.get(asset.id);
|
||||
processedSize += stats?.size || 0;
|
||||
}
|
||||
checkProgressBar?.increment(processedSize);
|
||||
},
|
||||
{ concurrency, retry: 3 },
|
||||
);
|
||||
@@ -221,6 +258,10 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas
|
||||
|
||||
const queue = new Queue<string, AssetBulkUploadCheckItem[]>(
|
||||
async (filepath: string): Promise<AssetBulkUploadCheckItem[]> => {
|
||||
const stats = statsMap.get(filepath);
|
||||
if (!stats) {
|
||||
throw new Error(`Stats not found for ${filepath}`);
|
||||
}
|
||||
const dto = { id: filepath, checksum: await sha1(filepath) };
|
||||
|
||||
results.push(dto);
|
||||
@@ -231,7 +272,7 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas
|
||||
void checkBulkUploadQueue.push(batch);
|
||||
}
|
||||
|
||||
hashProgressBar?.increment();
|
||||
hashProgressBar?.increment(stats.size);
|
||||
return results;
|
||||
},
|
||||
{ concurrency, retry: 3 },
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { getMyUser } from '@immich/sdk';
|
||||
import { getMyUser, Permission } from '@immich/sdk';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { mkdir, unlink } from 'node:fs/promises';
|
||||
import { BaseOptions, connect, getAuthFilePath, logError, withError, writeAuthFile } from 'src/utils';
|
||||
import {
|
||||
BaseOptions,
|
||||
connect,
|
||||
getAuthFilePath,
|
||||
logError,
|
||||
requirePermissions,
|
||||
withError,
|
||||
writeAuthFile,
|
||||
} from 'src/utils';
|
||||
|
||||
export const login = async (url: string, key: string, options: BaseOptions) => {
|
||||
console.log(`Logging in to ${url}`);
|
||||
@@ -9,6 +17,7 @@ export const login = async (url: string, key: string, options: BaseOptions) => {
|
||||
const { configDirectory: configDir } = options;
|
||||
|
||||
await connect(url, key);
|
||||
await requirePermissions([Permission.UserRead]);
|
||||
|
||||
const [error, user] = await withError(getMyUser());
|
||||
if (error) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { getAssetStatistics, getMyUser, getServerVersion, getSupportedMediaTypes } from '@immich/sdk';
|
||||
import { BaseOptions, authenticate } from 'src/utils';
|
||||
import { getAssetStatistics, getMyUser, getServerVersion, getSupportedMediaTypes, Permission } from '@immich/sdk';
|
||||
import { authenticate, BaseOptions, requirePermissions } from 'src/utils';
|
||||
|
||||
export const serverInfo = async (options: BaseOptions) => {
|
||||
const { url } = await authenticate(options);
|
||||
await requirePermissions([Permission.ServerAbout, Permission.AssetStatistics, Permission.UserRead]);
|
||||
|
||||
const [versionInfo, mediaTypes, stats, userInfo] = await Promise.all([
|
||||
getServerVersion(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getMyUser, init, isHttpError } from '@immich/sdk';
|
||||
import { ApiKeyResponseDto, getMyApiKey, getMyUser, init, isHttpError, Permission } from '@immich/sdk';
|
||||
import { convertPathToPattern, glob } from 'fast-glob';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { createReadStream } from 'node:fs';
|
||||
@@ -34,6 +34,36 @@ export const authenticate = async (options: BaseOptions): Promise<AuthDto> => {
|
||||
return auth;
|
||||
};
|
||||
|
||||
export const s = (count: number) => (count === 1 ? '' : 's');
|
||||
|
||||
let _apiKey: ApiKeyResponseDto;
|
||||
export const requirePermissions = async (permissions: Permission[]) => {
|
||||
if (!_apiKey) {
|
||||
_apiKey = await getMyApiKey();
|
||||
}
|
||||
|
||||
if (_apiKey.permissions.includes(Permission.All)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const missing: Permission[] = [];
|
||||
|
||||
for (const permission of permissions) {
|
||||
if (!_apiKey.permissions.includes(permission)) {
|
||||
missing.push(permission);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
const combined = missing.map((permission) => `"${permission}"`).join(', ');
|
||||
console.log(
|
||||
`Missing required permission${s(missing.length)}: ${combined}.
|
||||
Please make sure your API key has the correct permissions.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
export const connect = async (url: string, key: string) => {
|
||||
const wellKnownUrl = new URL('.well-known/immich', url);
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tools]
|
||||
terragrunt = "0.93.10"
|
||||
opentofu = "1.10.7"
|
||||
terragrunt = "0.98.0"
|
||||
opentofu = "1.11.4"
|
||||
|
||||
[tasks."tg:fmt"]
|
||||
run = "terragrunt hclfmt"
|
||||
|
||||
@@ -127,7 +127,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:9@sha256:546304417feac0874c3dd576e0952c6bb8f06bb4093ea0c9ca303c73cf458f63
|
||||
image: docker.io/valkey/valkey:9@sha256:930b41430fb727f533c5982fe509b6f04233e26d0f7354e04de4b0d5c706e44e
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:9@sha256:546304417feac0874c3dd576e0952c6bb8f06bb4093ea0c9ca303c73cf458f63
|
||||
image: docker.io/valkey/valkey:9@sha256:930b41430fb727f533c5982fe509b6f04233e26d0f7354e04de4b0d5c706e44e
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
@@ -97,7 +97,7 @@ services:
|
||||
command: ['./run.sh', '-disable-reporting']
|
||||
ports:
|
||||
- 3000:3000
|
||||
image: grafana/grafana:12.3.1-ubuntu@sha256:d57f1365197aec34c4d80869d8ca45bb7787c7663904950dab214dfb40c1c2fd
|
||||
image: grafana/grafana:12.3.2-ubuntu@sha256:6cca4b429a1dc0d37d401dee54825c12d40056c3c6f3f56e3f0d6318ce77749b
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
|
||||
|
||||
100
docker/docker-compose.rootless.yml
Normal file
100
docker/docker-compose.rootless.yml
Normal file
@@ -0,0 +1,100 @@
|
||||
#
|
||||
# WARNING: To install Immich, follow our guide: https://docs.immich.app/install/docker-compose
|
||||
#
|
||||
# Make sure to use the docker-compose.yml of the current release:
|
||||
#
|
||||
# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||
#
|
||||
# The compose file on main may not be compatible with the latest release.
|
||||
|
||||
name: immich
|
||||
|
||||
services:
|
||||
immich-server:
|
||||
container_name: immich_server
|
||||
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
||||
# extends:
|
||||
# file: hwaccel.transcoding.yml
|
||||
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
||||
user: '1000:1000'
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
volumes:
|
||||
# Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file
|
||||
- ${UPLOAD_LOCATION}:/data
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- '2283:2283'
|
||||
depends_on:
|
||||
- redis
|
||||
- database
|
||||
restart: always
|
||||
healthcheck:
|
||||
disable: false
|
||||
|
||||
immich-machine-learning:
|
||||
container_name: immich_machine_learning
|
||||
# For hardware acceleration, add one of -[armnn, cuda, rocm, openvino, rknn] to the image tag.
|
||||
# Example tag: ${IMMICH_VERSION:-release}-cuda
|
||||
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
|
||||
# extends: # uncomment this section for hardware acceleration - see https://docs.immich.app/features/ml-hardware-acceleration
|
||||
# file: hwaccel.ml.yml
|
||||
# service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference - use the `-wsl` version for WSL2 where applicable
|
||||
user: '1000:1000'
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
volumes:
|
||||
- ./ml-model-cache:/cache
|
||||
- ./ml-dotcache:/.cache
|
||||
- ./ml-config:/.config
|
||||
env_file:
|
||||
- .env
|
||||
restart: always
|
||||
healthcheck:
|
||||
disable: false
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:9@sha256:930b41430fb727f533c5982fe509b6f04233e26d0f7354e04de4b0d5c706e44e
|
||||
user: '1000:1000'
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
volumes:
|
||||
- ./redis:/data
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
|
||||
database:
|
||||
container_name: immich_postgres
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23
|
||||
user: '1000:1000'
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_USER: ${DB_USERNAME}
|
||||
POSTGRES_DB: ${DB_DATABASE_NAME}
|
||||
POSTGRES_INITDB_ARGS: '--data-checksums'
|
||||
# Uncomment the DB_STORAGE_TYPE: 'HDD' var if your database isn't stored on SSDs
|
||||
# DB_STORAGE_TYPE: 'HDD'
|
||||
volumes:
|
||||
# Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file
|
||||
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
|
||||
shm_size: 128mb
|
||||
restart: always
|
||||
healthcheck:
|
||||
disable: false
|
||||
|
||||
volumes:
|
||||
model-cache:
|
||||
@@ -49,7 +49,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:9@sha256:546304417feac0874c3dd576e0952c6bb8f06bb4093ea0c9ca303c73cf458f63
|
||||
image: docker.io/valkey/valkey:9@sha256:930b41430fb727f533c5982fe509b6f04233e26d0f7354e04de4b0d5c706e44e
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
|
||||
@@ -402,6 +402,9 @@ To decrease Redis logs, you can add the following line to the `redis:` section o
|
||||
### How can I run Immich as a non-root user?
|
||||
|
||||
You can change the user in the container by setting the `user` argument in `docker-compose.yml` for each service.
|
||||
|
||||
[Example docker-compose.yml file](https://github.com/immich-app/immich/blob/main/docker/docker-compose.rootless.yml)
|
||||
|
||||
You may need to add mount points or docker volumes for the following internal container paths:
|
||||
|
||||
- `immich-machine-learning:/.config`
|
||||
|
||||
@@ -140,7 +140,8 @@ For advanced users or automated recovery scenarios, you can restore a database b
|
||||
|
||||
```bash title='Backup'
|
||||
# Replace <DB_USERNAME> with the database username - usually postgres unless you have changed it.
|
||||
docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=<DB_USERNAME> | gzip > "/path/to/backup/dump.sql.gz"
|
||||
# Replace <DB_DATABASE_NAME> with the database name - usually immich unless you have changed it.
|
||||
docker exec -t immich_postgres pg_dump --clean --if-exists --dbname=<DB_DATABASE_NAME> --username=<DB_USERNAME> | gzip > "/path/to/backup/dump.sql.gz"
|
||||
```
|
||||
|
||||
```bash title='Restore'
|
||||
@@ -153,9 +154,10 @@ docker start immich_postgres # Start Postgres server
|
||||
sleep 10 # Wait for Postgres server to start up
|
||||
# Check the database user if you deviated from the default
|
||||
# Replace <DB_USERNAME> with the database username - usually postgres unless you have changed it.
|
||||
# Replace <DB_DATABASE_NAME> with the database name - usually immich unless you have changed it.
|
||||
gunzip --stdout "/path/to/backup/dump.sql.gz" \
|
||||
| sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \
|
||||
| docker exec -i immich_postgres psql --dbname=postgres --username=<DB_USERNAME> # Restore Backup
|
||||
| docker exec -i immich_postgres psql --dbname=<DB_DATABASE_NAME> --username=<DB_USERNAME> --single-transaction --set ON_ERROR_STOP=on # Restore Backup
|
||||
docker compose up -d # Start remainder of Immich apps
|
||||
```
|
||||
|
||||
@@ -164,7 +166,8 @@ docker compose up -d # Start remainder of Immich apps
|
||||
|
||||
```powershell title='Backup'
|
||||
# Replace <DB_USERNAME> with the database username - usually postgres unless you have changed it.
|
||||
[System.IO.File]::WriteAllLines("C:\absolute\path\to\backup\dump.sql", (docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=<DB_USERNAME>))
|
||||
# Replace <DB_DATABASE_NAME> with the database name - usually immich unless you have changed it.
|
||||
[System.IO.File]::WriteAllLines("C:\absolute\path\to\backup\dump.sql", (docker exec -t immich_postgres pg_dump --clean --if-exists --dbname=<DB_DATABASE_NAME> --username=<DB_USERNAME>))
|
||||
```
|
||||
|
||||
```powershell title='Restore'
|
||||
@@ -179,8 +182,9 @@ sleep 10 # Wait for Postgres server to
|
||||
docker exec -it immich_postgres bash # Enter the Docker shell and run the following command
|
||||
# If your backup ends in `.gz`, replace `cat` with `gunzip --stdout`
|
||||
# Replace <DB_USERNAME> with the database username - usually postgres unless you have changed it.
|
||||
# Replace <DB_DATABASE_NAME> with the database name - usually immich unless you have changed it.
|
||||
|
||||
cat "/dump.sql" | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" | psql --dbname=postgres --username=<DB_USERNAME>
|
||||
cat "/dump.sql" | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" | psql --dbname=<DB_DATABASE_NAME> --username=<DB_USERNAME> --single-transaction --set ON_ERROR_STOP=on
|
||||
exit # Exit the Docker shell
|
||||
docker compose up -d # Start remainder of Immich apps
|
||||
```
|
||||
@@ -188,6 +192,10 @@ docker compose up -d # Start remainder of Immich ap
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::warning
|
||||
The backup and restore process changed in v2.5.0, if you have a backup created with an older version of Immich, use the documentation version selector to find manual restore instructions for your backup.
|
||||
:::
|
||||
|
||||
:::note
|
||||
For the database restore to proceed properly, it requires a completely fresh install (i.e., the Immich server has never run since creating the Docker containers). If the Immich app has run, you may encounter Postgres conflicts (relation already exists, violated foreign key constraints, etc.). In this case, delete the `DB_DATA_LOCATION` folder to reset the database.
|
||||
:::
|
||||
@@ -196,6 +204,10 @@ For the database restore to proceed properly, it requires a completely fresh ins
|
||||
Some deployment methods make it difficult to start the database without also starting the server. In these cases, set the environment variable `DB_SKIP_MIGRATIONS=true` before starting the services. This prevents the server from running migrations that interfere with the restore process. Remove this variable and restart services after the database is restored.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
The provided restore process ensures your database is never in a broken state by committing all changes in one transaction. This may be undesirable behaviour in some circumstances, you can disable it by removing `--single-transaction --set ON_ERROR_STOP=on` from the command.
|
||||
:::
|
||||
|
||||
## Filesystem
|
||||
|
||||
Immich stores two types of content in the filesystem: (a) original, unmodified assets (photos and videos), and (b) generated content. We recommend backing up the entire contents of `UPLOAD_LOCATION`, but only the original content is critical, which is stored in the following folders:
|
||||
|
||||
@@ -56,11 +56,13 @@ Once you have a new OAuth client application configured, Immich can be configure
|
||||
| Setting | Type | Default | Description |
|
||||
| ---------------------------------------------------- | ------- | -------------------- | ----------------------------------------------------------------------------------- |
|
||||
| Enabled | boolean | false | Enable/disable OAuth |
|
||||
| Issuer URL | URL | (required) | Required. Self-discovery URL for client (from previous step) |
|
||||
| Client ID | string | (required) | Required. Client ID (from previous step) |
|
||||
| Client Secret | string | (required) | Required. Client Secret (previous step) |
|
||||
| Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) |
|
||||
| Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
||||
| `issuer_url` | URL | (required) | Required. Self-discovery URL for client (from previous step) |
|
||||
| `client_id` | string | (required) | Required. Client ID (from previous step) |
|
||||
| `client_secret` | string | (required) | Required. Client Secret (previous step) |
|
||||
| `scope` | string | openid email profile | Full list of scopes to send with the request (space delimited) |
|
||||
| `id_token_signed_response_alg` | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
||||
| `userinfo_signed_response_alg` | string | none | The algorithm used to sign the userinfo response (examples: RS256, HS256) |
|
||||
| Request timeout | string | 30,000 (30 seconds) | Number of milliseconds to wait for http requests to complete before giving up |
|
||||
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**š** |
|
||||
| Role Claim | string | immich_role | Claim mapping for the user's role. (should return "user" or "admin")**š** |
|
||||
| Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage**š** |
|
||||
|
||||
@@ -88,7 +88,7 @@ The easiest option is to have both extensions installed during the migration:
|
||||
<details>
|
||||
<summary>Migration steps (automatic)</summary>
|
||||
1. Ensure you still have pgvecto.rs installed
|
||||
2. Install `pgvector` (`>= 0.7.0, < 1.0.0`). The easiest way to do this is on Debian/Ubuntu by adding the [PostgreSQL Apt repository][pg-apt] and then running `apt install postgresql-NN-pgvector`, where `NN` is your Postgres version (e.g., `16`)
|
||||
2. Install `pgvector` (`>= 0.7, < 0.9`). The easiest way to do this is on Debian/Ubuntu by adding the [PostgreSQL Apt repository][pg-apt] and then running `apt install postgresql-NN-pgvector`, where `NN` is your Postgres version (e.g., `16`)
|
||||
3. [Install VectorChord][vchord-install]
|
||||
4. Add `shared_preload_libraries= 'vchord.so, vectors.so'` to your `postgresql.conf`, making sure to include _both_ `vchord.so` and `vectors.so`. You may include other libraries here as well if needed
|
||||
5. Restart the Postgres database
|
||||
|
||||
@@ -98,7 +98,6 @@ entryPoints:
|
||||
respondingTimeouts:
|
||||
readTimeout: 600s
|
||||
idleTimeout: 600s
|
||||
writeTimeout: 600s
|
||||
```
|
||||
|
||||
The second part is in the `docker-compose.yml` file where immich is in. Add the Traefik specific labels like in the example.
|
||||
|
||||
@@ -90,10 +90,13 @@ To see local changes to `@immich/ui` in Immich, do the following:
|
||||
|
||||
#### Setup
|
||||
|
||||
1. Setup Flutter toolchain using FVM.
|
||||
2. Run `flutter pub get` to install the dependencies.
|
||||
3. Run `make translation` to generate the translation file.
|
||||
4. Run `fvm flutter run` to start the app.
|
||||
1. [Install mise](https://mise.jdx.dev/installing-mise.html).
|
||||
2. Change to the immich (root) directory and trust the mise config with `mise trust`.
|
||||
3. Install tools with mise: `mise install`.
|
||||
4. Change to the `mobile/` directory.
|
||||
5. Run `flutter pub get` to install the dependencies.
|
||||
6. Run `make translation` to generate the translation file.
|
||||
7. Run `flutter run` to start the app.
|
||||
|
||||
#### Translation
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ For example to get a list of files that would be uploaded for further
|
||||
processing:
|
||||
|
||||
```bash
|
||||
immich upload --dry-run . | tail -n +6 | jq .newFiles[]
|
||||
immich upload --dry-run --json-output . | tail -n +6 | jq .newFiles[]
|
||||
```
|
||||
|
||||
### Obtain the API Key
|
||||
|
||||
@@ -86,8 +86,8 @@ You do not need to redo any machine learning jobs after enabling hardware accele
|
||||
## Setup
|
||||
|
||||
1. If you do not already have it, download the latest [`hwaccel.ml.yml`][hw-file] file and ensure it's in the same folder as the `docker-compose.yml`.
|
||||
2. In the `docker-compose.yml` under `immich-machine-learning`, uncomment the `extends` section and change `cpu` to the appropriate backend.
|
||||
3. Still in `immich-machine-learning`, add one of -[armnn, cuda, rocm, openvino, rknn] to the `image` section's tag at the end of the line.
|
||||
2. In `immich-machine-learning`, add one of -[armnn, cuda, rocm, openvino, rknn] to the `image` section's tag at the end of the line.
|
||||
3. Still in the `docker-compose.yml` under `immich-machine-learning`, uncomment the `extends` section and change `cpu` to the appropriate backend.
|
||||
4. Redeploy the `immich-machine-learning` container with these updated settings.
|
||||
|
||||
### Confirming Device Usage
|
||||
|
||||
@@ -66,7 +66,7 @@ Now make sure that the local album is selected in the backup screen (steps 1-2 a
|
||||
- **Keep on device:** You can choose to restrict removal to `Always keep` **All photos** or **All videos**, regardless of other settings. This setting can hamper freeing up space significantly â with 80 GB of videos and 40 GB photos, selecting `Always keep photos` retains thousands of photos on your device.
|
||||
|
||||
2. **Scan & Review:** Before any files are removed, you are presented with a review screen to verify which items will be deleted and how much storage is reclamable.
|
||||
3. **Deletion:** Confirmed items are moved to your device's native Trash/Recycle Bin.
|
||||
3. **Deletion:** Confirmed items are moved to your device's native Trash/Recycle Bin. For large queues, Immich processes deletion in batches for stability (`2000` assets per batch on Android, `10000` per batch on iOS).
|
||||
|
||||
:::info reclaim storage
|
||||
To use the reclaimed space right away, you must empty the system/gallery trash manually outside of Immich.
|
||||
|
||||
@@ -26,6 +26,16 @@ docker image prune
|
||||
[breaking]: https://github.com/immich-app/immich/discussions?discussions_q=label%3Achangelog%3Abreaking-change+sort%3Adate_created
|
||||
[releases]: https://github.com/immich-app/immich/releases
|
||||
|
||||
## Versioning Policy
|
||||
|
||||
Immich follows [semantic versioning][semver], which tags releases in the format `<major>.<minor>.<patch>`. We intend for breaking changes to be limited to major version releases.
|
||||
You can configure your Docker image to point to the current major version by using a metatag, such as `:v2`.
|
||||
|
||||
Currently, we have no plans to backport patches to earlier versions. We encourage all users to run the most recent release of Immich.
|
||||
Switching back to an earlier version, even within the same minor release tag, is not supported.
|
||||
|
||||
[semver]: https://semver.org/
|
||||
|
||||
## Migrating to VectorChord
|
||||
|
||||
:::info
|
||||
|
||||
@@ -32,3 +32,7 @@ If you would like to migrate from one media location to another, simply successf
|
||||
4. Start up Immich
|
||||
|
||||
After version `1.136.0`, Immich can detect when a media location has moved and will automatically update the database paths to keep them in sync.
|
||||
|
||||
## Schema drift
|
||||
|
||||
Schema drift is when the database schema is out of sync with the code. This could be the result of manual database tinkering, issues during a database restore, or something else. Schema drift can lead to data corruption, application bugs, and other unpredictable behavior. Please reconcile the differences as soon as possible. Specifically, missing `CONSTRAINT`s can result in duplicate assets being uploaded, since the server relies on a checksum `CONSTRAINT` to prevent duplicates.
|
||||
|
||||
4
docs/static/archived-versions.json
vendored
4
docs/static/archived-versions.json
vendored
@@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"label": "v2.5.2",
|
||||
"url": "https://docs.v2.5.2.archive.immich.app"
|
||||
"label": "v2.5.6",
|
||||
"url": "https://docs.v2.5.6.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v2.4.1",
|
||||
|
||||
@@ -70,7 +70,7 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
image: redis:6.2-alpine@sha256:37e002448575b32a599109664107e374c8709546905c372a34d64919043b9ceb
|
||||
image: redis:6.2-alpine@sha256:46884be93652d02a96a176ccf173d1040bef365c5706aa7b6a1931caec8bfeef
|
||||
|
||||
database:
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:6f3e9d2c2177af16c2988ff71425d79d89ca630ec2f9c8db03209ab716542338
|
||||
|
||||
@@ -42,7 +42,7 @@ services:
|
||||
- 2285:2285
|
||||
|
||||
redis:
|
||||
image: redis:6.2-alpine@sha256:37e002448575b32a599109664107e374c8709546905c372a34d64919043b9ceb
|
||||
image: redis:6.2-alpine@sha256:46884be93652d02a96a176ccf173d1040bef365c5706aa7b6a1931caec8bfeef
|
||||
|
||||
database:
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:6f3e9d2c2177af16c2988ff71425d79d89ca630ec2f9c8db03209ab716542338
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.6",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
@@ -27,7 +27,7 @@
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@socket.io/component-emitter": "^3.1.2",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^24.10.9",
|
||||
"@types/node": "^24.10.11",
|
||||
"@types/pg": "^8.15.1",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
"@types/supertest": "^6.0.2",
|
||||
|
||||
@@ -14,7 +14,8 @@ export const playwrightDisableWebserver = process.env.PLAYWRIGHT_DISABLE_WEBSERV
|
||||
process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS = '1';
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: './src/web/specs',
|
||||
testDir: './src/specs/server',
|
||||
testMatch: /.*\.e2e-spec\.ts/,
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 4 : 0,
|
||||
@@ -28,54 +29,28 @@ const config: PlaywrightTestConfig = {
|
||||
},
|
||||
},
|
||||
|
||||
testMatch: /.*\.e2e-spec\.ts/,
|
||||
|
||||
workers: process.env.CI ? 4 : Math.round(cpus().length * 0.75),
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
name: 'web',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
testMatch: /.*\.e2e-spec\.ts/,
|
||||
testDir: './src/specs/web',
|
||||
workers: 1,
|
||||
},
|
||||
{
|
||||
name: 'ui',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
testMatch: /.*\.ui-spec\.ts/,
|
||||
testDir: './src/ui/specs',
|
||||
fullyParallel: true,
|
||||
workers: process.env.CI ? 3 : Math.max(1, Math.round(cpus().length * 0.75) - 1),
|
||||
},
|
||||
|
||||
// {
|
||||
// name: 'firefox',
|
||||
// use: { ...devices['Desktop Firefox'] },
|
||||
// },
|
||||
|
||||
// {
|
||||
// name: 'webkit',
|
||||
// use: { ...devices['Desktop Safari'] },
|
||||
// },
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
{
|
||||
name: 'maintenance',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
testDir: './src/specs/maintenance',
|
||||
workers: 1,
|
||||
},
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
|
||||
@@ -43,10 +43,10 @@ export const errorDto = {
|
||||
message: 'Invalid share key',
|
||||
correlationId: expect.any(String),
|
||||
},
|
||||
invalidSharePassword: {
|
||||
passwordRequired: {
|
||||
error: 'Unauthorized',
|
||||
statusCode: 401,
|
||||
message: 'Invalid password',
|
||||
message: 'Password required',
|
||||
correlationId: expect.any(String),
|
||||
},
|
||||
badRequest: (message: any = null) => ({
|
||||
|
||||
@@ -473,6 +473,7 @@ describe('/asset', () => {
|
||||
id: user1Assets[0].id,
|
||||
exifInfo: expect.objectContaining({
|
||||
dateTimeOriginal: '2023-11-20T01:11:00+00:00',
|
||||
timeZone: 'UTC-7',
|
||||
}),
|
||||
});
|
||||
expect(status).toEqual(200);
|
||||
@@ -239,7 +239,7 @@ describe('/shared-links', () => {
|
||||
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithPassword.key });
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.invalidSharePassword);
|
||||
expect(body).toEqual(errorDto.passwordRequired);
|
||||
});
|
||||
|
||||
it('should get data for correct password protected link', async () => {
|
||||
2
e2e/src/ui/generators/memory.ts
Normal file
2
e2e/src/ui/generators/memory.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { generateMemoriesFromTimeline, generateMemory } from './memory/model-objects';
|
||||
export type { MemoryConfig, MemoryYearConfig } from './memory/model-objects';
|
||||
84
e2e/src/ui/generators/memory/model-objects.ts
Normal file
84
e2e/src/ui/generators/memory/model-objects.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { MemoryType, type MemoryResponseDto, type OnThisDayDto } from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
import { toAssetResponseDto } from 'src/ui/generators/timeline/rest-response';
|
||||
import type { MockTimelineAsset } from 'src/ui/generators/timeline/timeline-config';
|
||||
import { SeededRandom, selectRandomMultiple } from 'src/ui/generators/timeline/utils';
|
||||
|
||||
export type MemoryConfig = {
|
||||
id?: string;
|
||||
ownerId: string;
|
||||
year: number;
|
||||
memoryAt: string;
|
||||
isSaved?: boolean;
|
||||
};
|
||||
|
||||
export type MemoryYearConfig = {
|
||||
year: number;
|
||||
assetCount: number;
|
||||
};
|
||||
|
||||
export function generateMemory(config: MemoryConfig, assets: MockTimelineAsset[]): MemoryResponseDto {
|
||||
const now = new Date().toISOString();
|
||||
const memoryId = config.id ?? faker.string.uuid();
|
||||
|
||||
return {
|
||||
id: memoryId,
|
||||
assets: assets.map((asset) => toAssetResponseDto(asset)),
|
||||
data: { year: config.year } as OnThisDayDto,
|
||||
memoryAt: config.memoryAt,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
isSaved: config.isSaved ?? false,
|
||||
ownerId: config.ownerId,
|
||||
type: MemoryType.OnThisDay,
|
||||
};
|
||||
}
|
||||
|
||||
export function generateMemoriesFromTimeline(
|
||||
timelineAssets: MockTimelineAsset[],
|
||||
ownerId: string,
|
||||
memoryConfigs: MemoryYearConfig[],
|
||||
seed: number = 42,
|
||||
): MemoryResponseDto[] {
|
||||
const rng = new SeededRandom(seed);
|
||||
const memories: MemoryResponseDto[] = [];
|
||||
const usedAssetIds = new Set<string>();
|
||||
|
||||
for (const config of memoryConfigs) {
|
||||
const yearAssets = timelineAssets.filter((asset) => {
|
||||
const assetYear = DateTime.fromISO(asset.fileCreatedAt).year;
|
||||
return assetYear === config.year && !usedAssetIds.has(asset.id);
|
||||
});
|
||||
|
||||
if (yearAssets.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const countToSelect = Math.min(config.assetCount, yearAssets.length);
|
||||
const selectedAssets = selectRandomMultiple(yearAssets, countToSelect, rng);
|
||||
|
||||
for (const asset of selectedAssets) {
|
||||
usedAssetIds.add(asset.id);
|
||||
}
|
||||
|
||||
selectedAssets.sort(
|
||||
(a, b) => DateTime.fromISO(b.fileCreatedAt).diff(DateTime.fromISO(a.fileCreatedAt)).milliseconds,
|
||||
);
|
||||
|
||||
const memoryAt = DateTime.now().set({ year: config.year }).toISO()!;
|
||||
|
||||
memories.push(
|
||||
generateMemory(
|
||||
{
|
||||
ownerId,
|
||||
year: config.year,
|
||||
memoryAt,
|
||||
},
|
||||
selectedAssets,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return memories;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { generateConsecutiveDays, generateDayAssets } from 'src/generators/timeline/model-objects';
|
||||
import { SeededRandom, selectRandomDays } from 'src/generators/timeline/utils';
|
||||
import { generateConsecutiveDays, generateDayAssets } from 'src/ui/generators/timeline/model-objects';
|
||||
import { SeededRandom, selectRandomDays } from 'src/ui/generators/timeline/utils';
|
||||
import type { MockTimelineAsset } from './timeline-config';
|
||||
import { GENERATION_CONSTANTS } from './timeline-config';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import sharp from 'sharp';
|
||||
import { SeededRandom } from 'src/generators/timeline/utils';
|
||||
import { SeededRandom } from 'src/ui/generators/timeline/utils';
|
||||
|
||||
export const randomThumbnail = async (seed: string, ratio: number) => {
|
||||
const height = 235;
|
||||
@@ -6,7 +6,7 @@ import { faker } from '@faker-js/faker';
|
||||
import { AssetVisibility } from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { SeededRandom } from 'src/generators/timeline/utils';
|
||||
import { SeededRandom } from 'src/ui/generators/timeline/utils';
|
||||
import type { DayPattern, MonthDistribution } from './distribution-patterns';
|
||||
import { ASSET_DISTRIBUTION, DAY_DISTRIBUTION } from './distribution-patterns';
|
||||
import type { MockTimelineAsset, MockTimelineData, SerializedTimelineData, TimelineConfig } from './timeline-config';
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
import { signupDto } from 'src/fixtures';
|
||||
import { parseTimeBucketKey } from 'src/generators/timeline/utils';
|
||||
import { parseTimeBucketKey } from 'src/ui/generators/timeline/utils';
|
||||
import type { MockTimelineAsset, MockTimelineData } from './timeline-config';
|
||||
|
||||
/**
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AssetVisibility } from '@immich/sdk';
|
||||
import { DayPattern, MonthDistribution } from 'src/generators/timeline/distribution-patterns';
|
||||
import { DayPattern, MonthDistribution } from 'src/ui/generators/timeline/distribution-patterns';
|
||||
|
||||
// Constants for generation parameters
|
||||
export const GENERATION_CONSTANTS = {
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import { GENERATION_CONSTANTS, MockTimelineAsset } from 'src/generators/timeline/timeline-config';
|
||||
import { GENERATION_CONSTANTS, MockTimelineAsset } from 'src/ui/generators/timeline/timeline-config';
|
||||
|
||||
/**
|
||||
* Linear Congruential Generator for deterministic pseudo-random numbers
|
||||
65
e2e/src/ui/mock-network/memory-network.ts
Normal file
65
e2e/src/ui/mock-network/memory-network.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { MemoryResponseDto } from '@immich/sdk';
|
||||
import { BrowserContext } from '@playwright/test';
|
||||
|
||||
export type MemoryChanges = {
|
||||
memoryDeletions: string[];
|
||||
assetRemovals: Map<string, string[]>;
|
||||
};
|
||||
|
||||
export const setupMemoryMockApiRoutes = async (
|
||||
context: BrowserContext,
|
||||
memories: MemoryResponseDto[],
|
||||
changes: MemoryChanges,
|
||||
) => {
|
||||
await context.route('**/api/memories*', async (route, request) => {
|
||||
const url = new URL(request.url());
|
||||
const pathname = url.pathname;
|
||||
|
||||
if (pathname === '/api/memories' && request.method() === 'GET') {
|
||||
const activeMemories = memories
|
||||
.filter((memory) => !changes.memoryDeletions.includes(memory.id))
|
||||
.map((memory) => {
|
||||
const removedAssets = changes.assetRemovals.get(memory.id) ?? [];
|
||||
return {
|
||||
...memory,
|
||||
assets: memory.assets.filter((asset) => !removedAssets.includes(asset.id)),
|
||||
};
|
||||
})
|
||||
.filter((memory) => memory.assets.length > 0);
|
||||
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
json: activeMemories,
|
||||
});
|
||||
}
|
||||
|
||||
const memoryMatch = pathname.match(/\/api\/memories\/([^/]+)$/);
|
||||
if (memoryMatch && request.method() === 'GET') {
|
||||
const memoryId = memoryMatch[1];
|
||||
const memory = memories.find((m) => m.id === memoryId);
|
||||
|
||||
if (!memory || changes.memoryDeletions.includes(memoryId)) {
|
||||
return route.fulfill({ status: 404 });
|
||||
}
|
||||
|
||||
const removedAssets = changes.assetRemovals.get(memoryId) ?? [];
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
json: {
|
||||
...memory,
|
||||
assets: memory.assets.filter((asset) => !removedAssets.includes(asset.id)),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (/\/api\/memories\/([^/]+)$/.test(pathname) && request.method() === 'DELETE') {
|
||||
const memoryId = pathname.split('/').pop()!;
|
||||
changes.memoryDeletions.push(memoryId);
|
||||
return route.fulfill({ status: 204 });
|
||||
}
|
||||
|
||||
await route.fallback();
|
||||
});
|
||||
};
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
randomPreview,
|
||||
randomThumbnail,
|
||||
TimelineData,
|
||||
} from 'src/generators/timeline';
|
||||
import { sleep } from 'src/web/specs/timeline/utils';
|
||||
} from 'src/ui/generators/timeline';
|
||||
import { sleep } from 'src/ui/specs/timeline/utils';
|
||||
|
||||
export class TimelineTestContext {
|
||||
slowBucket = false;
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
selectRandom,
|
||||
TimelineAssetConfig,
|
||||
TimelineData,
|
||||
} from 'src/generators/timeline';
|
||||
import { setupBaseMockApiRoutes } from 'src/mock-network/base-network';
|
||||
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network';
|
||||
} from 'src/ui/generators/timeline';
|
||||
import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network';
|
||||
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/ui/mock-network/timeline-network';
|
||||
import { utils } from 'src/utils';
|
||||
import { assetViewerUtils } from 'src/web/specs/timeline/utils';
|
||||
import { assetViewerUtils } from '../timeline/utils';
|
||||
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
test.describe('asset-viewer', () => {
|
||||
289
e2e/src/ui/specs/memory/memory-viewer.e2e-spec.ts
Normal file
289
e2e/src/ui/specs/memory/memory-viewer.e2e-spec.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { MemoryResponseDto } from '@immich/sdk';
|
||||
import { test } from '@playwright/test';
|
||||
import { generateMemoriesFromTimeline } from 'src/ui/generators/memory';
|
||||
import {
|
||||
Changes,
|
||||
createDefaultTimelineConfig,
|
||||
generateTimelineData,
|
||||
TimelineAssetConfig,
|
||||
TimelineData,
|
||||
} from 'src/ui/generators/timeline';
|
||||
import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network';
|
||||
import { MemoryChanges, setupMemoryMockApiRoutes } from 'src/ui/mock-network/memory-network';
|
||||
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/ui/mock-network/timeline-network';
|
||||
import { memoryAssetViewerUtils, memoryGalleryUtils, memoryViewerUtils } from './utils';
|
||||
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
|
||||
test.describe('Memory Viewer - Gallery Asset Viewer Navigation', () => {
|
||||
let adminUserId: string;
|
||||
let timelineRestData: TimelineData;
|
||||
let memories: MemoryResponseDto[];
|
||||
const assets: TimelineAssetConfig[] = [];
|
||||
const testContext = new TimelineTestContext();
|
||||
const changes: Changes = {
|
||||
albumAdditions: [],
|
||||
assetDeletions: [],
|
||||
assetArchivals: [],
|
||||
assetFavorites: [],
|
||||
};
|
||||
const memoryChanges: MemoryChanges = {
|
||||
memoryDeletions: [],
|
||||
assetRemovals: new Map(),
|
||||
};
|
||||
|
||||
test.beforeAll(async () => {
|
||||
adminUserId = faker.string.uuid();
|
||||
testContext.adminId = adminUserId;
|
||||
|
||||
timelineRestData = generateTimelineData({
|
||||
...createDefaultTimelineConfig(),
|
||||
ownerId: adminUserId,
|
||||
});
|
||||
|
||||
for (const timeBucket of timelineRestData.buckets.values()) {
|
||||
assets.push(...timeBucket);
|
||||
}
|
||||
|
||||
memories = generateMemoriesFromTimeline(
|
||||
assets,
|
||||
adminUserId,
|
||||
[
|
||||
{ year: 2024, assetCount: 3 },
|
||||
{ year: 2023, assetCount: 2 },
|
||||
{ year: 2022, assetCount: 4 },
|
||||
],
|
||||
42,
|
||||
);
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ context }) => {
|
||||
await setupBaseMockApiRoutes(context, adminUserId);
|
||||
await setupTimelineMockApiRoutes(context, timelineRestData, changes, testContext);
|
||||
await setupMemoryMockApiRoutes(context, memories, memoryChanges);
|
||||
});
|
||||
|
||||
test.afterEach(() => {
|
||||
testContext.slowBucket = false;
|
||||
changes.albumAdditions = [];
|
||||
changes.assetDeletions = [];
|
||||
changes.assetArchivals = [];
|
||||
changes.assetFavorites = [];
|
||||
memoryChanges.memoryDeletions = [];
|
||||
memoryChanges.assetRemovals.clear();
|
||||
});
|
||||
|
||||
test.describe('Asset viewer navigation from gallery', () => {
|
||||
test('shows both prev/next buttons for middle asset within a memory', async ({ page }) => {
|
||||
const firstMemory = memories[0];
|
||||
const middleAsset = firstMemory.assets[1];
|
||||
|
||||
await memoryViewerUtils.openMemoryPageWithAsset(page, middleAsset.id);
|
||||
await memoryGalleryUtils.clickThumbnail(page, middleAsset.id);
|
||||
|
||||
await memoryAssetViewerUtils.waitForViewerOpen(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, middleAsset);
|
||||
|
||||
await memoryAssetViewerUtils.expectPreviousButtonVisible(page);
|
||||
await memoryAssetViewerUtils.expectNextButtonVisible(page);
|
||||
});
|
||||
|
||||
test('shows next button when at last asset of first memory (next memory exists)', async ({ page }) => {
|
||||
const firstMemory = memories[0];
|
||||
const lastAssetOfFirstMemory = firstMemory.assets.at(-1)!;
|
||||
|
||||
await memoryViewerUtils.openMemoryPageWithAsset(page, lastAssetOfFirstMemory.id);
|
||||
await memoryGalleryUtils.clickThumbnail(page, lastAssetOfFirstMemory.id);
|
||||
|
||||
await memoryAssetViewerUtils.waitForViewerOpen(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, lastAssetOfFirstMemory);
|
||||
|
||||
await memoryAssetViewerUtils.expectNextButtonVisible(page);
|
||||
await memoryAssetViewerUtils.expectPreviousButtonVisible(page);
|
||||
});
|
||||
|
||||
test('shows prev button when at first asset of last memory (prev memory exists)', async ({ page }) => {
|
||||
const lastMemory = memories.at(-1)!;
|
||||
const firstAssetOfLastMemory = lastMemory.assets[0];
|
||||
|
||||
await memoryViewerUtils.openMemoryPageWithAsset(page, firstAssetOfLastMemory.id);
|
||||
await memoryGalleryUtils.clickThumbnail(page, firstAssetOfLastMemory.id);
|
||||
|
||||
await memoryAssetViewerUtils.waitForViewerOpen(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, firstAssetOfLastMemory);
|
||||
|
||||
await memoryAssetViewerUtils.expectPreviousButtonVisible(page);
|
||||
await memoryAssetViewerUtils.expectNextButtonVisible(page);
|
||||
});
|
||||
|
||||
test('can navigate from last asset of memory to first asset of next memory', async ({ page }) => {
|
||||
const firstMemory = memories[0];
|
||||
const secondMemory = memories[1];
|
||||
const lastAssetOfFirst = firstMemory.assets.at(-1)!;
|
||||
const firstAssetOfSecond = secondMemory.assets[0];
|
||||
|
||||
await memoryViewerUtils.openMemoryPageWithAsset(page, lastAssetOfFirst.id);
|
||||
await memoryGalleryUtils.clickThumbnail(page, lastAssetOfFirst.id);
|
||||
|
||||
await memoryAssetViewerUtils.waitForViewerOpen(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, lastAssetOfFirst);
|
||||
|
||||
await memoryAssetViewerUtils.clickNextButton(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, firstAssetOfSecond);
|
||||
|
||||
await memoryAssetViewerUtils.expectCurrentAssetId(page, firstAssetOfSecond.id);
|
||||
});
|
||||
|
||||
test('can navigate from first asset of memory to last asset of previous memory', async ({ page }) => {
|
||||
const firstMemory = memories[0];
|
||||
const secondMemory = memories[1];
|
||||
const lastAssetOfFirst = firstMemory.assets.at(-1)!;
|
||||
const firstAssetOfSecond = secondMemory.assets[0];
|
||||
|
||||
await memoryViewerUtils.openMemoryPageWithAsset(page, firstAssetOfSecond.id);
|
||||
await memoryGalleryUtils.clickThumbnail(page, firstAssetOfSecond.id);
|
||||
|
||||
await memoryAssetViewerUtils.waitForViewerOpen(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, firstAssetOfSecond);
|
||||
|
||||
await memoryAssetViewerUtils.clickPreviousButton(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, lastAssetOfFirst);
|
||||
});
|
||||
|
||||
test('hides prev button at very first asset (first memory, first asset, no prev memory)', async ({ page }) => {
|
||||
const firstMemory = memories[0];
|
||||
const veryFirstAsset = firstMemory.assets[0];
|
||||
|
||||
await memoryViewerUtils.openMemoryPageWithAsset(page, veryFirstAsset.id);
|
||||
await memoryGalleryUtils.clickThumbnail(page, veryFirstAsset.id);
|
||||
|
||||
await memoryAssetViewerUtils.waitForViewerOpen(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, veryFirstAsset);
|
||||
|
||||
await memoryAssetViewerUtils.expectPreviousButtonNotVisible(page);
|
||||
await memoryAssetViewerUtils.expectNextButtonVisible(page);
|
||||
});
|
||||
|
||||
test('hides next button at very last asset (last memory, last asset, no next memory)', async ({ page }) => {
|
||||
const lastMemory = memories.at(-1)!;
|
||||
const veryLastAsset = lastMemory.assets.at(-1)!;
|
||||
|
||||
await memoryViewerUtils.openMemoryPageWithAsset(page, veryLastAsset.id);
|
||||
await memoryGalleryUtils.clickThumbnail(page, veryLastAsset.id);
|
||||
|
||||
await memoryAssetViewerUtils.waitForViewerOpen(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, veryLastAsset);
|
||||
|
||||
await memoryAssetViewerUtils.expectNextButtonNotVisible(page);
|
||||
await memoryAssetViewerUtils.expectPreviousButtonVisible(page);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Keyboard navigation', () => {
|
||||
test('ArrowLeft navigates to previous asset across memory boundary', async ({ page }) => {
|
||||
const firstMemory = memories[0];
|
||||
const secondMemory = memories[1];
|
||||
const lastAssetOfFirst = firstMemory.assets.at(-1)!;
|
||||
const firstAssetOfSecond = secondMemory.assets[0];
|
||||
|
||||
await memoryViewerUtils.openMemoryPageWithAsset(page, firstAssetOfSecond.id);
|
||||
await memoryGalleryUtils.clickThumbnail(page, firstAssetOfSecond.id);
|
||||
|
||||
await memoryAssetViewerUtils.waitForViewerOpen(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, firstAssetOfSecond);
|
||||
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, lastAssetOfFirst);
|
||||
});
|
||||
|
||||
test('ArrowRight navigates to next asset across memory boundary', async ({ page }) => {
|
||||
const firstMemory = memories[0];
|
||||
const secondMemory = memories[1];
|
||||
const lastAssetOfFirst = firstMemory.assets.at(-1)!;
|
||||
const firstAssetOfSecond = secondMemory.assets[0];
|
||||
|
||||
await memoryViewerUtils.openMemoryPageWithAsset(page, lastAssetOfFirst.id);
|
||||
await memoryGalleryUtils.clickThumbnail(page, lastAssetOfFirst.id);
|
||||
|
||||
await memoryAssetViewerUtils.waitForViewerOpen(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, lastAssetOfFirst);
|
||||
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, firstAssetOfSecond);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Memory Viewer - Single Asset Memory Edge Cases', () => {
|
||||
let adminUserId: string;
|
||||
let timelineRestData: TimelineData;
|
||||
let memories: MemoryResponseDto[];
|
||||
const assets: TimelineAssetConfig[] = [];
|
||||
const testContext = new TimelineTestContext();
|
||||
const changes: Changes = {
|
||||
albumAdditions: [],
|
||||
assetDeletions: [],
|
||||
assetArchivals: [],
|
||||
assetFavorites: [],
|
||||
};
|
||||
const memoryChanges: MemoryChanges = {
|
||||
memoryDeletions: [],
|
||||
assetRemovals: new Map(),
|
||||
};
|
||||
|
||||
test.beforeAll(async () => {
|
||||
adminUserId = faker.string.uuid();
|
||||
testContext.adminId = adminUserId;
|
||||
|
||||
timelineRestData = generateTimelineData({
|
||||
...createDefaultTimelineConfig(),
|
||||
ownerId: adminUserId,
|
||||
});
|
||||
|
||||
for (const timeBucket of timelineRestData.buckets.values()) {
|
||||
assets.push(...timeBucket);
|
||||
}
|
||||
|
||||
memories = generateMemoriesFromTimeline(
|
||||
assets,
|
||||
adminUserId,
|
||||
[
|
||||
{ year: 2024, assetCount: 2 },
|
||||
{ year: 2023, assetCount: 1 },
|
||||
{ year: 2022, assetCount: 2 },
|
||||
],
|
||||
123,
|
||||
);
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ context }) => {
|
||||
await setupBaseMockApiRoutes(context, adminUserId);
|
||||
await setupTimelineMockApiRoutes(context, timelineRestData, changes, testContext);
|
||||
await setupMemoryMockApiRoutes(context, memories, memoryChanges);
|
||||
});
|
||||
|
||||
test.afterEach(() => {
|
||||
testContext.slowBucket = false;
|
||||
changes.albumAdditions = [];
|
||||
changes.assetDeletions = [];
|
||||
changes.assetArchivals = [];
|
||||
changes.assetFavorites = [];
|
||||
memoryChanges.memoryDeletions = [];
|
||||
memoryChanges.assetRemovals.clear();
|
||||
});
|
||||
|
||||
test('single asset memory shows both prev/next when surrounded by other memories', async ({ page }) => {
|
||||
const singleAssetMemory = memories[1];
|
||||
const singleAsset = singleAssetMemory.assets[0];
|
||||
|
||||
await memoryViewerUtils.openMemoryPageWithAsset(page, singleAsset.id);
|
||||
await memoryGalleryUtils.clickThumbnail(page, singleAsset.id);
|
||||
|
||||
await memoryAssetViewerUtils.waitForViewerOpen(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, singleAsset);
|
||||
|
||||
await memoryAssetViewerUtils.expectPreviousButtonVisible(page);
|
||||
await memoryAssetViewerUtils.expectNextButtonVisible(page);
|
||||
});
|
||||
});
|
||||
123
e2e/src/ui/specs/memory/utils.ts
Normal file
123
e2e/src/ui/specs/memory/utils.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type { AssetResponseDto } from '@immich/sdk';
|
||||
import { expect, Page } from '@playwright/test';
|
||||
|
||||
function getAssetIdFromUrl(url: URL): string | null {
|
||||
const pathMatch = url.pathname.match(/\/memory\/photos\/([^/]+)/);
|
||||
if (pathMatch) {
|
||||
return pathMatch[1];
|
||||
}
|
||||
return url.searchParams.get('id');
|
||||
}
|
||||
|
||||
export const memoryViewerUtils = {
|
||||
locator(page: Page) {
|
||||
return page.locator('#memory-viewer');
|
||||
},
|
||||
|
||||
async waitForMemoryLoad(page: Page) {
|
||||
await expect(this.locator(page)).toBeVisible();
|
||||
await expect(page.locator('#memory-viewer img').first()).toBeVisible();
|
||||
},
|
||||
|
||||
async openMemoryPage(page: Page) {
|
||||
await page.goto('/memory');
|
||||
await this.waitForMemoryLoad(page);
|
||||
},
|
||||
|
||||
async openMemoryPageWithAsset(page: Page, assetId: string) {
|
||||
await page.goto(`/memory?id=${assetId}`);
|
||||
await this.waitForMemoryLoad(page);
|
||||
},
|
||||
};
|
||||
|
||||
export const memoryGalleryUtils = {
|
||||
locator(page: Page) {
|
||||
return page.locator('#gallery-memory');
|
||||
},
|
||||
|
||||
thumbnailWithAssetId(page: Page, assetId: string) {
|
||||
return page.locator(`#gallery-memory [data-thumbnail-focus-container][data-asset="${assetId}"]`);
|
||||
},
|
||||
|
||||
async scrollToGallery(page: Page) {
|
||||
const showGalleryButton = page.getByLabel('Show gallery');
|
||||
if (await showGalleryButton.isVisible()) {
|
||||
await showGalleryButton.click();
|
||||
}
|
||||
await expect(this.locator(page)).toBeInViewport();
|
||||
},
|
||||
|
||||
async clickThumbnail(page: Page, assetId: string) {
|
||||
await this.scrollToGallery(page);
|
||||
await this.thumbnailWithAssetId(page, assetId).click();
|
||||
},
|
||||
|
||||
async getAllThumbnails(page: Page) {
|
||||
await this.scrollToGallery(page);
|
||||
return page.locator('#gallery-memory [data-thumbnail-focus-container]');
|
||||
},
|
||||
};
|
||||
|
||||
export const memoryAssetViewerUtils = {
|
||||
locator(page: Page) {
|
||||
return page.locator('#immich-asset-viewer');
|
||||
},
|
||||
|
||||
async waitForViewerOpen(page: Page) {
|
||||
await expect(this.locator(page)).toBeVisible();
|
||||
},
|
||||
|
||||
async waitForAssetLoad(page: Page, asset: AssetResponseDto) {
|
||||
const viewer = this.locator(page);
|
||||
const imgLocator = viewer.locator(`img[draggable="false"][src*="/api/assets/${asset.id}/thumbnail?size=preview"]`);
|
||||
const videoLocator = viewer.locator(`video[poster*="/api/assets/${asset.id}/thumbnail?size=preview"]`);
|
||||
|
||||
await imgLocator.or(videoLocator).waitFor({ timeout: 10_000 });
|
||||
},
|
||||
|
||||
nextButton(page: Page) {
|
||||
return page.getByLabel('View next asset');
|
||||
},
|
||||
|
||||
previousButton(page: Page) {
|
||||
return page.getByLabel('View previous asset');
|
||||
},
|
||||
|
||||
async expectNextButtonVisible(page: Page) {
|
||||
await expect(this.nextButton(page)).toBeVisible();
|
||||
},
|
||||
|
||||
async expectNextButtonNotVisible(page: Page) {
|
||||
await expect(this.nextButton(page)).toHaveCount(0);
|
||||
},
|
||||
|
||||
async expectPreviousButtonVisible(page: Page) {
|
||||
await expect(this.previousButton(page)).toBeVisible();
|
||||
},
|
||||
|
||||
async expectPreviousButtonNotVisible(page: Page) {
|
||||
await expect(this.previousButton(page)).toHaveCount(0);
|
||||
},
|
||||
|
||||
async clickNextButton(page: Page) {
|
||||
await this.nextButton(page).click();
|
||||
},
|
||||
|
||||
async clickPreviousButton(page: Page) {
|
||||
await this.previousButton(page).click();
|
||||
},
|
||||
|
||||
async closeViewer(page: Page) {
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(this.locator(page)).not.toBeVisible();
|
||||
},
|
||||
|
||||
getCurrentAssetId(page: Page): string | null {
|
||||
const url = new URL(page.url());
|
||||
return getAssetIdFromUrl(url);
|
||||
},
|
||||
|
||||
async expectCurrentAssetId(page: Page, expectedAssetId: string) {
|
||||
await expect.poll(() => this.getCurrentAssetId(page)).toBe(expectedAssetId);
|
||||
},
|
||||
};
|
||||
@@ -6,10 +6,10 @@ import {
|
||||
generateTimelineData,
|
||||
TimelineAssetConfig,
|
||||
TimelineData,
|
||||
} from 'src/generators/timeline';
|
||||
import { setupBaseMockApiRoutes } from 'src/mock-network/base-network';
|
||||
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network';
|
||||
import { assetViewerUtils } from 'src/web/specs/timeline/utils';
|
||||
} from 'src/ui/generators/timeline';
|
||||
import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network';
|
||||
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/ui/mock-network/timeline-network';
|
||||
import { assetViewerUtils } from '../timeline/utils';
|
||||
|
||||
const buildSearchUrl = (assetId: string) => {
|
||||
const searchQuery = encodeURIComponent(JSON.stringify({ originalFileName: 'test' }));
|
||||
@@ -12,18 +12,15 @@ import {
|
||||
selectRandomMultiple,
|
||||
TimelineAssetConfig,
|
||||
TimelineData,
|
||||
} from 'src/generators/timeline';
|
||||
import { setupBaseMockApiRoutes } from 'src/mock-network/base-network';
|
||||
import { pageRoutePromise, setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network';
|
||||
import { utils } from 'src/utils';
|
||||
} from 'src/ui/generators/timeline';
|
||||
import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network';
|
||||
import {
|
||||
assetViewerUtils,
|
||||
padYearMonth,
|
||||
pageUtils,
|
||||
poll,
|
||||
thumbnailUtils,
|
||||
timelineUtils,
|
||||
} from 'src/web/specs/timeline/utils';
|
||||
pageRoutePromise,
|
||||
setupTimelineMockApiRoutes,
|
||||
TimelineTestContext,
|
||||
} from 'src/ui/mock-network/timeline-network';
|
||||
import { utils } from 'src/utils';
|
||||
import { assetViewerUtils, padYearMonth, pageUtils, poll, thumbnailUtils, timelineUtils } from './utils';
|
||||
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
test.describe('Timeline', () => {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BrowserContext, expect, Page } from '@playwright/test';
|
||||
import { DateTime } from 'luxon';
|
||||
import { TimelineAssetConfig } from 'src/generators/timeline';
|
||||
import { TimelineAssetConfig } from 'src/ui/generators/timeline';
|
||||
|
||||
export const sleep = (ms: number) => {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
@@ -15,7 +15,6 @@
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"rootDirs": ["src"],
|
||||
"baseUrl": "./"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
|
||||
@@ -3,14 +3,14 @@ import { defineConfig } from 'vitest/config';
|
||||
// skip `docker compose up` if `make e2e` was already run
|
||||
const globalSetup: string[] = [];
|
||||
try {
|
||||
await fetch('http://127.0.0.1:2285/api/server-info/ping');
|
||||
await fetch('http://127.0.0.1:2285/api/server/ping');
|
||||
} catch {
|
||||
globalSetup.push('src/setup/docker-compose.ts');
|
||||
globalSetup.push('src/docker-compose.ts');
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['src/{api,cli,immich-admin}/specs/*.e2e-spec.ts'],
|
||||
include: ['src/specs/server/**/*.e2e-spec.ts'],
|
||||
globalSetup,
|
||||
testTimeout: 15_000,
|
||||
pool: 'threads',
|
||||
|
||||
23
i18n/ar.json
23
i18n/ar.json
@@ -5,7 +5,7 @@
|
||||
"acknowledge": "ØŖŲØ¯ØąŲ ذŲŲ",
|
||||
"action": "ØšŲ
ŲŲØŠ",
|
||||
"action_common_update": "ØĒØØ¯ŲØĢ",
|
||||
"action_description": "Ų
ØŦŲ
ŲØšØŠ Ų
Ų Ø§ŲŲØšØ§ŲŲØ§ØĒ Ø§ŲØĒŲ ŲØŦب ØĒŲŲŲØ°Ųا ØšŲŲ Ø§ŲØŖØĩŲŲ Ø§ŲØĒŲ ØĒŲ
ØĒØĩŲŲØĒŲØ§",
|
||||
"action_description": "Ų
ØŦŲ
ŲØšØŠ Ų
Ų Ø§ŲŲØšØ§ŲŲØ§ØĒ Ø§ŲØĒŲ ØŗØĒŲŲØ° ØšŲŲ Ø§ŲØŖØĩŲŲ Ø§ŲØĒŲ ØĒŲ
ØĒØĩŲŲØĒŲØ§",
|
||||
"actions": "ØšŲ
ŲŲØ§ØĒ",
|
||||
"active": "ŲØ´Øˇ",
|
||||
"active_count": "ŲØšØ§Ų: {count}",
|
||||
@@ -272,7 +272,7 @@
|
||||
"oauth_auto_register": "Ø§ŲØĒØŗØŦŲŲ Ø§ŲØĒŲŲØ§ØĻŲ",
|
||||
"oauth_auto_register_description": "Ø§ŲØĒØŗØŦŲŲ Ø§ŲØĒŲŲØ§ØĻŲ ŲŲŲ
ØŗØĒ؎دŲ
ŲŲ Ø§ŲØŦدد بؚد ØĒØŗØŦŲŲ Ø§ŲØ¯ØŽŲŲ Ø¨Ø§ØŗØĒ؎داŲ
OAuth",
|
||||
"oauth_button_text": "ŲØĩ Ø§ŲØ˛Øą",
|
||||
"oauth_client_secret_description": "Ų
ØˇŲŲØ¨ اذاPKCE(Ų
ŲØĒØ§Ø Ø§ŲØ§ØĢباØĒ ŲØĒØ¨Ø§Ø¯Ų اŲŲŲØ¯) ŲŲ
ŲØĒŲ
ØĒŲŲŲØąŲ Ų
Ų Ų
Ø˛ŲØ¯ OAuth",
|
||||
"oauth_client_secret_description": "Ų
ØˇŲŲØ¨ ŲŲØšŲ
ŲŲ Ø§ŲØŗØąŲØ Ø§Ų Ø§Ø°Ø§ PKCE(Ų
ŲØĒØ§Ø Ø§ŲØ§ØĢباØĒ ŲØĒØ¨Ø§Ø¯Ų اŲŲŲØ¯) ŲŲØŗ Ų
دؚŲŲ
Ų
Ų Ø§ŲØšŲ
ŲŲ Ø§ŲØšØ§Ų
.",
|
||||
"oauth_enable_description": "ØĒØŗØŦŲŲ Ø§ŲØ¯ØŽŲŲ Ø¨Ø§ØŗØĒ؎داŲ
OAuth",
|
||||
"oauth_mobile_redirect_uri": "ØšŲŲØ§Ų URI ŲØĨؚاد؊ Ø§ŲØĒŲØŦŲŲ ØšŲŲ Ø§ŲŲØ§ØĒŲ",
|
||||
"oauth_mobile_redirect_uri_override": "ØĒØŦØ§ŲØ˛ ØšŲŲØ§Ų URI ŲØĨؚاد؊ Ø§ŲØĒŲØŦŲŲ ØšŲŲ Ø§ŲŲØ§ØĒŲ",
|
||||
@@ -572,6 +572,9 @@
|
||||
"asset_list_layout_sub_title": "ØĒØĩŲ
ŲŲ
",
|
||||
"asset_list_settings_subtitle": "ØĨؚداداØĒ ØĒØŽØˇŲØˇ Ø´Ø¨ŲØŠ Ø§ŲØĩŲØą",
|
||||
"asset_list_settings_title": "Ø´Ø¨ŲØŠ Ø§ŲØĩŲØą",
|
||||
"asset_not_found_on_device_android": "Ø§ŲØ§ØĩŲ ŲŲ
ŲØĒŲ
Ø§ŲØŦØ§Ø¯Ų ŲŲ Ø§ŲØŦŲØ§Ø˛",
|
||||
"asset_not_found_on_device_ios": "Ø§ŲØŖØĩŲ ŲŲ
ŲØĒŲ
Ø§ŲØŦØ§Ø¯Ų ŲŲ Ø§ŲØŦŲØ§Ø˛. اذا ØĒØŗØĒ؎دŲ
؎دŲ
ØŠ iCloud, ŲØ§ŲØŖØĩŲ ŲØ¯ ŲØ§ ŲØĒŲ
اŲŲØĩŲŲ ŲŲ Ø¨ØŗØ¨Ø¨ Ų
ŲŲ Ų
ØĒØļØ§ØąØ¨ Ų
ØŽØ˛ŲŲ ŲŲ iCloud",
|
||||
"asset_not_found_on_icloud": "Ø§ŲØŖØĩŲ ŲŲ
ŲØĒŲ
Ø§ŲØŦØ§Ø¯Ų ŲŲ Ø§ŲØŦŲØ§Ø˛, Ø§ŲØŖØĩŲ ŲØ¯ ŲØ§ ŲØĒŲ
اŲŲØĩŲŲ ŲŲ Ø¨ØŗØ¨Ø¨ Ų
ŲŲ Ų
ØĒØļØ§ØąØ¨ Ų
ØŽØ˛ŲŲ ŲŲ iCloud",
|
||||
"asset_offline": "اŲŲ
ØØĒŲŲ ØēŲØą Ø§ØĒØĩاŲ",
|
||||
"asset_offline_description": "ŲŲ
ب𨝠ب°Ø§ Ø§ŲØŖØĩŲ Ø§ŲØŽØ§ØąØŦŲ Ų
ŲØŦŲØ¯Ųا ØšŲŲ Ø§ŲŲØąØĩ. ŲØąØŦŲ Ø§ŲØ§ØĒØĩØ§Ų Ø¨Ų
ØŗØ¤ŲŲ Immich ŲŲØØĩŲŲ ØšŲŲ Ø§ŲŲ
ØŗØ§ØšØ¯ØŠ.",
|
||||
"asset_restored_successfully": "ØĒŲ
Ø§ØŗØĒؚاد؊ Ø§ŲØ§ØĩŲ Ø¨ŲØŦاØ",
|
||||
@@ -650,7 +653,7 @@
|
||||
"backup_controller_page_background_turn_off": "ŲŲ
بØĨŲŲØ§Ų ØĒØ´ØēŲŲ ØŽØ¯Ų
ØŠ Ø§ŲØŽŲŲŲØŠ",
|
||||
"backup_controller_page_background_turn_on": "ŲŲ
بØĒØ´ØēŲŲ ØŽØ¯Ų
ØŠ Ø§ŲØŽŲŲŲØŠ",
|
||||
"backup_controller_page_background_wifi": "ŲŲØˇ ØšŲŲ Wi-Fi",
|
||||
"backup_controller_page_backup": "دؚŲ
",
|
||||
"backup_controller_page_backup": "ŲØŗØŽ Ø§ØØĒŲØ§ØˇŲ",
|
||||
"backup_controller_page_backup_selected": "اŲŲ
ØØ¯Ø¯: ",
|
||||
"backup_controller_page_backup_sub": "اŲŲØŗØŽ Ø§ŲØ§ØØĒŲØ§ØˇŲ ŲŲØĩŲØą ŲŲ
ŲØ§ØˇØš اŲŲŲØ¯ŲŲ",
|
||||
"backup_controller_page_created": "Ø§ŲØ´ØĻ ŲŲ :{date}",
|
||||
@@ -779,6 +782,8 @@
|
||||
"client_cert_import": "Ø§ØŗØĒŲØąØ§Ø¯",
|
||||
"client_cert_import_success_msg": "ØĒŲ
Ø§ØŗØĒŲØąØ§Ø¯ Ø´ŲØ§Ø¯ØŠ Ø§ŲØšŲ
ŲŲ",
|
||||
"client_cert_invalid_msg": "Ų
ŲŲ Ø´ŲØ§Ø¯ØŠ ØšŲ
ŲŲ ØēŲØą ØĩØ§ŲØØŠ Ø§Ų ŲŲŲ
ØŠ ØŗØą ØēŲØą ØĩØŲØØŠ",
|
||||
"client_cert_password_message": "ØŖØ¯ØŽŲ ŲŲŲ
ØŠ اŲŲ
ØąŲØą Ø§Ų؎اØĩØŠ Ø¨ŲØ°Ų Ø§ŲØ´Ųاد؊",
|
||||
"client_cert_password_title": "ŲŲŲ
ØŠ اŲŲ
ØąŲØą Ø§Ų؎اØĩØŠ Ø¨Ø§ŲØ´Ųاد؊",
|
||||
"client_cert_remove_msg": "ØĒŲ
Ø§Ø˛Ø§ŲØŠ Ø´ŲØ§Ø¯ØŠ Ø§ŲØšŲ
ŲŲ",
|
||||
"client_cert_subtitle": "ŲØ¯ØšŲ
ØĩŲØē PKCS12 (.p12, .pfx)ŲŲØˇ. Ø§ØŗØĒŲØąØ§Ø¯/Ø§Ø˛Ø§ŲØŠ Ø§ŲØ´ŲاداØĒ Ų
ØĒØ§Ø ŲŲØˇ ŲØ¨Ų ØĒØŗØŦŲŲ Ø§ŲØ¯ØŽŲŲ",
|
||||
"client_cert_title": "Ø´ŲØ§Ø¯ØŠ Ų
ØŗØĒ؎دŲ
SSL [ØĒØŦØąŲØ¨ŲØŠ]",
|
||||
@@ -992,6 +997,11 @@
|
||||
"editor_close_without_save_prompt": "ŲŲ ŲØĒŲ
ØŲظ Ø§ŲØĒØēŲبਧØĒ",
|
||||
"editor_close_without_save_title": "ØĨØēŲØ§Ų اŲŲ
ØØąØąØ",
|
||||
"editor_confirm_reset_all_changes": "ŲŲ ØŖŲØĒ Ų
ØĒØŖŲØ¯ Ų
Ų ØĨؚاد؊ ØļØ¨Øˇ ØŦŲ
ب𠨧بǨēŲبਧØĒØ",
|
||||
"editor_discard_edits_confirm": "ØĒØŦاŲŲ Ø§ŲØĒØšØ¯ŲŲØ§ØĒ",
|
||||
"editor_discard_edits_prompt": "ŲØ¯ŲŲ ØĒؚدŲŲØ§ØĒ ØēŲØą Ų
ØŲŲØ¸ØŠ. ŲŲ ØŖŲØĒ Ų
ØĒØŖŲØ¯ Ų
Ų ØąØēبØĒŲ ŲŲ ØĒØŦاŲŲŲØ§Ø",
|
||||
"editor_discard_edits_title": "ØĒØŦاŲŲ Ø§ŲØĒØšØ¯ŲŲØ§ØĒØ",
|
||||
"editor_edits_applied_error": "ŲØ´Ų ØĒØˇØ¨ŲŲ Ø§ŲØĒØšØ¯ŲŲØ§ØĒ",
|
||||
"editor_edits_applied_success": "ØĒŲ
ØĒØˇØ¨ŲŲ Ø§ŲØĒØšØ¯ŲŲØ§ØĒ Ø¨ŲØŦاØ",
|
||||
"editor_flip_horizontal": "اŲŲØ¨ ØŖŲŲŲŲØ§",
|
||||
"editor_flip_vertical": "اŲŲØ¨ ØšŲ
ŲØ¯ŲŲØ§",
|
||||
"editor_orientation": "اØĒØŦاŲ",
|
||||
@@ -1192,8 +1202,9 @@
|
||||
"features": "اŲŲ
ŲØ˛Ø§ØĒ",
|
||||
"features_in_development": "اŲŲ
ŲØ˛Ø§ØĒ ŲŲØ¯ Ø§ŲØĒØˇŲŲØą",
|
||||
"features_setting_description": "ØĨØ¯Ø§ØąØŠ Ų
ŲØ˛Ø§ØĒ Ø§ŲØĒØˇØ¨ŲŲ",
|
||||
"file_name": "Ø§ØŗŲ
اŲŲ
ŲŲ: {file_name}",
|
||||
"file_name_or_extension": "Ø§ØŗŲ
اŲŲ
ŲŲ ØŖŲ Ø§Ų
ØĒدادŲ",
|
||||
"file_name_text": "ØŖØŗŲ
اŲŲ
ŲŲ",
|
||||
"file_name_with_value": "Ø§ØŗŲ
اŲŲ
ŲŲ: {file_name}",
|
||||
"file_size": "ØØŦŲ
اŲŲ
ŲŲ",
|
||||
"filename": "Ø§ØŗŲ
اŲŲ
ŲŲ",
|
||||
"filetype": "Ųب𠨧ŲŲ
ŲŲ",
|
||||
@@ -1602,7 +1613,6 @@
|
||||
"not_available": "ØēŲØą Ų
ØĒاØ",
|
||||
"not_in_any_album": "ŲŲØŗØĒ ŲŲ ØŖŲ ØŖŲØ¨ŲŲ
",
|
||||
"not_selected": "ŲŲ
ŲØŽØĒØ§Øą",
|
||||
"note_apply_storage_label_to_previously_uploaded assets": "Ų
ŲØ§ØØ¸ØŠ: ŲØĒØˇØ¨ŲŲ ØŗŲ
ØŠ Ø§ŲØĒØŽØ˛ŲŲ ØšŲŲ Ø§ŲŲ
ØØĒŲŲØ§ØĒ Ø§ŲØĒŲ ØĒŲ
ØąŲØšŲا Ų
ØŗØ¨ŲŲØ§Ø ŲŲ
بØĒØ´ØēŲŲ",
|
||||
"notes": "Ų
ŲØ§ØØ¸Ø§ØĒ",
|
||||
"nothing_here_yet": "ŲØ§ ŲŲØŦد Ø´ŲØĄ ŲŲØ§ بؚد",
|
||||
"notification_permission_dialog_content": "ŲØĒŲ
ŲŲŲ Ø§ŲØĨØŽØˇØ§ØąØ§ØĒ Ø Ø§ŲØĒŲŲ ØĨŲŲ Ø§ŲØĨؚداداØĒ Ų Ø§ØŽØĒØ§Øą Ø§ŲØŗŲ
اØ.",
|
||||
@@ -1804,7 +1814,7 @@
|
||||
"reassigned_assets_to_new_person": "ØĒŲ
ØĒ ØĨؚاد؊ ØĒØšŲŲŲ {count, plural, one {# اŲŲ
ØØĒŲŲ} other {# اŲŲ
ØØĒŲŲØ§ØĒ}} ØĨŲŲ Ø´ØŽØĩ ØŦØ¯ŲØ¯",
|
||||
"reassing_hint": "ØĒØšŲŲŲ Ø§ŲŲ
ØØĒŲŲØ§ØĒ اŲŲ
ØØ¯Ø¯ØŠ ŲØ´ØŽØĩ Ų
ŲØŦŲØ¯",
|
||||
"recent": "ØØ¯ŲØĢ",
|
||||
"recent-albums": "ØŖŲØ¨ŲŲ
اØĒ Ø§ŲØØ¯ŲØĢØŠ",
|
||||
"recent_albums": "ØŖŲØ¨ŲŲ
اØĒ Ø§ŲØØ¯ŲØĢØŠ",
|
||||
"recent_searches": "ØšŲ
ŲŲØ§ØĒ Ø§ŲØ¨ØØĢ Ø§ŲØŖØŽŲØąØŠ",
|
||||
"recently_added": "اØļŲŲ Ų
Ø¤ØŽØąØ§",
|
||||
"recently_added_page_title": "ØŖØļŲŲ Ų
Ø¤ØŽØąØ§",
|
||||
@@ -2295,6 +2305,7 @@
|
||||
"upload_details": "ØĒŲØ§ØĩŲŲ Ø§ŲØąŲØš",
|
||||
"upload_dialog_info": "ŲŲ ØĒØąŲØ¯ اŲŲØŗØŽ Ø§ŲØ§ØØĒŲØ§ØˇŲ ŲŲØŖØĩŲŲ (Ø§ŲØŖØĩŲŲ) اŲŲ
ØØ¯Ø¯ØŠ ØĨŲŲ Ø§ŲØŽØ§Ø¯Ų
Ø",
|
||||
"upload_dialog_title": "ØĒØŲ
ŲŲ Ø§ŲØŖØĩŲŲ",
|
||||
"upload_error_with_count": "ØŽØˇØŖ ŲŲ ØąŲØš {count, plural, one {# اØĩŲ} other {# اØĩŲŲ}}",
|
||||
"upload_errors": "ØĨŲØĒŲ
Ų Ø§ŲØąŲØš Ų
Øš {count, plural, one {# ØŽØˇØŖ} other {# ØŖØŽØˇØ§ØĄ}}, ŲŲ
بØĒØØ¯ŲØĢ Ø§ŲØĩŲØØŠ ŲØąØ¤ŲØŠ Ø§ŲŲ
ØØĒŲŲØ§ØĒ Ø§ŲØŦØ¯ŲØ¯ØŠ Ø§ŲØĒŲ ØĒŲ
ØąŲØšŲا.",
|
||||
"upload_finished": "ØĒŲ
Ø§ŲØ§ŲØĒŲØ§ØĄ Ų
Ų Ø§ŲØąŲØš",
|
||||
"upload_progress": "Ų
ØĒبŲŲØŠ {remaining, number} - Ų
ØšØ§ŲØŦØŠ {processed, number}/{total, number}",
|
||||
|
||||
@@ -380,7 +380,6 @@
|
||||
"favorite": "ĐŖ айŅаĐŊŅĐŧ",
|
||||
"favorite_or_unfavorite_photo": "ĐадаŅŅ Đ°ĐąĐž вŅдаĐģŅŅŅ ŅĐžŅа С айŅаĐŊĐ°ĐŗĐ°",
|
||||
"favorites": "ĐĐąŅаĐŊŅŅ",
|
||||
"file_name": "ĐаСва ŅаКĐģа: {file_name}",
|
||||
"filename": "ĐаСва ŅаКĐģа",
|
||||
"filetype": "ĐĸŅĐŋ ŅаКĐģа",
|
||||
"filter": "ФŅĐģŅŅŅ",
|
||||
@@ -458,7 +457,7 @@
|
||||
"reassign": "ĐĐĩŅаĐŋŅŅСĐŊаŅŅŅŅ",
|
||||
"reassing_hint": "ĐŅŅĐŋŅŅаŅŅ Đ˛ŅĐąŅаĐŊŅŅ Đ°ĐēŅŅĐ˛Ņ ŅŅĐŊŅŅŅаК аŅОйĐĩ",
|
||||
"recent": "ĐŅдаŅĐŊŅ",
|
||||
"recent-albums": "ĐŅдаŅĐŊŅŅ Đ°ĐģŅйОĐŧŅ",
|
||||
"recent_albums": "ĐŅдаŅĐŊŅŅ Đ°ĐģŅйОĐŧŅ",
|
||||
"recent_searches": "ĐŅдаŅĐŊŅŅ ĐŋĐžŅŅĐēŅ",
|
||||
"recently_added": "ĐŅдаŅĐŊа дададСĐĩĐŊа",
|
||||
"refresh_faces": "ĐĐąĐŊавŅŅŅ ŅваŅŅ",
|
||||
|
||||
29
i18n/bg.json
29
i18n/bg.json
@@ -272,7 +272,7 @@
|
||||
"oauth_auto_register": "ĐвŅĐžĐŧаŅиŅĐŊа ŅĐĩĐŗĐ¸ŅŅŅаŅиŅ",
|
||||
"oauth_auto_register_description": "ĐвŅĐžĐŧаŅиŅĐŊĐž ŅĐĩĐŗĐ¸ŅŅŅиŅаĐŊĐĩ ĐŊа ĐŊОви ĐŋĐžŅŅĐĩйиŅĐĩĐģи ŅĐģĐĩĐ´ вĐģиСаĐŊĐĩ Ņ OAuth",
|
||||
"oauth_button_text": "ĐĸĐĩĐēŅŅ ĐŊа ĐąŅŅĐžĐŊа",
|
||||
"oauth_client_secret_description": "ĐСиŅĐēва ŅĐĩ, ĐēĐžĐŗĐ°ŅĐž Đ´ĐžŅŅавŅиĐēа ĐŊа OAuth ĐŊĐĩ ĐŋОддŅŅĐļа PKCE (Proof Key for Code Exchange)",
|
||||
"oauth_client_secret_description": "ĐадŅĐģĐļиŅĐĩĐģĐŊĐž Са ĐŋОвĐĩŅиŅĐĩĐģĐĩĐŊ ĐēĐģиĐĩĐŊŅ Đ¸Đģи ĐēĐžĐŗĐ°ŅĐž ĐŊĐĩ ŅĐĩ ĐŋОддŅŅĐļа PKCE (Proof Key for Code Exchange) Са ĐŋŅĐąĐģиŅĐĩĐŊ ĐēĐģиĐĩĐŊŅ.",
|
||||
"oauth_enable_description": "ĐĐģиСаĐŊĐĩ Ņ OAuth",
|
||||
"oauth_mobile_redirect_uri": "URI Са ĐŧОйиĐģĐŊĐž ĐŋŅĐĩĐŊаŅĐžŅваĐŊĐĩ",
|
||||
"oauth_mobile_redirect_uri_override": "URI ĐŋŅĐĩĐŊаŅĐžŅваĐŊĐĩ Са ĐŧОйиĐģĐŊи ŅŅŅŅОКŅŅва",
|
||||
@@ -383,7 +383,7 @@
|
||||
"transcoding_hardware_acceleration": "ĐĨаŅĐ´ŅĐĩŅĐŊĐž ŅŅĐēĐžŅĐĩĐŊиĐĩ",
|
||||
"transcoding_hardware_acceleration_description": "ĐĐēŅĐŋĐĩŅиĐŧĐĩĐŊŅаĐģĐŊĐž: ĐŧĐŊĐžĐŗĐž ĐŋĐž-ĐąŅŅСО ŅŅаĐŊŅĐēОдиŅаĐŊĐĩ, ĐŊĐž ĐŧĐžĐļĐĩ да ĐŋĐžĐŊиĐļи ĐēаŅĐĩŅŅвОŅĐž ĐŋŅи ŅŅŅĐ¸Ņ ĐąĐ¸ŅŅĐĩĐšŅ",
|
||||
"transcoding_hardware_decoding": "ĐĨаŅĐ´ŅĐĩŅĐŊĐž Đ´ĐĩĐēОдиŅаĐŊĐĩ",
|
||||
"transcoding_hardware_decoding_setting_description": "ĐŅиĐģĐ°ĐŗĐ° ŅĐĩ ŅаĐŧĐž Са NVENC, QSV и RKMPP. ĐĐēŅивиŅа ŅŅĐēĐžŅĐĩĐŊиĐĩ ĐžŅ ĐēŅаК Đ´Đž ĐēŅаК, вĐŧĐĩŅŅĐž ŅаĐŧĐž да ŅŅĐēĐžŅŅва ĐēОдиŅаĐŊĐĩŅĐž. ĐĐžĐļĐĩ да ĐŊĐĩ ŅайОŅи Ņ Đ˛ŅиŅĐēи видĐĩĐžĐēĐģиĐŋОвĐĩ.",
|
||||
"transcoding_hardware_decoding_setting_description": "ĐĐēŅивиŅа ŅŅĐēĐžŅĐĩĐŊиĐĩ ĐžŅ ĐēŅаК Đ´Đž ĐēŅаК, вĐŧĐĩŅŅĐž ŅаĐŧĐž да ŅŅĐēĐžŅŅва ĐēОдиŅаĐŊĐĩŅĐž. ĐĐžĐļĐĩ да ĐŊĐĩ ŅайОŅи Ņ Đ˛ŅиŅĐēи видĐĩĐžĐēĐģиĐŋОвĐĩ.",
|
||||
"transcoding_max_b_frames": "ĐаĐēŅиĐŧаĐģĐŊи B-ŅŅĐĩĐšĐŧа",
|
||||
"transcoding_max_b_frames_description": "ĐĐž-виŅĐžĐēиŅĐĩ ŅŅОКĐŊĐžŅŅи ĐŋОдОйŅŅĐ˛Đ°Ņ ĐĩŅĐĩĐēŅивĐŊĐžŅŅŅа ĐŊа ĐēĐžĐŧĐŋŅĐĩŅиŅŅа, ĐŊĐž СайавŅŅ ŅаСĐēОдиŅаĐŊĐĩŅĐž. ĐĐžĐļĐĩ да ĐŊĐĩ Đĩ ŅŅвĐŧĐĩŅŅиĐŧ Ņ Ņ
аŅĐ´ŅĐĩŅĐŊĐžŅĐž ŅŅĐēĐžŅĐĩĐŊиĐĩ ĐŊа ĐŋĐž-ŅŅаŅи ŅŅŅŅОКŅŅва. 0 Đ´ĐĩаĐēŅивиŅа B-ŅŅĐĩĐšĐŧа, Đ´ĐžĐēаŅĐž -1 Садава ŅаСи ŅŅОКĐŊĐžŅŅ Đ°Đ˛ŅĐžĐŧаŅиŅĐŊĐž.",
|
||||
"transcoding_max_bitrate": "ĐаĐēŅиĐŧаĐģĐĩĐŊ йиŅŅĐĩĐšŅ",
|
||||
@@ -572,6 +572,9 @@
|
||||
"asset_list_layout_sub_title": "РаСĐŋĐžĐģĐžĐļĐĩĐŊиĐĩ",
|
||||
"asset_list_settings_subtitle": "ĐаŅŅŅОКĐēи ĐŊа ĐŧŅĐĩĐļаŅа ĐŊа ŅаСĐŋĐžĐģĐ°ĐŗĐ°ĐŊĐĩ ĐŊа ŅĐŊиĐŧĐēи",
|
||||
"asset_list_settings_title": "РаСĐŋĐžĐģĐ°ĐŗĐ°ĐŊĐĩ ĐŊа ŅĐŊиĐŧĐēи",
|
||||
"asset_not_found_on_device_android": "ĐĐąĐĩĐēŅŅŅ ĐŊĐĩ Đĩ ĐŊаĐŧĐĩŅĐĩĐŊ ĐŊа ŅŅŅŅОКŅŅвОŅĐž",
|
||||
"asset_not_found_on_device_ios": "ĐĐąĐĩĐēŅŅŅ ĐŊĐĩ Đĩ ĐŊаĐŧĐĩŅĐĩĐŊ ĐŊа ŅŅŅŅОКŅŅвОŅĐž. ĐĐēĐž иСĐŋĐžĐģСваŅĐĩ iCloud, ОйĐĩĐēŅŅŅ ĐŧĐžĐļĐĩ да Đĩ ĐŊĐĩĐ´ĐžŅŅŅĐŋĐĩĐŊ ĐŋĐžŅади ĐŋОвŅĐĩĐ´ĐĩĐŊ ŅаКĐģ, ŅŅŅ
ŅаĐŊĐĩĐŊ в iCloud",
|
||||
"asset_not_found_on_icloud": "ĐĐąĐĩĐēŅŅŅ ĐŊĐĩ Đĩ ĐŊаĐŧĐĩŅĐĩĐŊ в iCloud. ĐĐąĐĩĐēŅŅŅ ĐŧĐžĐļĐĩ да Đĩ ĐŊĐĩĐ´ĐžŅŅŅĐŋĐĩĐŊ ĐŋĐžŅади ĐŋОвŅĐĩĐ´ĐĩĐŊ ŅаКĐģ, ŅŅŅ
ŅаĐŊĐĩĐŊ в iCloud",
|
||||
"asset_offline": "ĐĐģĐĩĐŧĐĩĐŊŅŅŅ Đĩ ĐžŅĐģаКĐŊ",
|
||||
"asset_offline_description": "ĐĸОСи вŅĐŊŅĐĩĐŊ аĐēŅив вĐĩŅĐĩ ĐŊĐĩ ŅĐĩ ĐŊаĐŧиŅа ĐŊа диŅĐēа. ĐĐžĐģŅ, ŅвŅŅĐļĐĩŅĐĩ ŅĐĩ Ņ Đ°Đ´ĐŧиĐŊиŅŅŅаŅĐžŅа ĐŊа Immich Са ĐŋĐžĐŧĐžŅ.",
|
||||
"asset_restored_successfully": "ĐŖŅĐŋĐĩŅĐŊĐž вŅСŅŅаĐŊОвĐĩĐŊ ОйĐĩĐēŅ",
|
||||
@@ -779,6 +782,8 @@
|
||||
"client_cert_import": "ĐĐŧĐŋĐžŅŅ",
|
||||
"client_cert_import_success_msg": "ĐĐģиĐĩĐŊŅŅĐēĐ¸Ņ ŅĐĩŅŅиŅиĐēĐ°Ņ Đĩ иĐŧĐŋĐžŅŅиŅаĐŊ",
|
||||
"client_cert_invalid_msg": "ĐĐĩваĐģидĐĩĐŊ ŅĐĩŅŅиŅиĐēĐ°Ņ Đ¸Đģи ĐŗŅĐĩŅĐŊа ĐŋаŅĐžĐģа",
|
||||
"client_cert_password_message": "ĐŅвĐĩĐ´ĐĩŅĐĩ ĐŋаŅĐžĐģа Са ŅОСи ŅĐĩŅŅиŅиĐēаŅ",
|
||||
"client_cert_password_title": "ĐаŅĐžĐģа Са ŅĐĩŅŅиŅиĐēаŅ",
|
||||
"client_cert_remove_msg": "ĐĐģиĐĩĐŊŅŅĐēĐ¸Ņ ŅĐĩŅŅиŅиĐēĐ°Ņ Đĩ ĐŋŅĐĩĐŧаŅ
ĐŊаŅ",
|
||||
"client_cert_subtitle": "ĐОддŅŅĐļа ŅĐĩ ŅаĐŧĐž ŅĐžŅĐŧĐ°Ņ PKCS12 (.p12, .pfx). ĐĐŧĐŋĐžŅŅ/ĐŋŅĐĩĐŧаŅ
ваĐŊĐĩ ĐŊа ŅĐĩŅŅиŅиĐēĐ°Ņ ĐŧĐžĐļĐĩ ŅаĐŧĐž ĐŋŅĐĩди вĐŋиŅваĐŊĐĩ в ŅиŅŅĐĩĐŧаŅа",
|
||||
"client_cert_title": "ĐĐģиĐĩĐŊŅŅĐēи SSL ŅĐĩŅŅиŅиĐēĐ°Ņ [ĐĐĐĄĐĐĐ ĐĐĐĐĐĸĐĐĐĐ]",
|
||||
@@ -992,6 +997,11 @@
|
||||
"editor_close_without_save_prompt": "ĐŅĐžĐŧĐĩĐŊиŅĐĩ ĐŊŅĐŧа да ĐąŅĐ´Đ°Ņ ĐˇĐ°ĐŋаСĐĩĐŊи",
|
||||
"editor_close_without_save_title": "ĐаŅваŅŅĐŊĐĩ ĐŊа ŅĐĩдаĐēŅĐžŅа?",
|
||||
"editor_confirm_reset_all_changes": "ĐĄĐ¸ĐŗŅŅĐŊи Đģи ŅŅĐĩ, ŅĐĩ иŅĐēаŅĐĩ да вŅСŅŅаĐŊОвиŅĐĩ вŅиŅĐēи ĐŋŅĐžĐŧĐĩĐŊи?",
|
||||
"editor_discard_edits_confirm": "ĐŅŅ
вŅŅĐģи ĐŋŅĐžĐŧĐĩĐŊиŅĐĩ",
|
||||
"editor_discard_edits_prompt": "ĐĐŧаŅĐĩ ĐŊĐĩСаĐŋаСĐĩĐŊи ĐŋŅĐžĐŧĐĩĐŊи. ĐаиŅŅиĐŊа Đģи иŅĐēаŅĐĩ да ĐŗĐ¸ ĐžŅŅ
вŅŅĐģиŅĐĩ?",
|
||||
"editor_discard_edits_title": "ĐŅŅ
вŅŅĐģŅĐŧĐĩ Đģи ĐŋŅĐžĐŧĐĩĐŊиŅĐĩ?",
|
||||
"editor_edits_applied_error": "ĐĐĩŅŅĐŋĐĩŅĐŊĐž ĐŋŅиĐģĐ°ĐŗĐ°ĐŊĐĩ ĐŊа ĐŋŅĐžĐŧĐĩĐŊиŅĐĩ",
|
||||
"editor_edits_applied_success": "ĐŖŅĐŋĐĩŅĐŊĐž ĐŋŅиĐģĐ°ĐŗĐ°ĐŊĐĩ ĐŊа ĐŋŅĐžĐŧĐĩĐŊиŅĐĩ",
|
||||
"editor_flip_horizontal": "ĐĐąŅŅĐŊи Ņ
ĐžŅиСОĐŊŅаĐģĐŊĐž",
|
||||
"editor_flip_vertical": "ĐĐąŅŅĐŊи вĐĩŅŅиĐēаĐģĐŊĐž",
|
||||
"editor_orientation": "ĐŅиĐĩĐŊŅаŅиŅ",
|
||||
@@ -1149,7 +1159,7 @@
|
||||
},
|
||||
"errors_text": "ĐŅĐĩŅĐēи",
|
||||
"exclusion_pattern": "ШайĐģĐžĐŊ Са иСĐēĐģŅŅĐĩĐŊиĐĩ",
|
||||
"exif": "Exif",
|
||||
"exif": "Đxif",
|
||||
"exif_bottom_sheet_description": "ĐОйави ĐĐŋиŅаĐŊиĐĩ...",
|
||||
"exif_bottom_sheet_description_error": "ĐĐĩŅŅĐŋĐĩŅĐŊĐž ОйĐŊОвŅваĐŊĐĩ ĐŊа ĐžĐŋиŅаĐŊиĐĩ",
|
||||
"exif_bottom_sheet_details": "ĐĐĐĐ ĐĐĐĐĐĄĐĸĐ",
|
||||
@@ -1192,8 +1202,9 @@
|
||||
"features": "ФŅĐŊĐēŅии",
|
||||
"features_in_development": "ФŅĐŊĐēŅии в ĐŋŅĐžŅĐĩŅ ĐŊа ŅаСŅайОŅĐēа",
|
||||
"features_setting_description": "ĐŖĐŋŅавĐģĐĩĐŊиĐĩ ĐŊа ŅŅĐŊĐēŅииŅĐĩ ĐŊа ĐŋŅиĐģĐžĐļĐĩĐŊиĐĩŅĐž",
|
||||
"file_name": "ĐĐŧĐĩ ĐŊа ŅаКĐģа: {file_name}",
|
||||
"file_name_or_extension": "ĐĐŧĐĩ ĐŊа ŅаКĐģ иĐģи ŅаСŅиŅĐĩĐŊиĐĩ",
|
||||
"file_name_text": "ĐĐŧe ĐŊа ŅаКĐģ",
|
||||
"file_name_with_value": "ĐĐŧĐĩ ĐŊа ŅаКĐģ: {file_name}",
|
||||
"file_size": "РаСĐŧĐĩŅ ĐŊа ŅаКĐģа",
|
||||
"filename": "ĐĐŧĐĩ ĐŊа ŅаКĐģ",
|
||||
"filetype": "ĐĸиĐŋ ĐŊа ŅаКĐģ",
|
||||
@@ -1215,7 +1226,7 @@
|
||||
"free_up_space_description": "ĐŅĐĩĐŧĐĩŅŅĐĩŅĐĩ аŅŅ
ивиŅаĐŊиŅĐĩ ŅĐŊиĐŧĐēи и видĐĩа в ĐēĐžŅŅĐĩŅĐž ĐŊа ŅŅŅŅОКŅŅвОŅĐž, Са да ĐžŅвОйОдиŅĐĩ ĐŧŅŅŅĐž. ĐĐžĐŋиŅŅа ĐŊа ŅŅŅвŅŅа ŅĐĩ ĐąŅĐ´Đ°Ņ ĐˇĐ°ĐŋаСĐĩĐŊи.",
|
||||
"free_up_space_settings_subtitle": "ĐŅвОйОĐļдаваĐŊĐĩ ĐŊа ĐŧŅŅŅĐž Са ŅŅŅ
ŅаĐŊĐĩĐŊиĐĩ ĐŊа ŅŅŅŅОКŅŅвОŅĐž",
|
||||
"full_path": "ĐŅĐģĐĩĐŊ ĐŋŅŅ: {path}",
|
||||
"gcast_enabled": "Google Cast",
|
||||
"gcast_enabled": "GООgle Cast",
|
||||
"gcast_enabled_description": "Đа да ŅайОŅи ŅаСи ŅŅĐŊĐēŅĐ¸Ņ ĐˇĐ°ŅĐĩĐļда вŅĐŊŅĐŊи ŅĐĩŅŅŅŅи ĐžŅ Google.",
|
||||
"general": "ĐĐąŅи",
|
||||
"geolocation_instruction_location": "ĐСйĐĩŅĐĩŅĐĩ ОйĐĩĐēŅ Ņ GPS ĐēООŅдиĐŊаŅи Са да иСĐŋĐžĐģСваŅĐĩ ŅŅŅ
иĐģи иСйĐĩŅĐĩŅĐĩ ĐŧŅŅŅĐž диŅĐĩĐēŅĐŊĐž ĐžŅ ĐēаŅŅаŅа",
|
||||
@@ -1404,7 +1415,7 @@
|
||||
"login_form_api_exception": "ĐŅĐĩŅĐēа в ĐēĐžĐŧŅĐŊиĐēаŅиŅŅа. ĐĐžĐģŅ, ĐŋŅОвĐĩŅи URL ĐŊа ŅŅŅвŅŅа и ĐžĐŋиŅаК ĐŋаĐē.",
|
||||
"login_form_back_button_text": "ĐĐąŅаŅĐŊĐž",
|
||||
"login_form_email_hint": "youremail@email.com",
|
||||
"login_form_endpoint_hint": "http://your-server-ip:port",
|
||||
"login_form_endpoint_hint": "http://yĐžur-server-ip:port",
|
||||
"login_form_endpoint_url": "URL адŅĐĩŅ ĐŊа ŅŅŅвŅŅа",
|
||||
"login_form_err_http": "ĐĐžĐģŅ, ĐžĐŋŅĐĩĐ´ĐĩĐģи ĐŋŅĐžŅĐžĐēĐžĐģа http:// иĐģи https://",
|
||||
"login_form_err_invalid_email": "ĐĐĩваĐģидĐĩĐŊ иĐŧĐĩĐšĐģ адŅĐĩŅ",
|
||||
@@ -1602,7 +1613,6 @@
|
||||
"not_available": "ĐĐĩĐŊаĐģиŅĐŊĐž",
|
||||
"not_in_any_album": "ĐĐĩ Đĩ в ĐŊиĐēОК аĐģĐąŅĐŧ",
|
||||
"not_selected": "ĐĐĩ Đĩ иСйŅаĐŊĐž",
|
||||
"note_apply_storage_label_to_previously_uploaded assets": "ĐайĐĩĐģĐĩĐļĐēа: Đа да ĐŋŅиĐģĐžĐļиŅĐĩ ĐĩŅиĐēĐĩŅа Са ŅŅŅ
ŅаĐŊĐĩĐŊиĐĩ ĐēŅĐŧ ĐŋŅĐĩдваŅиŅĐĩĐģĐŊĐž ĐēаŅĐĩĐŊи аĐēŅиви, ŅŅаŅŅиŅаКŅĐĩ",
|
||||
"notes": "ĐĐĩĐģĐĩĐļĐēи",
|
||||
"nothing_here_yet": "ĐаŅĐĩĐŗĐ° ŅŅĐē ĐŊŅĐŧа ĐŊиŅĐž",
|
||||
"notification_permission_dialog_content": "Đа да вĐēĐģŅŅĐ¸Ņ Đ¸ĐˇĐ˛ĐĩŅŅиŅŅа, ĐžŅиди в ĐаŅŅŅОКĐēи и иСйĐĩŅи РаСŅĐĩŅи.",
|
||||
@@ -1804,7 +1814,7 @@
|
||||
"reassigned_assets_to_new_person": "ĐŅĐĩĐŊаСĐŊаŅĐĩĐŊи {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅа}} ĐŊа ĐŊОв ŅОвĐĩĐē",
|
||||
"reassing_hint": "ĐаСĐŊаŅи иСйŅаĐŊиŅĐĩ ĐĩĐģĐĩĐŧĐĩĐŊŅи ĐŊа ŅŅŅĐĩŅŅвŅваŅĐž ĐģиŅĐĩ",
|
||||
"recent": "ĐĄĐēĐžŅĐžŅĐŊи",
|
||||
"recent-albums": "ĐĄĐēĐžŅĐžŅĐŊи ĐĐģĐąŅĐŧи",
|
||||
"recent_albums": "ĐĄĐēĐžŅĐžŅĐŊи ĐĐģĐąŅĐŧи",
|
||||
"recent_searches": "ĐĄĐēĐžŅĐžŅĐŊи ŅŅŅŅĐĩĐŊиŅ",
|
||||
"recently_added": "ĐаŅĐēĐžŅĐž дОйавĐĩĐŊĐž",
|
||||
"recently_added_page_title": "ĐаŅĐēĐžŅĐž дОйавĐĩĐŊĐž",
|
||||
@@ -2070,7 +2080,7 @@
|
||||
"shared_link_edit_expire_after_option_year": "{count} ĐŗĐžĐ´Đ¸ĐŊи",
|
||||
"shared_link_edit_password_hint": "ĐŅвĐĩди ĐŋаŅĐžĐģа Са Đ´ĐžŅŅŅĐŋ Đ´Đž ŅĐŋОдĐĩĐģĐĩĐŊ ŅĐĩŅŅŅŅ",
|
||||
"shared_link_edit_submit_button": "ĐĐąĐŊОви вŅŅСĐēаŅа",
|
||||
"shared_link_error_server_url_fetch": "ĐĐĩ ĐŧĐžĐļĐĩ да ŅĐĩ иСвĐģĐĩŅĐĩ URL адŅĐĩŅŅŅ ĐŊа ŅŅŅвŅŅа",
|
||||
"shared_link_error_server_url_fetch": "ĐĐĩ ĐŧĐžĐļĐĩ да ŅĐĩ иСвĐģĐĩŅĐĩ url-адŅĐĩŅŅŅ ĐŊа ŅŅŅвŅŅа",
|
||||
"shared_link_expires_day": "ĐСŅиŅа ŅĐģĐĩĐ´ {count} Đ´ĐĩĐŊ",
|
||||
"shared_link_expires_days": "ĐСŅиŅа ŅĐģĐĩĐ´ {count} Đ´ĐŊи",
|
||||
"shared_link_expires_hour": "ĐСŅиŅа ŅĐģĐĩĐ´ {count} ŅаŅ",
|
||||
@@ -2295,6 +2305,7 @@
|
||||
"upload_details": "ĐĐĩŅаКĐģи Са ĐēаŅваĐŊĐĩŅĐž",
|
||||
"upload_dialog_info": "ĐŅĐēаŅĐĩ Đģи да аŅŅ
ивиŅаŅĐĩ ĐŊа ŅŅŅвŅŅа иСйŅаĐŊиŅĐĩ ОйĐĩĐēŅи?",
|
||||
"upload_dialog_title": "ĐаŅи ОйĐĩĐēŅ",
|
||||
"upload_error_with_count": "ĐŅĐĩŅĐēа ĐŋŅи СаŅĐĩĐļдаĐŊĐĩ ĐŊа {count, plural, one {# ОйĐĩĐēŅ} other {# ОйĐĩĐēŅа}}",
|
||||
"upload_errors": "ĐаŅваĐŊĐĩŅĐž Đĩ СавŅŅĐĩĐŊĐž Ņ {count, plural, one {# ĐŗŅĐĩŅĐēа} other {# ĐŗŅĐĩŅĐēи}}, ОйĐŊОвĐĩŅĐĩ ŅŅŅаĐŊиŅаŅа Са да видиŅĐĩ ĐŊОвиŅĐĩ ĐĩĐģĐĩĐŧĐĩĐŊŅи.",
|
||||
"upload_finished": "ĐаŅваĐŊĐĩŅĐž СавŅŅŅи",
|
||||
"upload_progress": "ĐŅŅĐ°Đ˛Đ°Ņ {remaining, number} - ĐĐąŅайОŅĐĩĐŊи {processed, number}/{total, number}",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"readonly_mode_enabled": "Mod blo yu no save janjem i on",
|
||||
"reassigned_assets_to_new_person": "Janjem{count, plural, one {# asset} other {# assets}} blo nu man",
|
||||
"reassing_hint": "janjem ol sumtin yu bin joos i go blo wan man",
|
||||
"recent-albums": "album i no old tu mas",
|
||||
"recent_albums": "album i no old tu mas",
|
||||
"recent_searches": "lukabout wea i no old tu mas",
|
||||
"time_based_memories_duration": "hao mus second blo wan wan imij i stap lo scrin.",
|
||||
"timezone": "taemzon",
|
||||
|
||||
208
i18n/bn.json
208
i18n/bn.json
@@ -40,7 +40,9 @@
|
||||
"add_to_albums_count": "āĻ
ā§āϝāĻžāϞāĻŦāĻžāĻŽā§ āϝā§āĻ āĻāϰā§āύ ({count})",
|
||||
"add_to_bottom_bar": "āĻ āϝā§āĻ āĻāϰā§āύ",
|
||||
"add_to_shared_album": "āĻļā§āϝāĻŧāĻžāϰ āĻāϰāĻž āĻ
ā§āϝāĻžāϞāĻŦāĻžāĻŽā§ āϝā§āĻ āĻāϰā§āύ",
|
||||
"add_upload_to_stack": "āĻāĻĒāϞā§āĻĄ āϏā§āĻā§āϝāĻžāĻā§ āϝā§āĻ āĻāϰā§āύ",
|
||||
"add_url": "āϞāĻŋāĻā§āĻ āϝā§āĻ āĻāϰā§āύ",
|
||||
"add_workflow_step": "āĻāĻžāĻā§āϰ āϧāĻžāĻĒ āϝā§āĻ āĻāϰā§āύ",
|
||||
"added_to_archive": "āĻāϰā§āĻāĻžāĻāĻ āĻ āϝā§āĻ āĻāϰāĻž āĻšāϝāĻŧā§āĻā§",
|
||||
"added_to_favorites": "āĻĢā§āĻāĻžāϰāĻŋāĻā§ āϝā§āĻ āĻāϰāĻž āĻšāϝāĻŧā§āĻā§",
|
||||
"added_to_favorites_count": "āĻĒāĻāύā§āĻĻā§āϰ āϤāĻžāϞāĻŋāĻāĻžā§ {count, number} āϝā§āĻ āĻāϰāĻž āĻšā§ā§āĻā§",
|
||||
@@ -73,6 +75,7 @@
|
||||
"confirm_reprocess_all_faces": "āĻāĻĒāύāĻŋ āĻāĻŋ āύāĻŋāĻļā§āĻāĻŋāϤ āϝ⧠āĻāĻĒāύāĻŋ āϏāĻŽāϏā§āϤ āĻŽā§āĻ āĻĒā§āύāϰāĻžāϝāĻŧ āĻĒā§āϰāĻā§āϰāĻŋāϝāĻŧāĻž āĻāϰāϤ⧠āĻāĻžāύ? āĻāĻāĻŋ āύāĻžāĻŽāϝā§āĻā§āϤ āĻŦā§āϝāĻā§āϤāĻŋāĻĻā§āϰāĻ āĻŽā§āĻā§ āĻĢā§āϞāĻŦā§āĨ¤",
|
||||
"confirm_user_password_reset": "āĻāĻĒāύāĻŋ āĻāĻŋ āύāĻŋāĻļā§āĻāĻŋāϤ āϝ⧠āĻāĻĒāύāĻŋ {user} āĻāϰ āĻĒāĻžāϏāĻāϝāĻŧāĻžāϰā§āĻĄ āϰāĻŋāϏā§āĻ āĻāϰāϤ⧠āĻāĻžāύ?",
|
||||
"confirm_user_pin_code_reset": "āĻāĻĒāύāĻŋ āĻāĻŋ āύāĻŋāĻļā§āĻāĻŋāϤ āϝ⧠āĻāĻĒāύāĻŋ {user} āĻāϰ āĻĒāĻŋāύ āĻā§āĻĄ āϰāĻŋāϏā§āĻ āĻāϰāϤ⧠āĻāĻžāύ?",
|
||||
"copy_config_to_clipboard_description": "āĻŦāϰā§āϤāĻŽāĻžāύ āϏāĻŋāϏā§āĻā§āĻŽ āĻāύāĻĢāĻŋāĻāĻžāϰā§āĻļāύ āĻāĻāĻāĻŋ JSON āĻ
āĻŦāĻā§āĻā§āĻ āĻšāĻŋāϏā§āĻŦā§ āĻā§āϞāĻŋāĻĒāĻŦā§āϰā§āĻĄā§ āĻāĻĒāĻŋ āĻāϰā§āύ",
|
||||
"create_job": "job āϤā§āϰāĻŋ āĻāϰā§āύ",
|
||||
"cron_expression": "āĻā§āϰā§āύ āĻāĻā§āϏāĻĒā§āϰā§āĻļāύ",
|
||||
"cron_expression_description": "āĻā§āϰā§āύ āĻĢāϰā§āĻŽā§āϝāĻžāĻ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻāϰ⧠āϏā§āĻā§āϝāĻžāύāĻŋāĻ āĻŦā§āϝāĻŦāϧāĻžāύ āϏā§āĻ āĻāϰā§āύāĨ¤ āĻāϰāĻ āϤāĻĨā§āϝā§āϰ āĻāύā§āϝ āĻĻāϝāĻŧāĻž āĻāϰ⧠āĻĻā§āĻā§āύ āϝā§āĻŽāύ <link>Crontab Guru</link>",
|
||||
@@ -80,6 +83,8 @@
|
||||
"disable_login": "āϞāĻāĻāύ āĻ
āĻā§āώāĻŽ āĻāϰā§āύ",
|
||||
"duplicate_detection_job_description": "āĻ
āύā§āϰā§āĻĒ āĻāĻŦāĻŋ āϏāύāĻžāĻā§āϤ āĻāϰāϤ⧠āϏāĻŽā§āĻĒāĻĻāĻā§āϞāĻŋāϤ⧠āĻŽā§āĻļāĻŋāύ āϞāĻžāϰā§āύāĻŋāĻ āĻāĻžāϞāĻžāύāĨ¤ āϏā§āĻŽāĻžāϰā§āĻ āĻ
āύā§āϏāύā§āϧāĻžāύā§āϰ āĻāĻĒāϰ āύāĻŋāϰā§āĻāϰ āĻāϰā§",
|
||||
"exclusion_pattern_description": "āĻāĻā§āϏāĻā§āϞā§āĻļāύ āĻĒā§āϝāĻžāĻāĻžāϰā§āύ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻāϰ⧠āĻāĻĒāύāĻŋ āĻāĻĒāύāĻžāϰ āϞāĻžāĻāĻŦā§āϰā§āϰāĻŋ āϏā§āĻā§āϝāĻžāύ āĻāϰāĻžāϰ āϏāĻŽāϝāĻŧ āĻĢāĻžāĻāϞ āĻāĻŦāĻ āĻĢā§āϞā§āĻĄāĻžāϰāĻā§āϞāĻŋāĻā§ āĻāĻĒā§āĻā§āώāĻž āĻāϰāϤ⧠āĻĒāĻžāϰāĻŦā§āύāĨ¤ āϝāĻĻāĻŋ āĻāĻĒāύāĻžāϰ āĻāĻŽāύ āĻĢā§āϞā§āĻĄāĻžāϰ āĻĨāĻžāĻā§ āϝā§āĻāĻžāύ⧠āĻāĻŽāύ āĻĢāĻžāĻāϞ āĻĨāĻžāĻā§ āϝāĻž āĻāĻĒāύāĻŋ āĻāĻŽāĻĻāĻžāύāĻŋ āĻāϰāϤ⧠āĻāĻžāύ āύāĻž, āϝā§āĻŽāύ RAW āĻĢāĻžāĻāϞāĨ¤",
|
||||
"export_config_as_json_description": "āĻŦāϰā§āϤāĻŽāĻžāύ āϏāĻŋāϏā§āĻā§āĻŽ āĻāύāĻĢāĻŋāĻāĻžāϰā§āĻļāύ āĻāĻāĻāĻŋ JSON āĻĢāĻžāĻāϞ āĻšāĻŋāϏā§āĻŦā§ āĻĄāĻžāĻāύāϞā§āĻĄ āĻāϰā§āύ",
|
||||
"external_libraries_page_description": "āĻ
ā§āϝāĻžāĻĄāĻŽāĻŋāύ external āϞāĻžāĻāĻŦā§āϰā§āϰāĻŋ āĻĒā§āĻ",
|
||||
"face_detection": "āĻŽā§āĻ āϏāύāĻžāĻā§āϤāĻāϰāĻŖ",
|
||||
"face_detection_description": "āĻŽā§āĻļāĻŋāύ āϞāĻžāϰā§āύāĻŋāĻ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻāϰ⧠āĻ
ā§āϝāĻžāϏā§āĻā§ āĻĨāĻžāĻāĻž āĻŽā§āĻ/āĻā§āĻšāĻžāϰāĻž āĻā§āϞāĻŋ āϏāύāĻžāĻā§āϤ āĻāϰā§āύāĨ¤ āĻāĻŋāĻĄāĻŋāĻ āĻā§āϞāĻŋāϰ āĻāύā§āϝ, āĻļā§āϧā§āĻŽāĻžāϤā§āϰ āĻĨāĻžāĻŽā§āĻŦāύā§āĻāϞ āĻŦāĻŋāĻŦā§āĻāύāĻž āĻāϰāĻž āĻšāϝāĻŧāĨ¤ \"āϰāĻŋāĻĢā§āϰā§āĻļ\" (āĻĒā§āύāϰāĻžāϝāĻŧ) āϏāĻŽāϏā§āϤ āĻ
ā§āϝāĻžāϏā§āĻ āĻĒā§āϰāĻā§āϰāĻŋāϝāĻŧāĻž āĻāϰā§āĨ¤ \"āϰāĻŋāϏā§āĻ\" āĻāϰāĻžāϰ āĻŽāĻžāϧā§āϝāĻŽā§ āĻ
āϤāĻŋāϰāĻŋāĻā§āϤāĻāĻžāĻŦā§ āϏāĻŽāϏā§āϤ āĻŦāϰā§āϤāĻŽāĻžāύ āĻŽā§āĻā§āϰ āĻĄā§āĻāĻž āϏāĻžāĻĢ āĻāϰā§āĨ¤ \"āĻ
āύā§āĻĒāϏā§āĻĨāĻŋāϤ\" āĻ
ā§āϝāĻžāϏā§āĻāĻā§āϞāĻŋāĻā§ āϏāĻžāϰāĻŋāĻŦāĻĻā§āϧ āĻāϰ⧠āϝāĻž āĻāĻāύāĻ āĻĒā§āϰāĻā§āϰāĻŋāϝāĻŧāĻž āĻāϰāĻž āĻšāϝāĻŧāύāĻŋāĨ¤ āϏāύāĻžāĻā§āϤ āĻāϰāĻž āĻŽā§āĻāĻā§āϞāĻŋāĻā§ āĻĢā§āϏāĻŋāϝāĻŧāĻžāϞ āϰāĻŋāĻāĻāύāĻŋāĻļāύā§āϰ āĻāύā§āϝ āϏāĻžāϰāĻŋāĻŦāĻĻā§āϧ āĻāϰāĻž āĻšāĻŦā§, āĻĢā§āϏāĻŋāϝāĻŧāĻžāϞ āĻĄāĻŋāĻā§āĻāĻļāύ āϏāĻŽā§āĻĒā§āϰā§āĻŖ āĻšāĻāϝāĻŧāĻžāϰ āĻĒāϰā§, āĻŦāĻŋāĻĻā§āϝāĻŽāĻžāύ āĻŦāĻž āύāϤā§āύ āĻŦā§āϝāĻā§āϤāĻŋāĻĻā§āϰ āĻŽāϧā§āϝ⧠āĻā§āώā§āĻ ā§āĻŦāĻĻā§āϧ āĻāϰā§āĨ¤",
|
||||
"facial_recognition_job_description": "āĻļāύāĻžāĻā§āϤ āĻāϰāĻž āĻŽā§āĻāĻā§āϞāĻŋāĻā§ āĻŽāĻžāύā§āώā§āϰ āĻŽāϧā§āϝ⧠āĻā§āώā§āĻ ā§āĻā§āĻā§āϤ/āĻā§āϰā§āĻĒ āĻāϰā§āύāĨ¤ āĻŽā§āĻ āϏāύāĻžāĻā§āϤāĻāϰāĻŖ āϏāĻŽā§āĻĒā§āϰā§āĻŖ āĻšāĻāϝāĻŧāĻžāϰ āĻĒāϰ⧠āĻāĻ āϧāĻžāĻĒāĻāĻŋ āĻāϞā§āĨ¤ \"āϰāĻŋāϏā§āĻ\" (āĻĒā§āύāϰāĻžāϝāĻŧ) āϏāĻŽāϏā§āϤ āĻŽā§āĻāĻā§ āĻā§āϞāĻžāϏā§āĻāĻžāϰ āĻāϰā§āĨ¤ \"āĻ
āύā§āĻĒāϏā§āĻĨāĻŋāϤ/āĻŽāĻŋāϏāĻŋāĻ\" āĻŽā§āĻāĻā§āϞāĻŋāĻā§ āϏāĻžāϰāĻŋāϤ⧠āϰāĻžāĻā§ āϝā§āĻā§āϞ⧠āĻā§āύāĻ āĻŦā§āϝāĻā§āϤāĻŋāĻā§ āĻāϏāĻžāĻāύ/āĻŦāϰāĻžāĻĻā§āĻĻ āĻāϰāĻž āĻšāϝāĻŧāύāĻŋāĨ¤",
|
||||
@@ -99,6 +104,8 @@
|
||||
"image_preview_description": "āϏā§āĻā§āϰāĻŋāĻĒāĻĄ āĻŽā§āĻāĻžāĻĄā§āĻāĻž āϏāĻš āĻŽāĻžāĻāĻžāϰāĻŋ āĻāĻāĻžāϰā§āϰ āĻāĻŦāĻŋ, āĻāĻāĻāĻŋ āĻāĻāĻ āϏāĻŽā§āĻĒāĻĻ āĻĻā§āĻāĻžāϰ āϏāĻŽāϝāĻŧ āĻāĻŦāĻ āĻŽā§āĻļāĻŋāύ āϞāĻžāϰā§āύāĻŋāĻāϝāĻŧā§āϰ āĻāύā§āϝ āĻŦā§āϝāĻŦāĻšā§āϤ āĻšāϝāĻŧ",
|
||||
"image_preview_quality_description": "ā§§-ā§§ā§Ļā§Ļ āĻāϰ āĻŽāϧā§āϝ⧠āĻĒā§āϰāĻŋāĻāĻŋāĻ āĻā§āϝāĻŧāĻžāϞāĻŋāĻāĻŋāĨ¤ āĻŦā§āĻļāĻŋ āĻšāϞ⧠āĻāĻžāϞā§, āĻāĻŋāύā§āϤ⧠āĻŦāĻĄāĻŧ āĻĢāĻžāĻāϞ āϤā§āϰāĻŋ āĻšāϝāĻŧ āĻāĻŦāĻ āĻ
ā§āϝāĻžāĻĒā§āϰ āĻĒā§āϰāϤāĻŋāĻā§āϰāĻŋāϝāĻŧāĻžāĻļā§āϞāϤāĻž āĻāĻŽāĻžāϤ⧠āĻĒāĻžāϰā§āĨ¤ āĻāĻŽ āĻŽāĻžāύ āϏā§āĻ āĻāϰāϞ⧠āĻŽā§āĻļāĻŋāύ āϞāĻžāϰā§āύāĻŋāĻ āĻā§āϝāĻŧāĻžāϞāĻŋāĻāĻŋāϰ āĻāĻĒāϰ āĻĒā§āϰāĻāĻžāĻŦ āĻĒāĻĄāĻŧāϤ⧠āĻĒāĻžāϰā§āĨ¤",
|
||||
"image_preview_title": "āĻĒā§āϰāĻŋāĻāĻŋāĻ āϏā§āĻāĻŋāĻāϏ",
|
||||
"image_progressive": "āĻĒā§āϰāĻā§āϰā§āϏāĻŋāĻ",
|
||||
"image_progressive_description": "āϧā§āϰ⧠āϧā§āϰ⧠āϞā§āĻĄ āĻšāĻā§āĻžāϰ āϏā§āĻŦāĻŋāϧāĻžāϰā§āĻĨā§ JPEG āĻāĻŦāĻŋāĻā§āϞ⧠āĻĒā§āϰāĻā§āϰā§āϏāĻŋāĻāĻāĻžāĻŦā§ āĻāύāĻā§āĻĄ āĻāϰā§āύāĨ¤ WebP āĻāĻŦāĻŋāϰ āĻā§āώā§āϤā§āϰ⧠āĻāĻāĻŋ āĻā§āύ⧠āĻĒā§āϰāĻāĻžāĻŦ āĻĢā§āϞāĻŦā§ āύāĻž",
|
||||
"image_quality": "āĻā§āĻŖāĻŽāĻžāύ",
|
||||
"image_resolution": "āϰā§āĻā§āϞāĻŋāĻāĻļāύ",
|
||||
"image_resolution_description": "āĻāĻā§āĻ āϰā§āĻā§āϞāĻŋāĻāĻļāύā§āϰ āĻā§āώā§āϤā§āϰ⧠āĻāϰāĻ āĻŦāĻŋāϏā§āϤāĻžāϰāĻŋāϤ āϤāĻĨā§āϝ āϏāĻāϰāĻā§āώāĻŖ āĻāϰāĻž āϏāĻŽā§āĻāĻŦ āĻāĻŋāύā§āϤ⧠āĻāύāĻā§āĻĄ āĻāϰāϤ⧠āĻŦā§āĻļāĻŋ āϏāĻŽāϝāĻŧ āϞāĻžāĻā§, āĻĢāĻžāĻāϞā§āϰ āĻāĻāĻžāϰ āĻŦāĻĄāĻŧ āĻšāϝāĻŧ āĻāĻŦāĻ āĻ
ā§āϝāĻžāĻĒā§āϰ āĻĒā§āϰāϤāĻŋāĻā§āϰāĻŋāϝāĻŧāĻžāĻļā§āϞāϤāĻž āĻāĻŽāĻžāϤ⧠āĻĒāĻžāϰā§āĨ¤",
|
||||
@@ -107,6 +114,7 @@
|
||||
"image_thumbnail_description": "āĻŽā§āĻāĻžāĻĄā§āĻāĻž āĻŦāĻžāĻĻ āĻĻā§āĻā§āĻž āĻā§āĻ āĻĨāĻžāĻŽā§āĻŦāύā§āĻāϞ, āĻŽā§āϞ āĻāĻžāĻāĻŽāϞāĻžāĻāύā§āϰ āĻŽāϤ⧠āĻāĻŦāĻŋāϰ āĻā§āϰā§āĻĒ āĻĻā§āĻāĻžāϰ āϏāĻŽāϝāĻŧ āĻŦā§āϝāĻŦāĻšā§āϤ āĻšā§",
|
||||
"image_thumbnail_quality_description": "āĻĨāĻžāĻŽā§āĻŦāύā§āĻāϞā§āϰ āĻŽāĻžāύ ā§§-ā§§ā§Ļā§ĻāĨ¤ āĻŦā§āĻļāĻŋ āĻšāϞ⧠āĻāĻžāϞā§, āĻāĻŋāύā§āϤ⧠āĻŦāĻĄāĻŧ āĻĢāĻžāĻāϞ āϤā§āϰāĻŋ āĻšāϝāĻŧ āĻāĻŦāĻ āĻ
ā§āϝāĻžāĻĒā§āϰ āĻĒā§āϰāϤāĻŋāĻā§āϰāĻŋāϝāĻŧāĻžāĻļā§āϞāϤāĻž āĻāĻŽāĻžāϤ⧠āĻĒāĻžāϰā§āĨ¤",
|
||||
"image_thumbnail_title": "āĻĨāĻžāĻŽā§āĻŦāύā§āϞ āϏā§āĻāĻŋāĻāϏ",
|
||||
"import_config_from_json_description": "āĻāĻāĻāĻŋ JSON āĻāύāĻĢāĻŋāĻ āĻĢāĻžāĻāϞ āĻāĻĒāϞā§āĻĄ āĻāϰ⧠āϏāĻŋāϏā§āĻā§āĻŽ āĻāύāĻĢāĻŋāĻāĻžāϰā§āĻļāύ āĻāĻŽāĻĒā§āϰā§āĻ āĻāϰā§āύāĨ¤",
|
||||
"job_concurrency": "{job} āĻāύāĻāĻžāϰā§āύā§āϏāĻŋ",
|
||||
"job_created": "Job āϤā§āϰāĻŋ āĻšāϝāĻŧā§āĻā§",
|
||||
"job_not_concurrency_safe": "āĻāĻ āĻāĻžāĻāĻāĻŋ āϏāĻŽāĻžāύā§āϤāϰāĻžāϞāĻāĻžāĻŦā§ āĻāĻžāϞāĻžāύ⧠āύāĻŋāϰāĻžāĻĒāĻĻ āύā§",
|
||||
@@ -114,14 +122,20 @@
|
||||
"job_settings_description": "āĻāĻžāĻā§āϰ āϏāĻŽāĻžāύā§āϤāϰāĻžāϞāϤāĻž āĻĒāϰāĻŋāĻāĻžāϞāύāĻž āĻāϰā§āύ",
|
||||
"jobs_delayed": "{jobCount, plural, other {# āĻŦāĻŋāϞāĻŽā§āĻŦāĻŋāϤ}}",
|
||||
"jobs_failed": "{jobCount, plural, other {# āĻŦā§āϝāϰā§āĻĨ}}",
|
||||
"jobs_over_time": "āϏāĻŽā§ āĻ
āύā§āϝāĻžā§ā§ āĻāĻžāĻāϏāĻŽā§āĻš",
|
||||
"library_created": "āϞāĻžāĻāĻŦā§āϰā§āϰāĻŋ āϤā§āϰāĻŋ āĻāϰāĻž āĻšāϝāĻŧā§āĻā§āĻ {library}",
|
||||
"library_deleted": "āϞāĻžāĻāĻŦā§āϰā§āϰāĻŋ āĻŽā§āĻā§ āĻĢā§āϞāĻž āĻšāϝāĻŧā§āĻā§",
|
||||
"library_details": "āϞāĻžāĻāĻŦā§āϰā§āϰāĻŋāϰ āĻŦāĻŋāĻŦāϰāĻŖ",
|
||||
"library_folder_description": "āĻāĻŽāĻĒā§āϰā§āĻ āĻāϰāĻžāϰ āĻāύā§āϝ āĻāĻāĻāĻŋ āĻĢā§āϞā§āĻĄāĻžāϰ āύāĻŋāϰā§āĻĻāĻŋāώā§āĻ āĻāϰā§āύāĨ¤ āĻāĻ āĻĢā§āϞā§āĻĄāĻžāϰ āĻāĻŦāĻ āĻāϰ āĻā§āϤāϰā§āϰ āϏāĻŽāϏā§āϤ āĻĢā§āϞā§āĻĄāĻžāϰ āĻāĻŦāĻŋ āĻ āĻāĻŋāĻĄāĻŋāĻāϰ āĻāύā§āϝ āϏā§āĻā§āϝāĻžāύ āĻāϰāĻž āĻšāĻŦā§āĨ¤",
|
||||
"library_remove_exclusion_pattern_prompt": "āĻāĻĒāύāĻŋ āĻāĻŋ āύāĻŋāĻļā§āĻāĻŋāϤ āϝ⧠āĻāĻĒāύāĻŋ āĻāĻ āĻāĻā§āϏāĻā§āϞā§āĻļāύ āĻĒā§āϝāĻžāĻāĻžāϰā§āύāĻāĻŋ āĻŽā§āĻā§ āĻĢā§āϞāϤ⧠āĻāĻžāύ?",
|
||||
"library_remove_folder_prompt": "āĻāĻĒāύāĻŋ āĻāĻŋ āύāĻŋāĻļā§āĻāĻŋāϤ āϝ⧠āĻāĻĒāύāĻŋ āĻāĻ āĻāĻŽāĻĒā§āϰā§āĻ āĻĢā§āϞā§āĻĄāĻžāϰāĻāĻŋ āĻŽā§āĻā§ āĻĢā§āϞāϤ⧠āĻāĻžāύ?",
|
||||
"library_scanning": "āĻĒāϰā§āϝāĻžāϝāĻŧāĻā§āϰāĻŽāĻŋāĻ āϏā§āĻā§āϝāĻžāύāĻŋāĻ",
|
||||
"library_scanning_description": "āĻĒāϰā§āϝāĻžāϝāĻŧāĻā§āϰāĻŽāĻŋāĻ āϞāĻžāĻāĻŦā§āϰā§āϰāĻŋ āϏā§āĻā§āϝāĻžāύāĻŋāĻ āĻāύāĻĢāĻŋāĻāĻžāϰ āĻāϰā§āύ",
|
||||
"library_scanning_enable_description": "āĻĒāϰā§āϝāĻžāϝāĻŧāĻā§āϰāĻŽāĻŋāĻ āϞāĻžāĻāĻŦā§āϰā§āϰāĻŋ āϏā§āĻā§āϝāĻžāύāĻŋāĻ āϏāĻā§āώāĻŽ āĻāϰā§āύ",
|
||||
"library_settings": "āĻŦāĻšāĻŋāϰāĻžāĻāϤ āϞāĻžāĻāĻŦā§āϰā§āϰāĻŋ",
|
||||
"library_settings_description": "āĻŦāĻšāĻŋāϰāĻžāĻāϤ āϞāĻžāĻāĻŦā§āϰā§āϰāĻŋ āϏā§āĻāĻŋāĻāϏ āĻĒāϰāĻŋāĻāĻžāϞāύāĻž āĻāϰā§āύ",
|
||||
"library_tasks_description": "āύāϤā§āύ āĻāĻŦāĻ/āĻ
āĻĨāĻŦāĻž āĻĒāϰāĻŋāĻŦāϰā§āϤāĻŋāϤ āϏāĻŽā§āĻĒāĻĻā§āϰ āĻāύā§āϝ āĻŦāĻšāĻŋāϰāĻžāĻāϤ āϞāĻžāĻāĻŦā§āϰā§āϰāĻŋ āϏā§āĻā§āϝāĻžāύ āĻāϰā§āύ",
|
||||
"library_updated": "āĻāĻĒāĻĄā§āĻāĻā§āϤ āϞāĻžāĻāĻŦā§āϰā§āϰāĻŋāĨ¤",
|
||||
"library_watching_enable_description": "āĻĢāĻžāĻāϞ āĻĒāϰāĻŋāĻŦāϰā§āϤāύā§āϰ āĻāύā§āϝ āĻŦāĻšāĻŋāϰāĻžāĻāϤ āϞāĻžāĻāĻŦā§āϰā§āϰāĻŋāĻā§āϞāĻŋ āĻĻā§āĻā§āύ",
|
||||
"library_watching_settings": "āϞāĻžāĻāĻŦā§āϰā§āϰāĻŋ āĻĻā§āĻāĻž (āĻĒāϰā§āĻā§āώāĻžāĻŽā§āϞāĻ)",
|
||||
"library_watching_settings_description": "āĻĒāϰāĻŋāĻŦāϰā§āϤāĻŋāϤ āĻĢāĻžāĻāϞāĻā§āϞāĻŋāϰ āĻāύā§āϝ āϏā§āĻŦāϝāĻŧāĻāĻā§āϰāĻŋāϝāĻŧāĻāĻžāĻŦā§ āύāĻāϰ āϰāĻžāĻā§āύ",
|
||||
@@ -133,9 +147,199 @@
|
||||
"machine_learning_availability_checks_enabled": "āĻĒā§āϰāĻžāĻĒā§āϝāϤāĻž āĻĒāϰā§āĻā§āώāĻž āϏāĻā§āώāĻŽ āĻāϰā§āύ",
|
||||
"machine_learning_availability_checks_interval": "āĻā§āĻ āĻŦā§āϝāĻŦāϧāĻžāύ",
|
||||
"machine_learning_availability_checks_interval_description": "āĻĒā§āϰāĻžāĻĒā§āϝāϤāĻž āĻĒāϰā§āĻā§āώāĻžāĻā§āϞāĻŋāϰ āĻŽāϧā§āϝ⧠āĻŦā§āϝāĻŦāϧāĻžāύ āĻŽāĻŋāϞāĻŋāϏā§āĻā§āύā§āĻĄā§",
|
||||
"machine_learning_availability_checks_timeout": "āĻ
āύā§āϰā§āϧā§āϰ āϏāĻŽā§āϏā§āĻŽāĻž āĻļā§āώ",
|
||||
"machine_learning_availability_checks_timeout_description": "āĻĒā§āϰāĻžāĻĒā§āϝāϤāĻžāϰ āĻĒāϰā§āĻā§āώāĻžāϰ āĻāύā§āϝ āĻŽāĻŋāϞāĻŋāϏā§āĻā§āύā§āĻĄā§ āϏāĻŽā§āϏā§āĻŽāĻžāĨ¤",
|
||||
"machine_learning_clip_model": "CLIP āĻŽāĻĄā§āϞ",
|
||||
"machine_learning_clip_model_description": "<link>āĻāĻāĻžāύā§</link> āϤāĻžāϞāĻŋāĻāĻžāĻā§āĻā§āϤ āĻāĻāĻāĻŋ CLIP āĻŽāĻĄā§āϞā§āϰ āύāĻžāĻŽāĨ¤ āĻŽāύ⧠āϰāĻžāĻāĻŦā§āύ, āĻŽāĻĄā§āϞ āĻĒāϰāĻŋāĻŦāϰā§āϤāύā§āϰ āĻĒāϰ āϏāĻŦ āĻāĻŦāĻŋāϰ āĻāύā§āϝ āĻ
āĻŦāĻļā§āϝāĻ âSmart Searchâ āĻāĻžāĻāĻāĻŋ āĻāĻŦāĻžāϰ āĻāĻžāϞāĻžāϤ⧠āĻšāĻŦā§āĨ¤",
|
||||
"machine_learning_duplicate_detection": "āĻĒā§āύāϰāĻžāĻŦā§āϤā§āϤāĻŋ āϏāύāĻžāĻā§āϤāĻāϰāĻŖ",
|
||||
"machine_learning_duplicate_detection_enabled": "āĻĒā§āύāϰāĻžāĻŦā§āϤā§āϤāĻŋ āĻļāύāĻžāĻā§āϤāĻāϰāĻŖ āĻāĻžāϞ⧠āĻāϰā§āύ"
|
||||
}
|
||||
"machine_learning_duplicate_detection_enabled": "āĻĒā§āύāϰāĻžāĻŦā§āϤā§āϤāĻŋ āĻļāύāĻžāĻā§āϤāĻāϰāĻŖ āĻāĻžāϞ⧠āĻāϰā§āύ",
|
||||
"machine_learning_duplicate_detection_enabled_description": "āύāĻŋāώā§āĻā§āϰāĻŋā§ āĻĨāĻžāĻāϞā§āĻ āĻšā§āĻŦāĻšā§ āĻāĻāĻ āϏāĻŽā§āĻĒāĻĻāĻā§āϞā§āϰ āĻĄā§āĻĒā§āϞāĻŋāĻā§āĻ āϏāϰāĻŋā§ā§ āĻĢā§āϞāĻž āĻšāĻŦā§āĨ¤",
|
||||
"machine_learning_duplicate_detection_setting_description": "āϏāĻŽā§āĻāĻžāĻŦā§āϝ āĻĄā§āĻĒā§āϞāĻŋāĻā§āĻ āĻā§āĻāĻā§ āĻŦā§āϰ āĻāϰāϤ⧠CLIP āĻāĻŽā§āĻŦā§āĻĄāĻŋāĻ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻāϰā§āύāĨ¤",
|
||||
"machine_learning_enabled": "Machine Learning āϏāĻā§āώāĻŽ āĻāϰā§āύ",
|
||||
"machine_learning_enabled_description": "āύāĻŋāώā§āĻā§āϰāĻŋāϝāĻŧ āĻĨāĻžāĻāϞ⧠āύāĻŋāĻā§āϰ āϏā§āĻāĻŋāĻāϏ āύāĻŋāϰā§āĻŦāĻŋāĻļā§āώ⧠āϏāĻŽāϏā§āϤ ML āĻŦā§āĻļāĻŋāώā§āĻā§āϝ āύāĻŋāώā§āĻā§āϰāĻŋāϝāĻŧ āĻāϰāĻž āĻšāĻŦā§āĨ¤",
|
||||
"machine_learning_facial_recognition": "āĻĢā§āϏāĻŋāϝāĻŧāĻžāϞ āϰāĻŋāĻāĻāύāĻŋāĻļāύ",
|
||||
"machine_learning_facial_recognition_description": "āĻāĻŦāĻŋāϤ⧠āĻŽā§āĻ āϏāύāĻžāĻā§āϤ āĻāϰā§āύ, āĻāĻŋāύā§āύ āĻāĻŦāĻ āĻā§āϰā§āĻĒ āĻāϰā§āύāĨ¤",
|
||||
"machine_learning_facial_recognition_model": "āĻĢā§āϏāĻŋāϝāĻŧāĻžāϞ āϰāĻŋāĻāĻāύāĻŋāĻļāύ āĻŽāĻĄā§āϞ",
|
||||
"machine_learning_facial_recognition_model_description": "āĻŽāĻĄā§āϞāĻā§āϞāĻŋ āĻāĻāĻžāϰā§āϰ āĻ
āϧāĻāĻā§āϰāĻŽ āĻ
āύā§āϝāĻžāϝāĻŧā§ āϤāĻžāϞāĻŋāĻāĻžāĻā§āĻā§āϤ āĻāϰāĻž āĻšāϝāĻŧā§āĻā§āĨ¤ āĻŦā§ āĻŽāĻĄā§āϞāĻā§āϞāĻŋ āϧā§āϰāĻāϤāĻŋāϰ āĻāĻŦāĻ āĻŦā§āĻļāĻŋ āĻŽā§āĻŽāϰāĻŋ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻāϰā§, āϤāĻŦā§ āĻāύā§āύāϤ āĻĢāϞāĻžāĻĢāϞ āĻĒā§āϰāĻĻāĻžāύ āĻāϰā§āĨ¤ āĻŽāύ⧠āϰāĻžāĻāĻŦā§āύ āϝ⧠āĻāĻāĻāĻŋ āĻŽāĻĄā§āϞ āĻĒāϰāĻŋāĻŦāϰā§āϤāύ āĻāϰāĻžāϰ āĻĒāϰ āĻāĻĒāύāĻžāĻā§ āϏāĻŽāϏā§āϤ āĻāĻŦāĻŋāϰ āĻāύā§āϝ āĻĢā§āϏ āĻĄāĻŋāĻā§āĻāĻļāύ (Face Detection) āĻāĻžāĻāĻāĻŋ āĻĒā§āύāϰāĻžāϝāĻŧ āĻāĻžāϞāĻžāϤ⧠āĻšāĻŦā§āĨ¤",
|
||||
"machine_learning_facial_recognition_setting": "āĻĢā§āϏāĻŋāϝāĻŧāĻžāϞ āϰāĻŋāĻāĻāύāĻŋāĻļāύ āϏāĻā§āώāĻŽ āĻāϰā§āύ",
|
||||
"machine_learning_facial_recognition_setting_description": "āύāĻŋāώā§āĻā§āϰāĻŋā§ āĻĨāĻžāĻāϞā§, āĻĢā§āϏāĻŋāϝāĻŧāĻžāϞ āϰāĻŋāĻāĻāύāĻŋāĻļāύā§āϰ āĻāύā§āϝ āĻāĻŦāĻŋāĻā§āϞ⧠āĻāύāĻā§āĻĄ āĻāϰāĻž āĻšāĻŦā§ āύāĻž āĻāĻŦāĻ āĻāĻā§āϏāĻĒā§āϞā§āϰ āĻĒā§āĻā§āϰ āĻĒāĻŋāĻĒāϞ (People) āϏā§āĻāĻļāύāĻāĻŋ āĻĒā§āϰā§āĻŖ āĻšāĻŦā§ āύāĻžāĨ¤",
|
||||
"machine_learning_max_detection_distance": "āϏāϰā§āĻŦā§āĻā§āĻ āĻļāύāĻžāĻā§āϤāĻāϰāĻŖ āĻĻā§āϰāϤā§āĻŦ",
|
||||
"machine_learning_max_detection_distance_description": "āĻĻā§āĻāĻŋ āĻāĻŦāĻŋāĻā§ āĻĄā§āĻĒā§āϞāĻŋāĻā§āĻ āĻšāĻŋāϏā§āĻŦā§ āĻāĻŖā§āϝ āĻāϰāĻžāϰ āĻāύā§āϝ āϤāĻžāĻĻā§āϰ āĻŽāϧā§āϝāĻāĻžāϰ āϏāϰā§āĻŦā§āĻā§āĻ āĻĻā§āϰāϤā§āĻŦ, āϝāĻžāϰ āĻĒāϰāĻŋāϏā§āĻŽāĻž ā§Ļ.ā§Ļā§Ļā§§-ā§Ļ.ā§§āĨ¤ āĻŽāĻžāύ āϝāϤ āĻŦā§āĻļāĻŋ āĻšāĻŦā§ āϤāϤ āĻŦā§āĻļāĻŋ āĻĄā§āĻĒā§āϞāĻŋāĻā§āĻ āĻļāύāĻžāĻā§āϤ āĻšāĻŦā§, āϤāĻŦā§ āĻāϤ⧠āĻā§āϞ āĻļāύāĻžāĻā§āϤāĻāϰāĻŖā§āϰ (false positives) āϏāĻŽā§āĻāĻžāĻŦāύāĻž āĻĨāĻžāĻāϤ⧠āĻĒāĻžāϰā§āĨ¤",
|
||||
"machine_learning_max_recognition_distance": "āϏāϰā§āĻŦā§āĻā§āĻ āĻāĻŋāĻšā§āύāĻŋāϤāĻāϰāĻŖ āĻĻā§āϰāϤā§āĻŦ",
|
||||
"machine_learning_max_recognition_distance_description": "āĻĻā§āĻāĻŋ āĻŽā§āĻāĻā§ āĻāĻāĻ āĻŦā§āϝāĻā§āϤāĻŋ āĻšāĻŋāϏā§āĻŦā§ āĻāĻŖā§āϝ āĻāϰāĻžāϰ āĻāύā§āϝ āϤāĻžāĻĻā§āϰ āĻŽāϧā§āϝāĻāĻžāϰ āϏāϰā§āĻŦā§āĻā§āĻ āĻĻā§āϰāϤā§āĻŦ, āϝāĻžāϰ āĻĒāϰāĻŋāϏā§āĻŽāĻž ā§Ļ-⧍āĨ¤ āĻāĻ āĻŽāĻžāύ āĻāĻŽāĻžāϞ⧠āĻĻā§âāĻāύ āĻāĻŋāύā§āύ āĻŦā§āϝāĻā§āϤāĻŋāĻā§ āĻāĻāĻ āĻŦā§āϝāĻā§āϤāĻŋ āĻšāĻŋāϏā§āĻŦā§ āĻāĻŋāĻšā§āύāĻŋāϤ āĻāϰāĻžāϰ āϏāĻŽā§āĻāĻžāĻŦāύāĻž āĻāĻŽā§, āĻāϰ āĻŽāĻžāύ āĻŦāĻžā§āĻžāϞ⧠āĻāĻāĻ āĻŦā§āϝāĻā§āϤāĻŋāĻā§ āĻĻā§âāĻāύ āĻāĻŋāύā§āύ āĻŦā§āϝāĻā§āϤāĻŋ āĻšāĻŋāϏā§āĻŦā§ āĻāĻŋāĻšā§āύāĻŋāϤ āĻāϰāĻžāϰ āϏāĻŽā§āĻāĻžāĻŦāύāĻž āĻāĻŽā§āĨ¤ āĻŽāύ⧠āϰāĻžāĻāĻŦā§āύ āϝā§, āĻĻā§âāĻāύ āĻŦā§āϝāĻā§āϤāĻŋāĻā§ āĻāĻāϤā§āϰāĻŋāϤ āĻāϰāĻž (merge) āĻ
āĻĒā§āĻā§āώāĻžāĻā§āϤ āϏāĻšāĻ āĻāĻŋāύā§āϤ⧠āĻāĻāĻāύāĻā§ āĻĻā§âāĻāĻžāĻā§ āĻāĻžāĻ āĻāϰāĻž āĻāĻ āĻŋāύ, āϤāĻžāĻ āϏāĻŽā§āĻāĻŦ āĻšāϞ⧠āĻĨā§āϰā§āĻļāĻšā§āϞā§āĻĄ (threshold) āĻāĻŽ āϰāĻžāĻāĻžāĻ āĻāĻžāϞā§āĨ¤",
|
||||
"machine_learning_min_detection_score": "āϏāϰā§āĻŦāύāĻŋāĻŽā§āύ āĻļāύāĻžāĻā§āϤāĻāϰāĻŖ āϏā§āĻā§āϰ",
|
||||
"machine_learning_min_detection_score_description": "āĻāĻŦāĻŋāϤ⧠āĻŽā§āĻ āĻļāύāĻžāĻā§āϤ āĻāϰāĻžāϰ āĻāύā§āϝ ā§Ļ-ā§§ āĻāϰ āĻŽāϧā§āϝ⧠āϏāϰā§āĻŦāύāĻŋāĻŽā§āύ āĻāύāĻĢāĻŋāĻĄā§āύā§āϏ āϏā§āĻā§āϰāĨ¤ āĻŽāĻžāύ āϝāϤ āĻāĻŽ āĻšāĻŦā§ āϤāϤ āĻŦā§āĻļāĻŋ āĻŽā§āĻ āĻļāύāĻžāĻā§āϤ āĻšāĻŦā§, āϤāĻŦā§ āĻāϤ⧠āĻā§āϞ āĻļāύāĻžāĻā§āϤāĻāϰāĻŖā§āϰ (false positives) āϏāĻŽā§āĻāĻžāĻŦāύāĻž āĻĨāĻžāĻāϤ⧠āĻĒāĻžāϰā§āĨ¤",
|
||||
"machine_learning_min_recognized_faces": "āϏāϰā§āĻŦāύāĻŋāĻŽā§āύ āϏā§āĻŦā§āĻā§āϤ āĻŽā§āĻā§āϰ āϏāĻāĻā§āϝāĻž",
|
||||
"machine_learning_min_recognized_faces_description": "āĻāĻāĻāύ āĻŦā§āϝāĻā§āϤāĻŋ āĻšāĻŋāϏā§āĻŦā§ āϤā§āϰāĻŋ āĻšāĻāϝāĻŧāĻžāϰ āĻāύā§āϝ āϏā§āĻŦā§āĻā§āϤ āĻŽā§āĻā§āϰ āϏāϰā§āĻŦāύāĻŋāĻŽā§āύ āϏāĻāĻā§āϝāĻžāĨ¤ āĻāĻāĻŋ āĻŦāĻžāĻĄāĻŧāĻžāϞ⧠āĻĢā§āϏāĻŋāϝāĻŧāĻžāϞ āϰāĻŋāĻāĻāύāĻŋāĻļāύ āĻāϰāĻ āύāĻŋāĻā§āĻāϤ āĻšāϝāĻŧ, āϤāĻŦā§ āĻāϤ⧠āĻā§āύ⧠āĻŽā§āĻ āĻā§āύ⧠āĻŦā§āϝāĻā§āϤāĻŋāϰ āϏāĻžāĻĨā§ āϏāĻāϝā§āĻā§āϤ āύāĻž āĻšāĻāϝāĻŧāĻžāϰ āϏāĻŽā§āĻāĻžāĻŦāύāĻžāĻ āĻŦā§āĻĻā§āϧāĻŋ āĻĒāĻžāϝāĻŧāĨ¤",
|
||||
"machine_learning_ocr": "OCR",
|
||||
"machine_learning_ocr_description": "āĻāĻŦāĻŋāϤ⧠āĻā§āĻā§āϏāĻ (Text) āĻļāύāĻžāĻā§āϤ āĻāϰāϤ⧠āĻŽā§āĻļāĻŋāύ āϞāĻžāϰā§āύāĻŋāĻ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻāϰā§āύāĨ¤",
|
||||
"machine_learning_ocr_enabled": "OCR āϏāĻā§āώāĻŽ āĻāϰā§āύ",
|
||||
"machine_learning_ocr_enabled_description": "āύāĻŋāώā§āĻā§āϰāĻŋāϝāĻŧ āĻĨāĻžāĻāϞā§, āĻāĻŦāĻŋāĻā§āϞā§āϤ⧠āĻā§āĻā§āϏāĻ āĻļāύāĻžāĻā§āϤāĻāϰāĻŖ āĻāϰāĻž āĻšāĻŦā§ āύāĻžāĨ¤",
|
||||
"machine_learning_ocr_max_resolution": "āϏāϰā§āĻŦā§āĻā§āĻ āϰā§āĻā§āϞāĻŋāĻāĻļāύ(Resolution)",
|
||||
"machine_learning_ocr_max_resolution_description": "āĻāĻ āϰā§āĻā§āϞāĻŋāĻāĻļāύā§āϰ āĻāĻĒāϰā§āϰ āĻĒā§āϰāĻŋāĻāĻŋāĻāĻā§āϞā§āϰ āĻ
ā§āϝāĻžāϏāĻĒā§āĻā§āĻ āϰā§āĻļāĻŋāĻ (āĻāĻāĻžāϰ āĻ āĻ
āύā§āĻĒāĻžāϤ) āĻ āĻŋāĻ āϰā§āĻā§ āϰāĻŋāϏāĻžāĻāĻ āĻāϰāĻž āĻšāĻŦā§āĨ¤ āĻŽāĻžāύ āϝāϤ āĻŦā§āĻļāĻŋ āĻšāĻŦā§ āĻĢāϞāĻžāĻĢāϞ āϤāϤ āĻŦā§āĻļāĻŋ āύāĻŋāĻā§āĻāϤ āĻšāĻŦā§, āϤāĻŦā§ āĻāĻāĻŋ āĻĒā§āϰāϏā§āϏ āĻāϰāϤ⧠āϏāĻŽā§ āĻŦā§āĻļāĻŋ āϞāĻžāĻāĻŦā§ āĻāĻŦāĻ āĻŽā§āĻŽāϰāĻŋ āĻŦā§āĻļāĻŋ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻāϰāĻŦā§āĨ¤",
|
||||
"machine_learning_ocr_min_detection_score": "āϏāϰā§āĻŦāύāĻŋāĻŽā§āύ āĻļāύāĻžāĻā§āϤāĻāϰāĻŖ āϏā§āĻā§āϰ",
|
||||
"machine_learning_ocr_min_detection_score_description": "āĻā§āĻā§āϏāĻ āĻļāύāĻžāĻā§āϤ āĻāϰāĻžāϰ āĻāύā§āϝ ā§Ļ-ā§§ āĻāϰ āĻŽāϧā§āϝ⧠āύā§āϝā§āύāϤāĻŽ āĻāύāĻĢāĻŋāĻĄā§āύā§āϏ āϏā§āĻā§āϰāĨ¤ āĻŽāĻžāύ āϝāϤ āĻāĻŽ āĻšāĻŦā§ āϤāϤ āĻŦā§āĻļāĻŋ āĻā§āĻā§āϏāĻ āĻļāύāĻžāĻā§āϤ āĻšāĻŦā§, āϤāĻŦā§ āĻāϤ⧠āĻā§āϞ āĻļāύāĻžāĻā§āϤāĻāϰāĻŖā§āϰ (false positives) āϏāĻŽā§āĻāĻžāĻŦāύāĻž āĻĨāĻžāĻāϤ⧠āĻĒāĻžāϰā§āĨ¤",
|
||||
"machine_learning_ocr_min_recognition_score": "āϏāϰā§āĻŦāύāĻŋāĻŽā§āύ āĻāĻŋāĻšā§āύāĻŋāϤāĻāϰāĻŖ (Recognition)āϏā§āĻā§āϰ",
|
||||
"machine_learning_ocr_min_score_recognition_description": "āĻļāύāĻžāĻā§āϤāĻā§āϤ āĻā§āĻā§āϏāĻ āĻāĻŋāĻšā§āύāĻŋāϤ āĻāϰāĻžāϰ āĻāύā§āϝ ā§Ļ-ā§§ āĻāϰ āĻŽāϧā§āϝ⧠āύā§āϝā§āύāϤāĻŽ āĻāύāĻĢāĻŋāĻĄā§āύā§āϏ āϏā§āĻā§āϰāĨ¤ āĻŽāĻžāύ āϝāϤ āĻāĻŽ āĻšāĻŦā§ āϤāϤ āĻŦā§āĻļāĻŋ āĻā§āĻā§āϏāĻ āĻāĻŋāĻšā§āύāĻŋāϤ āĻšāĻŦā§, āϤāĻŦā§ āĻāϤ⧠āĻā§āϞ āĻļāύāĻžāĻā§āϤāĻāϰāĻŖā§āϰ (false positives) āϏāĻŽā§āĻāĻžāĻŦāύāĻž āĻĨāĻžāĻāϤ⧠āĻĒāĻžāϰā§āĨ¤",
|
||||
"machine_learning_ocr_model": "OCR āĻŽāĻĄā§āϞ",
|
||||
"machine_learning_ocr_model_description": "āϏāĻžāϰā§āĻāĻžāϰ āĻŽāĻĄā§āϞāĻā§āϞ⧠āĻŽā§āĻŦāĻžāĻāϞ āĻŽāĻĄā§āϞā§āϰ āϤā§āϞāύāĻžā§ āĻŦā§āĻļāĻŋ āύāĻŋāϰā§āĻā§āϞ, āϤāĻŦā§ āĻāĻā§āϞ⧠āĻĒā§āϰāϏā§āϏ āĻāϰāϤ⧠āϏāĻŽā§ āĻŦā§āĻļāĻŋ āϞāĻžāĻā§ āĻāĻŦāĻ āĻŽā§āĻŽāϰāĻŋ āĻŦā§āĻļāĻŋ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻāϰā§āĨ¤",
|
||||
"machine_learning_settings": "āĻŽā§āĻļāĻŋāύ āϞāĻžāϰā§āύāĻŋāĻ āϏā§āĻāĻŋāĻāϏ (Machine Learning Settings)",
|
||||
"machine_learning_settings_description": "āĻŽā§āĻļāĻŋāύ āϞāĻžāϰā§āύāĻŋāĻ āĻŦā§āĻļāĻŋāώā§āĻā§āϝ āĻāĻŦāĻ āϏā§āĻāĻŋāĻāϏ āĻĒāϰāĻŋāĻāĻžāϞāύāĻž āĻāϰā§āύ",
|
||||
"machine_learning_smart_search": "āϏā§āĻŽāĻžāϰā§āĻ āϏāĻžāϰā§āĻ (Smart Search)",
|
||||
"machine_learning_smart_search_description": "CLIP āĻāĻŽāĻŦā§āĻĄāĻŋāĻ (embeddings) āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻāϰ⧠āĻāĻŦāĻŋāϰ āĻŦāĻŋāώā§āĻŦāϏā§āϤ⧠āĻ
āύā§āϝāĻžā§ā§ āĻ
āύā§āϏāύā§āϧāĻžāύ āĻāϰā§āύ",
|
||||
"machine_learning_smart_search_enabled": "āϏā§āĻŽāĻžāϰā§āĻ āϏāĻžāϰā§āĻ āϏāĻā§āώāĻŽ āĻāϰā§āύ",
|
||||
"machine_learning_smart_search_enabled_description": "āύāĻŋāώā§āĻā§āϰāĻŋāϝāĻŧ āĻĨāĻžāĻāϞā§, āϏā§āĻŽāĻžāϰā§āĻ āϏāĻžāϰā§āĻā§āϰ āĻāύā§āϝ āĻāĻŦāĻŋāĻā§āϞ⧠āĻāύāĻā§āĻĄ (encode) āĻāϰāĻž āĻšāĻŦā§ āύāĻžāĨ¤",
|
||||
"machine_learning_url_description": "āĻŽā§āĻļāĻŋāύ āϞāĻžāϰā§āύāĻŋāĻ āϏāĻžāϰā§āĻāĻžāϰā§āϰ URLāĨ¤ āϝāĻĻāĻŋ āĻāĻā§āϰ āĻŦā§āĻļāĻŋ URL āĻĒā§āϰāĻĻāĻžāύ āĻāϰāĻž āĻšā§, āϤāĻŦā§ āĻāĻāĻāĻŋ āϏāĻĢāϞāĻāĻžāĻŦā§ āϏāĻžā§āĻž āύāĻž āĻĻā§āĻā§āĻž āĻĒāϰā§āϝāύā§āϤ āĻĒā§āϰāϤāĻŋāĻāĻŋ āϏāĻžāϰā§āĻāĻžāϰ⧠āĻāĻ āĻāĻ āĻāϰ⧠āĻā§āώā§āĻāĻž āĻāϰāĻž āĻšāĻŦā§ (āĻĒā§āϰāĻĨāĻŽ āĻĨā§āĻā§ āĻļā§āώ āĻā§āϰāĻŽāĻžāύā§āϏāĻžāϰā§)āĨ¤ āϝ⧠āϏāĻžāϰā§āĻāĻžāϰāĻā§āϞ⧠āϏāĻžā§āĻž āĻĻā§āĻŦā§ āύāĻž, āϏā§āĻā§āϞ⧠āĻĒā§āύāϰāĻžā§ āϏāĻāϞ āĻšāĻā§āĻž āĻĒāϰā§āϝāύā§āϤ āϏāĻžāĻŽā§āĻŋāĻāĻāĻžāĻŦā§ āĻāĻĒā§āĻā§āώāĻž āĻāϰāĻž āĻšāĻŦā§āĨ¤",
|
||||
"maintenance_delete_backup": "āĻŦā§āϝāĻžāĻāĻāĻĒ (Backup)āĻŽā§āĻā§āύ",
|
||||
"maintenance_delete_backup_description": "āĻāĻ āĻĢāĻžāĻāϞāĻāĻŋ āĻāĻŋāϰāϤāϰ⧠āĻŽā§āĻā§ āĻĢā§āϞāĻž āĻšāĻŦā§āĨ¤",
|
||||
"maintenance_delete_error": "āĻŦā§āϝāĻžāĻāĻāĻĒ āĻŽā§āĻāϤ⧠āĻŦā§āϝāϰā§āĻĨ āĻšā§ā§āĻā§āĨ¤",
|
||||
"maintenance_restore_backup": "āĻŦā§āϝāĻžāĻāĻāĻĒ āĻĒā§āύāϰā§āĻĻā§āϧāĻžāϰ(Restore) āĻāϰā§āύ",
|
||||
"maintenance_restore_backup_description": "Immich āĻŽā§āĻā§ āĻĢā§āϞāĻž āĻšāĻŦā§ āĻāĻŦāĻ āύāĻŋāϰā§āĻŦāĻžāĻāĻŋāϤ āĻŦā§āϝāĻžāĻāĻāĻĒ āĻĨā§āĻā§ āĻĒā§āύāϰā§āĻĻā§āϧāĻžāϰ āĻāϰāĻž āĻšāĻŦā§āĨ¤ āĻāĻžāϰā§āϝāĻā§āϰāĻŽ āĻāĻžāϞāĻŋā§ā§ āϝāĻžāĻā§āĻžāϰ āĻāĻā§ āĻāĻāĻāĻŋ āĻŦā§āϝāĻžāĻāĻāĻĒ āϤā§āϰāĻŋ āĻāϰāĻž āĻšāĻŦā§āĨ¤",
|
||||
"maintenance_restore_backup_different_version": "āĻāĻ āĻŦā§āϝāĻžāĻāĻāĻĒāĻāĻŋ Immich-āĻāϰ āĻāĻāĻāĻŋ āĻāĻŋāύā§āύ āϏāĻāϏā§āĻāϰāĻŖā§āϰ āĻŽāĻžāϧā§āϝāĻŽā§ āϤā§āϰāĻŋ āĻāϰāĻž āĻšā§ā§āĻāĻŋāϞ!",
|
||||
"maintenance_restore_backup_unknown_version": "āĻŦā§āϝāĻžāĻāĻāĻĒ āϏāĻāϏā§āĻāϰāĻŖ āύāĻŋāϰā§āϧāĻžāϰāĻŖ āĻāϰāĻž āϏāĻŽā§āĻāĻŦ āĻšāϝāĻŧāύāĻŋāĨ¤",
|
||||
"maintenance_restore_database_backup": "āĻĄā§āĻāĻžāĻŦā§āϏ āĻŦā§āϝāĻžāĻāĻāĻĒ āĻĒā§āύāϰā§āĻĻā§āϧāĻžāϰ āĻāϰā§āύ",
|
||||
"maintenance_restore_database_backup_description": "āĻāĻāĻāĻŋ āĻŦā§āϝāĻžāĻāĻāĻĒ āĻĢāĻžāĻāϞ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻāϰ⧠āĻĄā§āĻāĻžāĻŦā§āϏāĻā§ āĻĒā§āϰā§āĻŦāĻŦāϰā§āϤ⧠āĻ
āĻŦāϏā§āĻĨāĻžāϝāĻŧ āĻĢāĻŋāϰāĻŋā§ā§ āĻāύā§āύāĨ¤",
|
||||
"maintenance_settings": "āϰāĻā§āώāĻŖāĻžāĻŦā§āĻā§āώāĻŖ (Maintenance)",
|
||||
"maintenance_settings_description": "Immich-āĻā§ āϰāĻā§āώāĻŖāĻžāĻŦā§āĻā§āώāĻŖ āĻŽā§āĻĄā§ (maintenance mode) āϰāĻžāĻā§āύāĨ¤",
|
||||
"maintenance_start": "āϰāĻā§āώāĻŖāĻžāĻŦā§āĻā§āώāĻŖ āĻŽā§āĻĄā§ āĻĒāϰāĻŋāĻŦāϰā§āϤāύ āĻāϰā§āύ",
|
||||
"maintenance_start_error": "āϰāĻā§āώāĻŖāĻžāĻŦā§āĻā§āώāĻŖ āĻŽā§āĻĄ āĻāĻžāϞ⧠āĻāϰāϤ⧠āĻŦā§āϝāϰā§āĻĨ āĻšā§ā§āĻā§āĨ¤",
|
||||
"maintenance_upload_backup": "āĻĄā§āĻāĻžāĻŦā§āϏ āĻŦā§āϝāĻžāĻāĻāĻĒ āĻĢāĻžāĻāϞ āĻāĻĒāϞā§āĻĄ āĻāϰā§āύ",
|
||||
"maintenance_upload_backup_error": "āĻŦā§āϝāĻžāĻāĻāĻĒ āĻāĻĒāϞā§āĻĄ āĻāϰāĻž āϝāĻžā§āύāĻŋ, āĻāĻāĻŋ āĻāĻŋ āĻā§āύ⧠.sql/.sql.gz āĻĢāĻžāĻāϞ?",
|
||||
"manage_concurrency": "āĻāύāĻāĻžāϰā§āύā§āϏāĻŋ āĻĒāϰāĻŋāĻāĻžāϞāύāĻž āĻāϰā§āύ (Manage Concurrency)",
|
||||
"manage_concurrency_description": "āĻāĻŦ āĻāύāĻāĻžāϰā§āύā§āϏāĻŋ āĻĒāϰāĻŋāĻāĻžāϞāύāĻž āĻāϰāϤ⧠'āĻāĻŦāϏ' (Jobs) āĻĒāĻžāϤāĻžāϝāĻŧ āϝāĻžāύāĨ¤",
|
||||
"manage_log_settings": "āϞāĻ āϏā§āĻāĻŋāĻāϏ āĻĒāϰāĻŋāĻāĻžāϞāύāĻž āĻāϰā§āύ",
|
||||
"map_dark_style": "āĻĄāĻžāϰā§āĻ āϏā§āĻāĻžāĻāϞ (Dark style)",
|
||||
"map_enable_description": "āĻŽā§āϝāĻžāĻĒ āĻĢāĻŋāĻāĻžāϰāĻā§āϞ⧠āϏāĻā§āϰāĻŋā§ āĻāϰā§āύ (Enable map features)",
|
||||
"map_gps_settings": "āĻŽā§āϝāĻžāĻĒ āĻāĻŦāĻ āĻāĻŋāĻĒāĻŋāĻāϏ āϏā§āĻāĻŋāĻāϏ (Map & GPS Settings)",
|
||||
"map_gps_settings_description": "āĻŽā§āϝāĻžāĻĒ āĻāĻŦāĻ āĻāĻŋāĻĒāĻŋāĻāϏ (āϰāĻŋāĻāĻžāϰā§āϏ āĻāĻŋāĻāĻā§āĻĄāĻŋāĻ) āϏā§āĻāĻŋāĻāϏ āĻĒāϰāĻŋāĻāĻžāϞāύāĻž āĻāϰā§āύ (Manage Map & GPS (Reverse Geocoding) Settings)",
|
||||
"map_implications": "āĻŽā§āϝāĻžāĻĒ āĻĢāĻŋāĻāĻžāϰāĻāĻŋ āĻāĻāĻāĻŋ āĻāĻā§āϏāĻāĻžāϰā§āύāĻžāϞ āĻāĻžāĻāϞ āϏāĻžāϰā§āĻāĻŋāϏā§āϰ (tiles.immich.cloud) āĻāĻĒāϰ āύāĻŋāϰā§āĻāϰ āĻāϰā§āĨ¤",
|
||||
"map_light_style": "āϞāĻžāĻāĻ āϏā§āĻāĻžāĻāϞ (Light style)",
|
||||
"map_manage_reverse_geocoding_settings": "<link>āϰāĻŋāĻāĻžāϰā§āϏ āĻāĻŋāĻāĻā§āĻĄāĻŋāĻ</link> āϏā§āĻāĻŋāĻāϏ āĻĒāϰāĻŋāĻāĻžāϞāύāĻž āĻāϰā§āύ",
|
||||
"map_reverse_geocoding": "āϰāĻŋāĻāĻžāϰā§āϏ āĻāĻŋāĻāĻā§āĻĄāĻŋāĻ (Reverse Geocoding)",
|
||||
"map_reverse_geocoding_enable_description": "āϰāĻŋāĻāĻžāϰā§āϏ āĻāĻŋāĻāĻā§āĻĄāĻŋāĻ āϏāĻā§āϰāĻŋā§ āĻāϰā§āύ (Enable reverse geocoding)",
|
||||
"map_reverse_geocoding_settings": "āϰāĻŋāĻāĻžāϰā§āϏ āĻāĻŋāĻāĻā§āĻĄāĻŋāĻ āϏā§āĻāĻŋāĻāϏ (Reverse Geocoding Settings)",
|
||||
"map_settings": "āĻŽāĻžāύāĻāĻŋāϤā§āϰ (Map)",
|
||||
"map_settings_description": "āĻŽāĻžāύāĻāĻŋāϤā§āϰā§āϰ āϏā§āĻāĻŋāĻāϏ āĻĒāϰāĻŋāĻāĻžāϞāύāĻž āĻāϰā§āύ (Manage map settings)",
|
||||
"map_style_description": "āĻāĻāĻāĻŋ style.json āĻŽā§āϝāĻžāĻĒ āĻĨāĻŋāĻŽā§āϰ URL (URL to a style.json map theme)",
|
||||
"memory_cleanup_job": "āĻŽā§āĻŽāϰāĻŋ āĻā§āϞāĻŋāύāĻāĻĒ (Memory cleanup)",
|
||||
"memory_generate_job": "āϏā§āĻŽā§āϤāĻŋ āϤā§āϰāĻŋ āĻāϰāĻž(Memory generation)",
|
||||
"metadata_extraction_job": "āĻŽā§āĻāĻžāĻĄā§āĻāĻž āĻāĻā§āϏāĻā§āϰā§āϝāĻžāĻā§āĻ āĻāϰā§āύ (Extract metadata)",
|
||||
"metadata_extraction_job_description": "āĻĒā§āϰāϤāĻŋāĻāĻŋ āĻ
ā§āϝāĻžāϏā§āĻ (Asset) āĻĨā§āĻā§ āĻŽā§āĻāĻžāĻĄā§āĻāĻž āϤāĻĨā§āϝ āĻāĻā§āϏāĻā§āϰā§āϝāĻžāĻā§āĻ āĻāϰā§āύ, āϝā§āĻŽāύ: āĻāĻŋāĻĒāĻŋāĻāϏ (GPS), āĻā§āĻšāĻžāϰāĻž (faces) āĻāĻŦāĻ āϰā§āĻā§āϞāĻŋāĻāĻļāύ (resolution)āĨ¤",
|
||||
"metadata_faces_import_setting": "āĻĢā§āϏ āĻāĻŽā§āĻĒā§āϰā§āĻ āϏāĻā§āϰāĻŋā§ āĻāϰā§āύ (Enable face import)",
|
||||
"metadata_faces_import_setting_description": "āĻāĻŦāĻŋāϰ EXIF āĻĄā§āĻāĻž āĻāĻŦāĻ āϏāĻžāĻāĻĄāĻāĻžāϰ (sidecar) āĻĢāĻžāĻāϞ āĻĨā§āĻā§ āĻā§āĻšāĻžāϰāĻž (faces) āĻāĻŽā§āĻĒā§āϰā§āĻ āĻāϰā§āύāĨ¤",
|
||||
"metadata_settings": "āĻŽā§āĻāĻžāĻĄā§āĻāĻž āϏā§āĻāĻŋāĻāϏ (Metadata Settings)",
|
||||
"metadata_settings_description": "āĻŽā§āĻāĻžāĻĄā§āĻāĻž āϏā§āĻāĻŋāĻāϏ āĻĒāϰāĻŋāĻāĻžāϞāύāĻž āĻāϰā§āύ (Manage metadata settings)",
|
||||
"migration_job": "āĻŽāĻžāĻāĻā§āϰā§āĻļāύ (Migration)",
|
||||
"migration_job_description": "āĻ
ā§āϝāĻžāϏā§āĻ āĻāĻŦāĻ āĻĢā§āϏ āĻĨāĻžāĻŽā§āĻŦāύā§āĻāϞāĻā§āϞā§āĻā§ āϏāϰā§āĻŦāĻļā§āώ āĻĢā§āϞā§āĻĄāĻžāϰ āϏā§āĻā§āϰāĻžāĻāĻāĻžāϰ⧠āĻŽāĻžāĻāĻā§āϰā§āĻ āĻāϰā§āύāĨ¤ (Migrate thumbnails for assets and faces to the latest folder structure)",
|
||||
"nightly_tasks_database_cleanup_setting": "āĻĄā§āĻāĻžāĻŦā§āϏ āĻā§āϞāĻŋāύāĻāĻĒ āĻāĻžāϏā§āĻāϏāĻŽā§āĻš (Database cleanup tasks)",
|
||||
"nightly_tasks_database_cleanup_setting_description": "āĻĄā§āĻāĻžāĻŦā§āϏ āĻĨā§āĻā§ āĻĒā§āϰā§āύ⧠āĻāĻŦāĻ āĻŽā§ā§āĻžāĻĻā§āϤā§āϤā§āϰā§āĻŖ āĻĄā§āĻāĻž āĻŽā§āĻā§ āĻĢā§āϞā§āύ",
|
||||
"nightly_tasks_generate_memories_setting": "āĻŽā§āĻŽā§āϰāĻŋāĻ āϤā§āϰāĻŋ āĻāϰā§āύ (Generate memories)",
|
||||
"nightly_tasks_generate_memories_setting_description": "āĻ
ā§āϝāĻžāϏā§āĻāĻā§āϞ⧠āĻĨā§āĻā§ āύāϤā§āύ āĻŽā§āĻŽā§āϰāĻŋāĻ āϤā§āϰāĻŋ āĻāϰā§āύ",
|
||||
"nightly_tasks_missing_thumbnails_setting": "āĻšāĻžāϰāĻŋāϝāĻŧā§ āϝāĻžāĻāϝāĻŧāĻž āĻĨāĻžāĻŽā§āĻŦāύā§āĻāϞāĻā§āϞ⧠āϤā§āϰāĻŋ āĻāϰā§āύ",
|
||||
"nightly_tasks_missing_thumbnails_setting_description": "āĻĨāĻžāĻŽā§āĻŦāύā§āĻāϞ āύā§āĻ āĻāĻŽāύ āĻĢāĻžāĻāϞāĻā§āϞā§āĻā§ āĻāĻŋāĻāϤ⧠(Queue) āϝā§āĻ āĻāϰā§āύ",
|
||||
"nightly_tasks_settings": "āύāĻžāĻāĻāϞāĻŋ āĻāĻžāϏā§āĻ āϏā§āĻāĻŋāĻāϏ (Nightly Tasks Settings)",
|
||||
"nightly_tasks_settings_description": "āύāĻžāĻāĻāϞāĻŋ āĻāĻžāϏā§āĻ āĻĒāϰāĻŋāĻāĻžāϞāύāĻž āĻāϰā§āύ (Manage nightly tasks)",
|
||||
"nightly_tasks_start_time_setting": "āĻļā§āϰ⧠āĻāϰāĻžāϰ āϏāĻŽā§ (Start time)",
|
||||
"nightly_tasks_start_time_setting_description": "āϏāĻžāϰā§āĻāĻžāϰ āϝāĻāύ āύāĻžāĻāĻāϞāĻŋ āĻāĻžāϏā§āĻ (nightly tasks) āĻāĻžāϞāĻžāύ⧠āĻļā§āϰ⧠āĻāϰ⧠āϏā§āĻ āϏāĻŽāϝāĻŧ",
|
||||
"nightly_tasks_sync_quota_usage_setting": "āĻā§āĻāĻž āĻŦā§āϝāĻŦāĻšāĻžāϰā§āϰ āϤāĻĨā§āϝ āϏāĻŋāĻā§āĻ āĻāϰā§āύ (Sync quota usage)",
|
||||
"nightly_tasks_sync_quota_usage_setting_description": "āĻŦāϰā§āϤāĻŽāĻžāύ āĻŦā§āϝāĻŦāĻšāĻžāϰā§āϰ āĻāĻĒāϰ āĻāĻŋāϤā§āϤāĻŋ āĻāϰ⧠āĻŦā§āϝāĻŦāĻšāĻžāϰāĻāĻžāϰā§āϰ āϏā§āĻā§āϰā§āĻ āĻā§āĻāĻž āĻāĻĒāĻĄā§āĻ āĻāϰā§āύāĨ¤",
|
||||
"no_paths_added": "āĻā§āύ⧠āĻĒāĻžāĻĨ āϝā§āĻ āĻāϰāĻž āĻšāϝāĻŧāύāĻŋ (No paths added)",
|
||||
"no_pattern_added": "āĻā§āύ⧠āĻĒā§āϝāĻžāĻāĻžāϰā§āύ āϝā§āĻ āĻāϰāĻž āĻšāϝāĻŧāύāĻŋ (No pattern added)",
|
||||
"note_apply_storage_label_previous_assets": "āĻĻā§āϰāώā§āĻāĻŦā§āϝ: āĻĒā§āϰā§āĻŦā§ āĻāĻĒāϞā§āĻĄ āĻāϰāĻž āĻ
ā§āϝāĻžāϏā§āĻāĻā§āϞā§āϤ⧠āϏā§āĻā§āϰā§āĻ āϞā§āĻŦā§āϞ (Storage Label) āĻĒā§āϰāϝāĻŧā§āĻ āĻāϰāϤ⧠āύāĻŋāĻā§āϰ āĻāĻŽāĻžāύā§āĻĄāĻāĻŋ āϰāĻžāύ āĻāϰā§āύâ",
|
||||
"note_cannot_be_changed_later": "āϏāϤāϰā§āĻāĻŦāĻžāϰā§āϤāĻž: āĻāĻāĻŋ āĻĒāϰāĻŦāϰā§āϤā§āϤ⧠āĻĒāϰāĻŋāĻŦāϰā§āϤāύ āĻāϰāĻž āϝāĻžāĻŦā§ āύāĻž!",
|
||||
"notification_email_from_address": "āĻĒā§āϰā§āϰāĻā§āϰ āĻ āĻŋāĻāĻžāύāĻž (From address)",
|
||||
"notification_email_from_address_description": "āĻĒā§āϰā§āϰāĻā§āϰ āĻāĻŽā§āϞ āĻ āĻŋāĻāĻžāύāĻž, āĻāĻĻāĻžāĻšāϰāĻŖāϏā§āĻŦāϰā§āĻĒ: \"Immich Photo Server noreply@example.com\"āĨ¤ āύāĻŋāĻļā§āĻāĻŋāϤ āĻāϰā§āύ āϝ⧠āĻāĻĒāύāĻŋ āĻāĻŽāύ āĻāĻāĻāĻŋ āĻ āĻŋāĻāĻžāύāĻž āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻāϰāĻā§āύ āϝāĻž āĻĨā§āĻā§ āĻāĻŽā§āϞ āĻĒāĻžāĻ āĻžāύā§āϰ āĻ
āύā§āĻŽāϤāĻŋ āĻāĻĒāύāĻžāϰ āĻāĻā§āĨ¤",
|
||||
"notification_email_host_description": "āĻāĻŽā§āϞ āϏāĻžāϰā§āĻāĻžāϰā§āϰ āĻšā§āϏā§āĻ (āϝā§āĻŽāύ: smtp.immich.app)",
|
||||
"notification_email_ignore_certificate_errors": "āϏāĻžāϰā§āĻāĻŋāĻĢāĻŋāĻā§āĻ āϤā§āϰā§āĻāĻŋāĻā§āϞ⧠āĻāĻĒā§āĻā§āώāĻž āĻāϰā§āύ (Ignore certificate errors)",
|
||||
"notification_email_ignore_certificate_errors_description": "TLS āϏāĻžāϰā§āĻāĻŋāĻĢāĻŋāĻā§āĻ āĻā§āϝāĻžāϞāĻŋāĻĄā§āĻļāύ āϤā§āϰā§āĻāĻŋāĻā§āϞ⧠āĻāĻĒā§āĻā§āώāĻž āĻāϰā§āύ (āĻĒā§āϰāϏā§āϤāĻžāĻŦāĻŋāϤ āύāϝāĻŧ)",
|
||||
"notification_email_password_description": "āĻāĻŽā§āϞ āϏāĻžāϰā§āĻāĻžāϰ⧠āĻ
āĻĨā§āύā§āĻāĻŋāĻā§āĻļāύ āĻŦāĻž āϏāϤā§āϝāϤāĻž āϝāĻžāĻāĻžāĻā§ā§āϰ āĻāύā§āϝ āĻŦā§āϝāĻŦāĻšā§āϤ āĻĒāĻžāϏāĻā§āĻžāϰā§āĻĄ",
|
||||
"notification_email_port_description": "āĻāĻŽā§āϞ āϏāĻžāϰā§āĻāĻžāϰā§āϰ āĻĒā§āϰā§āĻ (āϝā§āĻŽāύ: ⧍ā§Ģ, ā§Ēā§Ŧā§Ģ, āĻ
āĻĨāĻŦāĻž ā§Ģā§Žā§)",
|
||||
"notification_email_secure": "SMTPS (āϏā§āĻŽāĻžāϰā§āĻ āĻŽā§āĻāϞ āĻā§āϰāĻžāύā§āϏāĻĢāĻžāϰ āĻĒā§āϰā§āĻā§āĻāϞ āϏāĻŋāĻāĻŋāĻāϰ)",
|
||||
"notification_email_secure_description": "SMTPS (SMTP over TLS) āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻāϰā§āύ",
|
||||
"notification_email_sent_test_email_button": "āĻā§āϏā§āĻ āĻāĻŽā§āϞ āĻĒāĻžāĻ āĻžāύ āĻāĻŦāĻ āϏā§āĻ āĻāϰā§āύ",
|
||||
"oauth_enable_description": "OAuth-āĻāϰ āĻŽāĻžāϧā§āϝāĻŽā§ āϞāĻāĻāύ āĻāϰā§āύ",
|
||||
"oauth_mobile_redirect_uri": "āĻŽā§āĻŦāĻžāĻāϞ āϰāĻŋāĻĄāĻžāĻāϰā§āĻā§āĻ āĻāĻāĻāϰāĻāĻ (URI)",
|
||||
"oauth_mobile_redirect_uri_override": "āĻŽā§āĻŦāĻžāĻāϞ āϰāĻŋāĻĄāĻžāĻāϰā§āĻā§āĻ āĻāĻāĻāϰāĻāĻ (URI) āĻāĻāĻžāϰāϰāĻžāĻāĻĄ",
|
||||
"oauth_mobile_redirect_uri_override_description": "āϝāĻāύ OAuth āĻĒā§āϰā§āĻāĻžāĻāĻĄāĻžāϰ āĻŽā§āĻŦāĻžāĻāϞ āĻāĻāĻāϰāĻāĻ (URI) āĻ
āύā§āĻŽāϤāĻŋ āĻĻā§ā§ āύāĻž, āϝā§āĻŽāύ ''{callback}'', āϤāĻāύ āĻāĻāĻŋ āϏāĻā§āϰāĻŋā§ āĻāϰā§āύāĨ¤",
|
||||
"oauth_role_claim": "āϰā§āϞ āĻā§āϞā§āĻāĻŽ (Role Claim)",
|
||||
"oauth_role_claim_description": "āĻāĻ āĻā§āϞā§āĻāĻŽāĻāĻŋāϰ āĻāĻĒāϏā§āĻĨāĻŋāϤāĻŋāϰ āĻāĻĒāϰ āĻāĻŋāϤā§āϤāĻŋ āĻāϰ⧠āϏā§āĻŦāϝāĻŧāĻāĻā§āϰāĻŋāϝāĻŧāĻāĻžāĻŦā§ āĻ
ā§āϝāĻžāĻĄāĻŽāĻŋāύ āĻ
ā§āϝāĻžāĻā§āϏā§āϏ āĻĒā§āϰāĻĻāĻžāύ āĻāϰā§āύāĨ¤ āĻā§āϞā§āĻāĻŽāĻāĻŋāϤ⧠'user' āĻ
āĻĨāĻŦāĻž 'admin' āϝā§āĻā§āύ⧠āĻāĻāĻāĻŋ āĻĨāĻžāĻāϤ⧠āĻĒāĻžāϰā§āĨ¤",
|
||||
"oauth_settings": "OAuth",
|
||||
"oauth_settings_description": "OAuth āϞāĻāĻāύ āϏā§āĻāĻŋāĻāϏ āĻŽā§āϝāĻžāύā§āĻ āĻāϰā§āύ",
|
||||
"oauth_settings_more_details": "āĻāĻ āĻĢāĻŋāĻāĻžāϰā§āϰ āĻŦā§āϝāĻžāĻĒāĻžāϰ⧠āĻāϰāĻ āĻŦāĻŋāϏā§āϤāĻžāϰāĻŋāϤ āĻāĻžāύāϤā§, <link>āĻĄāĻā§āĻŽā§āύā§āĻāϏ</link> āĻĻā§āĻā§āύāĨ¤",
|
||||
"oauth_storage_label_claim": "āϏā§āĻā§āϰā§āĻ āϞā§āĻŦā§āϞ āĻā§āϞā§āĻāĻŽ (Storage label claim)",
|
||||
"oauth_storage_label_claim_description": "āĻāĻ āĻā§āϞā§āĻāĻŽ-āĻāϰ āĻā§āϝāĻžāϞ⧠āĻ
āύā§āϝāĻžā§ā§ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻāĻžāϰā§āϰ āϏā§āĻā§āϰā§āĻ āϞā§āĻŦā§āϞ āϏā§āĻŦāϝāĻŧāĻāĻā§āϰāĻŋāϝāĻŧāĻāĻžāĻŦā§ āϏā§āĻ āĻāϰā§āύāĨ¤",
|
||||
"oauth_storage_quota_claim": "āϏā§āĻā§āϰā§āĻ āĻā§āĻāĻž āĻā§āϞā§āĻāĻŽ (Storage quota claim)",
|
||||
"oauth_storage_quota_claim_description": "āĻāĻ āĻā§āϞā§āĻāĻŽ-āĻāϰ āĻā§āϝāĻžāϞ⧠āĻ
āύā§āϝāĻžā§ā§ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻāĻžāϰā§āϰ āϏā§āĻā§āϰā§āĻ āĻā§āĻāĻž āϏā§āĻŦāϝāĻŧāĻāĻā§āϰāĻŋāϝāĻŧāĻāĻžāĻŦā§ āϏā§āĻ āĻāϰā§āύāĨ¤",
|
||||
"oauth_storage_quota_default": "āĻĄāĻŋāĻĢāϞā§āĻ āϏā§āĻā§āϰā§āĻ āĻā§āĻāĻž (GiB)",
|
||||
"oauth_storage_quota_default_description": "āĻā§āϞā§āĻāĻŽ āύāĻž āĻĻā§āĻā§āĻž āĻĨāĻžāĻāϞ⧠āϝ⧠āϏā§āĻā§āϰā§āĻ āĻā§āĻāĻž (GiB-āϤā§) āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻāϰāĻž āĻšāĻŦā§āĨ¤",
|
||||
"oauth_timeout": "āϰāĻŋāĻā§āϝāĻŧā§āϏā§āĻ āĻāĻžāĻāĻŽ-āĻāĻāĻ (Request Timeout)",
|
||||
"oauth_timeout_description": "āĻŽāĻŋāϞāĻŋāϏā§āĻā§āύā§āĻĄā§ āϰāĻŋāĻā§ā§ā§āϏā§āĻā§āϰ āĻāĻžāĻāĻŽ-āĻāĻāĻ (Timeout for requests in milliseconds)",
|
||||
"ocr_job_description": "āĻāĻŦāĻŋ āĻĨā§āĻā§ āĻā§āĻā§āϏāĻ āĻļāύāĻžāĻā§āϤ āĻāϰāϤ⧠āĻŽā§āĻļāĻŋāύ āϞāĻžāϰā§āύāĻŋāĻ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻāϰā§āύ",
|
||||
"password_enable_description": "āĻāĻŽā§āϞ āĻāĻŦāĻ āĻĒāĻžāϏāĻāϝāĻŧāĻžāϰā§āĻĄ āĻĻāĻŋāϝāĻŧā§ āϞāĻāĻāύ āĻāϰā§āύ",
|
||||
"password_settings": "āĻĒāĻžāϏāĻāϝāĻŧāĻžāϰā§āĻĄ āϞāĻāĻāύ (Password Login)",
|
||||
"password_settings_description": "āĻĒāĻžāϏāĻāϝāĻŧāĻžāϰā§āĻĄ āϞāĻāĻāύ āϏā§āĻāĻŋāĻāϏ āĻŽā§āϝāĻžāύā§āĻ āĻāϰā§āύ",
|
||||
"paths_validated_successfully": "āϏāĻŦāĻā§āϞ⧠āĻĒāĻžāĻĨ (path) āϏāĻĢāϞāĻāĻžāĻŦā§ āϝāĻžāĻāĻžāĻ āĻāϰāĻž āĻšāϝāĻŧā§āĻā§",
|
||||
"person_cleanup_job": "āĻĒāĻžāϰāϏāύ āĻā§āϞāĻŋāύāĻāĻĒ (Person Cleanup)",
|
||||
"queue_details": "āĻāĻŋāĻ āĻĄāĻŋāĻā§āĻāϞāϏ (Queue Details)",
|
||||
"queues": "āĻāĻŦ āĻāĻŋāĻ (Job Queues)",
|
||||
"queues_page_description": "āĻ
ā§āϝāĻžāĻĄāĻŽāĻŋāύ āĻāĻŦ āĻāĻŋāĻ (Job Queues) āĻĒā§āĻ",
|
||||
"quota_size_gib": "āĻā§āĻāĻž āϏāĻžāĻāĻ (GiB)",
|
||||
"refreshing_all_libraries": "āϏāĻŦāĻā§āϞ⧠āϞāĻžāĻāĻŦā§āϰā§āϰāĻŋ āϰāĻŋāĻĢā§āϰā§āĻļ āĻāϰāĻž āĻšāĻā§āĻā§",
|
||||
"registration": "āĻ
ā§āϝāĻžāĻĄāĻŽāĻŋāύ āϰā§āĻāĻŋāϏā§āĻā§āϰā§āĻļāύ (Admin Registration)",
|
||||
"registration_description": "āϝā§āĻšā§āϤ⧠āĻāĻĒāύāĻŋ āĻāĻ āϏāĻŋāϏā§āĻā§āĻŽā§āϰ āĻĒā§āϰāĻĨāĻŽ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻāĻžāϰā§, āϤāĻžāĻ āĻāĻĒāύāĻžāĻā§ āĻ
ā§āϝāĻžāĻĄāĻŽāĻŋāύ (Admin) āĻšāĻŋāϏā§āĻŦā§ āύāĻŋāϝā§āĻā§āϤ āĻāϰāĻž āĻšāĻŦā§āĨ¤ āĻāĻĒāύāĻŋ āϏāĻŽāϏā§āϤ āĻĒā§āϰāĻļāĻžāϏāύāĻŋāĻ āĻāĻžāĻā§āϰ āĻāύā§āϝ āĻĻāĻžāϝāĻŧā§ āĻĨāĻžāĻāĻŦā§āύ āĻāĻŦāĻ āĻĒāϰāĻŦāϰā§āϤ⧠āĻŦā§āϝāĻŦāĻšāĻžāϰāĻāĻžāϰā§āϰāĻž āĻāĻĒāύāĻžāϰ āĻŽāĻžāϧā§āϝāĻŽā§āĻ āϤā§āϰāĻŋ āĻšāĻŦā§āĨ¤",
|
||||
"remove_failed_jobs": "āĻŦā§āϝāϰā§āĻĨ āĻšāĻāϝāĻŧāĻž āĻāĻžāĻāĻā§āϞ⧠āĻŽā§āĻā§ āĻĢā§āϞā§āύ (Remove failed jobs)",
|
||||
"require_password_change_on_login": "āĻĒā§āϰāĻĨāĻŽāĻŦāĻžāϰ āϞāĻāĻāύ āĻāϰāĻžāϰ āϏāĻŽā§ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻāĻžāϰā§āϰ āĻĒāĻžāϏāĻā§āĻžāϰā§āĻĄ āĻĒāϰāĻŋāĻŦāϰā§āϤāύ āĻāϰāĻž āĻŦāĻžāϧā§āϝāϤāĻžāĻŽā§āϞāĻ āĻāϰā§āύ",
|
||||
"reset_settings_to_default": "āϏā§āĻāĻŋāĻāϏ āϰāĻŋāϏā§āĻ āĻāϰ⧠āĻĄāĻŋāĻĢāϞā§āĻ āĻ
āĻŦāϏā§āĻĨāĻžā§ āĻĢāĻŋāϰāĻŋā§ā§ āĻāύā§āύ (Reset settings to default)",
|
||||
"reset_settings_to_recent_saved": "āϏāĻŽā§āĻĒā§āϰāϤāĻŋ āϏā§āĻ āĻāϰāĻž āϏā§āĻāĻŋāĻāϏ⧠āϰāĻŋāϏā§āĻ āĻāϰā§āύ (Reset settings to the recent saved settings)",
|
||||
"scanning_library": "āϞāĻžāĻāĻŦā§āϰā§āϰāĻŋ āϏā§āĻā§āϝāĻžāύ āĻāϰāĻž āĻšāĻā§āĻā§ (Scanning library)",
|
||||
"search_jobs": "āĻāĻŦ āϏāĻžāϰā§āĻ āĻāϰā§āύâĻ",
|
||||
"send_welcome_email": "āϏā§āĻŦāĻžāĻāϤ āĻāĻŽā§āϞ āĻĒāĻžāĻ āĻžāύ",
|
||||
"server_external_domain_settings": "āĻāĻā§āϏāĻāĻžāϰā§āύāĻžāϞ āĻĄā§āĻŽā§āĻāύ (External Domain)",
|
||||
"server_external_domain_settings_description": "āĻĒāĻžāĻŦāϞāĻŋāĻ āĻļā§āϝāĻŧāĻžāϰāĻŋāĻ āϞāĻŋāĻā§āĻā§āϰ āĻāύā§āϝ āĻĄā§āĻŽā§āĻāύ (http(s):// āϏāĻš)",
|
||||
"server_public_users": "āĻĒāĻžāĻŦāϞāĻŋāĻ āĻāĻāĻāĻžāϰ (Public Users)",
|
||||
"server_public_users_description": "āĻļā§āϝāĻŧāĻžāϰ āĻāϰāĻž āĻ
ā§āϝāĻžāϞāĻŦāĻžāĻŽā§ āĻā§āύ⧠āĻŦā§āϝāĻŦāĻšāĻžāϰāĻāĻžāϰā§āĻā§ āϝā§āĻ āĻāϰāĻžāϰ āϏāĻŽāϝāĻŧ āϏāĻŽāϏā§āϤ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻāĻžāϰā§āϰ (āύāĻžāĻŽ āĻāĻŦāĻ āĻāĻŽā§āϞ) āϤāĻžāϞāĻŋāĻāĻž āĻĻā§āĻāĻžāύ⧠āĻšāϝāĻŧāĨ¤ āĻāĻāĻŋ āύāĻŋāώā§āĻā§āϰāĻŋāϝāĻŧ (Disabled) āĻāϰāĻž āĻšāϞā§, āĻŦā§āϝāĻŦāĻšāĻžāϰāĻāĻžāϰā§āϰ āϤāĻžāϞāĻŋāĻāĻž āĻļā§āϧā§āĻŽāĻžāϤā§āϰ āĻ
ā§āϝāĻžāĻĄāĻŽāĻŋāύāĻĻā§āϰ āĻāύā§āϝ āĻāĻĒāϞāĻŦā§āϧ āĻšāĻŦā§āĨ¤",
|
||||
"server_settings": "āϏāĻžāϰā§āĻāĻžāϰ āϏā§āĻāĻŋāĻāϏ (Server Settings)",
|
||||
"server_settings_description": "āϏāĻžāϰā§āĻāĻžāϰ āϏā§āĻāĻŋāĻāϏ āĻŽā§āϝāĻžāύā§āĻ āĻāϰā§āύ (Manage server settings)",
|
||||
"server_stats_page_description": "āĻ
ā§āϝāĻžāĻĄāĻŽāĻŋāύ āϏāĻžāϰā§āĻāĻžāϰ āϏā§āĻā§āϝāĻžāĻāĻŋāϏā§āĻāĻŋāĻāϏ (Server Statistics) āĻĒā§āĻ",
|
||||
"server_welcome_message": "āϏā§āĻŦāĻžāĻāϤ āĻŦāĻžāϰā§āϤāĻž (Welcome message)",
|
||||
"server_welcome_message_description": "āϞāĻāĻāύ āĻĒā§āĻā§ āĻĒā§āϰāĻĻāϰā§āĻļāĻŋāϤ āĻāĻāĻāĻŋ āĻŦāĻžāϰā§āϤāĻžāĨ¤",
|
||||
"settings_page_description": "āĻ
ā§āϝāĻžāĻĄāĻŽāĻŋāύ āϏā§āĻāĻŋāĻāϏ āĻĒā§āĻ",
|
||||
"sidecar_job": "āϏāĻžāĻāĻĄāĻāĻžāϰ āĻŽā§āĻāĻžāĻĄā§āĻāĻž (Sidecar Metadata)",
|
||||
"sidecar_job_description": "āĻĢāĻžāĻāϞāϏāĻŋāϏā§āĻā§āĻŽ āĻĨā§āĻā§ āϏāĻžāĻāĻĄāĻāĻžāϰ āĻŽā§āĻāĻžāĻĄā§āĻāĻž āĻ
āύā§āϏāύā§āϧāĻžāύ āĻŦāĻž āϏāĻŋāĻā§āĻā§āϰā§āύāĻžāĻāĻ āĻāϰā§āύ",
|
||||
"slideshow_duration_description": "āĻĒā§āϰāϤāĻŋāĻāĻŋ āĻāĻŦāĻŋ āĻĻā§āĻāĻžāύā§āϰ āϏāĻŽā§āĻāĻžāϞ (āϏā§āĻā§āύā§āĻĄā§)",
|
||||
"smart_search_job_description": "āϏā§āĻŽāĻžāϰā§āĻ āϏāĻžāϰā§āĻā§āϰ āϏā§āĻŦāĻŋāϧāĻžāϰā§āĻĨā§ āĻ
ā§āϝāĻžāϏā§āĻāĻā§āϞā§āϰ āĻāĻĒāϰ āĻŽā§āĻļāĻŋāύ āϞāĻžāϰā§āύāĻŋāĻ āĻĒāϰāĻŋāĻāĻžāϞāύāĻž āĻāϰā§āύ",
|
||||
"storage_template_date_time_description": "āĻ
ā§āϝāĻžāϏā§āĻ āϤā§āϰāĻŋāϰ āϏāĻŽā§āĻāĻžāϞ (Timestamp) āϤāĻžāϰāĻŋāĻ āĻ āϏāĻŽā§ā§āϰ āϤāĻĨā§āϝā§āϰ āĻāύā§āϝ āĻŦā§āϝāĻŦāĻšā§āϤ āĻšā§",
|
||||
"storage_template_date_time_sample": "āύāĻŽā§āύāĻž āϏāĻŽā§ {date}",
|
||||
"storage_template_enable_description": "āϏā§āĻā§āϰā§āĻ āĻā§āĻŽāĻĒā§āϞā§āĻ āĻāĻā§āĻāĻŋāύ āϏāĻā§āϰāĻŋā§ āĻāϰā§āύ",
|
||||
"storage_template_hash_verification_enabled": "āĻšā§āϝāĻžāĻļ āĻā§āϰāĻŋāĻĢāĻŋāĻā§āĻļāύ (Hash Verification) āϏāĻā§āϰāĻŋā§ āĻāϰāĻž āĻšā§ā§āĻā§",
|
||||
"storage_template_hash_verification_enabled_description": "āĻšā§āϝāĻžāĻļ āĻā§āϰāĻŋāĻĢāĻŋāĻā§āĻļāύ (Hash Verification) āϏāĻā§āϰāĻŋā§ āĻāϰā§; āĻāϰ āĻĒā§āϰāĻāĻžāĻŦ āϏāĻŽā§āĻĒāϰā§āĻā§ āύāĻŋāĻļā§āĻāĻŋāϤ āύāĻž āĻšā§ā§ āĻāĻāĻŋ āύāĻŋāώā§āĻā§āϰāĻŋā§ āĻāϰāĻŦā§āύ āύāĻž",
|
||||
"storage_template_migration": "āϏā§āĻā§āϰā§āĻ āĻā§āĻŽāĻĒā§āϞā§āĻ āĻŽāĻžāĻāĻā§āϰā§āĻļāύ (Storage Template Migration)",
|
||||
"storage_template_migration_description": "āĻĒā§āϰā§āĻŦā§ āĻāĻĒāϞā§āĻĄ āĻāϰāĻž āĻ
ā§āϝāĻžāϏā§āĻāĻā§āϞā§āϤ⧠āĻŦāϰā§āϤāĻŽāĻžāύ <link>{template}</link> āĻĒā§āϰāϝāĻŧā§āĻ āĻāϰā§āύ",
|
||||
"storage_template_migration_info": "āϏā§āĻā§āϰā§āĻ āĻā§āĻŽāĻĒā§āϞā§āĻāĻāĻŋ āϏāĻŽāϏā§āϤ āĻāĻā§āϏāĻā§āύāĻļāύāĻā§ āĻā§āĻ āĻšāĻžāϤā§āϰ āĻ
āĻā§āώāϰ⧠(lowercase) āϰā§āĻĒāĻžāύā§āϤāϰ āĻāϰāĻŦā§āĨ¤ āĻā§āĻŽāĻĒā§āϞā§āĻā§āϰ āĻĒāϰāĻŋāĻŦāϰā§āϤāύāĻā§āϞ⧠āĻā§āĻŦāϞ āύāϤā§āύ āĻ
ā§āϝāĻžāϏā§āĻāĻā§āϞā§āϰ āĻā§āώā§āϤā§āϰ⧠āĻĒā§āϰāϝā§āĻā§āϝ āĻšāĻŦā§āĨ¤ āĻĒā§āϰā§āĻŦā§ āĻāĻĒāϞā§āĻĄ āĻāϰāĻž āĻ
ā§āϝāĻžāϏā§āĻāĻā§āϞā§āϤ⧠āĻāĻ āĻā§āĻŽāĻĒā§āϞā§āĻāĻāĻŋ āĻā§āϤāĻžāĻĒā§āĻā§āώāĻāĻžāĻŦā§ (retroactively) āĻĒā§āϰāϝāĻŧā§āĻ āĻāϰāϤ⧠<link>{job}</link> āϰāĻžāύ āĻāϰā§āύāĨ¤",
|
||||
"storage_template_migration_job": "āϏā§āĻā§āϰā§āĻ āĻā§āĻŽāĻĒā§āϞā§āĻ āĻŽāĻžāĻāĻā§āϰā§āĻļāύ āĻāĻŦ",
|
||||
"storage_template_more_details": "āĻāĻ āĻĢāĻŋāĻāĻžāϰāĻāĻŋ āϏāĻŽā§āĻĒāϰā§āĻā§ āĻāϰāĻ āĻŦāĻŋāϏā§āϤāĻžāϰāĻŋāϤ āĻāĻžāύāϤā§, <template-link>Storage Template</template-link> āĻāĻŦāĻ āĻāϰ <implications-link>āĻĒā§āϰāĻāĻžāĻŦāĻā§āϞ⧠(implications)</implications-link> āĻĻā§āĻā§āύāĨ¤",
|
||||
"storage_template_onboarding_description_v2": "āĻāĻāĻŋ āϏāĻā§āϰāĻŋā§ āĻĨāĻžāĻāϞā§, āĻĢāĻŋāĻāĻžāϰāĻāĻŋ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻāĻžāϰā§āϰ āύāĻŋāϰā§āϧāĻžāϰāĻŋāϤ āĻā§āĻŽāĻĒā§āϞā§āĻ āĻ
āύā§āϝāĻžā§ā§ āĻĢāĻžāĻāϞāĻā§āϞā§āĻā§ āϏā§āĻŦāϝāĻŧāĻāĻā§āϰāĻŋāϝāĻŧāĻāĻžāĻŦā§ āĻ
āϰā§āĻāĻžāύāĻžāĻāĻ (Auto-organize) āĻāϰāĻŦā§āĨ¤ āĻāϰāĻ āϤāĻĨā§āϝā§āϰ āĻāύā§āϝ āĻ
āύā§āĻā§āϰāĻš āĻāϰ⧠<link>āĻĄāĻā§āĻŽā§āύā§āĻā§āĻļāύ</link> āĻĻā§āĻā§āύāĨ¤",
|
||||
"storage_template_path_length": "āĻāύā§āĻŽāĻžāύāĻŋāĻ āĻĒāĻžāĻĨ āϞā§āύā§āĻĨ āϞāĻŋāĻŽāĻŋāĻ (Path length limit): <b>{length, number}</b>/{limit, number}",
|
||||
"storage_template_settings": "āϏā§āĻā§āϰā§āĻ āĻā§āĻŽāĻĒā§āϞā§āĻ (Storage Template)",
|
||||
"storage_template_settings_description": "āĻāĻĒāϞā§āĻĄ āĻāϰāĻž āĻ
ā§āϝāĻžāϏā§āĻā§āϰ āĻĢā§āϞā§āĻĄāĻžāϰ āϏā§āĻā§āϰāĻžāĻāĻāĻžāϰ āĻāĻŦāĻ āĻĢāĻžāĻāϞ āύā§āĻŽ āĻŽā§āϝāĻžāύā§āĻ āĻāϰā§āύ",
|
||||
"storage_template_user_label": "<code>{label}</code> āĻšāϞ⧠āĻŦā§āϝāĻŦāĻšāĻžāϰāĻāĻžāϰā§āϰ āϏā§āĻā§āϰā§āĻ āϞā§āĻŦā§āϞ (Storage Label)",
|
||||
"theme_settings_description": "āĻāĻŽāĻŋāĻ (Immich) āĻāϝāĻŧā§āĻŦ āĻāύā§āĻāĻžāϰāĻĢā§āϏā§āϰ āĻāĻžāϏā§āĻāĻŽāĻžāĻāĻā§āĻļāύ āĻŽā§āϝāĻžāύā§āĻ āĻāϰā§āύ",
|
||||
"thumbnail_generation_job": "āĻĨāĻžāĻŽā§āĻŦāύā§āĻāϞ āϤā§āϰāĻŋ āĻāϰā§āύ (Generate Thumbnails)",
|
||||
"thumbnail_generation_job_description": "āĻĒā§āϰāϤāĻŋāĻāĻŋ āĻ
ā§āϝāĻžāϏā§āĻā§āϰ āĻāύā§āϝ āĻŦā§, āĻā§āĻ āĻāĻŦāĻ āĻŦā§āϞāĻžāϰ (āĻ
āϏā§āĻĒāώā§āĻ) āĻĨāĻžāĻŽā§āĻŦāύā§āĻāϞ āϤā§āϰāĻŋ āĻāϰā§āύ, āϏā§āĻ āϏāĻžāĻĨā§ āĻĒā§āϰāϤāĻŋāĻāĻŋ āĻŦā§āϝāĻā§āϤāĻŋāϰ āĻāύā§āϝāĻ āĻĨāĻžāĻŽā§āĻŦāύā§āĻāϞ āϤā§āϰāĻŋ āĻāϰā§āύāĨ¤",
|
||||
"transcoding_acceleration_api": "āĻ
ā§āϝāĻžāĻā§āϏāĻŋāϞāĻžāϰā§āĻ āĻāĻĒāĻŋāĻāĻ (Acceleration API)",
|
||||
"transcoding_acceleration_api_description": "āĻā§āϰāĻžāύāϏāĻā§āĻĄāĻŋāĻ (transcoding) āĻĻā§āϰā§āϤ āĻāϰāĻžāϰ āĻāύā§āϝ āĻāĻĒāύāĻžāϰ āĻĄāĻŋāĻāĻžāĻāϏā§āϰ āϏāĻžāĻĨā§ āϝ⧠API āĻāύā§āĻāĻžāϰāĻ
ā§āϝāĻžāĻā§āĻ āĻāϰāĻŦā§āĨ¤ āĻāĻ āϏā§āĻāĻŋāĻāϏāĻāĻŋ 'āϏāĻžāϧā§āϝāĻŽāϤā§' (best effort) āĻāĻžāĻ āĻāϰāĻŦā§: āĻŦā§āϝāϰā§āĻĨ āĻšāϞ⧠āĻāĻāĻŋ āĻĒā§āύāϰāĻžā§ āϏāĻĢāĻāĻā§ā§āϝāĻžāϰ āĻā§āϰāĻžāύāϏāĻā§āĻĄāĻŋāĻā§ā§ āĻĢāĻŋāϰ⧠āĻāϏāĻŦā§āĨ¤ āĻšāĻžāϰā§āĻĄāĻā§ā§āϝāĻžāϰā§āϰ āĻāĻĒāϰ āĻāĻŋāϤā§āϤāĻŋ āĻāϰ⧠VP9 āĻāĻžāĻ āĻāϰāϤā§āĻ āĻĒāĻžāϰā§, āĻāĻŦāĻžāϰ āύāĻžāĻ āĻāϰāϤ⧠āĻĒāĻžāϰā§āĨ¤",
|
||||
"transcoding_acceleration_nvenc": "NVENC (NVIDIA GPU āĻĒā§āϰā§ā§āĻāύ)",
|
||||
"transcoding_acceleration_qsv": "Quick Sync (ā§āĻŽ āĻĒā§āϰāĻāύā§āĻŽā§āϰ āĻāύāĻā§āϞ CPU āĻŦāĻž āĻĒāϰāĻŦāϰā§āϤ⧠āĻāĻžāϰā§āϏāύ āĻĒā§āϰā§ā§āĻāύ)",
|
||||
"transcoding_acceleration_rkmpp": "RKMPP (āĻļā§āϧā§āĻŽāĻžāϤā§āϰ Rockchip SOC-āĻāϰ āĻāύā§āϝ)",
|
||||
"transcoding_acceleration_vaapi": "VA-API (āĻāĻŋāĻĄāĻŋāĻ āĻ
ā§āϝāĻžāĻā§āϏāĻŋāϞāĻžāϰā§āĻļāύ āĻāĻĒāĻŋāĻāĻ)",
|
||||
"transcoding_accepted_audio_codecs": "āĻā§āϰāĻšāĻŖāϝā§āĻā§āϝ āĻ
āĻĄāĻŋāĻ āĻā§āĻĄā§āĻāϏāĻŽā§āĻš (Accepted audio codecs)",
|
||||
"transcoding_accepted_audio_codecs_description": "āĻā§āύ āĻ
āĻĄāĻŋāĻ āĻā§āĻĄā§āĻāĻā§āϞ⧠āĻā§āϰāĻžāύāϏāĻā§āĻĄ āĻāϰāĻžāϰ āĻĒā§āϰā§ā§āĻāύ āύā§āĻ āϤāĻž āύāĻŋāϰā§āĻŦāĻžāĻāύ āĻāϰā§āύāĨ¤ āĻāĻāĻŋ āĻļā§āϧā§āĻŽāĻžāϤā§āϰ āύāĻŋāϰā§āĻĻāĻŋāώā§āĻ āĻā§āϰāĻžāύāϏāĻā§āĻĄ āĻĒāϞāĻŋāϏāĻŋāϰ (transcode policies) āĻāύā§āϝ āĻŦā§āϝāĻŦāĻšā§āϤ āĻšā§āĨ¤",
|
||||
"transcoding_accepted_containers": "āĻā§āϰāĻšāĻŖāϝā§āĻā§āϝ āĻāύā§āĻā§āĻāύāĻžāϰāϏāĻŽā§āĻš (Accepted containers)"
|
||||
},
|
||||
"yes": "āĻšā§āϝāĻžāĻ",
|
||||
"you_dont_have_any_shared_links": "āĻāĻĒāύāĻžāϰ āĻā§āύ⧠āĻļā§ā§āĻžāϰ āĻāϰāĻž āϞāĻŋāĻā§āĻ āύā§āĻ (You don't have any shared links)",
|
||||
"your_wifi_name": "āĻāĻĒāύāĻžāϰ āĻāϝāĻŧāĻžāĻ-āĻĢāĻžāĻ āĻāϰ āύāĻžāĻŽ (Your Wi-Fi name)",
|
||||
"zero_to_clear_rating": "āĻ
ā§āϝāĻžāϏā§āĻ āϰā§āĻāĻŋāĻ āĻŽā§āĻā§ āĻĢā§āϞāϤ⧠ā§Ļ āĻāĻžāĻĒā§āύ",
|
||||
"zoom_image": "āĻāĻŦāĻŋ āĻā§āĻŽ āĻāϰā§āύ (Zoom Image)",
|
||||
"zoom_to_bounds": "āĻŦāĻžāĻāύā§āĻĄāϏ āĻ
āύā§āϝāĻžā§ā§ āĻā§āĻŽ āĻāϰā§āύ (Zoom to bounds)"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user