@brftech/filex-core 0.32.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 (52) hide show
  1. package/dist/{CsvViewer-DxujFEM3.js → CsvViewer-CqWeV8VO.js} +22 -22
  2. package/dist/CsvViewer-CqWeV8VO.js.map +1 -0
  3. package/dist/{DrawioViewer-DCvWnX2t.js → DrawioViewer-BNALOB04.js} +16 -16
  4. package/dist/DrawioViewer-BNALOB04.js.map +1 -0
  5. package/dist/filex-core.js +5932 -5841
  6. package/dist/filex-core.js.map +1 -1
  7. package/dist/filex-core.umd.cjs +43 -43
  8. package/dist/filex-core.umd.cjs.map +1 -1
  9. package/dist/index.d.ts +49 -8
  10. package/package.json +1 -1
  11. package/src/FileExplorer.vue +267 -235
  12. package/src/components/CommandPalette.vue +3 -3
  13. package/src/components/ConnectionsPanel.vue +2 -1
  14. package/src/components/GalleryView.vue +3 -3
  15. package/src/components/GridView.vue +4 -4
  16. package/src/components/ListView.vue +4 -4
  17. package/src/components/NFSExportsPanel.vue +2 -1
  18. package/src/components/PendingOpsTray.vue +1 -1
  19. package/src/components/PresenceBar.vue +3 -2
  20. package/src/components/S3KeysPanel.vue +2 -1
  21. package/src/components/SSHKeysPanel.vue +2 -1
  22. package/src/components/SecondaryPane.vue +13 -12
  23. package/src/components/StarButton.vue +2 -1
  24. package/src/components/TabBar.vue +2 -2
  25. package/src/components/ThemeGallery.vue +1 -1
  26. package/src/components/TokensPanel.vue +2 -1
  27. package/src/components/Toolbar.vue +9 -9
  28. package/src/composables/useFileApi.ts +4 -2
  29. package/src/composables/useLocale.ts +1 -1
  30. package/src/composables/useOperations.ts +3 -3
  31. package/src/composables/usePendingOps.ts +1 -1
  32. package/src/composables/useTabs.ts +1 -1
  33. package/src/composables/useUploadChunked.ts +39 -7
  34. package/src/index.ts +2 -2
  35. package/src/lib/themes.ts +7 -7
  36. package/src/lib/transfer.ts +6 -6
  37. package/src/locales/en.ts +32 -0
  38. package/src/locales/index.ts +1 -0
  39. package/src/locales/resolve.ts +66 -0
  40. package/src/locales/tr.ts +33 -0
  41. package/src/modals/ConvertModal.vue +14 -12
  42. package/src/modals/NewFolderModal.vue +1 -1
  43. package/src/modals/PermissionsModal.vue +12 -11
  44. package/src/modals/PreviewModal.vue +9 -9
  45. package/src/styles/base.css +29 -26
  46. package/src/styles/variables.css +1 -1
  47. package/src/types/ExplorerConfig.ts +1 -1
  48. package/src/types/FileNode.ts +1 -1
  49. package/src/viewers/CsvViewer.vue +4 -4
  50. package/src/viewers/DrawioViewer.vue +2 -2
  51. package/dist/CsvViewer-DxujFEM3.js.map +0 -1
  52. package/dist/DrawioViewer-DCvWnX2t.js.map +0 -1
package/src/lib/themes.ts CHANGED
@@ -105,7 +105,7 @@ export const THEMES: ThemeDef[] = [
105
105
  },
106
106
  },
107
107
  {
108
- // Gece Mavisi — deep indigo/navy.
108
+ // Night Blue — deep indigo/navy.
109
109
  id: 'night',
110
110
  nameKey: 'theme.name.night',
111
111
  light: {
@@ -140,7 +140,7 @@ export const THEMES: ThemeDef[] = [
140
140
  },
141
141
  },
142
142
  {
143
- // Orman — calm greens.
143
+ // Forest — calm greens.
144
144
  id: 'forest',
145
145
  nameKey: 'theme.name.forest',
146
146
  light: {
@@ -175,7 +175,7 @@ export const THEMES: ThemeDef[] = [
175
175
  },
176
176
  },
177
177
  {
178
- // Kehribar — warm amber/bronze.
178
+ // Amber — warm amber/bronze.
179
179
  id: 'amber',
180
180
  nameKey: 'theme.name.amber',
181
181
  light: {
@@ -210,7 +210,7 @@ export const THEMES: ThemeDef[] = [
210
210
  },
211
211
  },
212
212
  {
213
- // Leylak — soft purple.
213
+ // Lilac — soft purple.
214
214
  id: 'lilac',
215
215
  nameKey: 'theme.name.lilac',
216
216
  light: {
@@ -245,7 +245,7 @@ export const THEMES: ThemeDef[] = [
245
245
  },
246
246
  },
247
247
  {
248
- // Yüksek Kontrast — black/white with strong borders, saturated accents.
248
+ // High Contrast — black/white with strong borders, saturated accents.
249
249
  id: 'contrast',
250
250
  nameKey: 'theme.name.contrast',
251
251
  light: {
@@ -284,7 +284,7 @@ export const THEMES: ThemeDef[] = [
284
284
  },
285
285
  },
286
286
  {
287
- // Yumuşak Gri — desaturated, quiet neutral.
287
+ // Soft Gray — desaturated, quiet neutral.
288
288
  id: 'gray',
289
289
  nameKey: 'theme.name.gray',
290
290
  light: {
@@ -319,7 +319,7 @@ export const THEMES: ThemeDef[] = [
319
319
  },
320
320
  },
321
321
  {
322
- // Terminal Yeşili — phosphor green, monospace face for the full CRT vibe.
322
+ // Terminal Green — phosphor green, monospace face for the full CRT vibe.
323
323
  id: 'terminal',
324
324
  nameKey: 'theme.name.terminal',
325
325
  light: {
@@ -3,14 +3,14 @@
3
3
  *
4
4
  * Three gestures land on the same wire call — drag a row onto a folder, paste
5
5
  * after Ctrl+X, paste after Ctrl+C — and they do NOT mean the same thing once
6
- * the two ends live in different depolar:
6
+ * the two ends live in different storages:
7
7
  *
8
8
  * • Ctrl+C → paste is a copy, wherever it lands.
9
- * • Ctrl+X → paste is a move, wherever it lands. Across depolar the server
9
+ * • Ctrl+X → paste is a move, wherever it lands. Across storages the server
10
10
  * streams the bytes over and then deletes the original; before v0.27.0 the
11
11
  * explorer quietly downgraded this to a copy and the user was left with the
12
12
  * file in both places, believing they had moved it.
13
- * • Dragging is a move inside one depo and a COPY across two — the rule
13
+ * • Dragging is a move inside one storage and a COPY across two — the rule
14
14
  * Explorer and Finder have taught everyone: a drag between drives copies.
15
15
  *
16
16
  * Kept as a pure function so both the pane path and the clipboard path ask the
@@ -23,13 +23,13 @@ export function wireAdapterOf(p: string): string {
23
23
  return i === -1 ? '' : p.slice(0, i);
24
24
  }
25
25
 
26
- /** What the caller asked for. `auto` is a drag: let the depolar decide. */
26
+ /** What the caller asked for. `auto` is a drag: let the storages decide. */
27
27
  export type TransferIntent = 'auto' | 'copy' | 'move';
28
28
 
29
29
  export interface TransferPlan {
30
30
  /** What to actually ask the server for. */
31
31
  kind: 'copy' | 'move';
32
- /** True when at least one source lives in another depo than the target. */
32
+ /** True when at least one source lives in another storage than the target. */
33
33
  cross: boolean;
34
34
  }
35
35
 
@@ -40,7 +40,7 @@ export function resolveTransfer(
40
40
  ): TransferPlan {
41
41
  const target = wireAdapterOf(targetWire);
42
42
  // A source with no prefix is a legacy embedder's bare path: it can only mean
43
- // "the same depo I am looking at", so it never counts as crossing.
43
+ // "the same storage I am looking at", so it never counts as crossing.
44
44
  const cross = sources.some((p) => {
45
45
  const a = wireAdapterOf(p);
46
46
  return a !== '' && target !== '' && a !== target;
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',
@@ -1026,4 +1046,16 @@ export const en: Record<string, string> = {
1026
1046
  'inspector.activity.comment': 'Comment by {who}',
1027
1047
  'inspector.activity.select': 'Select a file or folder to see its activity.',
1028
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',
1029
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',
@@ -1029,4 +1049,17 @@ export const tr: Record<string, string> = {
1029
1049
  'inspector.activity.comment': '{who} yorum yaptı',
1030
1050
  'inspector.activity.select': 'Etkinliğini görmek için bir dosya ya da klasör seç.',
1031
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',
1032
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;
@@ -36,7 +37,7 @@ const props = defineProps<{
36
37
  }>();
37
38
  const emit = defineEmits<{ (e: 'close'): void }>();
38
39
 
39
- const tr = computed(() => (props.locale ?? 'tr') !== 'en');
40
+ const tr = computed(() => (resolveLocale(props.locale)) !== 'en');
40
41
  function L(t: string, e: string): string {
41
42
  return tr.value ? t : e;
42
43
  }
@@ -300,7 +301,7 @@ async function inviteCreateUser() {
300
301
  const r = await props.api.invitePermission({
301
302
  path: props.path, email: email.value.trim().toLowerCase(),
302
303
  level: level.value, create_user: true, role: createRole.value, is_dir: !!props.isDir,
303
- locale: props.locale ?? 'tr',
304
+ locale: resolveLocale(props.locale),
304
305
  });
305
306
  inviteResult.value = { tempPassword: r.temp_password };
306
307
  notice.value = r.emailed
@@ -314,7 +315,7 @@ async function inviteCreateUser() {
314
315
  busy.value = false;
315
316
  }
316
317
  }
317
- // "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
318
319
  // owner creates a link (with their chosen expiry/PIN) and mails it there.
319
320
  function gotoShareWithMail() {
320
321
  shareMailTo.value = email.value.trim().toLowerCase();
@@ -388,7 +389,7 @@ async function sendShareMail() {
388
389
  path: props.path, emails: list, url: shareResult.value.url,
389
390
  pin: shareResult.value.pin ?? undefined,
390
391
  expires_days: shareExpiry.value || undefined,
391
- locale: props.locale ?? 'tr',
392
+ locale: resolveLocale(props.locale),
392
393
  is_dir: !!props.isDir,
393
394
  size: props.size,
394
395
  });
@@ -456,7 +457,7 @@ async function sendDropMail() {
456
457
  path: props.path, emails: list, url: dropResult.value.url,
457
458
  pin: dropResult.value.pin ?? undefined,
458
459
  expires_days: dropExpiry.value || undefined,
459
- locale: props.locale ?? 'tr',
460
+ locale: resolveLocale(props.locale),
460
461
  is_dir: true,
461
462
  mode: 'drop',
462
463
  });
@@ -756,7 +757,7 @@ async function nativeShare(body: { title: string; text: string }) {
756
757
  </div>
757
758
  <div v-if="shareMailNotice" class="fx-perm-notice">{{ shareMailNotice }}</div>
758
759
 
759
- <!-- native share (OS share sheet) — same as the fishapp Paylaş button -->
760
+ <!-- native share (OS share sheet) — same as the fishapp Share button -->
760
761
  <button v-if="canShare" type="button" class="fx-perm-btn fx-perm-btn--ghost fx-perm-sharebtn" @click="nativeShare(shareBody())">
761
762
  <span aria-hidden="true">📤</span> {{ L('Paylaş', 'Share') }}
762
763
  </button>
@@ -855,7 +856,7 @@ async function nativeShare(body: { title: string; text: string }) {
855
856
  </div>
856
857
  <div v-if="dropMailNotice" class="fx-perm-notice">{{ dropMailNotice }}</div>
857
858
 
858
- <!-- native share (OS share sheet) — same as the fishapp Paylaş button -->
859
+ <!-- native share (OS share sheet) — same as the fishapp Share button -->
859
860
  <button v-if="canShare" type="button" class="fx-perm-btn fx-perm-btn--ghost fx-perm-sharebtn" @click="nativeShare(dropBody())">
860
861
  <span aria-hidden="true">📤</span> {{ L('Paylaş', 'Share') }}
861
862
  </button>
@@ -1026,7 +1027,7 @@ async function nativeShare(body: { title: string; text: string }) {
1026
1027
  .fx-perm-hint { font-size: 13px; color: var(--fe-text-muted); margin: 10px 0 0; }
1027
1028
  .fx-perm-ttlhint { margin: 4px 0 8px; }
1028
1029
  .fx-perm-validuntil { margin: 4px 0 6px; }
1029
- /* native "Paylaş" button — sits under the mail row, full width */
1030
+ /* native "Share" button — sits under the mail row, full width */
1030
1031
  .fx-perm-sharebtn { display: flex; width: 100%; align-items: center; justify-content: center; gap: 6px; margin-top: 8px; }
1031
1032
 
1032
1033
  /* file-drop tab */
@@ -12,7 +12,7 @@
12
12
  * office (docx/xlsx/pptx) → OnlyOffice iframe (config.onlyOfficeBase)
13
13
  * plain text (txt/log/conf) → CodeMirror when saveText is set,
14
14
  * `<pre>` otherwise
15
- * anything else → "indir" fallback
15
+ * anything else → "Download" fallback
16
16
  *
17
17
  * Monaco is dynamic-imported at FileExplorer onMounted; by the time the
18
18
  * user clicks an editable code file the chunk is usually already cached.
@@ -46,7 +46,7 @@ const props = defineProps<{
46
46
  pdfSaveUrl?: string | null;
47
47
  /** Standalone full-screen viewer route — `?path=…&storage=…&type=…`. */
48
48
  viewerBaseUrl?: string | null;
49
- /* wiring:e2 — false hides the "Düzenle" / "Yeni Sekmede " buttons.
49
+ /* wiring:e2 — false hides the "Edit" / "Open in New Tab" buttons.
50
50
  * The standalone route fetches RAW server bytes, which inside an
51
51
  * E2E-encrypted folder is ciphertext (and its save path could clobber
52
52
  * the encrypted blob) — the host disables the escape hatches there. */
@@ -258,7 +258,7 @@ function buildAndOpenStandalone(mode: 'view' | 'edit'): void {
258
258
 
259
259
  /**
260
260
  * Extensions that have a meaningful "edit" surface. Read-only kinds
261
- * (image/video/audio/3D/archive) don't surface a "Düzenle" button.
261
+ * (image/video/audio/3D/archive) don't surface an "Edit" button.
262
262
  */
263
263
  const EDITABLE_EXTS = new Set([
264
264
  // OnlyOffice — open the in-page office editor (or new tab) with
@@ -311,7 +311,7 @@ function onMdInput() {
311
311
  mdReRenderTimer = setTimeout(() => {
312
312
  renderMarkdown(rawText.value);
313
313
  }, 250);
314
- // Autosave 1.5s after last keystroke — manual Kaydet button + Ctrl+S
314
+ // Autosave 1.5s after last keystroke — the manual Save button + Ctrl+S
315
315
  // still work; saveMarkdown() guards against overlap.
316
316
  if (mdAutosaveTimer) clearTimeout(mdAutosaveTimer);
317
317
  mdAutosaveTimer = setTimeout(() => {
@@ -785,7 +785,7 @@ async function mountOnlyOfficeEditor(): Promise<void> {
785
785
  officeError.value = null;
786
786
  if (!props.file || kind.value !== 'office') return;
787
787
  if (!props.onlyOfficeConfigEndpoint) {
788
- officeError.value = 'OnlyOffice yapılandırması yok';
788
+ officeError.value = t('viewer.office_unconfigured');
789
789
  return;
790
790
  }
791
791
  if (!officeEl.value) return;
@@ -916,8 +916,8 @@ function loadOnlyOfficeScript(base: string): Promise<void> {
916
916
  <object :data="src" type="application/pdf" class="fe-preview__iframe">
917
917
  <div class="fe-preview__fallback">
918
918
  <span class="fe-preview__fallback-icon">📕</span>
919
- <p>Tarayıcı PDF'i inline açamadı.</p>
920
- <a :href="download" class="fe-btn fe-btn--primary" target="_blank" rel="noopener">PDF'i Yeni Sekmede Aç</a>
919
+ <p>{{ t('viewer.pdf_inline_failed') }}</p>
920
+ <a :href="download" class="fe-btn fe-btn--primary" target="_blank" rel="noopener">{{ t('viewer.open_in_new_tab') }}</a>
921
921
  </div>
922
922
  </object>
923
923
  </template>
@@ -925,7 +925,7 @@ function loadOnlyOfficeScript(base: string): Promise<void> {
925
925
  <template v-else-if="kind === 'markdown'">
926
926
  <div v-if="loading" class="fe-preview__fallback">{{ t('viewer.loading') }}</div>
927
927
  <div v-else-if="tooLarge" class="fe-preview__fallback">
928
- Dosya çok büyük (>1 MB). <a :href="download" class="fe-btn">{{ t('viewer.download') }}</a>
928
+ {{ t('viewer.too_large') }} <a :href="download" class="fe-btn">{{ t('viewer.download') }}</a>
929
929
  </div>
930
930
  <div
931
931
  v-else-if="openMode === 'edit' && saveTextEndpoint"
@@ -995,7 +995,7 @@ function loadOnlyOfficeScript(base: string): Promise<void> {
995
995
  <template v-else-if="kind === 'code' || kind === 'text'">
996
996
  <div v-if="loading" class="fe-preview__fallback">{{ t('viewer.loading') }}</div>
997
997
  <div v-else-if="tooLarge" class="fe-preview__fallback">
998
- Dosya çok büyük (>1 MB). <a :href="download" class="fe-btn">{{ t('viewer.download') }}</a>
998
+ {{ t('viewer.too_large') }} <a :href="download" class="fe-btn">{{ t('viewer.download') }}</a>
999
999
  </div>
1000
1000
  <div v-else-if="fetchError" class="fe-preview__fallback">
1001
1001
  <p>{{ fetchError }}</p>