@brftech/filex-core 0.32.0 → 0.34.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.
Files changed (55) hide show
  1. package/README.md +23 -0
  2. package/dist/{CsvViewer-DxujFEM3.js → CsvViewer-CqWeV8VO.js} +22 -22
  3. package/dist/CsvViewer-CqWeV8VO.js.map +1 -0
  4. package/dist/{DrawioViewer-DCvWnX2t.js → DrawioViewer-BNALOB04.js} +16 -16
  5. package/dist/DrawioViewer-BNALOB04.js.map +1 -0
  6. package/dist/filex-core.js +5721 -5615
  7. package/dist/filex-core.js.map +1 -1
  8. package/dist/filex-core.umd.cjs +44 -44
  9. package/dist/filex-core.umd.cjs.map +1 -1
  10. package/dist/index.d.ts +49 -8
  11. package/package.json +1 -1
  12. package/src/FileExplorer.vue +267 -235
  13. package/src/components/CommandPalette.vue +3 -3
  14. package/src/components/ConnectionsPanel.vue +2 -1
  15. package/src/components/GalleryView.vue +3 -3
  16. package/src/components/GridView.vue +4 -4
  17. package/src/components/ListView.vue +4 -4
  18. package/src/components/NFSExportsPanel.vue +2 -1
  19. package/src/components/PendingOpsTray.vue +1 -1
  20. package/src/components/PresenceBar.vue +3 -2
  21. package/src/components/S3KeysPanel.vue +2 -1
  22. package/src/components/SSHKeysPanel.vue +2 -1
  23. package/src/components/SecondaryPane.vue +13 -12
  24. package/src/components/StarButton.vue +2 -1
  25. package/src/components/TabBar.vue +2 -2
  26. package/src/components/ThemeGallery.vue +1 -1
  27. package/src/components/TokensPanel.vue +2 -1
  28. package/src/components/Toolbar.vue +9 -9
  29. package/src/composables/useFileApi.ts +4 -2
  30. package/src/composables/useLocale.ts +1 -1
  31. package/src/composables/useOperations.ts +3 -3
  32. package/src/composables/usePendingOps.ts +1 -1
  33. package/src/composables/useRealtime.ts +12 -15
  34. package/src/composables/useTabs.ts +1 -1
  35. package/src/composables/useUploadChunked.ts +39 -7
  36. package/src/index.ts +2 -2
  37. package/src/lib/burstDebounce.ts +57 -0
  38. package/src/lib/themes.ts +7 -7
  39. package/src/lib/transfer.ts +6 -6
  40. package/src/locales/en.ts +32 -0
  41. package/src/locales/index.ts +1 -0
  42. package/src/locales/resolve.ts +66 -0
  43. package/src/locales/tr.ts +33 -0
  44. package/src/modals/ConvertModal.vue +14 -12
  45. package/src/modals/NewFolderModal.vue +1 -1
  46. package/src/modals/PermissionsModal.vue +12 -11
  47. package/src/modals/PreviewModal.vue +9 -9
  48. package/src/styles/base.css +29 -26
  49. package/src/styles/variables.css +1 -1
  50. package/src/types/ExplorerConfig.ts +1 -1
  51. package/src/types/FileNode.ts +1 -1
  52. package/src/viewers/CsvViewer.vue +4 -4
  53. package/src/viewers/DrawioViewer.vue +2 -2
  54. package/dist/CsvViewer-DxujFEM3.js.map +0 -1
  55. package/dist/DrawioViewer-DCvWnX2t.js.map +0 -1
@@ -87,12 +87,12 @@ import { useOperations } from './composables/useOperations';
87
87
  /* wiring:c4 */
88
88
  import OnboardingTour from './components/OnboardingTour.vue';
89
89
  /* /wiring:c4 */
90
- /* wiring:d1 — sekmeler + tab başına split */
90
+ /* wiring:d1 — tabs + per-tab split */
91
91
  import TabBar from './components/TabBar.vue';
92
92
  import SecondaryPane from './components/SecondaryPane.vue';
93
93
  import { useTabs, type TabState } from './composables/useTabs';
94
94
  /* /wiring:d1 */
95
- /* wiring:e2 — uçtan uca şifreli klasörler (docs/E2E-ENCRYPTION.md) */
95
+ /* wiring:e2 — end-to-end encrypted folders (docs/E2E-ENCRYPTION.md) */
96
96
  import EncryptedFolderModal from './components/EncryptedFolderModal.vue';
97
97
  import RecoveryKeyModal from './components/RecoveryKeyModal.vue';
98
98
  import E2eRecoveryUnlockModal from './components/E2eRecoveryUnlockModal.vue';
@@ -158,6 +158,7 @@ import ShareModal from './modals/ShareModal.vue';
158
158
  import PreviewModal from './modals/PreviewModal.vue';
159
159
  import ConvertModal from './modals/ConvertModal.vue';
160
160
  import PermissionsModal from './modals/PermissionsModal.vue';
161
+ import { resolveLocale } from './locales/resolve';
161
162
 
162
163
  const props = defineProps<{
163
164
  config: ExplorerConfig;
@@ -187,7 +188,7 @@ const api = useFileApi(props.config);
187
188
  // Locale up-front: the pendingOps onSettled callback below (and the undo-toast
188
189
  // helpers) need `t()` at runtime, so the catalogue must be constructed before
189
190
  // they are wired. Depends only on props — safe this early.
190
- const locale = computed(() => props.config.locale || 'tr');
191
+ const locale = computed(() => resolveLocale(props.config.locale));
191
192
  const { t } = useLocale(locale);
192
193
 
193
194
  // Live collaboration (WebSocket file-change events + presence), bundled into the
@@ -239,20 +240,20 @@ const pendingOps = usePendingOps(props.config, api, {
239
240
  const undo = opUndo.get(op.id);
240
241
  opUndo.delete(op.id);
241
242
  if (op.status === 'error') {
242
- flashToast(op.error_message || 'İşlem başarısız');
243
+ flashToast(op.error_message || t('toast.failed'));
243
244
  } else if (undo) {
244
245
  undoToast(`${undo.message} (${op.progress_total})`, undo.fn);
245
246
  } else {
246
247
  const verb =
247
248
  op.op_type === 'copy'
248
- ? 'Kopyalandı'
249
+ ? t('toast.copied')
249
250
  : op.op_type === 'move'
250
- ? 'Taşındı'
251
- : 'Silindi';
251
+ ? t('toast.moved')
252
+ : t('toast.deleted');
252
253
  flashToast(`${verb} (${op.progress_total})`);
253
254
  }
254
255
  void load();
255
- void splitPaneRef.value?.reload(); /* wiring:d1 — ikincil panel de tazelenir */
256
+ void splitPaneRef.value?.reload(); /* wiring:d1 — refresh the secondary pane too */
256
257
  },
257
258
  });
258
259
 
@@ -661,8 +662,9 @@ const showDelete = ref(false);
661
662
  const showShare = ref(false);
662
663
  const showPreview = ref(false);
663
664
  const renameTarget = ref<FileNode | null>(null);
664
- /* ui-fix — açık olan rename/delete/new-folder modal yan panele mi ait?
665
- * (menü ana panelle aynı; mutasyonlar doğru panele yönlensin diye.) */
665
+ /* ui-fix — does the open rename/delete/new-folder modal belong to the side
666
+ * pane? (the menu is identical to the main pane's; this routes the mutation
667
+ * to the right one.) */
666
668
  const mutationInPane = ref(false);
667
669
  const shareTarget = ref<FileNode | null>(null);
668
670
  const activeShare = ref<(ShareInfo & { url: string; filename?: string }) | null>(null);
@@ -1506,7 +1508,7 @@ async function load(path?: string) {
1506
1508
  currentPath.value = '';
1507
1509
  adapter.value = '';
1508
1510
  dirname.value = '';
1509
- e2eRoot.value = ''; /* wiring:e2 — sanal kökte kilit ekranı olmaz */
1511
+ e2eRoot.value = ''; /* wiring:e2 — no lock screen at the virtual root */
1510
1512
  files.value = virtualStorageRows();
1511
1513
  return;
1512
1514
  }
@@ -1553,7 +1555,7 @@ async function load(path?: string) {
1553
1555
  // show the dedicated not-found state instead of a toast over a stale
1554
1556
  // listing that reads as "this folder is empty".
1555
1557
  notFoundPath.value = String(requested);
1556
- e2eRoot.value = ''; /* wiring:e2 — ölü linkte kilit ekranı kalmasın */
1558
+ e2eRoot.value = ''; /* wiring:e2 — no lock screen left over on a dead link */
1557
1559
  files.value = [];
1558
1560
  emit('error', { message: e, context: { path } });
1559
1561
  return;
@@ -1599,7 +1601,7 @@ async function loadTrash() {
1599
1601
  loading.value = true;
1600
1602
  trashOrigin.value = adapter.value || '';
1601
1603
  trashMode.value = true;
1602
- e2eRoot.value = ''; /* wiring:e2 — çöp görünümü şifreli bağlamın dışındadır */
1604
+ e2eRoot.value = ''; /* wiring:e2 — the trash view is outside the encrypted context */
1603
1605
  selection.clear();
1604
1606
  try {
1605
1607
  const { entries } = await api.listTrash();
@@ -1678,10 +1680,10 @@ function wireParent(p: string): string {
1678
1680
  return slash === -1 ? prefix : prefix + rel.slice(0, slash);
1679
1681
  }
1680
1682
 
1681
- /* ui-fix — iki wire yolun AYNI dizini gösterip göstermediği (trailing-slash
1682
- * ve kök `adapter://` güvenli). Bir öğeyi ZATEN bulunduğu klasöre bırakma
1683
- * (source parent === target) no-op olmalı; aksi halde backend "kendine
1684
- * kopyala" 400'ü döner. */
1683
+ /* ui-fix — do two wire paths point at the SAME directory (safe against a
1684
+ * trailing slash and against the bare `adapter://` root)? Dropping an item
1685
+ * into the folder it is ALREADY in (source parent === target) must be a
1686
+ * no-op; otherwise the backend answers with a "copy onto itself" 400. */
1685
1687
  function sameDir(a: string, b: string): boolean {
1686
1688
  const norm = (s: string) => {
1687
1689
  const i = s.indexOf('://');
@@ -1931,7 +1933,7 @@ async function openSearchHit(hit: GlobalSearchHit) {
1931
1933
 
1932
1934
  useKeyboardShortcuts(rootEl, {
1933
1935
  onDelete: () => {
1934
- /* ui-fix — kısayol da aktif panele gider (menüyle tutarlı). */
1936
+ /* ui-fix — the shortcut goes to the active pane too (consistent with the menu). */
1935
1937
  if (paneIsActive.value) {
1936
1938
  const psel = splitPaneRef.value?.selectedNodes() ?? [];
1937
1939
  if (psel.length) {
@@ -2011,7 +2013,7 @@ useKeyboardShortcuts(rootEl, {
2011
2013
  onToggleHidden: () => toggleHiddenFiles(),
2012
2014
  onStar: () => void toggleStar(selection.nodes.value) /* yildiz:s1 */,
2013
2015
  onToggleInspector: () => toggleInspector() /* koru:k1 */,
2014
- /* wiring:d1 — sekme aksiyonları (registry: tab-new/close/next/prev) */
2016
+ /* wiring:d1 — tab actions (registry: tab-new/close/next/prev) */
2015
2017
  onTabNew: () => newTabHere(),
2016
2018
  onTabClose: () => closeTabById(tabsActiveId.value),
2017
2019
  onTabNext: () => nextTab(),
@@ -2060,9 +2062,10 @@ function openNode(n: FileNode) {
2060
2062
  void load(target);
2061
2063
  return;
2062
2064
  }
2063
- /* wiring:e2 — şifreli klasörde dosya açma: kilit açıksa çöz + salt-okunur
2064
- önizleme (blob URL mevcut viewer'lara gider); kilitliyken hiçbir şey
2065
- açılmaz (kilit ekranı zaten listeyi kapatır). */
2065
+ /* wiring:e2 — opening a file inside an encrypted folder: while unlocked,
2066
+ decrypt + show a read-only preview (the blob URL feeds the existing
2067
+ viewers); while locked nothing opens at all (the lock screen already
2068
+ hides the listing). */
2066
2069
  if (e2eUnlocked.value && n.type === 'file') {
2067
2070
  void e2eOpenPreview(n);
2068
2071
  return;
@@ -2126,7 +2129,7 @@ async function restoreSelection(targets?: FileNode[]) {
2126
2129
  .map((n) => (n as { id?: number }).id)
2127
2130
  .filter((x): x is number => typeof x === 'number');
2128
2131
  const { restored } = await api.restoreIds(ids);
2129
- flashToast(`${restored} öğe geri getirildi`);
2132
+ flashToast(t('toast.restored', { n: restored }));
2130
2133
  selection.clear();
2131
2134
  await loadTrash();
2132
2135
  return;
@@ -2135,7 +2138,7 @@ async function restoreSelection(targets?: FileNode[]) {
2135
2138
  if (!api.endpoints.restore) return;
2136
2139
  const items = nodes.map((n) => n.path); // qualified
2137
2140
  const { restored } = await api.restore(items);
2138
- flashToast(`${restored} öğe geri getirildi`);
2141
+ flashToast(t('toast.restored', { n: restored }));
2139
2142
  selection.clear();
2140
2143
  await load();
2141
2144
  } catch (err) {
@@ -2144,7 +2147,7 @@ async function restoreSelection(targets?: FileNode[]) {
2144
2147
  }
2145
2148
 
2146
2149
  function previewNode(n: FileNode) {
2147
- /* wiring:e2 — önizleme de çözülmüş blob'dan beslenir */
2150
+ /* wiring:e2 — the preview is fed from the decrypted blob as well */
2148
2151
  if (e2eUnlocked.value && n.type === 'file') {
2149
2152
  void e2eOpenPreview(n);
2150
2153
  return;
@@ -2171,8 +2174,9 @@ function openNodeInNewTab(n: FileNode) {
2171
2174
  void load(target);
2172
2175
  return;
2173
2176
  }
2174
- /* wiring:e2 — standalone editör rotası sunucudan HAM (şifreli) baytı
2175
- çeker; şifreli klasörde her "Aç" in-page çözülmüş önizlemeye iner. */
2177
+ /* wiring:e2 — the standalone editor route pulls the RAW (encrypted) bytes
2178
+ from the server, so inside an encrypted folder every "Aç" falls back to
2179
+ the in-page decrypted preview. */
2176
2180
  if (e2eActive.value) {
2177
2181
  if (e2eUnlocked.value) void e2eOpenPreview(n);
2178
2182
  return;
@@ -2207,10 +2211,10 @@ function openNodeInNewTab(n: FileNode) {
2207
2211
  void markRecent(n);
2208
2212
  }
2209
2213
 
2210
- type ContextMode = 'selection' | 'breadcrumb' | 'pane' /* ui-fix — yan panel sağ-tık */;
2214
+ type ContextMode = 'selection' | 'breadcrumb' | 'pane' /* ui-fix — side-pane right-click */;
2211
2215
  const ctxMode = ref<ContextMode>('selection');
2212
2216
  const breadcrumbCtxPath = ref<string>('');
2213
- /* ui-fix — yan panel sağ-tık menüsünün hedef node'ları (pane selection). */
2217
+ /* ui-fix — target nodes of the side pane's right-click menu (pane selection). */
2214
2218
  const paneCtxTargets = ref<FileNode[]>([]);
2215
2219
  const breadcrumbCtxLabel = ref<string>('');
2216
2220
 
@@ -2415,15 +2419,17 @@ function onCrumbContext(payload: { x: number; y: number; adapterPath: string; la
2415
2419
  ctxRef.value?.show({ clientX: payload.x, clientY: payload.y }, []);
2416
2420
  }
2417
2421
 
2418
- /* ui-fix — yan (ikincil) panelde sağ-tık: pane'i aktifleştir + menüyü aç.
2419
- * Menü ana panelle BİREBİR aynı (selectionActionList tek kaynak); aksiyonlar
2420
- * dispatchItemAction'a gider ve ctxMode==='pane' iken pane-route'lanır. */
2422
+ /* ui-fix — right-click in the side (secondary) pane: activate the pane, then
2423
+ * open the menu. The menu is EXACTLY the main pane's (selectionActionList is
2424
+ * the single source); actions go to dispatchItemAction and are pane-routed
2425
+ * while ctxMode==='pane'. */
2421
2426
  function onPaneContext(node: FileNode | null, ev: MouseEvent) {
2422
2427
  activePane.value = 'split';
2423
2428
  void refreshKept();
2424
2429
  const sel = splitPaneRef.value?.selectedNodes() ?? [];
2425
- // node=null (boş alana sağ-tık): seçimsiz menü (Yeni Klasör + Yapıştır).
2426
- // Aksi halde pane seçimi (yoksa tıklanan node) hedeftir.
2430
+ // node=null (right-click on empty space): the selection-less menu
2431
+ // ("Yeni Klasör" + "Yapıştır"). Otherwise the pane selection is the target
2432
+ // (falling back to the clicked node).
2427
2433
  paneCtxTargets.value = node ? (sel.length > 0 ? sel : [node]) : [];
2428
2434
  ctxMode.value = 'pane';
2429
2435
  ctxRef.value?.show({ clientX: ev.clientX, clientY: ev.clientY }, paneCtxTargets.value);
@@ -2436,10 +2442,10 @@ const contextActions = computed<ContextAction[]>(() => {
2436
2442
  { key: 'copy-path', label: t('breadcrumb.copy_path'), icon: '📋' },
2437
2443
  ];
2438
2444
  }
2439
- if (ctxMode.value === 'pane' /* ui-fix — yan panel menüsü ana panelle BİREBİR aynı */) {
2445
+ if (ctxMode.value === 'pane' /* ui-fix — side-pane menu is EXACTLY the main pane's */) {
2440
2446
  const psel = paneCtxTargets.value;
2441
2447
  if (psel.length === 0) {
2442
- // Boş alana sağ-tık: ana paneldeki canvas menüsüyle aynı.
2448
+ // Right-click on empty space: same as the main pane's canvas menu.
2443
2449
  return [
2444
2450
  { key: 'new-folder', label: t('toolbar.new_folder'), icon: '📁' },
2445
2451
  { key: 'paste', label: t('ctx.paste'), icon: '📋', disabled: !clipboard.value.mode },
@@ -2470,7 +2476,7 @@ const contextActions = computed<ContextAction[]>(() => {
2470
2476
  // PRIOR BUG: this used `currentPath === '/'` but the load() branch
2471
2477
  // for the virtual root sets currentPath to EMPTY string, not '/'.
2472
2478
  // So the guard never fired and every mutation action leaked into
2473
- // the menu at the depo listing — including new-folder + paste,
2479
+ // the menu at the storage listing — including new-folder + paste,
2474
2480
  // which Ada called out in the most direct possible terms. Use
2475
2481
  // the same empty-after-trim test as `atVirtualRoot` above.
2476
2482
  const trimmedPath = (currentPath.value ?? '').replace(/^\/+|\/+$/g, '');
@@ -2512,9 +2518,10 @@ const contextActions = computed<ContextAction[]>(() => {
2512
2518
 
2513
2519
  // selectionActionList — the SINGLE source of truth for the actions offered on a
2514
2520
  // selection. BOTH the right-click context menu AND the top toolbar render this
2515
- // exact list so they can never drift apart (Ada: "sağ klik menüyle üst menü
2516
- // tutmuyor"). The toolbar filters out dividers/hidden; the context menu shows
2517
- // them. Action handling is unified in dispatchItemAction().
2521
+ // exact list so they can never drift apart (Ada, translated from Turkish: "the
2522
+ // right-click menu and the top menu don't match"). The toolbar filters out
2523
+ // dividers/hidden; the context menu shows them. Action handling is unified in
2524
+ // dispatchItemAction().
2518
2525
  function selectionActionList(sel: FileNode[]): ContextAction[] {
2519
2526
  const any = sel.length > 0;
2520
2527
  const single = sel.length === 1;
@@ -2537,11 +2544,11 @@ function selectionActionList(sel: FileNode[]): ContextAction[] {
2537
2544
  const accessLabel = locale.value === 'en' ? 'Share / Permissions' : 'Paylaş / İzinler';
2538
2545
  return [
2539
2546
  { key: 'open', label: t('ctx.open'), icon: '↗', hidden: !single },
2540
- { key: 'open-tab', label: t('ctx.open_new_tab'), icon: '⧉', hidden: !single || sel[0]?.type !== 'dir' } /* wiring:d1 — klasörü yeni sekmede */,
2547
+ { key: 'open-tab', label: t('ctx.open_new_tab'), icon: '⧉', hidden: !single || sel[0]?.type !== 'dir' } /* wiring:d1 — open the folder in a new tab */,
2541
2548
  { key: 'preview', label: t('ctx.preview'), icon: '👁', hidden: !single, disabled: !isFile },
2542
2549
  { key: 'download', label: t('ctx.download'), icon: '⬇', hidden: !single, disabled: !isFile },
2543
- { key: 'convert', label: t('ctx.convert'), icon: '🔄', hidden: !single || !effectiveConvertUrl.value || !w || e2eActive.value /* wiring:e2 — convert ciphertext'e anlamsız */, disabled: !isFile },
2544
- { key: 'access', label: accessLabel, icon: '🔗', hidden: !single || !w || e2eActive.value /* wiring:e2 — paylaşım MVP'de kapalı (link ciphertext verir) */ },
2550
+ { key: 'convert', label: t('ctx.convert'), icon: '🔄', hidden: !single || !effectiveConvertUrl.value || !w || e2eActive.value /* wiring:e2 — convert is meaningless on ciphertext */, disabled: !isFile },
2551
+ { key: 'access', label: accessLabel, icon: '🔗', hidden: !single || !w || e2eActive.value /* wiring:e2 — sharing is off in the MVP (the link would serve ciphertext) */ },
2545
2552
  { key: 'details', label: t('ctx.details'), icon: 'ℹ', hidden: !any } /* koru:k1 */,
2546
2553
  { key: 'copy-id', label: copyIdLabel, icon: '🆔', hidden: !singleHasId, disabled: !singleHasId },
2547
2554
  { divider: true, key: 'sep1', label: '', hidden: !w },
@@ -2596,7 +2603,7 @@ async function onContextAction(action: ContextAction, targets: FileNode[]) {
2596
2603
  }
2597
2604
  return;
2598
2605
  }
2599
- if (ctxMode.value === 'pane' /* ui-fix — yan panel: aynı dispatch, pane-route */) {
2606
+ if (ctxMode.value === 'pane' /* ui-fix — side pane: same dispatch, pane-routed */) {
2600
2607
  await dispatchItemAction(action.key, paneCtxTargets.value);
2601
2608
  return;
2602
2609
  }
@@ -2692,23 +2699,23 @@ async function dispatchItemAction(key: string, targets: FileNode[]) {
2692
2699
  }
2693
2700
  break;
2694
2701
  case 'cut':
2695
- /* ui-fix — pane bağlamında pano kaynağı pane'in dizini olmalı. */
2702
+ /* ui-fix — in a pane context the clipboard source must be the pane's dir. */
2696
2703
  if (ctxMode.value === 'pane') paneCut();
2697
2704
  else {
2698
2705
  clipboard.value = { mode: 'cut', items: targets, sourcePath: currentPath.value };
2699
- flashToast('Kes → Yapıştır hazır');
2706
+ flashToast(t('toast.cut_ready'));
2700
2707
  }
2701
2708
  break;
2702
2709
  case 'copy':
2703
2710
  if (ctxMode.value === 'pane') paneCopy();
2704
2711
  else {
2705
2712
  clipboard.value = { mode: 'copy', items: targets, sourcePath: currentPath.value };
2706
- flashToast('Kopyala → Yapıştır hazır');
2713
+ flashToast(t('toast.copy_ready'));
2707
2714
  }
2708
2715
  break;
2709
2716
  case 'paste':
2710
- /* ui-fix — sağ-klik menüsünden yapıştırma da aktif panele gider
2711
- * (klavye kısayolu zaten pane-route'luydu; menü değildi). */
2717
+ /* ui-fix — pasting from the right-click menu goes to the active pane too
2718
+ * (the keyboard shortcut was already pane-routed; the menu was not). */
2712
2719
  if (ctxMode.value === 'pane' || paneIsActive.value) await panePaste();
2713
2720
  else await paste();
2714
2721
  break;
@@ -2729,7 +2736,7 @@ async function dispatchItemAction(key: string, targets: FileNode[]) {
2729
2736
  case 'duplicate':
2730
2737
  if (targets[0]) await duplicate(targets[0]);
2731
2738
  break;
2732
- /* wiring:d1 — sağ-tık "Yeni sekmede aç" */
2739
+ /* wiring:d1 — right-click "Yeni sekmede aç" (open in new tab) */
2733
2740
  case 'open-tab':
2734
2741
  if (targets[0]) openNodeInTab(targets[0]);
2735
2742
  break;
@@ -2740,13 +2747,13 @@ async function dispatchItemAction(key: string, targets: FileNode[]) {
2740
2747
  function cut() {
2741
2748
  if (selection.isEmpty.value) return;
2742
2749
  clipboard.value = { mode: 'cut', items: selection.nodes.value, sourcePath: currentPath.value };
2743
- flashToast('Kesildi');
2750
+ flashToast(t('toast.cut'));
2744
2751
  }
2745
2752
 
2746
2753
  function copyToClipboard() {
2747
2754
  if (selection.isEmpty.value) return;
2748
2755
  clipboard.value = { mode: 'copy', items: selection.nodes.value, sourcePath: currentPath.value };
2749
- flashToast('Kopyalandı');
2756
+ flashToast(t('toast.copied'));
2750
2757
  }
2751
2758
 
2752
2759
  async function paste() {
@@ -2757,25 +2764,26 @@ async function paste() {
2757
2764
  const sourceDir = cb.sourcePath || '';
2758
2765
  const sameDir = cb.mode === 'cut' && sourceDir === currentPath.value;
2759
2766
  if (sameDir) {
2760
- flashToast('Aynı klasöre kesilemez');
2767
+ flashToast(t('toast.same_folder_cut'));
2761
2768
  return;
2762
2769
  }
2763
2770
 
2764
2771
  const targetWire = qualify(currentPath.value);
2765
- // Depo farkı yalnız mesajı değiştirir: kes KESTİR, kopyala KOPYALAR
2766
- // hedef başka depo olsa da. Aktarımı sunucu yapar (ops kuyruğu hem
2767
- // kaynak hem hedef depoyu taşır).
2772
+ // A different storage only changes the message: cut still CUTS
2773
+ // and copy still COPIES, even when the target lives on another storage.
2774
+ // The server does the transfer (the ops queue carries both the source
2775
+ // and the target storage).
2768
2776
  const plan = resolveTransfer(items, targetWire, cb.mode === 'cut' ? 'move' : 'copy');
2769
2777
  if (cb.mode === 'cut') {
2770
2778
  const originWire = qualify(sourceDir) || undefined;
2771
2779
  const { op } = await api.moveAsync(items, targetWire, originWire);
2772
2780
  registerMoveUndo(op.id, items, targetWire, originWire);
2773
2781
  pendingOps.register(op);
2774
- flashToast(plan.cross ? t('split.cross_move') : 'Taşıma kuyruğa alındı');
2782
+ flashToast(plan.cross ? t('split.cross_move') : t('split.move_queued'));
2775
2783
  } else {
2776
2784
  const { op } = await api.copy(items, targetWire);
2777
2785
  pendingOps.register(op);
2778
- flashToast(plan.cross ? t('split.cross_copy') : 'Kopyalama kuyruğa alındı');
2786
+ flashToast(plan.cross ? t('split.cross_copy') : t('split.copy_queued'));
2779
2787
  }
2780
2788
  clipboard.value = { mode: null, items: [], sourcePath: null };
2781
2789
  } catch (err) {
@@ -2793,8 +2801,9 @@ async function duplicate(n: FileNode) {
2793
2801
  }
2794
2802
 
2795
2803
  function downloadFile(n: FileNode) {
2796
- /* wiring:e2 — şifreli klasörde indirme: baytları çek, çöz, orijinal adla
2797
- kaydet (ham ciphertext'i kullanıcıya vermek anlamsız). */
2804
+ /* wiring:e2 — downloading inside an encrypted folder: fetch the bytes,
2805
+ decrypt them, save under the original name (handing the user raw
2806
+ ciphertext would be pointless). */
2798
2807
  if (e2eUnlocked.value) {
2799
2808
  void e2eDownload(n);
2800
2809
  return;
@@ -2810,7 +2819,7 @@ function downloadFile(n: FileNode) {
2810
2819
  // ------- Modals -------
2811
2820
 
2812
2821
  async function submitNewFolder(name: string) {
2813
- const inPane = mutationInPane.value; /* ui-fix — yan panelde yeni klasör */
2822
+ const inPane = mutationInPane.value; /* ui-fix — new folder in the side pane */
2814
2823
  try {
2815
2824
  const dirWire = inPane ? qualify(splitPaneRef.value?.getPath() ?? '') : qualify(currentPath.value);
2816
2825
  await api.newFolder(dirWire, name);
@@ -2825,7 +2834,7 @@ async function submitNewFolder(name: string) {
2825
2834
  async function submitRename(name: string) {
2826
2835
  const target = renameTarget.value;
2827
2836
  if (!target) return;
2828
- const inPane = mutationInPane.value; /* ui-fix — yan panelden yeniden adlandırma */
2837
+ const inPane = mutationInPane.value; /* ui-fix — rename from the side pane */
2829
2838
  try {
2830
2839
  const dirWire = inPane ? qualify(splitPaneRef.value?.getPath() ?? '') : qualify(currentPath.value);
2831
2840
  const oldPath = target.path; // qualified
@@ -2853,7 +2862,7 @@ async function confirmDelete() {
2853
2862
  // offer Restore here rather than a delete that would just re-trash a path.
2854
2863
  if (trashMode.value) {
2855
2864
  showDelete.value = false;
2856
- flashToast('Çöpteki öğeler saklama süresi sonunda otomatik silinir. Kalıcı silme yönetici panelinden yapılır.');
2865
+ flashToast(t('toast.trash_retention'));
2857
2866
  return;
2858
2867
  }
2859
2868
  /* ui-fix — yan panelden silme: hedefler + dizin + tazeleme pane'e ait. */
@@ -2885,7 +2894,7 @@ async function confirmDelete() {
2885
2894
  opUndo.set(op.id, { message: t('toast.trashed'), fn: restoreUndo });
2886
2895
  }
2887
2896
  pendingOps.register(op);
2888
- flashToast('Silme kuyruğa alındı');
2897
+ flashToast(t('toast.delete_queued'));
2889
2898
  } else {
2890
2899
  await api.deleteItems(dirWire, items);
2891
2900
  if (inPane) await splitPaneRef.value?.reload();
@@ -2962,9 +2971,9 @@ function onFilePicked(ev: Event) {
2962
2971
 
2963
2972
  async function uploadFiles(list: File[]) {
2964
2973
  if (list.length === 0) return;
2965
- /* wiring:e2 — şifreli klasörde upload şeffaf şifrelenir. Kilitliyken
2966
- yükleme yok (düz metin sızdırma kapısı olurdu); 200MB üstü MVP
2967
- tek-shot sınırına takılır ve uyarıyla atlanır. */
2974
+ /* wiring:e2 — uploads into an encrypted folder are encrypted transparently.
2975
+ No upload while locked (that would be a plaintext-leak door); anything
2976
+ over 200MB hits the MVP single-shot limit and is skipped with a warning. */
2968
2977
  if (e2eLocked.value) {
2969
2978
  flashToast(t('e2e.upload.locked'));
2970
2979
  return;
@@ -3135,9 +3144,10 @@ function isExternalFileDrag(ev: DragEvent): boolean {
3135
3144
  const dt = ev.dataTransfer;
3136
3145
  if (!dt) return false;
3137
3146
  if (dt.types && dt.types.includes(FE_DND_MIME)) return false;
3138
- /* wiring:f1 — kendi başlattığımız işletim sistemi sürüklemesi 'Files' taşır
3139
- ama YÜKLEME değildir: aynı baytları sunucudan indirip geri yüklemek
3140
- (taşıma yerine kopya, üstelik iki kat trafik) olurdu. */
3147
+ /* wiring:f1 — an OS drag we started ourselves also carries 'Files', but it
3148
+ is NOT an upload: it would mean downloading the same bytes from the
3149
+ server and posting them straight back (a copy instead of a move, at
3150
+ twice the traffic). */
3141
3151
  if (activeNativeDrag()) return false;
3142
3152
  // Some browsers expose `items` early in the drag, others only on
3143
3153
  // drop. When `items` is available we use it as the authoritative
@@ -3168,9 +3178,10 @@ function onDragLeave() {
3168
3178
  if (dragCounter.value === 0) dragOver.value = false;
3169
3179
  }
3170
3180
  function onDragOver(ev: DragEvent) {
3171
- /* wiring:d1 — sürüklemeler kök gövdeye de bırakılabilir olmalı ki split
3172
- panelinden ana panelin BOŞLUĞUNA bırakmak çalışsın (origin dragover'da
3173
- okunamazkarar drop anında verilir; aynı-klasör drop'u no-op kalır). */
3181
+ /* wiring:d1 — internal drags must be droppable on the root body too, so that
3182
+ dropping from the split pane onto the main pane's EMPTY SPACE works (the
3183
+ origin can't be read during dragover the decision is made at drop time;
3184
+ a same-folder drop stays a no-op). */
3174
3185
  if (hasInternalDrag(ev)) {
3175
3186
  ev.preventDefault();
3176
3187
  return;
@@ -3181,8 +3192,9 @@ function onDragOver(ev: DragEvent) {
3181
3192
  }
3182
3193
  }
3183
3194
  function onDropUpload(ev: DragEvent) {
3184
- /* wiring:d1 — split panelinden ana panelin boşluğuna bırakma = geçerli
3185
- klasöre aktar (aynı klasörden gelenler no-op, eski davranış korunur). */
3195
+ /* wiring:d1 — dropping from the split pane onto the main pane's empty space
3196
+ = transfer into the current folder (items from the same folder stay a
3197
+ no-op, so the old behaviour is preserved). */
3186
3198
  if (hasInternalDrag(ev)) {
3187
3199
  const d1Origin = internalDragOrigin(ev) || '';
3188
3200
  const d1Here = qualify(currentPath.value);
@@ -3243,13 +3255,14 @@ onMounted(() => {
3243
3255
  window.addEventListener('drop', onWindowDrop);
3244
3256
  window.addEventListener('pointerup', onGlobalPointerUp);
3245
3257
  window.addEventListener('blur', onGlobalPointerUp);
3246
- /* wiring:f1 — kabuk hazırlarken tek bir "hazırlanıyor" der; her dosyada
3247
- toast atmak ilerlemeyi değil gürültüyü gösterirdi. Bitişi 'hazır'
3248
- toast'ı (prepareDragOut) duyurur. */
3258
+ /* wiring:f1 — while the shell prepares, say "preparing" exactly once; a
3259
+ toast per file would show noise rather than progress. The finish is
3260
+ announced by the 'ready' toast (prepareDragOut). */
3249
3261
  dragOut.value?.onProgress?.((p) => {
3250
- // Bırakma SONRASI (yer tutucu yolu) her zaman duyurulur kullanıcı
3251
- // dosyayı bir klasöre bıraktı, orada ne olduğunu bilmeye hakkı var.
3252
- // Sessizlik yalnız kimsenin istemediği ön-hazırlık için geçerli.
3262
+ // Work that happens AFTER the drop (the placeholder path) is always
3263
+ // announced the user dropped a file into a folder and has a right to
3264
+ // know what happened there. Silence only applies to the pre-preparation
3265
+ // nobody asked for.
3253
3266
  const afterDrop = !!p?.dropped;
3254
3267
  if (p?.error === 'drop_not_found') {
3255
3268
  flashToast(t('dragout.not_found'));
@@ -3267,18 +3280,18 @@ onMounted(() => {
3267
3280
  if (!p?.finished && p?.done === 0) flashToast(t('dragout.preparing'));
3268
3281
  });
3269
3282
  });
3270
- /* wiring:f1 — işletim sistemi sürüklemesi bizde 'dragend' üretmez (HTML5
3271
- sürüklemesi hiç başlamadı). Fare bırakıldığında kaydı düşürüyoruz; aksi
3272
- halde sonraki normal sürükleme bir önceki seçimi taşıdığını sanırdı. */
3283
+ /* wiring:f1 — an OS drag never fires 'dragend' for us (the HTML5 drag never
3284
+ started). We drop the record when the mouse is released; otherwise the next
3285
+ ordinary drag would think it was carrying the previous selection. */
3273
3286
  function onGlobalPointerUp() {
3274
3287
  if (activeNativeDrag()) {
3275
3288
  endNativeDrag();
3276
- // Bırakma bizim penceremizde OLMAYABİLİR de; kabuğun izlemesini yalnız
3277
- // kendi bırakma yollarımız iptal eder (onDropUpload / onItemDropInto).
3289
+ // The drop MAY NOT have landed in our own window; only our own drop paths
3290
+ // cancel the shell's watch (onDropUpload / onItemDropInto).
3278
3291
  }
3279
3292
  }
3280
3293
 
3281
- /** Sürükleme uygulama içinde bitti: kabuk artık bir bırakma beklemesin. */
3294
+ /** The drag ended inside the app: the shell should stop waiting for a drop. */
3282
3295
  function cancelShellDrag() {
3283
3296
  if (dragOut.value?.cancel) void Promise.resolve(dragOut.value.cancel()).catch(() => undefined);
3284
3297
  }
@@ -3316,14 +3329,15 @@ function onItemDragStart(node: FileNode, ev: DragEvent) {
3316
3329
  .filter((n) => n.basename !== '.trash')
3317
3330
  .map((n) => ({ path: n.path, basename: n.basename, type: n.type })); // qualified
3318
3331
 
3319
- /* wiring:f1 — dışarı sürükleme (masaüstü / başka uygulama).
3320
- Kabuk varsa sürükleme HER ZAMAN işletim sistemi sürüklemesidir: klasörler
3321
- ve çoklu seçim ayrı ayrı GERÇEK dosya olarak düşer, BOYUT SINIRI YOK.
3322
- Baytlar hazırsa gerçek dosyalar verilir; değilse kabuk boş "yer tutucu"
3323
- verir, nereye bırakıldığını bulur ve indirmeyi ORAYA yapar (bkz.
3324
- desktop/src/dropwatch.ts). Uygulama İÇİNDE bırakılırsa sürükleme yine
3325
- sunucu tarafı taşımadır payload bizde dururve kabuğa "vazgeç" denir
3326
- ki sürücüleri boşuna dinlemesin. */
3332
+ /* wiring:f1 — drag-out (to the desktop / another application).
3333
+ When a shell is present the drag is ALWAYS an OS drag: folders and
3334
+ multi-selections land as REAL files, one by one, with NO SIZE LIMIT.
3335
+ If the bytes are ready, real files are handed over; if not, the shell
3336
+ hands over an empty "placeholder", finds out where it was dropped and
3337
+ downloads THERE (see desktop/src/dropwatch.ts). If the drop lands INSIDE
3338
+ the app the drag is still a server-side move the payload stays with us —
3339
+ and the shell is told to "give up" so it doesn't watch the drives for
3340
+ nothing. */
3327
3341
  if (dragOut.value && items.length > 0) {
3328
3342
  ev.preventDefault();
3329
3343
  beginNativeDrag(items, qualify(currentPath.value));
@@ -3336,14 +3350,15 @@ function onItemDragStart(node: FileNode, ev: DragEvent) {
3336
3350
  }
3337
3351
 
3338
3352
  ev.dataTransfer.setData(FE_DND_MIME, JSON.stringify(items));
3339
- ev.dataTransfer.setData(FE_DND_SRC_MIME, qualify(currentPath.value)); /* wiring:d1 — paneller arası origin damgası */
3353
+ ev.dataTransfer.setData(FE_DND_SRC_MIME, qualify(currentPath.value)); /* wiring:d1 — cross-pane origin stamp */
3340
3354
  ev.dataTransfer.setData('text/plain', items.map((i) => i.path).join('\n'));
3341
3355
  ev.dataTransfer.effectAllowed = 'move';
3342
3356
 
3343
- /* Tek dosya + çerezli oturum: tarayıcının kendi indirme yolu (DownloadURL)
3344
- bırakma anında dosyayı masaüstüne indirir; hiçbir hazırlık gerekmez.
3345
- Bearer token'lı kurulumda (masaüstü uygulaması) bu yol kimliksiz gider,
3346
- o yüzden orada üstteki yerel yol devrededir bkz. lib/dragOut.ts. */
3357
+ /* Single file + a cookie session: the browser's own download path
3358
+ (DownloadURL) fetches the file onto the desktop at drop time; no
3359
+ preparation is needed at all. In a bearer-token setup (the desktop app)
3360
+ that path goes out unauthenticated, which is why the local path above
3361
+ takes over there — see lib/dragOut.ts. */
3347
3362
  if (items.length === 1 && items[0] && canDownloadUrlDrag(props.config.auth)) {
3348
3363
  const payload = downloadUrlPayload(items[0], api.downloadUrl(items[0].path), node.mime_type);
3349
3364
  if (payload) ev.dataTransfer.setData('DownloadURL', payload);
@@ -3352,13 +3367,13 @@ function onItemDragStart(node: FileNode, ev: DragEvent) {
3352
3367
  if (dragOut.value && items.length > 0) void prepareDragOut(items);
3353
3368
  }
3354
3369
 
3355
- /* === wiring:f1 — dışarı sürükleme hazırlığı ===
3370
+ /* === wiring:f1 — drag-out preparation ===
3356
3371
  *
3357
- * Baytlar sürükleme BAŞLAMADAN diskte olmak zorunda (işletim sistemi bırakma
3358
- * anında yoldan kopyalar), o yüzden hazırlık ayrı bir adımdır. Bitince
3359
- * kullanıcıya "hazır" denir; ikinci sürükleme artık işletim sistemi
3360
- * sürüklemesidir ve anında başlar. Bu bilgisayarda tutulan (senkron) dosyalar
3361
- * için hazırlık ilk seferde de anında biter kopya zaten yerelde.
3372
+ * The bytes have to be on disk BEFORE the drag starts (the OS copies from the
3373
+ * path at drop time), so preparation is a separate step. When it finishes the
3374
+ * user is told "ready"; the second drag is now an OS drag and starts
3375
+ * instantly. For files kept on this computer (synced) preparation finishes
3376
+ * instantly on the first go toothe copy is already local.
3362
3377
  */
3363
3378
  const dragOut = computed(() => props.config.dragOut ?? null);
3364
3379
  const dragOutReadyKey = ref('');
@@ -3366,11 +3381,12 @@ const dragOutBusy = ref(false);
3366
3381
  /** True while a preparation nobody asked for is running. */
3367
3382
  let dragOutQuiet = false;
3368
3383
 
3369
- /* Küçük seçimler SEÇİLDİĞİ anda hazırlanır, çünkü hazırlık bittikten sonraki
3370
- sürükleme işletim sistemi sürüklemesidir: bir belgeyi seçip masaüstüne
3371
- sürüklemek böylece İLK denemede çalışır. Tavan bilerek düşük tıklayarak
3372
- gezinen biri her satırda film indirmemeli; sınırın üstündekiler ilk
3373
- sürüklemede hazırlanır, ikinci sürükleme anında başlar. */
3384
+ /* Small selections are prepared the moment they are SELECTED, because once
3385
+ preparation is done the next drag is an OS drag: picking a document and
3386
+ dragging it to the desktop therefore works on the FIRST try. The ceiling is
3387
+ deliberately low someone click-browsing shouldn't download a movie on
3388
+ every row; anything above the limit is prepared on the first drag and the
3389
+ second drag starts instantly. */
3374
3390
  const DRAGOUT_PREFETCH_MAX_BYTES = 8 * 1024 * 1024;
3375
3391
  const DRAGOUT_PREFETCH_MAX_ITEMS = 10;
3376
3392
  let dragOutPrefetchTimer: ReturnType<typeof setTimeout> | undefined;
@@ -3382,8 +3398,8 @@ watch(
3382
3398
  clearTimeout(dragOutPrefetchTimer);
3383
3399
  const nodes = selection.nodes.value;
3384
3400
  if (nodes.length === 0 || nodes.length > DRAGOUT_PREFETCH_MAX_ITEMS) return;
3385
- // Klasörün boyutu listelemede bilinmez; onu tahmin etmek yerine ilk
3386
- // sürüklemeye bırakıyoruz.
3401
+ // A folder's size isn't known from the listing; rather than guess it, we
3402
+ // leave it to the first drag.
3387
3403
  if (nodes.some((n) => n.type !== 'file')) return;
3388
3404
  const total = nodes.reduce((sum, n) => sum + (n.size ?? 0), 0);
3389
3405
  if (total > DRAGOUT_PREFETCH_MAX_BYTES) return;
@@ -3404,8 +3420,9 @@ async function prepareDragOut(items: DragItem[], quiet = false): Promise<void> {
3404
3420
  const res = await hook.prepare(items);
3405
3421
  if (res?.ready) {
3406
3422
  dragOutReadyKey.value = key;
3407
- // Sessiz tur seçimle tetiklenir; kullanıcı bir şey İSTEMEDİ, o yüzden
3408
- // ona bir şey söylemek de gerekmez. Sürüklemeyle başlayan tur söyler.
3423
+ // The quiet round is triggered by a selection; the user ASKED for
3424
+ // nothing, so there's nothing to tell them either. A round that starts
3425
+ // from an actual drag does speak up.
3409
3426
  if (!quiet) flashToast(t('dragout.ready'));
3410
3427
  } else if (res?.error && !quiet) {
3411
3428
  flashToast(res.error);
@@ -3420,12 +3437,12 @@ async function prepareDragOut(items: DragItem[], quiet = false): Promise<void> {
3420
3437
 
3421
3438
  async function moveSourcesAsync(sources: string[], targetDir: string, opLabel: string, originOverride?: string): Promise<void> {
3422
3439
  try {
3423
- const originWire = originOverride ?? qualify(currentPath.value); /* wiring:d1 — split panelinden gelen sürüklemede gerçek kaynak klasör */
3440
+ const originWire = originOverride ?? qualify(currentPath.value); /* wiring:d1 — the real source folder for a drag coming from the split pane */
3424
3441
  if (api.endpoints.moveAsync) {
3425
3442
  const { op } = await api.moveAsync(sources, targetDir, originWire);
3426
3443
  registerMoveUndo(op.id, sources, targetDir, originWire);
3427
3444
  pendingOps.register(op);
3428
- flashToast('Taşıma kuyruğa alındı');
3445
+ flashToast(t('split.move_queued'));
3429
3446
  } else {
3430
3447
  await api.move(originWire, sources, targetDir);
3431
3448
  await load();
@@ -3451,11 +3468,12 @@ async function onItemDropInto(target: FileNode, ev: DragEvent) {
3451
3468
  const targetDir = target.path; // qualified
3452
3469
  const sources = items
3453
3470
  .map((i) => i.path)
3454
- // ui-fix — öğe zaten targetDir'in içindeyse (parent===target) atla: yerinde
3455
- // bırakma no-op, backend "kendine kopyala" 400'ü tetiklemez.
3471
+ // ui-fix — skip items already inside targetDir (parent===target): an
3472
+ // in-place drop is a no-op and must not trigger the backend's "copy onto
3473
+ // itself" 400.
3456
3474
  .filter((p) => p && p !== targetDir && !targetDir.startsWith(p + '/') && !sameDir(wireParent(p), targetDir));
3457
- if (sources.length === 0) return; // sessiz no-op (yerinde bırakma)
3458
- await transferItems(sources, targetDir, dndOrigin(ev)); /* wiring:d1 — depo-farkında aktarım */
3475
+ if (sources.length === 0) return; // silent no-op (in-place drop)
3476
+ await transferItems(sources, targetDir, dndOrigin(ev)); /* wiring:d1 — storage-aware transfer */
3459
3477
  }
3460
3478
 
3461
3479
  async function onCrumbDropInto(adapterPath: string, ev: DragEvent) {
@@ -3467,10 +3485,10 @@ async function onCrumbDropInto(adapterPath: string, ev: DragEvent) {
3467
3485
  const targetDir = adapterPath; // already qualified by breadcrumb
3468
3486
  const sources = items
3469
3487
  .map((i) => i.path)
3470
- // ui-fix — kırıntıya (aynı klasöre) yerinde bırakma no-op.
3488
+ // ui-fix — an in-place drop onto the breadcrumb (the same folder) is a no-op.
3471
3489
  .filter((p) => p && p !== targetDir && !targetDir.startsWith(p + '/') && !sameDir(wireParent(p), targetDir));
3472
3490
  if (sources.length === 0) return;
3473
- await transferItems(sources, targetDir, dndOrigin(ev)); /* wiring:d1 — depo-farkında aktarım */
3491
+ await transferItems(sources, targetDir, dndOrigin(ev)); /* wiring:d1 — storage-aware transfer */
3474
3492
  }
3475
3493
 
3476
3494
  function onCancelUpload(job: UploadJob) {
@@ -3613,7 +3631,7 @@ function quickLookToggle() {
3613
3631
  const sel = selection.nodes.value;
3614
3632
  const n = sel.length === 1 ? sel[0] : null;
3615
3633
  if (!n || n.type !== 'file' || n.basename === '.trash') return;
3616
- /* wiring:e2 — Space peek şifreli klasörde önce çözer, sonra açar */
3634
+ /* wiring:e2 — in an encrypted folder the Space peek decrypts first, then opens */
3617
3635
  if (e2eActive.value) {
3618
3636
  if (!e2eUnlocked.value) return;
3619
3637
  void (async () => {
@@ -3657,14 +3675,14 @@ watch(
3657
3675
  (nodes) => {
3658
3676
  if (!quickLookOpen.value) return;
3659
3677
  const n = nodes.length === 1 && nodes[0].type === 'file' ? nodes[0] : null;
3660
- /* wiring:e2 — ok tuşlarıyla gezerken de hedef atanmadan ÖNCE çöz,
3661
- yoksa viewer bir anlığına ham ciphertext URL'i alır. */
3678
+ /* wiring:e2 — when arrowing through files, decrypt BEFORE assigning the
3679
+ target as well, otherwise the viewer briefly gets the raw ciphertext URL. */
3662
3680
  if (n && n.path !== quickLookTarget.value?.path && e2eUnlocked.value) {
3663
3681
  void (async () => {
3664
3682
  try {
3665
3683
  await e2eFetchDecrypted(n);
3666
3684
  } catch {
3667
- /* çözülemedihedefi yine de değiştir, viewer hata gösterir */
3685
+ /* decryption failed switch the target anyway, the viewer shows the error */
3668
3686
  }
3669
3687
  quickLookTarget.value = n;
3670
3688
  void markRecent(n);
@@ -3759,16 +3777,17 @@ onBeforeUnmount(() => {
3759
3777
  });
3760
3778
  /* === /wiring:c4 === */
3761
3779
 
3762
- /* === wiring:d1 — sekmeler (tab şeridi) + tab başına split ===
3780
+ /* === wiring:d1 — tabs (tab strip) + per-tab split ===
3763
3781
  *
3764
- * useTabs, mevcut konum state'inin (currentPath/viewMode) ÜSTÜNDE bir
3765
- * katmandır: aktif tab gezinmeleri watch ile dinleyip snapshot'ını
3766
- * günceller; tab geçişi mevcut load(path) yolunu çağırır yeni fetch
3767
- * mantığı yok. Şerit tek sekmede hiç render edilmez (embed pixel-aynı).
3782
+ * useTabs is a layer ON TOP of the existing location state
3783
+ * (currentPath/viewMode): the active tab watches navigations and updates its
3784
+ * snapshot; switching tabs calls the existing load(path) pathno new fetch
3785
+ * logic. The strip is not rendered at all on a single tab (embeds stay
3786
+ * pixel-identical).
3768
3787
  *
3769
- * Persist: `filex.tabs` — pathPersist scope mantığı izlenir: mode 'none'
3770
- * ise persist kapalı; rootPath confine anahtara eklenir ki farklı
3771
- * confine'lı embed'ler birbirinin sekmelerini ezmesin.
3788
+ * Persist: `filex.tabs` — it follows the pathPersist scope logic: with mode
3789
+ * 'none' persistence is off; the rootPath confine is added to the key so that
3790
+ * embeds with different confines don't overwrite each other's tabs.
3772
3791
  */
3773
3792
  const FE_DND_SRC_MIME = 'application/x-brf-files-src';
3774
3793
 
@@ -3783,7 +3802,7 @@ const tabsActiveId = tabsApi.activeId;
3783
3802
 
3784
3803
  const activeSplit = computed(() => tabsApi.activeTab.value?.split ?? null);
3785
3804
 
3786
- // Sekme adı OTOMATİK = güncel klasör adı (kök = depo adı / kök etiketi).
3805
+ // Tab name is AUTOMATIC = the current folder name (root = storage name / root label).
3787
3806
  function tabLabel(path: string): string {
3788
3807
  const p = (path || '').replace(/^\/+|\/+$/g, '');
3789
3808
  // gezinti:g1 — the virtual views park a sentinel in the path. Translate via
@@ -3815,12 +3834,13 @@ const tabsVisible = computed(
3815
3834
  (props.config.tabStrip !== 'auto' && tabItems.value.length > 0)),
3816
3835
  );
3817
3836
 
3818
- // Aktif tab kullanıcıyı izler: gezinme + görünüm değişimi snapshot'a yazılır.
3837
+ // The active tab follows the user: navigation + view changes go into the snapshot.
3819
3838
  watch(currentPath, (p) => tabsApi.syncActive({ path: p }));
3820
3839
  watch(viewMode, (v) => tabsApi.syncActive({ viewMode: v }));
3821
3840
 
3822
- // İlk sekme, ilk konum belli olur olmaz tohumlanır (restore varsa dokunma
3823
- // aktif snapshot ilk load sonrası currentPath watcher'ıyla zaten senkronlanır).
3841
+ // The first tab is seeded as soon as the first location is known (don't touch
3842
+ // it if a restore happened the active snapshot is already synced by the
3843
+ // currentPath watcher after the first load).
3824
3844
  onMounted(() => {
3825
3845
  if (!tabsRestored) tabsApi.seed(currentPath.value ?? '', viewMode.value);
3826
3846
  });
@@ -3838,7 +3858,7 @@ function activateTab(id: string) {
3838
3858
  if (tb) applyTabLocation(tb);
3839
3859
  }
3840
3860
  function newTabHere() {
3841
- // Mevcut konumu klonlar; görünüm zaten oradadır, load gerekmez.
3861
+ // Clones the current location; the view is already there, so no load needed.
3842
3862
  tabsApi.openTab(currentPath.value ?? '', { viewMode: viewMode.value, background: false });
3843
3863
  }
3844
3864
  function closeTabById(id: string) {
@@ -3854,16 +3874,17 @@ function prevTab() {
3854
3874
  if (tb) applyTabLocation(tb);
3855
3875
  }
3856
3876
 
3857
- /** Bir klasörü ARKA PLANDA yeni sekmede (orta-tık / sağ-tık / palet). */
3877
+ /** Open a folder in a new tab IN THE BACKGROUND (middle-click / right-click / palette). */
3858
3878
  function openNodeInTab(n: FileNode) {
3859
3879
  if (n.type !== 'dir' || n.basename === '.trash') return;
3860
3880
  const target = multiStorageRoot.value ? wireToVirtual(n.path) : stripAdapter(n.path);
3861
3881
  tabsApi.openTab(target, { viewMode: viewMode.value, background: true });
3862
3882
  }
3863
3883
 
3864
- // Orta-tık delegasyonu: ListView/GridView satırları data-fe-path taşır; kendi
3865
- // keydown/emit zinciri eklemek yerine kökte tek auxclick dinleyicisi yeter.
3866
- // (SecondaryPane kendi satırlarında stopPropagation ile kendisi halleder.)
3884
+ // Middle-click delegation: ListView/GridView rows carry data-fe-path, so a
3885
+ // single auxclick listener at the root is enough instead of adding a
3886
+ // keydown/emit chain of our own. (SecondaryPane handles its own rows via
3887
+ // stopPropagation.)
3867
3888
  function onListAuxClick(ev: MouseEvent) {
3868
3889
  if (ev.button !== 1) return;
3869
3890
  const host = ev.target as HTMLElement | null;
@@ -3875,9 +3896,9 @@ function onListAuxClick(ev: MouseEvent) {
3875
3896
  ev.preventDefault();
3876
3897
  openNodeInTab(node);
3877
3898
  }
3878
- // Orta-tuş mousedown satırlar üzerinde iptal: scroll'lu gövdede Chromium'un
3879
- // autoscroll'u devreye girer ve auxclick HİÇ üretilmez (canlı teşhis) —
3880
- // preventDefault autoscroll'u bastırır, auxclick yeniden akar.
3899
+ // Cancel the middle-button mousedown over rows: in a scrollable body Chromium's
3900
+ // autoscroll kicks in and auxclick is NEVER produced (diagnosed live) —
3901
+ // preventDefault suppresses autoscroll and auxclick flows again.
3881
3902
  function onListMiddleDown(ev: MouseEvent) {
3882
3903
  if (ev.button !== 1) return;
3883
3904
  const host = ev.target as HTMLElement | null;
@@ -3894,10 +3915,10 @@ onBeforeUnmount(() => {
3894
3915
  rootEl.value?.removeEventListener('mousedown', onListMiddleDown);
3895
3916
  });
3896
3917
 
3897
- // ---- split (tab başına ikincil panel) ------------------------------
3918
+ // ---- split (per-tab secondary pane) --------------------------------
3898
3919
 
3899
3920
  const splitPaneRef = ref<InstanceType<typeof SecondaryPane> | null>(null);
3900
- // Dar modda split devre dışı (state korunur, genişleyince geri gelir).
3921
+ // Split is disabled in narrow mode (the state is kept and comes back on widen).
3901
3922
  const splitVisible = computed(
3902
3923
  () => !!activeSplit.value && !isNarrow.value && !simpleUi.value /* gezinti:g1 */,
3903
3924
  );
@@ -3918,16 +3939,18 @@ function closeSplit() {
3918
3939
  function onPaneNavigate(p: string) {
3919
3940
  tabsApi.setSplit({ ...(activeSplit.value ?? {}), path: p });
3920
3941
  }
3921
- /* ui-fix — çöp kutusu satırı yan panelde açılınca: trash görünümü (geri
3922
- * yükleme aksiyonlarıyla) ana panele aittir ana paneli aktif edip aç. */
3942
+ /* ui-fix — when the trash row is opened from the side pane: the trash view
3943
+ * (with its restore actions) belongs to the main pane activate the main
3944
+ * pane and open it there. */
3923
3945
  function onPaneOpenTrash() {
3924
3946
  activePane.value = 'main';
3925
3947
  void loadTrash();
3926
3948
  }
3927
- /* ui-fix — pane'in KENDİ görünüm modu: split açılırken ana panelinkini
3928
- * devralır, sonrasında bağımsız. Toolbar'ın görünüm değiştiricisi ve palet
3929
- * toggle AKTİF panele yazar (Ada: "B tıklıyken ikon değiştir dersem
3930
- * B'nin değişmesi lazım"). */
3949
+ /* ui-fix — the pane's OWN view mode: it inherits the main pane's when the
3950
+ * split opens, and is independent afterwards. The toolbar's view switcher and
3951
+ * the palette toggle write to the ACTIVE pane (Ada, translated from Turkish:
3952
+ * "if I say change the icon while B is focused, B is the one that has to
3953
+ * change"). */
3931
3954
  const paneViewMode = computed<ViewMode>(() => activeSplit.value?.viewMode ?? viewMode.value);
3932
3955
  function setPaneViewMode(v: ViewMode) {
3933
3956
  if (!activeSplit.value) return;
@@ -3941,7 +3964,7 @@ function setDisplayedViewMode(v: ViewMode) {
3941
3964
  else viewMode.value = v;
3942
3965
  }
3943
3966
 
3944
- // Aktif panel: kısayollar aktif panele gider; panel tıklamayla aktifleşir.
3967
+ // Active pane: shortcuts go to the active pane; a pane is activated by clicking it.
3945
3968
  const activePane = ref<'main' | 'split'>('main');
3946
3969
  function setPaneMain() {
3947
3970
  activePane.value = 'main';
@@ -3952,7 +3975,7 @@ watch(splitVisible, (v) => {
3952
3975
  const paneIsActive = computed(() => activePane.value === 'split' && splitVisible.value);
3953
3976
  const mainPaneFocus = computed(() => splitVisible.value && activePane.value === 'main');
3954
3977
 
3955
- // Pane yardımcılarıhep ana panelin mevcut dönüştürücülerini sarar.
3978
+ // Pane helpersalways wrap the main pane's existing converters.
3956
3979
  function paneToUser(wire: string): string {
3957
3980
  return multiStorageRoot.value ? wireToVirtual(wire) : stripAdapter(wire);
3958
3981
  }
@@ -3963,20 +3986,20 @@ function paneClamp(p: string): string {
3963
3986
  return clean;
3964
3987
  }
3965
3988
 
3966
- // Pano (clipboard) aktif panele göre: kes/kopyala pane seçiminden beslenir,
3967
- // yapıştır pane klasörüne iner. State ana panelinkiyle ORTAK panolar arası
3968
- // kes-yapıştır bedavaya çalışır.
3989
+ // The clipboard follows the active pane: cut/copy is fed from the pane
3990
+ // selection and paste lands in the pane's folder. The state is SHARED with the
3991
+ // main pane's — so cut-and-paste between panes works for free.
3969
3992
  function paneCut() {
3970
3993
  const nodes = splitPaneRef.value?.selectedNodes() ?? [];
3971
3994
  if (nodes.length === 0) return;
3972
3995
  clipboard.value = { mode: 'cut', items: nodes, sourcePath: splitPaneRef.value?.getPath() ?? '' };
3973
- flashToast('Kesildi');
3996
+ flashToast(t('toast.cut'));
3974
3997
  }
3975
3998
  function paneCopy() {
3976
3999
  const nodes = splitPaneRef.value?.selectedNodes() ?? [];
3977
4000
  if (nodes.length === 0) return;
3978
4001
  clipboard.value = { mode: 'copy', items: nodes, sourcePath: splitPaneRef.value?.getPath() ?? '' };
3979
- flashToast('Kopyalandı');
4002
+ flashToast(t('toast.copied'));
3980
4003
  }
3981
4004
  async function panePaste() {
3982
4005
  const cb = clipboard.value;
@@ -3985,28 +4008,29 @@ async function panePaste() {
3985
4008
  const targetWire = qualify(pane.getPath() ?? '');
3986
4009
  const originWire = qualify(cb.sourcePath || '') || undefined;
3987
4010
  if (cb.mode === 'cut' && originWire === targetWire) {
3988
- flashToast('Aynı klasöre kesilemez');
4011
+ flashToast(t('toast.same_folder_cut'));
3989
4012
  return;
3990
4013
  }
3991
4014
  await transferItems(cb.items.map((n) => n.path), targetWire, originWire, cb.mode === 'copy' ? 'copy' : 'move');
3992
4015
  clipboard.value = { mode: null, items: [], sourcePath: null };
3993
4016
  }
3994
4017
 
3995
- // ---- paneller arası aktarım ----------------------------------------
4018
+ // ---- cross-pane transfer -------------------------------------------
3996
4019
  function dndOrigin(ev: DragEvent): string | undefined {
3997
4020
  return internalDragOrigin(ev);
3998
4021
  }
3999
4022
 
4000
4023
  /**
4001
- * transferItems — panel-arası / pano aktarımının tek kapısı.
4024
+ * transferItems — the single gate for cross-pane / clipboard transfers.
4002
4025
  *
4003
- * Ne yapılacağını `resolveTransfer` söyler (lib/transfer.ts): sürükleme aynı
4004
- * depoda TAŞI, depolar arasında KOPYALA; panodan gelen kes/kopyala ise ne
4005
- * dendiyse odur kes, hedef başka depo olsa da TAŞIR (sunucu baytları
4006
- * aktarıp kaynağı siler). Eskiden depolar arası her aktarım sessizce
4007
- * kopyaya düşüyordu: kullanıcı "kes" deyip dosyayı iki yerde buluyordu.
4008
- * Bitince ikincil panel de tazelenir (ana panel moveSourcesAsync /
4009
- * pendingOps onSettled üzerinden zaten tazelenir).
4026
+ * `resolveTransfer` decides what to do (lib/transfer.ts): a drag MOVES within
4027
+ * the same storage and COPIES across storages; a cut/copy coming from
4028
+ * the clipboard does exactly what it says cut MOVES even when the target is
4029
+ * another storage (the server transfers the bytes and deletes the source).
4030
+ * Cross-storage transfers used to silently fall back to a copy: the user
4031
+ * said "cut" and found the file in two places. When it's done the secondary
4032
+ * pane is refreshed too (the main pane is already refreshed via
4033
+ * moveSourcesAsync / pendingOps onSettled).
4010
4034
  */
4011
4035
  async function transferItems(
4012
4036
  sources: string[],
@@ -4014,8 +4038,8 @@ async function transferItems(
4014
4038
  originWire?: string,
4015
4039
  intent: TransferIntent = 'auto',
4016
4040
  ): Promise<void> {
4017
- // ui-fix — yerinde bırakma (source parent === target) no-op: backend
4018
- // "kendine kopyala" 400 engellenir (paneller arası + pano yolu).
4041
+ // ui-fix — an in-place drop (source parent === target) is a no-op: this
4042
+ // avoids the backend's "copy onto itself" 400 (cross-pane + clipboard paths).
4019
4043
  const list = sources.filter(
4020
4044
  (p) => p && p !== targetWire && !targetWire.startsWith(p + '/') && !sameDir(wireParent(p), targetWire),
4021
4045
  );
@@ -4027,9 +4051,10 @@ async function transferItems(
4027
4051
  pendingOps.register(op);
4028
4052
  flashToast(plan.cross ? t('split.cross_copy') : t('split.copy_queued'));
4029
4053
  } catch (err) {
4030
- // ⚠ Sunucunun kendi mesajını göster. Burada sabit bir "depolar arası
4031
- // desteklenmiyor" metni vardı; artık DESTEKLENİYOR, yani o metin
4032
- // gerçek sebebi (izin, salt-okunur depo, dolu kota) örterdi.
4054
+ // ⚠ Show the server's own message. There used to be a hard-coded
4055
+ // "cross-storage is not supported" text here; it IS supported now, so
4056
+ // that text would mask the real cause (permissions, a read-only storage,
4057
+ // a full quota).
4033
4058
  emit('error', { message: (err as Error).message, context: { op: 'transfer', targetWire } });
4034
4059
  flashToast((err as Error).message);
4035
4060
  return;
@@ -4046,21 +4071,23 @@ function onPaneTransfer(p: { sources: string[]; targetWire: string; originWire?:
4046
4071
  }
4047
4072
  /* === /wiring:d1 === */
4048
4073
 
4049
- /* === wiring:e2 — uçtan uca şifreli klasörler ===
4074
+ /* === wiring:e2 — end-to-end encrypted folders ===
4050
4075
  *
4051
- * Kripto şeması + tehdit modeli: docs/E2E-ENCRYPTION.md ve lib/e2ecrypto.ts.
4052
- * Burada yalnız orkestrasyon var: backend listing yanıtındaki `e2e_root`
4053
- * kilit ekranını sürer; parola marker'a karşı TARAYICIDA doğrulanır (sunucuya
4054
- * hiçbir şey gitmez); türetilen klasör anahtarı (KEK) YALNIZ bellekte yaşar
4055
- * (`e2eRing`) — localStorage/sessionStorage'a asla yazılmaz. Upload şeffaf
4056
- * şifrelenir, önizleme/indirme şeffaf çözülür (blob URL mevcut viewer'lara).
4076
+ * Crypto scheme + threat model: docs/E2E-ENCRYPTION.md and lib/e2ecrypto.ts.
4077
+ * Only the orchestration lives here: `e2e_root` in the backend listing
4078
+ * response drives the lock screen; the password is verified against the
4079
+ * marker IN THE BROWSER (nothing goes to the server); the derived folder key
4080
+ * (KEK) lives ONLY in memory (`e2eRing`) — it is never written to
4081
+ * localStorage/sessionStorage. Uploads are encrypted transparently and
4082
+ * previews/downloads decrypted transparently (a blob URL to the existing
4083
+ * viewers).
4057
4084
  */
4058
4085
  const e2eRing = createKeyRing();
4059
- // Map'ler reaktif değil sürüm sayacı computed'ları tetikler.
4086
+ // Maps aren't reactivea version counter drives the computeds.
4060
4087
  const e2eRingVer = ref(0);
4061
- // İçinde bulunulan şifreli kökün wire yolu ('' = şifreli bağlam yok).
4088
+ // Wire path of the encrypted root we are inside ('' = no encrypted context).
4062
4089
  const e2eRoot = ref('');
4063
- // path → çözülmüş blob objectURL (önizleme). Kilitleme/unmount'ta revoke.
4090
+ // path → decrypted blob objectURL (preview). Revoked on lock/unmount.
4064
4091
  const e2eUrls = new Map<string, string>();
4065
4092
 
4066
4093
  const e2eActive = computed(() => !!e2eRoot.value && !trashMode.value);
@@ -4073,15 +4100,15 @@ const e2eLocked = computed(() => {
4073
4100
  return e2eActive.value && !e2eRing.has(e2eRoot.value);
4074
4101
  });
4075
4102
 
4076
- // Kilit ekranı formu.
4103
+ // Lock screen form.
4077
4104
  const e2ePw = ref('');
4078
4105
  const e2eUnlockBusy = ref(false);
4079
4106
  const e2eUnlockErr = ref('');
4080
- // Şifreli klasör oluşturma modalı.
4107
+ // Encrypted-folder creation modal.
4081
4108
  const showEncFolder = ref(false);
4082
4109
  const e2eCreateBusy = ref(false);
4083
4110
 
4084
- /* wiring:e2 recovery — kurtarma anahtarı + escrow.
4111
+ /* wiring:e2 recovery — recovery key + escrow.
4085
4112
  *
4086
4113
  * The marker of the folder we are looking at is cached here while the lock
4087
4114
  * screen is up: the recovery dialog needs to know which doors this folder
@@ -4151,7 +4178,7 @@ function e2eRevokeAll() {
4151
4178
  }
4152
4179
  onBeforeUnmount(e2eRevokeAll);
4153
4180
 
4154
- /** Kilidi aç: marker kökten çek, parolayı YERELDE doğrula, KEK'i belleğe koy. */
4181
+ /** Unlock: fetch the marker from the root, verify the password LOCALLY, put the KEK in memory. */
4155
4182
  async function e2eUnlock() {
4156
4183
  if (!e2ePw.value || e2eUnlockBusy.value || !e2eRoot.value) return;
4157
4184
  e2eUnlockBusy.value = true;
@@ -4207,7 +4234,7 @@ async function e2eUnlock() {
4207
4234
  }
4208
4235
  }
4209
4236
 
4210
- /** "Kilitle": bellekteki anahtarı ve çözülmüş blob'ları at. */
4237
+ /** "Kilitle" (Lock): drop the in-memory key and the decrypted blobs. */
4211
4238
  function e2eLock() {
4212
4239
  if (!e2eRoot.value) return;
4213
4240
  e2eRing.lock(e2eRoot.value);
@@ -4216,9 +4243,9 @@ function e2eLock() {
4216
4243
  flashToast(t('e2e.locked_toast'));
4217
4244
  }
4218
4245
 
4219
- // Uzantıönizleme MIME'ı: çözülmüş blob'un <img>/<video>/<object>
4220
- // etiketlerinde doğru render'ı için (sunucu şifreli dosyayı octet-stream
4221
- // bilir, oradan gelen tip işe yaramaz).
4246
+ // Extensionpreview MIME: so the decrypted blob renders correctly in
4247
+ // <img>/<video>/<object> tags (the server knows the encrypted file as
4248
+ // octet-stream, so the type coming from there is useless).
4222
4249
  const E2E_MIME: Record<string, string> = {
4223
4250
  txt: 'text/plain', md: 'text/markdown', log: 'text/plain', csv: 'text/csv',
4224
4251
  json: 'application/json', xml: 'application/xml', html: 'text/html',
@@ -4235,9 +4262,10 @@ function e2eMimeFor(n: FileNode): string {
4235
4262
  }
4236
4263
 
4237
4264
  /**
4238
- * Dosyayı çek + çöz + objectURL'ini cache'le. Magic'siz (ör. DAV'la düz
4239
- * yazılmış) dosyada null döner çağıran normal ham akışa düşer. Yanlış
4240
- * anahtar/bozuk veri E2eDecryptError fırlatır (çağıran toast'lar).
4265
+ * Fetch the file + decrypt it + cache its objectURL. Returns null for a file
4266
+ * with no magic (e.g. written in the clear over DAV) — the caller then falls
4267
+ * back to the normal raw flow. A wrong key / corrupt data throws
4268
+ * E2eDecryptError (the caller toasts it).
4241
4269
  */
4242
4270
  async function e2eFetchDecrypted(n: FileNode): Promise<string | null> {
4243
4271
  const cached = e2eUrls.get(n.path);
@@ -4252,12 +4280,12 @@ async function e2eFetchDecrypted(n: FileNode): Promise<string | null> {
4252
4280
  return url;
4253
4281
  }
4254
4282
 
4255
- /** PreviewModal/QuickLook'a giden URL sağlayıcı: çözülmüş blob > ham URL. */
4283
+ /** URL provider handed to PreviewModal/QuickLook: decrypted blob > raw URL. */
4256
4284
  function e2ePreviewSrc(p: string): string {
4257
4285
  return e2eUrls.get(p) ?? api.previewUrl(p);
4258
4286
  }
4259
4287
 
4260
- /** Çöz + salt-okunur in-page önizleme (openNode/previewNode buraya iner). */
4288
+ /** Decrypt + read-only in-page preview (openNode/previewNode land here). */
4261
4289
  async function e2eOpenPreview(n: FileNode) {
4262
4290
  try {
4263
4291
  await e2eFetchDecrypted(n);
@@ -4272,7 +4300,7 @@ async function e2eOpenPreview(n: FileNode) {
4272
4300
  void markRecent(n);
4273
4301
  }
4274
4302
 
4275
- /** Çöz + orijinal adla indir. Magic'siz dosya olduğu gibi iner. */
4303
+ /** Decrypt + download under the original name. A file with no magic comes down as-is. */
4276
4304
  async function e2eDownload(n: FileNode) {
4277
4305
  try {
4278
4306
  const buf = await api.fetchArrayBuffer(n.path);
@@ -4295,7 +4323,7 @@ async function e2eDownload(n: FileNode) {
4295
4323
  }
4296
4324
  }
4297
4325
 
4298
- /** Upload listesişifreli File listesi (200MB üstü + marker adı atlanır). */
4326
+ /** Upload listencrypted File list (anything over 200MB + the marker name is skipped). */
4299
4327
  async function e2eEncryptUploads(list: File[]): Promise<File[]> {
4300
4328
  const kek = e2eKek();
4301
4329
  if (!kek) return [];
@@ -4316,10 +4344,10 @@ async function e2eEncryptUploads(list: File[]): Promise<File[]> {
4316
4344
  return out;
4317
4345
  }
4318
4346
 
4319
- /** EncryptedFolderModal submit'i: klasörü + marker yükle + kilidi açık bırak. */
4347
+ /** EncryptedFolderModal submit: create the folder + upload the marker + leave it unlocked. */
4320
4348
  async function submitEncryptedFolder(payload: { name: string; password: string }) {
4321
4349
  if (e2eActive.value) {
4322
- // İç içe şifreli klasör kök tespitini bulanıklaştırır — MVP'de yok.
4350
+ // Nested encrypted folders blur root detectionnot in the MVP.
4323
4351
  flashToast(t('e2e.create.nested'));
4324
4352
  return;
4325
4353
  }
@@ -4339,7 +4367,7 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
4339
4367
  });
4340
4368
  const newDirWire = wireJoin(dirWire, payload.name);
4341
4369
  await api.uploadMultipart(newDirWire, [markerFile]);
4342
- // Oluşturan oturumda kilit açık başlar (parolayı az önce kendisi girdi).
4370
+ // It starts unlocked in the creating session (they just typed the password).
4343
4371
  e2eRing.set(newDirWire, fmk);
4344
4372
  e2eRingVer.value++;
4345
4373
  showEncFolder.value = false;
@@ -4610,7 +4638,7 @@ function closeRecoveryKey() {
4610
4638
  @drop="onDropUpload"
4611
4639
  @contextmenu="onContextCanvas"
4612
4640
  >
4613
- <!-- wiring:d1 — sekme şeridi: TEK sekmede hiç render edilmez (embed pixel-aynı) -->
4641
+ <!-- wiring:d1 — tab strip: not rendered at all on a SINGLE tab (embeds stay pixel-identical) -->
4614
4642
  <TabBar
4615
4643
  v-if="tabsVisible"
4616
4644
  :tabs="tabItems"
@@ -4627,7 +4655,7 @@ function closeRecoveryKey() {
4627
4655
  <!-- /wiring:d1 -->
4628
4656
  <Toolbar
4629
4657
  ref="toolbarRef"
4630
- :view-mode="displayedViewMode /* ui-fix — aktif panelin modu */"
4658
+ :view-mode="displayedViewMode /* ui-fix — the active pane's mode */"
4631
4659
  :search-query="searchQuery"
4632
4660
  :trash-active="trashActive"
4633
4661
  :actions="toolbarActions"
@@ -4650,7 +4678,7 @@ function closeRecoveryKey() {
4650
4678
  @toggle-inspector="toggleInspector /* koru:k1 */"
4651
4679
  @toggle-nav="toggleSideNav /* gezinti:g1 */"
4652
4680
  @open-theme="showThemeGallery = true /* wiring:c1 */"
4653
- @update:view-mode="setDisplayedViewMode($event) /* ui-fix — aktif panele */"
4681
+ @update:view-mode="setDisplayedViewMode($event) /* ui-fix — to the active pane */"
4654
4682
  @update:search-query="searchQuery = $event"
4655
4683
  @update:density="density = $event"
4656
4684
  @open-shortcut-settings="showShortcutSettings = true /* wiring:c2 */"
@@ -4713,13 +4741,14 @@ function closeRecoveryKey() {
4713
4741
  :aria-label="t('sidenav.close')"
4714
4742
  @click="closeNavDrawer"
4715
4743
  ></button>
4716
- <!-- ui-fix — sol panelin başlığı (breadcrumb + durum şeritleri + body)
4717
- tek bir sarmalda: split modunda bu sarmal sol yarıya sığar, böylece
4718
- breadcrumb tüm sayfayı değil kendi panelini kaplar (SecondaryPane'in
4719
- kendi kırıntısıyla simetrik). Aktif-panel vurgusu da bu sarmalda. -->
4744
+ <!-- ui-fix — the left pane's header (breadcrumb + status strips + body)
4745
+ in one wrapper: in split mode this wrapper fits the left half, so the
4746
+ breadcrumb spans its own pane rather than the whole page (symmetric
4747
+ with SecondaryPane's own crumbs). The active-pane accent lives on
4748
+ this wrapper too. -->
4720
4749
  <div
4721
4750
  class="fe__primary"
4722
- :class="{ 'fe-pane--focus': mainPaneFocus } /* wiring:d1 — aktif panel vurgusu */"
4751
+ :class="{ 'fe-pane--focus': mainPaneFocus } /* wiring:d1 — active-pane accent */"
4723
4752
  >
4724
4753
  <!-- surucu:d1 — the breadcrumb row. In the drive shell it also carries the
4725
4754
  two controls that belong to a LISTING rather than to the app (the view
@@ -4802,8 +4831,9 @@ function closeRecoveryKey() {
4802
4831
  </span>
4803
4832
  </div>
4804
4833
 
4805
- <!-- wiring:e2 — kilit açık şeridi: şifreli klasörde anahtar bellekteyken
4806
- görünür; "Kilitle" anahtarı ve çözülmüş blob'ları atar. -->
4834
+ <!-- wiring:e2 — unlocked strip: visible in an encrypted folder while the
4835
+ key is in memory; "Kilitle" (Lock) drops the key and the decrypted
4836
+ blobs. -->
4807
4837
  <!-- wiring:e2 recovery — a v1 folder just opened by password. Offer it
4808
4838
  recovery HERE, visibly, rather than doing anything silently: this is
4809
4839
  the only moment filex holds the password, and (when the install has
@@ -4916,7 +4946,7 @@ function closeRecoveryKey() {
4916
4946
  keeps the current list, exactly as before. -->
4917
4947
  <div v-if="loading && files.length === 0" class="fe__skeleton" role="status">
4918
4948
  <span class="fe-sr-only">{{ t('loading') }}</span>
4919
- <div v-if="viewMode !== 'list' /* wiring:d2 — galeri de grid iskeletini kullanır */" class="fe-skel-grid" aria-hidden="true">
4949
+ <div v-if="viewMode !== 'list' /* wiring:d2 — the gallery uses the grid skeleton too */" class="fe-skel-grid" aria-hidden="true">
4920
4950
  <div v-for="i in 8" :key="i" class="fe-skel-card">
4921
4951
  <div class="fe-skel fe-skel--thumb"></div>
4922
4952
  <div class="fe-skel fe-skel--label"></div>
@@ -4994,9 +5024,10 @@ function closeRecoveryKey() {
4994
5024
  </details>
4995
5025
  <!-- /wiring:c4 -->
4996
5026
  </div>
4997
- <!-- wiring:e2 — şifreli klasör kilit ekranı: parola doğru girilene dek
4998
- listeleme render edilmez. Parola tarayıcıda marker'a karşı
4999
- doğrulanır; sunucuya gitmez. -->
5027
+ <!-- wiring:e2 — encrypted-folder lock screen: the listing is not
5028
+ rendered until the correct password is entered. The password is
5029
+ verified against the marker in the browser; it never reaches the
5030
+ server. -->
5000
5031
  <div v-else-if="e2eLocked" class="fe-state fe-e2e-lock">
5001
5032
  <svg
5002
5033
  class="fe-state__art"
@@ -5238,7 +5269,7 @@ function closeRecoveryKey() {
5238
5269
  @item-drop-into="onItemDropInto"
5239
5270
  @star-change="onStarChange"
5240
5271
  />
5241
- <!-- wiring:d2 — galeri görünümü (GridView ile aynı event sözleşmesi) -->
5272
+ <!-- wiring:d2 — gallery view (same event contract as GridView) -->
5242
5273
  <GalleryView
5243
5274
  v-else
5244
5275
  :files="displayFiles /* surucu:d1 */"
@@ -5264,9 +5295,9 @@ function closeRecoveryKey() {
5264
5295
  </div>
5265
5296
  </div><!-- /fe__primary ui-fix -->
5266
5297
 
5267
- <!-- wiring:d1 — tab başına split: sağ ikincil panel (dar modda kapalı).
5268
- :key tab kimliğine bağlı tab geçişinde pane kendi konumuyla temiz
5269
- remount olur. -->
5298
+ <!-- wiring:d1 — per-tab split: the secondary pane on the right (off in
5299
+ narrow mode). :key is bound to the tab id on a tab switch the pane
5300
+ remounts cleanly with its own location. -->
5270
5301
  <SecondaryPane
5271
5302
  :keep-badge-for="desktopSync ? keepBadgeFor : undefined"
5272
5303
  v-if="splitVisible && activeSplit"
@@ -5285,15 +5316,15 @@ function closeRecoveryKey() {
5285
5316
  :active="paneIsActive"
5286
5317
  :view-mode="paneViewMode /* ui-fix */"
5287
5318
  :thumb-src="thumbs.src /* ui-fix */"
5288
- :trash-visible="config.trashVisible !== false /* ui-fix — çöp satırı simetrisi */"
5289
- :nav-offers-trash="navOffersTrash /* surucu:d1 — aynı kapı iki kez açılmaz */"
5319
+ :trash-visible="config.trashVisible !== false /* ui-fix — trash row symmetry */"
5320
+ :nav-offers-trash="navOffersTrash /* surucu:d1 — the same door is not opened twice */"
5290
5321
  @navigate="onPaneNavigate"
5291
5322
  @activate="activePane = 'split'"
5292
5323
  @close="closeSplit"
5293
5324
  @open-tab="(p: string) => tabsApi.openTab(p, { viewMode: viewMode, background: true })"
5294
5325
  @transfer="onPaneTransfer"
5295
- @context="onPaneContext /* ui-fix — yan panel sağ-tık menüsü */"
5296
- @open-trash="onPaneOpenTrash /* ui-fix — çöp kutusu ana panelde açılır */"
5326
+ @context="onPaneContext /* ui-fix — side-pane right-click menu */"
5327
+ @open-trash="onPaneOpenTrash /* ui-fix — the trash opens in the main pane */"
5297
5328
  />
5298
5329
  <!-- /wiring:d1 -->
5299
5330
 
@@ -5364,7 +5395,7 @@ function closeRecoveryKey() {
5364
5395
  <div v-if="dragOver" class="fe__dragover">
5365
5396
  <div class="fe__dragover-card">
5366
5397
  <span class="fe-icon">⬆</span>
5367
- <p>Dosyaları buraya bırak</p>
5398
+ <p>{{ t('dropzone.hint') }}</p>
5368
5399
  </div>
5369
5400
  </div>
5370
5401
 
@@ -5441,12 +5472,12 @@ function closeRecoveryKey() {
5441
5472
  <NewFolderModal
5442
5473
  :open="showNewFolder"
5443
5474
  :locale="locale"
5444
- :encrypted-option="!e2eActive /* wiring:e2 — içe şifreli klasör yok */"
5475
+ :encrypted-option="!e2eActive /* wiring:e2 — no nested encrypted folders */"
5445
5476
  @close="showNewFolder = false"
5446
5477
  @submit="submitNewFolder"
5447
5478
  @encrypted="showNewFolder = false; showEncFolder = true /* wiring:e2 */"
5448
5479
  />
5449
- <!-- wiring:e2 — şifreli klasör oluşturma modalı -->
5480
+ <!-- wiring:e2 — encrypted-folder creation modal -->
5450
5481
  <EncryptedFolderModal
5451
5482
  :open="showEncFolder"
5452
5483
  :locale="locale"
@@ -5506,12 +5537,12 @@ function closeRecoveryKey() {
5506
5537
  :locale="locale"
5507
5538
  :file="previewTarget"
5508
5539
  :theme="themeMode"
5509
- :preview-url="(p) => e2ePreviewSrc(p) /* wiring:e2 — çözülmüş blob > ham URL */"
5540
+ :preview-url="(p) => e2ePreviewSrc(p) /* wiring:e2 — decrypted blob > raw URL */"
5510
5541
  :download-url="(p) => (e2eUnlocked ? e2ePreviewSrc(p) : api.downloadUrl(p)) /* wiring:e2 */"
5511
- :only-office-base="e2eActive ? null : effectiveOnlyOfficeBase /* wiring:e2 — OO ciphertext açamaz */"
5542
+ :only-office-base="e2eActive ? null : effectiveOnlyOfficeBase /* wiring:e2 — OO cannot open ciphertext */"
5512
5543
  :only-office-config-endpoint="effectiveOnlyOfficeConfigEndpoint"
5513
- :new-tab-enabled="!e2eActive /* wiring:e2 — standalone rota ham baytı çeker */"
5514
- :save-text-endpoint="e2eActive ? null : api.endpoints.saveText || null /* wiring:e2 — düz metin kaydı sızıntı olur */"
5544
+ :new-tab-enabled="!e2eActive /* wiring:e2 — the standalone route pulls raw bytes */"
5545
+ :save-text-endpoint="e2eActive ? null : api.endpoints.saveText || null /* wiring:e2 — a plaintext save would be a leak */"
5515
5546
  :open-mode="previewMode"
5516
5547
  :auth-headers="() => buildAuthHeaders({ 'Content-Type': 'application/json' })"
5517
5548
  :auth-credentials="api.credentialsMode()"
@@ -5527,6 +5558,7 @@ function closeRecoveryKey() {
5527
5558
  :file-name="convertTarget?.basename || convertTarget?.path || ''"
5528
5559
  :fetch-bytes="() => api.fetchArrayBuffer(convertTarget?.path ?? '')"
5529
5560
  :upload="(f) => api.uploadMultipart(qualify(currentPath), [f]).then(() => {})"
5561
+ :locale="locale"
5530
5562
  @close="showConvert = false"
5531
5563
  @done="onConvertDone"
5532
5564
  />
@@ -5619,7 +5651,7 @@ function closeRecoveryKey() {
5619
5651
  @navigate="(p: string) => load(p)"
5620
5652
  @new-folder="showNewFolder = true"
5621
5653
  @upload="triggerUpload"
5622
- @toggle-view="setDisplayedViewMode(displayedViewMode === 'list' ? 'grid' : displayedViewMode === 'grid' ? 'gallery' : 'list') /* wiring:d2 + ui-fix — 3 mod döngüsü, aktif panele */"
5654
+ @toggle-view="setDisplayedViewMode(displayedViewMode === 'list' ? 'grid' : displayedViewMode === 'grid' ? 'gallery' : 'list') /* wiring:d2 + ui-fix — 3-mode cycle, to the active pane */"
5623
5655
  @open-trash="loadTrash"
5624
5656
  @refresh="() => load()"
5625
5657
  @go-up="goUp"