@brftech/filex-core 0.31.0 → 0.33.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 (62) hide show
  1. package/README.md +26 -2
  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 +9346 -8069
  7. package/dist/filex-core.js.map +1 -1
  8. package/dist/filex-core.umd.cjs +49 -49
  9. package/dist/filex-core.umd.cjs.map +1 -1
  10. package/dist/index.d.ts +235 -16
  11. package/dist/style.css +1 -1
  12. package/package.json +1 -1
  13. package/src/FileExplorer.vue +776 -246
  14. package/src/components/CommandPalette.vue +21 -5
  15. package/src/components/ConnectionsPanel.vue +2 -1
  16. package/src/components/E2eRecoveryUnlockModal.vue +29 -4
  17. package/src/components/FilterBar.vue +244 -0
  18. package/src/components/GalleryView.vue +12 -1
  19. package/src/components/GridView.vue +48 -5
  20. package/src/components/InspectorPanel.vue +231 -8
  21. package/src/components/ListView.vue +12 -3
  22. package/src/components/NFSExportsPanel.vue +2 -1
  23. package/src/components/PendingOpsTray.vue +1 -1
  24. package/src/components/PresenceBar.vue +3 -2
  25. package/src/components/S3KeysPanel.vue +2 -1
  26. package/src/components/SSHKeysPanel.vue +2 -1
  27. package/src/components/SecondaryPane.vue +28 -13
  28. package/src/components/SideNav.vue +161 -1
  29. package/src/components/StarButton.vue +2 -1
  30. package/src/components/TabBar.vue +2 -2
  31. package/src/components/ThemeGallery.vue +1 -1
  32. package/src/components/TokensPanel.vue +2 -1
  33. package/src/components/Toolbar.vue +244 -58
  34. package/src/components/ViewSwitcher.vue +81 -0
  35. package/src/composables/useFileApi.ts +49 -2
  36. package/src/composables/useLocale.ts +1 -1
  37. package/src/composables/useOperations.ts +3 -3
  38. package/src/composables/usePendingOps.ts +1 -1
  39. package/src/composables/useTabs.ts +1 -1
  40. package/src/composables/useUploadChunked.ts +39 -7
  41. package/src/index.ts +8 -2
  42. package/src/lib/e2ecrypto.ts +191 -3
  43. package/src/lib/fileFilters.ts +143 -0
  44. package/src/lib/listing.ts +47 -0
  45. package/src/lib/themes.ts +7 -7
  46. package/src/lib/transfer.ts +6 -6
  47. package/src/locales/en.ts +115 -0
  48. package/src/locales/index.ts +1 -0
  49. package/src/locales/resolve.ts +66 -0
  50. package/src/locales/tr.ts +116 -0
  51. package/src/modals/ConvertModal.vue +14 -12
  52. package/src/modals/NewFolderModal.vue +1 -1
  53. package/src/modals/PermissionsModal.vue +30 -13
  54. package/src/modals/PreviewModal.vue +9 -9
  55. package/src/styles/base.css +520 -26
  56. package/src/styles/variables.css +1 -1
  57. package/src/types/ExplorerConfig.ts +18 -2
  58. package/src/types/FileNode.ts +1 -1
  59. package/src/viewers/CsvViewer.vue +4 -4
  60. package/src/viewers/DrawioViewer.vue +2 -2
  61. package/dist/CsvViewer-DxujFEM3.js.map +0 -1
  62. package/dist/DrawioViewer-DCvWnX2t.js.map +0 -1
package/src/locales/en.ts CHANGED
@@ -208,6 +208,25 @@ export const en: Record<string, string> = {
208
208
  'toast.renamed': 'Renamed',
209
209
  'toast.moved': 'Moved',
210
210
  'toast.trashed': 'Moved to trash',
211
+ /* i18n:hardcoded — toast copy that used to be Turkish string literals in
212
+ FileExplorer.vue, unreachable by any locale. */
213
+ 'toast.failed': 'Operation failed',
214
+ 'toast.copied': 'Copied',
215
+ 'toast.deleted': 'Deleted',
216
+ 'toast.cut': 'Cut',
217
+ 'toast.cut_ready': 'Cut — ready to paste',
218
+ 'toast.copy_ready': 'Copied — ready to paste',
219
+ 'toast.same_folder_cut': 'Cannot cut into the same folder',
220
+ 'toast.restored': '{n} item(s) restored',
221
+ 'toast.delete_queued': 'Delete queued',
222
+ 'toast.trash_retention': 'Items in the trash are removed automatically once the retention window ends. Permanent deletion is done from the admin panel.',
223
+ 'dropzone.hint': 'Drop files here',
224
+ 'modal.newfolder.invalid': 'Invalid character',
225
+ 'viewer.office_unconfigured': 'OnlyOffice is not configured',
226
+ 'viewer.pdf_inline_failed': 'Your browser could not display this PDF inline.',
227
+ 'viewer.too_large': 'File is too large (>1 MB).',
228
+ 'viewer.csv_rows': '{n} rows',
229
+ 'presence.others': '{n} people',
211
230
  'conn.offline': 'No live connection — changes may be delayed',
212
231
  'conn.tooltip': 'Live connection to the server is unavailable; the list refreshes periodically. This notice disappears once the connection is back.',
213
232
 
@@ -383,6 +402,7 @@ export const en: Record<string, string> = {
383
402
  'split.error': 'Could not load the listing',
384
403
  'split.retry': 'Retry',
385
404
  'split.copy_queued': 'Copy queued',
405
+ 'split.move_queued': 'Move queued',
386
406
  'split.cross_copy': 'Different storages — copy queued instead',
387
407
  'dragout.downloading': 'Downloading into the folder you dropped on…',
388
408
  'dragout.not_found': 'Could not find where it was dropped — a drop onto an application (rather than a folder) cannot be filled in',
@@ -467,6 +487,10 @@ export const en: Record<string, string> = {
467
487
  'e2e.recover.escrow_warn_body':
468
488
  'Opening a folder with the escrow key notifies its owner. Use it only when you are entitled to.',
469
489
  'e2e.recover.escrow_kid': 'Escrow key',
490
+ 'e2e.recover.escrow_predates':
491
+ 'This installation has an escrow key, but this folder does not. It was created before escrow was turned on here, so its master key was never wrapped to the escrow identity. The escrow key will not open it. Nothing the operator can do changes that — adding a slot needs this folder\u2019s password, which the server has never had. The folder\u2019s owner can grant it: unlock with the password, then choose to give the operator a key. Until they do, use the password or the recovery key.',
492
+ 'e2e.recover.escrow_other_key':
493
+ 'This folder carries an escrow key from a DIFFERENT installation. It was created elsewhere and restored here, so this installation\u2019s escrow key does not open it. Only the private key belonging to the escrow key it was sealed to can.',
470
494
  'e2e.recover.escrow_placeholder': 'Paste the escrow private key (PKCS#8, base64 or PEM)',
471
495
  'e2e.recover.bad_format':
472
496
  'That does not look like a recovery key. It is 32 characters in eight groups of four.',
@@ -481,6 +505,25 @@ export const en: Record<string, string> = {
481
505
  'e2e.recover.escrow_done': 'Unlocked with the escrow key — the owner has been notified',
482
506
  'e2e.recover.unlock': 'Unlock',
483
507
  'e2e.recover.busy': 'Unlocking…',
508
+ 'e2e.escrowoffer.title': 'Give the operator a key to this folder?',
509
+ 'e2e.escrowoffer.body':
510
+ 'This folder was created before key escrow was turned on here, so the operator of this installation cannot open it. Right now — while your password is in memory — filex can seal this folder to the escrow key. Your files are not re-encrypted or moved; only the key file changes.',
511
+ 'e2e.escrowoffer.consequence':
512
+ 'What this means: the operator gains a second, permanent way into this folder, without your password. You are notified when that key is used, but that notification is an announcement rather than a control.',
513
+ 'e2e.escrowoffer.learn_more': 'What escrow can and cannot do',
514
+ 'e2e.escrowoffer.password_label': 'Folder password',
515
+ 'e2e.escrowoffer.password_required': 'Enter the folder password.',
516
+ 'e2e.escrowoffer.wrong_password': 'That is not this folder\u2019s password.',
517
+ 'e2e.escrowoffer.accept': 'Give the operator a key',
518
+ 'e2e.escrowoffer.decline': 'No, keep it to myself',
519
+ 'e2e.escrowoffer.cancel': 'Cancel',
520
+ 'e2e.escrowoffer.busy': 'Sealing…',
521
+ 'e2e.escrowoffer.done': 'The operator can now open this folder with the escrow key',
522
+ 'e2e.escrowoffer.declined_toast': 'Kept to yourself. filex will not ask again for this folder.',
523
+ 'e2e.escrowoffer.decline_failed':
524
+ 'Could not record your answer, so filex will ask again next time. Nothing about the folder changed.',
525
+ 'e2e.escrowoffer.failed': 'Could not give the operator a key to this folder',
526
+ 'e2e.escrowoffer.strip_action': 'Escrow key…',
484
527
  'e2e.upgrade.title': 'This folder has no recovery key',
485
528
  'e2e.upgrade.body':
486
529
  '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.',
@@ -943,4 +986,76 @@ export const en: Record<string, string> = {
943
986
  'conn.tokens.expiryNever': 'never',
944
987
  'conn.tokens.capNote': 'Your account and its permissions are the ceiling — asking for more than you have is refused, not granted.',
945
988
  /* === /gezinti:g1 === */
989
+
990
+ /* === surucu:d1 — the Drive shell (uiProfile: 'drive', GitHub #14) ===== */
991
+ 'drive.new': 'New',
992
+ 'drive.new.upload': 'Upload files',
993
+ 'drive.new.folder': 'New folder',
994
+ 'drive.new.request': 'Request files',
995
+ 'drive.search.placeholder': 'Search in {scope}',
996
+ 'drive.search.placeholder_all': 'Search your files',
997
+ 'drive.search.hint_title': 'Search everywhere and run commands ({combo})',
998
+ 'drive.storage.used': '{used} of {total} used',
999
+ 'drive.storage.used_unlimited': '{used} used',
1000
+ 'drive.storage.label': 'Storage',
1001
+ 'drive.section.folders': 'Folders',
1002
+ 'drive.section.files': 'Files',
1003
+ 'filter.aria': 'Filters',
1004
+ 'filter.type': 'Type',
1005
+ 'filter.type.any': 'Any type',
1006
+ 'filter.type.folder': 'Folders',
1007
+ 'filter.type.document': 'Documents',
1008
+ 'filter.type.spreadsheet': 'Spreadsheets',
1009
+ 'filter.type.presentation': 'Presentations',
1010
+ 'filter.type.pdf': 'PDFs',
1011
+ 'filter.type.image': 'Images',
1012
+ 'filter.type.video': 'Videos',
1013
+ 'filter.type.audio': 'Audio',
1014
+ 'filter.type.archive': 'Archives',
1015
+ 'filter.type.code': 'Code',
1016
+ 'filter.modified': 'Modified',
1017
+ 'filter.modified.any': 'Any time',
1018
+ 'filter.modified.today': 'Today',
1019
+ 'filter.modified.7d': 'Last 7 days',
1020
+ 'filter.modified.30d': 'Last 30 days',
1021
+ 'filter.modified.year': 'This year',
1022
+ 'filter.size': 'Size',
1023
+ 'filter.size.any': 'Any size',
1024
+ 'filter.size.lt1': 'Under 1 MB',
1025
+ 'filter.size.1to10': '1 - 10 MB',
1026
+ 'filter.size.10to100': '10 - 100 MB',
1027
+ 'filter.size.gt100': 'Over 100 MB',
1028
+ 'filter.clear': 'Clear filters',
1029
+ 'filter.empty.title': 'Nothing here matches those filters',
1030
+ 'filter.empty.hint': 'Clear a filter, or search everywhere with the field above.',
1031
+ 'filter.count': '{shown} of {total}',
1032
+ 'inspector.tab.details': 'Details',
1033
+ 'inspector.tab.activity': 'Activity',
1034
+ 'inspector.people': 'People with access',
1035
+ 'inspector.people.you': 'You',
1036
+ 'inspector.people.inherited': 'from a parent folder',
1037
+ 'inspector.people.empty': 'Only you, and anyone with a link below.',
1038
+ 'inspector.people.manage': 'Manage access',
1039
+ 'inspector.link': 'Shared link',
1040
+ 'inspector.link.none': 'Not shared',
1041
+ 'inspector.link.create': 'Create link',
1042
+ 'inspector.link.copy': 'Copy link',
1043
+ 'inspector.activity.empty': 'No activity recorded for this item yet.',
1044
+ 'inspector.activity.hint': 'Versions are written when a file changes; comments are written by people.',
1045
+ 'inspector.activity.version': 'Version {n} saved',
1046
+ 'inspector.activity.comment': 'Comment by {who}',
1047
+ 'inspector.activity.select': 'Select a file or folder to see its activity.',
1048
+ /* === /surucu:d1 === */
1049
+ /* i18n:hardcoded — the convert modal shipped with no keys at all: every
1050
+ label was a Turkish literal behind an optional `t` prop nobody passed. */
1051
+ 'convert.title': 'Convert',
1052
+ 'convert.loading': 'Loading the converter…',
1053
+ 'convert.done': 'Converted and uploaded to this folder.',
1054
+ 'convert.close': 'Close',
1055
+ 'convert.from': 'Source',
1056
+ 'convert.search_format': 'Search a target format (pdf, mp4, png…)',
1057
+ 'convert.no_format': 'No matching format.',
1058
+ 'convert.unsupported_input': 'This file type has no source format the converter can read.',
1059
+ 'convert.converting': 'Converting…',
1060
+ 'convert.convert': 'Convert',
946
1061
  };
@@ -10,3 +10,4 @@ import type { LocaleCode } from '../types/ExplorerConfig';
10
10
  export const messages: Record<LocaleCode, Record<string, string>> = { tr, en };
11
11
 
12
12
  export { tr, en };
13
+ export { resolveLocale, detectLocale } from './resolve';
@@ -0,0 +1,66 @@
1
+ import type { LocaleCode } from '../types/ExplorerConfig';
2
+
3
+ /** The locales this component actually has a catalogue for. */
4
+ const SUPPORTED: readonly LocaleCode[] = ['en', 'tr'];
5
+
6
+ /**
7
+ * The language filex falls back to when the host does not say.
8
+ *
9
+ * ⚠ It used to be `tr`, in eleven places. filex is an English-first
10
+ * open-source product, so `<filex-explorer api-base="…">` with no `locale`
11
+ * rendered a Turkish UI to whoever installed it — and the web component
12
+ * defaults `locale` to `''`, so "no locale" is the normal case for an embed.
13
+ * The admin SPA never showed it because it always passes one.
14
+ */
15
+ const FALLBACK: LocaleCode = 'en';
16
+
17
+ /**
18
+ * Resolve the locale to render in.
19
+ *
20
+ * Order, and the reasoning for it:
21
+ *
22
+ * 1. **What the host asked for.** An explicit `locale` always wins. A host
23
+ * that names a language has made a decision, and guessing over it would
24
+ * make the prop advisory.
25
+ * 2. **What the browser asks for**, via `navigator.languages` /
26
+ * `navigator.language` — the same signal `Accept-Language` carries, and
27
+ * the reason a Turkish reader gets Turkish without the embedder doing
28
+ * anything.
29
+ * 3. **`en`.**
30
+ *
31
+ * ⚠ Step 2 makes the same embed render differently for two people. That is
32
+ * deliberate — it is what "follow the browser" means — but it also means a
33
+ * screenshot, a test or a bug report about this component must state its
34
+ * locale, because the component alone no longer determines it. Pass an
35
+ * explicit `locale` anywhere the answer has to be stable.
36
+ *
37
+ * Matching is on the primary subtag, so `tr-TR` and `en-GB` resolve; anything
38
+ * with no catalogue falls through rather than half-rendering.
39
+ */
40
+ export function resolveLocale(explicit?: LocaleCode | '' | null): LocaleCode {
41
+ if (explicit && SUPPORTED.includes(explicit)) return explicit;
42
+ return detectLocale();
43
+ }
44
+
45
+ /**
46
+ * The browser's preference, or `en`.
47
+ *
48
+ * ⚠ Guarded for a non-browser runtime: this package is imported by SSR and by
49
+ * unit tests under Node, where `navigator` is absent — and an exception here
50
+ * would take down the whole explorer over a language choice.
51
+ */
52
+ export function detectLocale(): LocaleCode {
53
+ try {
54
+ const nav = typeof navigator === 'undefined' ? undefined : navigator;
55
+ if (!nav) return FALLBACK;
56
+ const wanted = nav.languages && nav.languages.length ? nav.languages : [nav.language];
57
+ for (const tag of wanted) {
58
+ if (!tag) continue;
59
+ const primary = String(tag).toLowerCase().split('-')[0] as LocaleCode;
60
+ if (SUPPORTED.includes(primary)) return primary;
61
+ }
62
+ } catch {
63
+ /* a hostile or exotic runtime is not a reason to fail to render */
64
+ }
65
+ return FALLBACK;
66
+ }
package/src/locales/tr.ts CHANGED
@@ -208,6 +208,25 @@ export const tr: Record<string, string> = {
208
208
  'toast.renamed': 'Yeniden adlandırıldı',
209
209
  'toast.moved': 'Taşındı',
210
210
  'toast.trashed': 'Çöpe taşındı',
211
+ /* i18n:hardcoded — FileExplorer.vue'da düz string olarak duran, hiçbir dilin
212
+ değiştiremediği bildirim metinleri. */
213
+ 'toast.failed': 'İşlem başarısız',
214
+ 'toast.copied': 'Kopyalandı',
215
+ 'toast.deleted': 'Silindi',
216
+ 'toast.cut': 'Kesildi',
217
+ 'toast.cut_ready': 'Kes → Yapıştır hazır',
218
+ 'toast.copy_ready': 'Kopyala → Yapıştır hazır',
219
+ 'toast.same_folder_cut': 'Aynı klasöre kesilemez',
220
+ 'toast.restored': '{n} öğe geri getirildi',
221
+ 'toast.delete_queued': 'Silme kuyruğa alındı',
222
+ 'toast.trash_retention': 'Çöpteki öğeler saklama süresi sonunda otomatik silinir. Kalıcı silme yönetici panelinden yapılır.',
223
+ 'dropzone.hint': 'Dosyaları buraya bırak',
224
+ 'modal.newfolder.invalid': 'Geçersiz karakter',
225
+ 'viewer.office_unconfigured': 'OnlyOffice yapılandırması yok',
226
+ 'viewer.pdf_inline_failed': 'Tarayıcı PDF\'i inline açamadı.',
227
+ 'viewer.too_large': 'Dosya çok büyük (>1 MB).',
228
+ 'viewer.csv_rows': '{n} satır',
229
+ 'presence.others': '{n} kişi',
211
230
  'conn.offline': 'Canlı bağlantı yok — değişiklikler gecikebilir',
212
231
  'conn.tooltip': 'Sunucuyla canlı bağlantı kurulamadı; liste belirli aralıklarla otomatik yenilenir. Bağlantı geri gelince bu uyarı kaybolur.',
213
232
 
@@ -383,6 +402,7 @@ export const tr: Record<string, string> = {
383
402
  'split.error': 'Liste yüklenemedi',
384
403
  'split.retry': 'Yeniden dene',
385
404
  'split.copy_queued': 'Kopyalama kuyruğa alındı',
405
+ 'split.move_queued': 'Taşıma kuyruğa alındı',
386
406
  'split.cross_copy': 'Depolar farklı — kopyalama kuyruğa alındı',
387
407
  'dragout.downloading': 'Bırakılan klasöre indiriliyor…',
388
408
  'dragout.not_found': 'Bırakılan yer bulunamadı — dosya bir klasöre değil bir uygulamaya bırakıldıysa indirme yapılamaz',
@@ -467,6 +487,10 @@ export const tr: Record<string, string> = {
467
487
  'e2e.recover.escrow_warn_body':
468
488
  'Emanet anahtarıyla açılan klasörün sahibine bildirim gönderilir. Yalnız yetkiniz olduğunda kullanın.',
469
489
  'e2e.recover.escrow_kid': 'Emanet anahtarı',
490
+ 'e2e.recover.escrow_predates':
491
+ 'Bu kurulumun emanet anahtarı var ama bu klasörün yok. Klasör, emanet burada açılmadan önce oluşturulmuş; ana anahtarı emanet kimliğine hiç sarmalanmadı. Emanet anahtarı bu klasörü açmaz. İşletmecinin yapabileceği hiçbir şey bunu değiştirmez — sarmalama için klasörün parolası gerekir ve sunucu onu hiçbir zaman görmedi. Klasörün sahibi verebilir: parolayla kilidi açıp işletmeciye anahtar vermeyi seçebilir. O yapılana kadar parolayı ya da kurtarma anahtarını kullanın.',
492
+ 'e2e.recover.escrow_other_key':
493
+ 'Bu klasör BAŞKA bir kurulumun emanet anahtarını taşıyor. Başka yerde oluşturulup buraya geri yüklenmiş, bu yüzden bu kurulumun emanet anahtarı onu açmaz. Yalnızca sarmalandığı emanet anahtarının özel yarısı açabilir.',
470
494
  'e2e.recover.escrow_placeholder': 'Emanet özel anahtarını yapıştırın (PKCS#8, base64 ya da PEM)',
471
495
  'e2e.recover.bad_format':
472
496
  'Bu bir kurtarma anahtarına benzemiyor. Dörtlü sekiz grup, toplam 32 karakter olmalı.',
@@ -481,6 +505,25 @@ export const tr: Record<string, string> = {
481
505
  'e2e.recover.escrow_done': 'Emanet anahtarıyla açıldı — klasör sahibine bildirildi',
482
506
  'e2e.recover.unlock': 'Kilidi aç',
483
507
  'e2e.recover.busy': 'Açılıyor…',
508
+ 'e2e.escrowoffer.title': 'İşletmeciye bu klasörün anahtarı verilsin mi?',
509
+ 'e2e.escrowoffer.body':
510
+ 'Bu klasör, burada anahtar emaneti açılmadan önce oluşturulmuş; bu yüzden kurulumun işletmecisi onu açamıyor. Şu anda — parolanız bellekteyken — filex bu klasörü emanet anahtarına sarmalayabilir. Dosyalarınız yeniden şifrelenmez, hiçbir yere taşınmaz; yalnızca anahtar dosyası değişir.',
511
+ 'e2e.escrowoffer.consequence':
512
+ 'Bunun anlamı: işletmeci, parolanız olmadan bu klasöre giren ikinci ve kalıcı bir yol kazanır. O anahtar kullanıldığında size bildirim gelir, ama bu bildirim bir denetim değil yalnızca bir duyurudur.',
513
+ 'e2e.escrowoffer.learn_more': 'Emanet anahtarı neyi yapar, neyi yapamaz',
514
+ 'e2e.escrowoffer.password_label': 'Klasör parolası',
515
+ 'e2e.escrowoffer.password_required': 'Klasör parolasını girin.',
516
+ 'e2e.escrowoffer.wrong_password': 'Bu, bu klasörün parolası değil.',
517
+ 'e2e.escrowoffer.accept': 'İşletmeciye anahtar ver',
518
+ 'e2e.escrowoffer.decline': 'Hayır, bende kalsın',
519
+ 'e2e.escrowoffer.cancel': 'Vazgeç',
520
+ 'e2e.escrowoffer.busy': 'Sarmalanıyor…',
521
+ 'e2e.escrowoffer.done': 'İşletmeci artık bu klasörü emanet anahtarıyla açabilir',
522
+ 'e2e.escrowoffer.declined_toast': 'Sizde kaldı. filex bu klasör için bir daha sormayacak.',
523
+ 'e2e.escrowoffer.decline_failed':
524
+ 'Cevabınız kaydedilemedi, bu yüzden filex bir dahakine yine soracak. Klasörde hiçbir şey değişmedi.',
525
+ 'e2e.escrowoffer.failed': 'İşletmeciye bu klasörün anahtarı verilemedi',
526
+ 'e2e.escrowoffer.strip_action': 'Emanet anahtarı…',
484
527
  'e2e.upgrade.title': 'Bu klasörün kurtarma anahtarı yok',
485
528
  'e2e.upgrade.body':
486
529
  '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.',
@@ -946,4 +989,77 @@ export const tr: Record<string, string> = {
946
989
  'conn.tokens.expiryNever': 'süresiz',
947
990
  'conn.tokens.capNote': 'Tavan hesabın ve yetkilerin — sahip olduğundan fazlasını istemek reddedilir, verilmez.',
948
991
  /* === /gezinti:g1 === */
992
+
993
+ /* === surucu:d1 — Sürücü kabuğu (uiProfile: 'drive', GitHub #14) ======== */
994
+ 'drive.new': 'Yeni',
995
+ 'drive.new.upload': 'Dosya yükle',
996
+ 'drive.new.folder': 'Yeni klasör',
997
+ 'drive.new.request': 'Dosya iste',
998
+ 'drive.search.placeholder': '{scope} içinde ara',
999
+ 'drive.search.placeholder_all': 'Dosyalarında ara',
1000
+ 'drive.search.hint_title': 'Her yerde ara ve komut çalıştır ({combo})',
1001
+ 'drive.storage.used': '{total} alanın {used} kadarı dolu',
1002
+ 'drive.storage.used_unlimited': '{used} kullanılıyor',
1003
+ 'drive.storage.label': 'Depolama',
1004
+ 'drive.section.folders': 'Klasörler',
1005
+ 'drive.section.files': 'Dosyalar',
1006
+ 'filter.aria': 'Süzgeçler',
1007
+ 'filter.type': 'Tür',
1008
+ 'filter.type.any': 'Her tür',
1009
+ 'filter.type.folder': 'Klasörler',
1010
+ 'filter.type.document': 'Belgeler',
1011
+ 'filter.type.spreadsheet': 'Hesap tabloları',
1012
+ 'filter.type.presentation': 'Sunumlar',
1013
+ 'filter.type.pdf': 'PDF’ler',
1014
+ 'filter.type.image': 'Görseller',
1015
+ 'filter.type.video': 'Videolar',
1016
+ 'filter.type.audio': 'Ses dosyaları',
1017
+ 'filter.type.archive': 'Arşivler',
1018
+ 'filter.type.code': 'Kod',
1019
+ 'filter.modified': 'Değiştirilme',
1020
+ 'filter.modified.any': 'Her zaman',
1021
+ 'filter.modified.today': 'Bugün',
1022
+ 'filter.modified.7d': 'Son 7 gün',
1023
+ 'filter.modified.30d': 'Son 30 gün',
1024
+ 'filter.modified.year': 'Bu yıl',
1025
+ 'filter.size': 'Boyut',
1026
+ 'filter.size.any': 'Her boyut',
1027
+ 'filter.size.lt1': '1 MB altı',
1028
+ 'filter.size.1to10': '1 - 10 MB',
1029
+ 'filter.size.10to100': '10 - 100 MB',
1030
+ 'filter.size.gt100': '100 MB üstü',
1031
+ 'filter.clear': 'Süzgeçleri temizle',
1032
+ 'filter.empty.title': 'Bu süzgeçlere uyan bir şey yok',
1033
+ 'filter.empty.hint': 'Bir süzgeci kaldır ya da yukarıdaki alandan her yerde ara.',
1034
+ 'filter.count': '{total} ögeden {shown} tanesi',
1035
+ 'inspector.tab.details': 'Ayrıntılar',
1036
+ 'inspector.tab.activity': 'Etkinlik',
1037
+ 'inspector.people': 'Erişimi olan kişiler',
1038
+ 'inspector.people.you': 'Sen',
1039
+ 'inspector.people.inherited': 'üst klasörden',
1040
+ 'inspector.people.empty': 'Yalnızca sen ve aşağıdaki bağlantıya sahip olanlar.',
1041
+ 'inspector.people.manage': 'Erişimi yönet',
1042
+ 'inspector.link': 'Paylaşım bağlantısı',
1043
+ 'inspector.link.none': 'Paylaşılmadı',
1044
+ 'inspector.link.create': 'Bağlantı oluştur',
1045
+ 'inspector.link.copy': 'Bağlantıyı kopyala',
1046
+ 'inspector.activity.empty': 'Bu öge için henüz etkinlik kaydı yok.',
1047
+ 'inspector.activity.hint': 'Sürümler dosya değiştikçe, yorumlar ise insanlar yazdıkça birikir.',
1048
+ 'inspector.activity.version': '{n}. sürüm kaydedildi',
1049
+ 'inspector.activity.comment': '{who} yorum yaptı',
1050
+ 'inspector.activity.select': 'Etkinliğini görmek için bir dosya ya da klasör seç.',
1051
+ /* === /surucu:d1 === */
1052
+ /* i18n:hardcoded — dönüştürme kutusunda hiç anahtar yoktu: her etiket,
1053
+ kimsenin geçirmediği isteğe bağlı bir `t` prop'unun arkasındaki düz
1054
+ Türkçe metindi. */
1055
+ 'convert.title': 'Dönüştür',
1056
+ 'convert.loading': 'Dönüştürücü yükleniyor…',
1057
+ 'convert.done': 'Dönüştürüldü ve klasöre yüklendi.',
1058
+ 'convert.close': 'Kapat',
1059
+ 'convert.from': 'Kaynak',
1060
+ 'convert.search_format': 'Hedef format ara (pdf, mp4, png…)',
1061
+ 'convert.no_format': 'Eşleşen format yok.',
1062
+ 'convert.unsupported_input': 'Bu dosya tipi için kaynak format bulunamadı.',
1063
+ 'convert.converting': 'Dönüştürülüyor…',
1064
+ 'convert.convert': 'Dönüştür',
949
1065
  };
@@ -15,6 +15,8 @@
15
15
  * 5. we wrap the bytes in a File and upload it to the current folder.
16
16
  */
17
17
  import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
18
+ import type { LocaleCode } from '../types/ExplorerConfig';
19
+ import { useLocale } from '../composables/useLocale';
18
20
 
19
21
  const props = defineProps<{
20
22
  /** Converter base, e.g. https://fm.example.com/convert */
@@ -25,9 +27,11 @@ const props = defineProps<{
25
27
  fetchBytes: () => Promise<ArrayBuffer>;
26
28
  /** Upload the produced File into the current folder (api.uploadMultipart bound to dir). */
27
29
  upload: (file: File) => Promise<void>;
28
- t?: (key: string, fallback: string) => string;
30
+ locale: LocaleCode;
29
31
  }>();
30
32
 
33
+ const { t } = useLocale(() => props.locale);
34
+
31
35
  const emit = defineEmits<{ (e: 'close'): void; (e: 'done', name: string): void }>();
32
36
 
33
37
  interface Fmt {
@@ -107,7 +111,7 @@ async function loadFormats() {
107
111
  formats.value = res.formats || [];
108
112
  status.value = 'ready';
109
113
  if (!fromFmt.value) {
110
- error.value = tt('convert.unsupportedInput', 'Bu dosya tipi için kaynak format bulunamadı.');
114
+ error.value = t('convert.unsupported_input');
111
115
  }
112
116
  } catch (e) {
113
117
  error.value = (e as Error).message;
@@ -139,8 +143,6 @@ async function doConvert() {
139
143
  }
140
144
  }
141
145
 
142
- function tt(k: string, fb: string) { return props.t ? props.t(k, fb) : fb; }
143
-
144
146
  onMounted(() => window.addEventListener('message', onMessage));
145
147
  onBeforeUnmount(() => window.removeEventListener('message', onMessage));
146
148
  </script>
@@ -149,27 +151,27 @@ onBeforeUnmount(() => window.removeEventListener('message', onMessage));
149
151
  <div class="filex-cv__bg" @click.self="emit('close')">
150
152
  <div class="filex-cv">
151
153
  <header class="filex-cv__head">
152
- <h3>{{ tt('convert.title', 'Dönüştür') }} — {{ fileName }}</h3>
154
+ <h3>{{ t('convert.title') }} — {{ fileName }}</h3>
153
155
  <button class="filex-cv__x" @click="emit('close')">✕</button>
154
156
  </header>
155
157
 
156
158
  <div v-if="status === 'loading'" class="filex-cv__msg">
157
- {{ tt('convert.loading', 'Dönüştürücü yükleniyor…') }}
159
+ {{ t('convert.loading') }}
158
160
  </div>
159
161
 
160
162
  <div v-else-if="status === 'done'" class="filex-cv__msg filex-cv__ok">
161
- <p>✓ {{ tt('convert.done', 'Dönüştürüldü ve klasöre yüklendi.') }}</p>
162
- <button class="filex-cv__convert" @click="emit('close')">{{ tt('convert.close', 'Kapat') }}</button>
163
+ <p>✓ {{ t('convert.done') }}</p>
164
+ <button class="filex-cv__convert" @click="emit('close')">{{ t('convert.close') }}</button>
163
165
  </div>
164
166
 
165
167
  <template v-else>
166
168
  <p class="filex-cv__src">
167
- {{ tt('convert.from', 'Kaynak') }}: <b>{{ srcExt || '?' }}</b>
169
+ {{ t('convert.from') }}: <b>{{ srcExt || '?' }}</b>
168
170
  </p>
169
171
  <input
170
172
  v-model="search"
171
173
  class="filex-cv__search"
172
- :placeholder="tt('convert.searchFmt', 'Hedef format ara (pdf, mp4, png…)')"
174
+ :placeholder="t('convert.search_format')"
173
175
  />
174
176
  <div class="filex-cv__list">
175
177
  <button
@@ -182,7 +184,7 @@ onBeforeUnmount(() => window.removeEventListener('message', onMessage));
182
184
  <small>{{ f.name }}</small>
183
185
  </button>
184
186
  <div v-if="toList.length === 0" class="filex-cv__empty">
185
- {{ tt('convert.noFmt', 'Eşleşen format yok.') }}
187
+ {{ t('convert.no_format') }}
186
188
  </div>
187
189
  </div>
188
190
  <div v-if="error" class="filex-cv__err">{{ error }}</div>
@@ -192,7 +194,7 @@ onBeforeUnmount(() => window.removeEventListener('message', onMessage));
192
194
  :disabled="!selectedTo || !fromFmt || status === 'converting'"
193
195
  @click="doConvert"
194
196
  >
195
- {{ status === 'converting' ? tt('convert.converting', 'Dönüştürülüyor…') : tt('convert.convert', 'Dönüştür') }}
197
+ {{ status === 'converting' ? t('convert.converting') : t('convert.convert') }}
196
198
  </button>
197
199
  </footer>
198
200
  </template>
@@ -38,7 +38,7 @@ function submit() {
38
38
  return;
39
39
  }
40
40
  if (/[\\/]/.test(clean) || clean === '.' || clean === '..') {
41
- err.value = 'Geçersiz karakter';
41
+ err.value = t('modal.newfolder.invalid');
42
42
  return;
43
43
  }
44
44
  emit('submit', clean);
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
- // Access modal — one popup combining "İzinler" (per-user RBAC grants,
3
- // owner-only) and "Bağlantı ile paylaş" (public share link, editor+). Opened
4
- // from the explorer's unified "Paylaş / İzinler" action. The layout is a fixed
2
+ // Access modal — one popup combining "Permissions" (per-user RBAC grants,
3
+ // owner-only) and "Share by link" (public share link, editor+). Opened
4
+ // from the explorer's unified "Share / Permissions" action. The layout is a fixed
5
5
  // header/tabs with a single scrollable body so the popup never grows into one
6
6
  // long scroll. Styling uses the SFC's --fe-* theme variables (light/dark).
7
7
  import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue';
@@ -9,6 +9,7 @@ import type { FileApi, Grant, UserSuggestion } from '../composables/useFileApi';
9
9
  import type { ShareInfo } from '../types/FileNode';
10
10
  import { shareCliCommand } from '../lib/shareCli';
11
11
  import { STOCK_EXPIRY_DAYS, clampExpiryOptions, defaultExpiryDays, ttlCeilingHint, validUntilLine } from '../lib/shareTtl';
12
+ import { resolveLocale } from '../locales/resolve';
12
13
 
13
14
  const props = defineProps<{
14
15
  api: FileApi;
@@ -19,10 +20,24 @@ const props = defineProps<{
19
20
  /** Server ceiling on a new link's life in days (capabilities.share_max_ttl_days).
20
21
  * undefined/0 = no ceiling. The expiry choices are derived from it. */
21
22
  shareMaxTtlDays?: number;
23
+ /**
24
+ * surucu:d1 — which tab this modal opens on. Absent = 'perms', the behaviour
25
+ * every existing caller has.
26
+ *
27
+ * The drive shell's "Request files" needs 'drop': the file-drop link already
28
+ * lives in this modal, and a menu entry that opens it on the permissions tab
29
+ * and leaves the user to find the third tab is a menu entry that lied about
30
+ * what it does.
31
+ *
32
+ * ⚠ A hint, not a lock: `load()` still falls back to 'share' when the caller
33
+ * turns out not to be an owner, because the permissions tab is not theirs to
34
+ * see.
35
+ */
36
+ initialTab?: 'perms' | 'share' | 'drop';
22
37
  }>();
23
38
  const emit = defineEmits<{ (e: 'close'): void }>();
24
39
 
25
- const tr = computed(() => (props.locale ?? 'tr') !== 'en');
40
+ const tr = computed(() => (resolveLocale(props.locale)) !== 'en');
26
41
  function L(t: string, e: string): string {
27
42
  return tr.value ? t : e;
28
43
  }
@@ -37,7 +52,7 @@ const pathParts = computed(() => {
37
52
  });
38
53
 
39
54
  type Tab = 'perms' | 'share' | 'drop';
40
- const tab = ref<Tab>('perms');
55
+ const tab = ref<Tab>(props.initialTab ?? 'perms');
41
56
  const canManage = ref(false); // owner/admin → can see the permissions tab
42
57
 
43
58
  // ── permissions state ──
@@ -185,7 +200,9 @@ async function reload() {
185
200
  if (st === 403) {
186
201
  canManage.value = false;
187
202
  // Editor (not owner): no permissions tab → fall back to the link tab.
188
- tab.value = 'share';
203
+ // Unless the caller asked for a specific one: "Request files" opens
204
+ // 'drop', which an editor may perfectly well use.
205
+ if (!props.initialTab || props.initialTab === 'perms') tab.value = 'share';
189
206
  } else {
190
207
  err.value = e instanceof Error ? e.message : String(e);
191
208
  }
@@ -284,7 +301,7 @@ async function inviteCreateUser() {
284
301
  const r = await props.api.invitePermission({
285
302
  path: props.path, email: email.value.trim().toLowerCase(),
286
303
  level: level.value, create_user: true, role: createRole.value, is_dir: !!props.isDir,
287
- locale: props.locale ?? 'tr',
304
+ locale: resolveLocale(props.locale),
288
305
  });
289
306
  inviteResult.value = { tempPassword: r.temp_password };
290
307
  notice.value = r.emailed
@@ -298,7 +315,7 @@ async function inviteCreateUser() {
298
315
  busy.value = false;
299
316
  }
300
317
  }
301
- // "Sadece paylaş" → jump to the share tab with the address prefilled so the
318
+ // "Just send a share link" → jump to the share tab with the address prefilled so the
302
319
  // owner creates a link (with their chosen expiry/PIN) and mails it there.
303
320
  function gotoShareWithMail() {
304
321
  shareMailTo.value = email.value.trim().toLowerCase();
@@ -372,7 +389,7 @@ async function sendShareMail() {
372
389
  path: props.path, emails: list, url: shareResult.value.url,
373
390
  pin: shareResult.value.pin ?? undefined,
374
391
  expires_days: shareExpiry.value || undefined,
375
- locale: props.locale ?? 'tr',
392
+ locale: resolveLocale(props.locale),
376
393
  is_dir: !!props.isDir,
377
394
  size: props.size,
378
395
  });
@@ -440,7 +457,7 @@ async function sendDropMail() {
440
457
  path: props.path, emails: list, url: dropResult.value.url,
441
458
  pin: dropResult.value.pin ?? undefined,
442
459
  expires_days: dropExpiry.value || undefined,
443
- locale: props.locale ?? 'tr',
460
+ locale: resolveLocale(props.locale),
444
461
  is_dir: true,
445
462
  mode: 'drop',
446
463
  });
@@ -740,7 +757,7 @@ async function nativeShare(body: { title: string; text: string }) {
740
757
  </div>
741
758
  <div v-if="shareMailNotice" class="fx-perm-notice">{{ shareMailNotice }}</div>
742
759
 
743
- <!-- native share (OS share sheet) — same as the fishapp Paylaş button -->
760
+ <!-- native share (OS share sheet) — same as the fishapp Share button -->
744
761
  <button v-if="canShare" type="button" class="fx-perm-btn fx-perm-btn--ghost fx-perm-sharebtn" @click="nativeShare(shareBody())">
745
762
  <span aria-hidden="true">📤</span> {{ L('Paylaş', 'Share') }}
746
763
  </button>
@@ -839,7 +856,7 @@ async function nativeShare(body: { title: string; text: string }) {
839
856
  </div>
840
857
  <div v-if="dropMailNotice" class="fx-perm-notice">{{ dropMailNotice }}</div>
841
858
 
842
- <!-- native share (OS share sheet) — same as the fishapp Paylaş button -->
859
+ <!-- native share (OS share sheet) — same as the fishapp Share button -->
843
860
  <button v-if="canShare" type="button" class="fx-perm-btn fx-perm-btn--ghost fx-perm-sharebtn" @click="nativeShare(dropBody())">
844
861
  <span aria-hidden="true">📤</span> {{ L('Paylaş', 'Share') }}
845
862
  </button>
@@ -1010,7 +1027,7 @@ async function nativeShare(body: { title: string; text: string }) {
1010
1027
  .fx-perm-hint { font-size: 13px; color: var(--fe-text-muted); margin: 10px 0 0; }
1011
1028
  .fx-perm-ttlhint { margin: 4px 0 8px; }
1012
1029
  .fx-perm-validuntil { margin: 4px 0 6px; }
1013
- /* native "Paylaş" button — sits under the mail row, full width */
1030
+ /* native "Share" button — sits under the mail row, full width */
1014
1031
  .fx-perm-sharebtn { display: flex; width: 100%; align-items: center; justify-content: center; gap: 6px; margin-top: 8px; }
1015
1032
 
1016
1033
  /* file-drop tab */