@brftech/filex-core 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -159,3 +159,82 @@ export async function hydrateTrashRow(
159
159
  /* keep the bare row */
160
160
  }
161
161
  }
162
+
163
+ /**
164
+ * Sentinel path segment → locale key, for the virtual views the explorer parks
165
+ * in `dirname` (trash, recent, starred, shared). None has a real folder behind
166
+ * it, so anything that renders a path segment has to translate them or it
167
+ * prints the sentinel.
168
+ *
169
+ * ⚠ ONE map, deliberately. `.trash` predates the other three and its
170
+ * translation was written twice — once in the breadcrumb, once in the tab
171
+ * label. When recent/starred/shared arrived, only the breadcrumb copy was
172
+ * extended, so the tab strip read ".shared" in front of users (reported
173
+ * 2026-09-04). A second copy of a mapping is a second chance to forget it.
174
+ */
175
+ export const VIRTUAL_SEGMENTS: Record<string, string> = {
176
+ '.trash': 'node.trash',
177
+ '.recent': 'node.recent',
178
+ '.starred': 'node.starred',
179
+ '.shared': 'node.shared',
180
+ };
181
+
182
+ /**
183
+ * The tag view's sentinel: `.tag~<name>`, ONE segment, e.g. `.tag~invoices`.
184
+ *
185
+ * Every other virtual view has a fixed label, so a segment→locale-key map is
186
+ * enough for them. A tag's label is the tag itself, so it cannot live in that
187
+ * map — but it must not become a SECOND place that knows about sentinels
188
+ * either, which is exactly how the tab strip came to print `.shared`. Hence
189
+ * `virtualSegmentLabel()` below: the map keeps the static views, this prefix
190
+ * keeps the dynamic one, and every surface that renders a path segment calls
191
+ * the one function that knows both.
192
+ *
193
+ * ⚠ `~` rather than `:` or `/`. `writePersistedPath` runs each segment through
194
+ * `encodeURIComponent`, which leaves `~ - _ . ! * ' ( )` alone and escapes `:`
195
+ * — so `#.tag~invoices` stays readable in the address bar while `.tag:` would
196
+ * show as `#.tag%3Ainvoices`. And one segment rather than two (`.tag/name`)
197
+ * because a two-segment path gives the breadcrumb a clickable `.tag` parent
198
+ * crumb that leads nowhere.
199
+ */
200
+ export const TAG_SEGMENT_PREFIX = '.tag~';
201
+
202
+ /** `invoices` → `.tag~invoices`. */
203
+ export function makeTagSegment(tag: string): string {
204
+ return `${TAG_SEGMENT_PREFIX}${tag}`;
205
+ }
206
+
207
+ /** `.tag~invoices` → `invoices`; '' for anything else (incl. a bare `.tag~`). */
208
+ export function tagOfSegment(segment: string): string {
209
+ return segment.startsWith(TAG_SEGMENT_PREFIX)
210
+ ? segment.slice(TAG_SEGMENT_PREFIX.length)
211
+ : '';
212
+ }
213
+
214
+ /** True when `path` (user-facing form) IS a tag listing. */
215
+ export function tagOfPath(path: string): string {
216
+ const clean = (path || '').replace(/^\/+|\/+$/g, '');
217
+ return tagOfSegment(clean.split('/').pop() || clean);
218
+ }
219
+
220
+ /** The locale key for a STATIC sentinel segment, or '' otherwise. Prefer
221
+ * `virtualSegmentLabel` — a tag segment has no locale key to return. */
222
+ export function virtualSegmentKey(segment: string): string {
223
+ return VIRTUAL_SEGMENTS[segment] ?? '';
224
+ }
225
+
226
+ /**
227
+ * What a path segment READS AS: the translated view name for a static
228
+ * sentinel, `#<tag>` for a tag view, '' when it is an ordinary folder (the
229
+ * caller then shows the segment itself).
230
+ *
231
+ * `#` and not the bare name: a lone `invoices` crumb between `/` and nothing
232
+ * is indistinguishable from a folder called invoices. The glyph needs no
233
+ * translation and carries the tag's own name verbatim, which is the point.
234
+ */
235
+ export function virtualSegmentLabel(segment: string, t: (key: string) => string): string {
236
+ const key = VIRTUAL_SEGMENTS[segment];
237
+ if (key) return t(key);
238
+ const tag = tagOfSegment(segment);
239
+ return tag ? `#${tag}` : '';
240
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * star.ts — the ONE place that talks to `POST /api/files/manager/star`.
3
+ *
4
+ * Starring grew a second surface (the context menu, then the grid/gallery
5
+ * cards) and a menu entry cannot render a component, so the request had to
6
+ * come out of `StarButton.vue` — but only ONCE. `StarButton` calls this, the
7
+ * explorer's menu action calls this, and there is no third copy of the URL,
8
+ * the credentials rule or the payload shape anywhere.
9
+ *
10
+ * ⚠ `credentials` defaults to 'same-origin', never 'include': a credentialed
11
+ * cross-origin request may not be answered with `Access-Control-Allow-Origin:
12
+ * *`, which is what filex sends — hardcoding 'include' silently broke starring
13
+ * in every embed served from a different origin to the API (the desktop app is
14
+ * one). Same trap as `loadStarred`/`fetchNavRows` in FileExplorer.
15
+ */
16
+
17
+ export interface StarRequestOptions {
18
+ apiBase?: string;
19
+ authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
20
+ authCredentials?: RequestCredentials;
21
+ }
22
+
23
+ /** Toggle the starred flag for ONE node. Throws on a non-2xx answer so the
24
+ * caller can roll its optimistic state back. */
25
+ export async function setNodeStarred(
26
+ nodeId: number,
27
+ starred: boolean,
28
+ opts: StarRequestOptions = {},
29
+ ): Promise<void> {
30
+ const headers = {
31
+ 'Content-Type': 'application/json',
32
+ ...(await (opts.authHeaders ?? (() => ({})))()),
33
+ };
34
+ const base = opts.apiBase ?? '';
35
+ const res = await fetch(`${base}/api/files/manager/star`, {
36
+ method: 'POST',
37
+ headers,
38
+ credentials: opts.authCredentials ?? 'same-origin',
39
+ body: JSON.stringify({ node_id: nodeId, starred }),
40
+ });
41
+ if (!res.ok) throw new Error(`star toggle failed: ${res.status}`);
42
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * tags.ts — the list of tags that exist, for the navigation panel.
3
+ *
4
+ * `GET /api/files/manager/tags/all` is one query per call, and the panel is
5
+ * rendered by every mounted explorer: the web app, the desktop app and every
6
+ * embed on a page (work.example.com renders two side by side). Asking on each mount
7
+ * would multiply a database scan by however many explorers a host happens to
8
+ * put on screen, for a list that changes when somebody edits a tag — i.e.
9
+ * rarely.
10
+ *
11
+ * So: a MODULE-level cache, shared by every instance in the page.
12
+ * - in-flight requests are deduped, so N explorers mounting in the same tick
13
+ * produce ONE request;
14
+ * - the answer is reused for TTL_MS;
15
+ * - `invalidateTagCache()` drops it the moment tags are written, so the
16
+ * panel is never stale after the user's own edit — which is the only
17
+ * staleness a user can actually notice.
18
+ *
19
+ * Keyed by apiBase: the desktop app can point at a different server between
20
+ * mounts and must not be handed the previous server's tags.
21
+ */
22
+
23
+ const TTL_MS = 60_000;
24
+
25
+ interface CacheEntry {
26
+ at: number;
27
+ tags: string[];
28
+ }
29
+
30
+ const cache = new Map<string, CacheEntry>();
31
+ const inflight = new Map<string, Promise<string[]>>();
32
+
33
+ export interface TagListOptions {
34
+ apiBase?: string;
35
+ authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
36
+ authCredentials?: RequestCredentials;
37
+ /** Skip the cache (after a tag edit). */
38
+ force?: boolean;
39
+ }
40
+
41
+ /** Drop every cached tag list. Called after a successful tag write. */
42
+ export function invalidateTagCache(): void {
43
+ cache.clear();
44
+ inflight.clear();
45
+ }
46
+
47
+ /**
48
+ * Every distinct tag, alphabetical, as the backend returns them. Never
49
+ * throws: an older backend with no such route, or a caller with no
50
+ * permission, gets an empty list and the panel simply shows no tag section.
51
+ */
52
+ export async function fetchAllTags(opts: TagListOptions = {}): Promise<string[]> {
53
+ const base = opts.apiBase ?? '';
54
+ const key = base || '(same-origin)';
55
+ if (!opts.force) {
56
+ const hit = cache.get(key);
57
+ if (hit && Date.now() - hit.at < TTL_MS) return hit.tags;
58
+ const pending = inflight.get(key);
59
+ if (pending) return pending;
60
+ }
61
+ const run = (async () => {
62
+ try {
63
+ const res = await fetch(`${base}/api/files/manager/tags/all`, {
64
+ headers: await (opts.authHeaders ?? (() => ({})))(),
65
+ credentials: opts.authCredentials ?? 'same-origin',
66
+ });
67
+ if (!res.ok) return [];
68
+ const body = await res.json();
69
+ const tags: string[] = Array.isArray(body?.tags)
70
+ ? body.tags.filter((x: unknown): x is string => typeof x === 'string' && x !== '')
71
+ : [];
72
+ cache.set(key, { at: Date.now(), tags });
73
+ return tags;
74
+ } catch {
75
+ return [];
76
+ } finally {
77
+ inflight.delete(key);
78
+ }
79
+ })();
80
+ inflight.set(key, run);
81
+ return run;
82
+ }
83
+
84
+ /**
85
+ * Nodes carrying `tag`, as raw node rows (`{id, path, name, type, …}`) — the
86
+ * same shape `star/list` and `recent` answer with, so the caller maps them
87
+ * through its own `nodeRowToFileNode`.
88
+ */
89
+ export async function fetchTaggedRows(
90
+ tag: string,
91
+ opts: TagListOptions = {},
92
+ limit = 200,
93
+ ): Promise<Record<string, unknown>[]> {
94
+ const base = opts.apiBase ?? '';
95
+ const res = await fetch(
96
+ `${base}/api/files/manager/tagged?tag=${encodeURIComponent(tag)}&limit=${limit}`,
97
+ {
98
+ headers: await (opts.authHeaders ?? (() => ({})))(),
99
+ credentials: opts.authCredentials ?? 'same-origin',
100
+ },
101
+ );
102
+ if (!res.ok) throw new Error(String(res.status));
103
+ const body = await res.json();
104
+ return Array.isArray(body?.nodes) ? body.nodes : [];
105
+ }
package/src/locales/en.ts CHANGED
@@ -186,6 +186,7 @@ export const en: Record<string, string> = {
186
186
  'shortcuts.toggle_hidden': 'Show/hide hidden files',
187
187
  'shortcuts.help': 'This help dialog',
188
188
  'shortcuts.select_all': 'Select all',
189
+ 'shortcuts.star': 'Star / unstar',
189
190
  'shortcuts.rename': 'Rename',
190
191
  'shortcuts.delete': 'Delete',
191
192
  'shortcuts.cut': 'Cut',
@@ -227,6 +228,10 @@ export const en: Record<string, string> = {
227
228
  /* === koru:k1 === */
228
229
  'toolbar.inspector': 'Details',
229
230
  'ctx.details': 'Details',
231
+ /* yildiz:s1 — starring is an action, so it has a verb in the menu. */
232
+ 'ctx.star': 'Star',
233
+ 'ctx.unstar': 'Unstar',
234
+ 'star.failed': 'Could not change the star',
230
235
  'shortcuts.inspector': 'Toggle details panel',
231
236
  'inspector.title': 'Details',
232
237
  'inspector.close': 'Close',
@@ -403,8 +408,8 @@ export const en: Record<string, string> = {
403
408
  'e2e.create.pw2_placeholder': 'Repeat the password',
404
409
  'e2e.create.warn_title': 'NO WAY BACK',
405
410
  'e2e.create.warn_body':
406
- 'Files open ONLY with this password. The password is NEVER stored on the server; if you forget it the content is lost forever. There is no recovery not even an admin can open it.',
407
- 'e2e.create.ack': 'I understand: if I lose the password my data cannot be recovered.',
411
+ 'Files in this folder can only be opened with this password or the recovery key shown once when the folder is created. The password is NEVER stored on the server, and filex keeps no copy of the recovery key either. Lose both and the contents are gone for good.',
412
+ 'e2e.create.ack': 'I understand: if I lose both the password and the recovery key, my data cannot be recovered.',
408
413
  'e2e.create.ack_required': 'Please confirm the warning to continue.',
409
414
  'e2e.create.pw_short': 'Password must be at least 8 characters.',
410
415
  'e2e.create.pw_mismatch': 'Passwords do not match.',
@@ -428,6 +433,63 @@ export const en: Record<string, string> = {
428
433
  'e2e.upload.locked': 'Unlock the folder first',
429
434
  'e2e.decrypting': 'Decrypting…',
430
435
  'e2e.decrypt_failed': 'Could not decrypt the file (password changed or file corrupted).',
436
+ /* wiring:e2 recovery — recovery keys + operator escrow */
437
+ 'e2e.create.escrow_title': 'This server holds a second key',
438
+ 'e2e.create.escrow_body':
439
+ 'Key escrow is enabled on this installation, so its operator can open this folder without your password. Using that key notifies you. Escrow is fixed when the server is installed and cannot be turned off for a folder.',
440
+ 'e2e.locked.use_recovery': 'Lost the password? Use a recovery key',
441
+ 'e2e.recovery.title': 'Save your recovery key',
442
+ 'e2e.recovery.title_upgraded': 'Your recovery key',
443
+ 'e2e.recovery.lead':
444
+ 'This key opens the folder without its password. It is shown once — filex does not store it and cannot show it again.',
445
+ 'e2e.recovery.lead_upgraded':
446
+ 'This folder now has a recovery key. It opens the folder without its password, is shown once, and is not stored by filex.',
447
+ 'e2e.recovery.copy': 'Copy',
448
+ 'e2e.recovery.copied': 'Copied',
449
+ 'e2e.recovery.download': 'Download as a file',
450
+ 'e2e.recovery.warn_title': 'TREAT THIS LIKE A PASSWORD',
451
+ 'e2e.recovery.warn_body':
452
+ 'Anyone holding this key can read the folder. Keep it somewhere separate from the password — a key stored next to the password protects you from forgetting, not from anyone else.',
453
+ 'e2e.recovery.escrow_title': 'This server also holds a key',
454
+ 'e2e.recovery.escrow_body':
455
+ 'Key escrow is enabled here, so the operator of this installation can open this folder without your password. You are notified when that key is used.',
456
+ 'e2e.recovery.escrow_kid': 'Escrow key',
457
+ 'e2e.recovery.ack': 'I have saved this key somewhere safe.',
458
+ 'e2e.recovery.done': 'Done',
459
+ 'e2e.recover.title': 'Unlock without the password',
460
+ 'e2e.recover.none':
461
+ 'This folder was created before recovery keys existed, so it has none. Its password is the only way in. Unlock it with the password once and filex will offer to add a recovery key.',
462
+ 'e2e.recover.tab_recovery': 'Recovery key',
463
+ 'e2e.recover.tab_escrow': 'Escrow key',
464
+ 'e2e.recover.recovery_hint': 'The key shown once when this folder was created.',
465
+ 'e2e.recover.recovery_placeholder': 'XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX',
466
+ 'e2e.recover.escrow_warn_title': 'The owner will be told',
467
+ 'e2e.recover.escrow_warn_body':
468
+ 'Opening a folder with the escrow key notifies its owner. Use it only when you are entitled to.',
469
+ 'e2e.recover.escrow_kid': 'Escrow key',
470
+ 'e2e.recover.escrow_placeholder': 'Paste the escrow private key (PKCS#8, base64 or PEM)',
471
+ 'e2e.recover.bad_format':
472
+ 'That does not look like a recovery key. It is 32 characters in eight groups of four.',
473
+ 'e2e.recover.escrow_required': 'Paste the escrow private key.',
474
+ 'e2e.recover.wrong_recovery': 'That recovery key does not open this folder.',
475
+ 'e2e.recover.wrong_escrow':
476
+ 'That escrow key does not open this folder. Folders created before escrow was enabled have no escrow key, and cannot be given one.',
477
+ 'e2e.recover.bad_escrow_key': 'That is not a readable private key (expected PKCS#8, base64 or PEM).',
478
+ 'e2e.recover.notify_failed':
479
+ 'Could not notify the folder owner, so the folder was not unlocked. Escrow use is always announced.',
480
+ 'e2e.recover.recovery_done': 'Unlocked with the recovery key',
481
+ 'e2e.recover.escrow_done': 'Unlocked with the escrow key — the owner has been notified',
482
+ 'e2e.recover.unlock': 'Unlock',
483
+ 'e2e.recover.busy': 'Unlocking…',
484
+ 'e2e.upgrade.title': 'This folder has no recovery key',
485
+ 'e2e.upgrade.body':
486
+ 'It was created before recovery keys existed, so its password is the only way in. Right now — and only right now, while the password is in memory — filex can add one. Your files are not re-encrypted or moved.',
487
+ 'e2e.upgrade.escrow_note':
488
+ 'Note: this installation has key escrow enabled, so adding a recovery key also lets its operator open this folder without your password.',
489
+ 'e2e.upgrade.accept': 'Create a recovery key',
490
+ 'e2e.upgrade.decline': 'Not now',
491
+ 'e2e.upgrade.busy': 'Creating…',
492
+ 'e2e.upgrade.failed': 'Could not add a recovery key to this folder',
431
493
  'e2e.download.failed': 'Could not download the encrypted file.',
432
494
  /* /wiring:e2 */
433
495
 
@@ -863,6 +925,13 @@ export const en: Record<string, string> = {
863
925
  'empty.starred.hint': 'Star a file to find it here',
864
926
  'empty.shared.title': 'Nothing has been shared with you',
865
927
  'empty.shared.hint': 'Folders and files other people share appear here',
928
+ /* etiket:t1 */
929
+ 'sidenav.tags': 'Tags',
930
+ 'sidenav.tags.empty': 'No tags yet — add one from a file’s right-click menu.',
931
+ 'sidenav.tags.more': 'Show {count} more',
932
+ 'sidenav.tags.less': 'Show fewer',
933
+ 'empty.tag.title': 'Nothing is tagged “{tag}”',
934
+ 'empty.tag.hint': 'Right-click a file and choose Tags to put it here',
866
935
  'sidenav.connections': 'Connections',
867
936
  'sidenav.connect': 'How to connect',
868
937
  'sidenav.apikeys': 'API keys',
package/src/locales/tr.ts CHANGED
@@ -186,6 +186,7 @@ export const tr: Record<string, string> = {
186
186
  'shortcuts.toggle_hidden': 'Gizli dosyaları göster/gizle',
187
187
  'shortcuts.help': 'Bu yardım penceresi',
188
188
  'shortcuts.select_all': 'Tümünü seç',
189
+ 'shortcuts.star': 'Yıldızla / yıldızı kaldır',
189
190
  'shortcuts.rename': 'Yeniden adlandır',
190
191
  'shortcuts.delete': 'Sil',
191
192
  'shortcuts.cut': 'Kes',
@@ -227,6 +228,10 @@ export const tr: Record<string, string> = {
227
228
  /* === koru:k1 === */
228
229
  'toolbar.inspector': 'Ayrıntılar',
229
230
  'ctx.details': 'Ayrıntılar',
231
+ /* yildiz:s1 — yıldızlamak bir eylem, menüde de öyle görünür. */
232
+ 'ctx.star': 'Yıldızla',
233
+ 'ctx.unstar': 'Yıldızı kaldır',
234
+ 'star.failed': 'Yıldız değiştirilemedi',
230
235
  'shortcuts.inspector': 'Ayrıntılar panelini aç/kapat',
231
236
  'inspector.title': 'Ayrıntılar',
232
237
  'inspector.close': 'Kapat',
@@ -403,8 +408,8 @@ export const tr: Record<string, string> = {
403
408
  'e2e.create.pw2_placeholder': 'Parolayı tekrar girin',
404
409
  'e2e.create.warn_title': 'GERİ DÖNÜŞÜ YOK',
405
410
  'e2e.create.warn_body':
406
- 'Dosyalar yalnız bu parolayla açılır. Parola sunucuda TUTULMAZ; unutursanız içerik sonsuza dek kaybolur. Kurtarma yolu yoktur yönetici dahil kimse açamaz.',
407
- 'e2e.create.ack': 'Anladım: parolayı kaybedersem verilerim kurtarılamaz.',
411
+ 'Bu klasördeki dosyalar yalnız bu parolayla ya da klasör oluşturulurken bir kez gösterilen kurtarma anahtarıyla açılır. Parola sunucuda TUTULMAZ; kurtarma anahtarının da filex\'te kopyası kalmaz. İkisini de kaybederseniz içerik sonsuza dek gider.',
412
+ 'e2e.create.ack': 'Anladım: parolayı da kurtarma anahtarını da kaybedersem verilerim kurtarılamaz.',
408
413
  'e2e.create.ack_required': 'Devam etmek için uyarıyı onaylayın.',
409
414
  'e2e.create.pw_short': 'Parola en az 8 karakter olmalı.',
410
415
  'e2e.create.pw_mismatch': 'Parolalar birbirini tutmuyor.',
@@ -428,6 +433,63 @@ export const tr: Record<string, string> = {
428
433
  'e2e.upload.locked': 'Önce klasörün kilidini açın',
429
434
  'e2e.decrypting': 'Çözülüyor…',
430
435
  'e2e.decrypt_failed': 'Dosya çözülemedi (parola değişmiş ya da dosya bozulmuş olabilir).',
436
+ /* wiring:e2 recovery — kurtarma anahtarı + yönetici escrow'u */
437
+ 'e2e.create.escrow_title': 'Bu sunucunun da bir anahtarı var',
438
+ 'e2e.create.escrow_body':
439
+ 'Bu kurulumda anahtar emaneti (escrow) açık: sunucunun işletmecisi bu klasörü parolanız olmadan açabilir. O anahtar kullanıldığında size bildirim gelir. Escrow sunucu kurulurken sabitlenir; bir klasör için kapatılamaz.',
440
+ 'e2e.locked.use_recovery': 'Parolayı mı kaybettiniz? Kurtarma anahtarı kullanın',
441
+ 'e2e.recovery.title': 'Kurtarma anahtarınızı saklayın',
442
+ 'e2e.recovery.title_upgraded': 'Kurtarma anahtarınız',
443
+ 'e2e.recovery.lead':
444
+ 'Bu anahtar klasörü parolasız açar. Yalnız bir kez gösterilir — filex bu anahtarı saklamaz ve bir daha gösteremez.',
445
+ 'e2e.recovery.lead_upgraded':
446
+ 'Bu klasörün artık bir kurtarma anahtarı var. Klasörü parolasız açar, yalnız bir kez gösterilir ve filex\'te kopyası kalmaz.',
447
+ 'e2e.recovery.copy': 'Kopyala',
448
+ 'e2e.recovery.copied': 'Kopyalandı',
449
+ 'e2e.recovery.download': 'Dosya olarak indir',
450
+ 'e2e.recovery.warn_title': 'BUNU PAROLA GİBİ SAKLAYIN',
451
+ 'e2e.recovery.warn_body':
452
+ 'Bu anahtarı eline geçiren herkes klasörü okuyabilir. Parolanızdan ayrı bir yerde tutun — parolanın yanında duran bir anahtar sizi unutmaya karşı korur, başkasına karşı değil.',
453
+ 'e2e.recovery.escrow_title': 'Bu sunucunun da bir anahtarı var',
454
+ 'e2e.recovery.escrow_body':
455
+ 'Bu kurulumda anahtar emaneti açık: sunucunun işletmecisi bu klasörü parolanız olmadan açabilir. O anahtar kullanıldığında size bildirim gelir.',
456
+ 'e2e.recovery.escrow_kid': 'Emanet anahtarı',
457
+ 'e2e.recovery.ack': 'Bu anahtarı güvenli bir yere kaydettim.',
458
+ 'e2e.recovery.done': 'Tamam',
459
+ 'e2e.recover.title': 'Parolasız kilit açma',
460
+ 'e2e.recover.none':
461
+ 'Bu klasör kurtarma anahtarları eklenmeden önce oluşturulmuş, bu yüzden anahtarı yok. Tek giriş yolu parolası. Bir kez parolayla açın, filex kurtarma anahtarı eklemeyi önerecek.',
462
+ 'e2e.recover.tab_recovery': 'Kurtarma anahtarı',
463
+ 'e2e.recover.tab_escrow': 'Emanet anahtarı',
464
+ 'e2e.recover.recovery_hint': 'Klasör oluşturulurken bir kez gösterilen anahtar.',
465
+ 'e2e.recover.recovery_placeholder': 'XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX',
466
+ 'e2e.recover.escrow_warn_title': 'Klasörün sahibine haber gidecek',
467
+ 'e2e.recover.escrow_warn_body':
468
+ 'Emanet anahtarıyla açılan klasörün sahibine bildirim gönderilir. Yalnız yetkiniz olduğunda kullanın.',
469
+ 'e2e.recover.escrow_kid': 'Emanet anahtarı',
470
+ 'e2e.recover.escrow_placeholder': 'Emanet özel anahtarını yapıştırın (PKCS#8, base64 ya da PEM)',
471
+ 'e2e.recover.bad_format':
472
+ 'Bu bir kurtarma anahtarına benzemiyor. Dörtlü sekiz grup, toplam 32 karakter olmalı.',
473
+ 'e2e.recover.escrow_required': 'Emanet özel anahtarını yapıştırın.',
474
+ 'e2e.recover.wrong_recovery': 'Bu kurtarma anahtarı bu klasörü açmıyor.',
475
+ 'e2e.recover.wrong_escrow':
476
+ 'Bu emanet anahtarı bu klasörü açmıyor. Escrow açılmadan önce oluşturulmuş klasörlerin emanet anahtarı yoktur ve sonradan da eklenemez.',
477
+ 'e2e.recover.bad_escrow_key': 'Bu okunabilir bir özel anahtar değil (PKCS#8, base64 ya da PEM bekleniyor).',
478
+ 'e2e.recover.notify_failed':
479
+ 'Klasör sahibine bildirim gönderilemedi, bu yüzden kilit açılmadı. Emanet anahtarının kullanımı her zaman duyurulur.',
480
+ 'e2e.recover.recovery_done': 'Kurtarma anahtarıyla açıldı',
481
+ 'e2e.recover.escrow_done': 'Emanet anahtarıyla açıldı — klasör sahibine bildirildi',
482
+ 'e2e.recover.unlock': 'Kilidi aç',
483
+ 'e2e.recover.busy': 'Açılıyor…',
484
+ 'e2e.upgrade.title': 'Bu klasörün kurtarma anahtarı yok',
485
+ 'e2e.upgrade.body':
486
+ 'Kurtarma anahtarları eklenmeden önce oluşturulmuş, bu yüzden tek giriş yolu parolası. Şu anda — ve yalnız şu anda, parola bellekteyken — filex bir tane ekleyebilir. Dosyalarınız yeniden şifrelenmez, hiçbir yere taşınmaz.',
487
+ 'e2e.upgrade.escrow_note':
488
+ 'Not: bu kurulumda anahtar emaneti açık; kurtarma anahtarı eklemek aynı zamanda işletmecinin bu klasörü parolanız olmadan açabilmesi demektir.',
489
+ 'e2e.upgrade.accept': 'Kurtarma anahtarı oluştur',
490
+ 'e2e.upgrade.decline': 'Şimdi değil',
491
+ 'e2e.upgrade.busy': 'Oluşturuluyor…',
492
+ 'e2e.upgrade.failed': 'Bu klasöre kurtarma anahtarı eklenemedi',
431
493
  'e2e.download.failed': 'Şifreli dosya indirilemedi.',
432
494
  /* /wiring:e2 */
433
495
 
@@ -866,6 +928,13 @@ export const tr: Record<string, string> = {
866
928
  'empty.starred.hint': 'Bir dosyayı yıldızla, burada bulasın',
867
929
  'empty.shared.title': 'Seninle henüz bir şey paylaşılmadı',
868
930
  'empty.shared.hint': 'Başkalarının paylaştığı klasör ve dosyalar burada görünür',
931
+ /* etiket:t1 */
932
+ 'sidenav.tags': 'Etiketler',
933
+ 'sidenav.tags.empty': 'Henüz etiket yok — bir dosyaya sağ tıklayıp Etiketler’den ekleyebilirsin.',
934
+ 'sidenav.tags.more': '{count} tane daha göster',
935
+ 'sidenav.tags.less': 'Daha az göster',
936
+ 'empty.tag.title': '“{tag}” etiketli hiçbir şey yok',
937
+ 'empty.tag.hint': 'Bir dosyaya sağ tıklayıp Etiketler’i seç, burada görünsün',
869
938
  'sidenav.connections': 'Bağlantılar',
870
939
  'sidenav.connect': 'Nasıl bağlanılır',
871
940
  'sidenav.apikeys': 'API anahtarları',