|
22 | 22 | type FileTreeNode, |
23 | 23 | type TreeItemState, |
24 | 24 | } from "./tree.svelte.js"; |
| 25 | + import { arabicNumbers } from "./utils.js"; |
25 | 26 |
|
26 | 27 | const sortCollator = new Intl.Collator(); |
27 | 28 |
|
28 | 29 | function sortComparator(x: FileTreeNode, y: FileTreeNode) { |
29 | 30 | return sortCollator.compare(x.name, y.name); |
30 | 31 | } |
31 | 32 |
|
| 33 | + const translations = { |
| 34 | + toast: { |
| 35 | + cannotMoveInsideItself: { |
| 36 | + en: (name: string) => `Cannot move "${name}" inside itself`, |
| 37 | + ar: (name: string) => `لا يمكن نقل "${name}" داخل نفسه`, |
| 38 | + }, |
| 39 | + itemAlreadyExists: { |
| 40 | + en: (name: string) => `An item named "${name}" already exists in this location`, |
| 41 | + ar: (name: string) => `عنصر باسم "${name}" موجود بالفعل في هذا الموقع`, |
| 42 | + }, |
| 43 | + failedToReadFiles: { |
| 44 | + en: "Failed to read uploaded files", |
| 45 | + ar: "فشل قراءة الملفات المرفوعة", |
| 46 | + }, |
| 47 | + }, |
| 48 | + dialog: { |
| 49 | + failedToCopyItems: { |
| 50 | + en: "Failed to copy items", |
| 51 | + ar: "فشل نسخ العناصر", |
| 52 | + }, |
| 53 | + failedToMoveItems: { |
| 54 | + en: "Failed to move items", |
| 55 | + ar: "فشل نقل العناصر", |
| 56 | + }, |
| 57 | + nameConflictDescription: { |
| 58 | + en: (name: string) => |
| 59 | + `An item named "${name}" already exists in this location. Do you want to skip it or cancel the operation entirely?`, |
| 60 | + ar: (name: string) => |
| 61 | + `عنصر باسم "${name}" موجود بالفعل في هذا الموقع. هل تريد تخطيه أو إلغاء العملية بالكامل؟`, |
| 62 | + }, |
| 63 | + skip: { |
| 64 | + en: "Skip", |
| 65 | + ar: "تخطي", |
| 66 | + }, |
| 67 | + deleteConfirmTitle: { |
| 68 | + en: (count: number) => `Are you sure you want to delete ${count} item(s)?`, |
| 69 | + ar: (count: number) => |
| 70 | + `هل أنت متأكد أنك تريد حذف ${arabicNumbers(count.toString())} عناصر؟`, |
| 71 | + }, |
| 72 | + deleteConfirmDescription: { |
| 73 | + en: "They will be permanently deleted. This action cannot be undone.", |
| 74 | + ar: "سيتم حذفها نهائياً. لا يمكن التراجع عن هذا الإجراء.", |
| 75 | + }, |
| 76 | + confirm: { |
| 77 | + en: "Confirm", |
| 78 | + ar: "تأكيد", |
| 79 | + }, |
| 80 | + cancel: { |
| 81 | + en: "Cancel", |
| 82 | + ar: "إلغاء", |
| 83 | + }, |
| 84 | + }, |
| 85 | + }; |
| 86 | +
|
32 | 87 | export type TreeProps = { |
33 | 88 | children: Snippet<[args: TreeChildrenSnippetArgs<FileNode, FolderNode>]>; |
34 | 89 | root: FileTree; |
| 90 | + lang?: "en" | "ar"; |
35 | 91 | class?: ClassValue; |
36 | 92 | style?: string; |
37 | 93 | }; |
38 | 94 |
|
39 | 95 | export type TreeContext = { |
| 96 | + getLang: () => "en" | "ar"; |
40 | 97 | getSelectedIds: () => Set<string>; |
41 | 98 | getExpandedIds: () => Set<string>; |
42 | 99 | getDraggedId: () => string | undefined; |
|
53 | 110 | </script> |
54 | 111 |
|
55 | 112 | <script lang="ts"> |
56 | | - const { children, root, class: className, style }: TreeProps = $props(); |
| 113 | + const { children, root, lang = "en", class: className, style }: TreeProps = $props(); |
57 | 114 |
|
58 | 115 | let tree: Tree<FileNode, FolderNode> | null = $state.raw(null); |
59 | 116 | const selectedIds = new SvelteSet<string>(); |
|
66 | 123 | let dialogTitle = $state.raw(""); |
67 | 124 | let dialogDescription = $state.raw(""); |
68 | 125 | let dialogConfirmLabel = $state.raw(""); |
| 126 | + let dialogCancelLabel = $state.raw(""); |
69 | 127 | let dialogTrigger: HTMLElement | null = null; |
70 | 128 | let dialogDidConfirm = false; |
71 | 129 | let dialogOnClose: (() => void) | undefined; |
|
74 | 132 | title, |
75 | 133 | description, |
76 | 134 | confirmLabel, |
| 135 | + cancelLabel, |
77 | 136 | onClose, |
78 | 137 | }: { |
79 | 138 | title: string; |
80 | 139 | description: string; |
81 | 140 | confirmLabel: string; |
| 141 | + cancelLabel: string; |
82 | 142 | onClose: () => void; |
83 | 143 | }) { |
84 | 144 | dialogOpen = true; |
85 | 145 | dialogTitle = title; |
86 | 146 | dialogDescription = description; |
87 | 147 | dialogConfirmLabel = confirmLabel; |
| 148 | + dialogCancelLabel = cancelLabel; |
88 | 149 | dialogTrigger = document.activeElement instanceof HTMLElement ? document.activeElement : null; |
89 | 150 | dialogDidConfirm = false; |
90 | 151 | dialogOnClose = onClose; |
|
95 | 156 | dialogTitle = ""; |
96 | 157 | dialogDescription = ""; |
97 | 158 | dialogConfirmLabel = ""; |
| 159 | + dialogCancelLabel = ""; |
98 | 160 | dialogTrigger?.focus(); |
99 | 161 | dialogTrigger = null; |
100 | 162 | dialogOnClose?.(); |
|
134 | 196 | let title; |
135 | 197 | switch (operation) { |
136 | 198 | case "copy": { |
137 | | - title = "Failed to copy items"; |
| 199 | + title = translations.dialog.failedToCopyItems[lang]; |
138 | 200 | break; |
139 | 201 | } |
140 | 202 | case "move": { |
141 | | - title = "Failed to move items"; |
| 203 | + title = translations.dialog.failedToMoveItems[lang]; |
142 | 204 | break; |
143 | 205 | } |
144 | 206 | } |
145 | 207 |
|
146 | 208 | showDialog({ |
147 | 209 | title, |
148 | | - description: `An item named "${name}" already exists in this location. Do you want to skip it or cancel the operation entirely?`, |
149 | | - confirmLabel: "Skip", |
| 210 | + description: translations.dialog.nameConflictDescription[lang](name), |
| 211 | + confirmLabel: translations.dialog.skip[lang], |
| 212 | + cancelLabel: translations.dialog.cancel[lang], |
150 | 213 | onClose: () => { |
151 | 214 | resolve(dialogDidConfirm ? "skip" : "cancel"); |
152 | 215 | }, |
|
155 | 218 | } |
156 | 219 |
|
157 | 220 | function onCircularReference({ source }: OnCircularReferenceArgs<FileNode, FolderNode>) { |
158 | | - toast.error(`Cannot move "${source.node.name}" inside itself`); |
| 221 | + toast.error(translations.toast.cannotMoveInsideItself[lang](source.node.name)); |
159 | 222 | } |
160 | 223 |
|
161 | 224 | function onCopy({ destination }: OnCopyArgs<FileNode, FolderNode>) { |
|
171 | 234 | function canRemove({ removed }: OnRemoveArgs<FileNode, FolderNode>) { |
172 | 235 | return new Promise<boolean>((resolve) => { |
173 | 236 | showDialog({ |
174 | | - title: `Are you sure you want to delete ${removed.length} item(s)?`, |
175 | | - description: "They will be permanently deleted. This action cannot be undone.", |
176 | | - confirmLabel: "Confirm", |
| 237 | + title: translations.dialog.deleteConfirmTitle[lang](removed.length), |
| 238 | + description: translations.dialog.deleteConfirmDescription[lang], |
| 239 | + confirmLabel: translations.dialog.confirm[lang], |
| 240 | + cancelLabel: translations.dialog.cancel[lang], |
177 | 241 | onClose: () => { |
178 | 242 | resolve(dialogDidConfirm); |
179 | 243 | }, |
|
236 | 300 | continue; |
237 | 301 | } |
238 | 302 |
|
239 | | - const firstSegment = entry.name.split("/")[0]; |
| 303 | + const firstSegment = entry.name.split("/")[0]!; |
240 | 304 | if (uniqueNames.has(firstSegment)) { |
241 | | - toast.error(`An item named "${firstSegment}" already exists in this location`); |
| 305 | + toast.error(translations.toast.itemAlreadyExists[lang](firstSegment)); |
242 | 306 | return; |
243 | 307 | } |
244 | 308 |
|
|
274 | 338 | await Promise.all(entries.map(readEntry)); |
275 | 339 | } catch (error) { |
276 | 340 | console.error(error); |
277 | | - toast.error("Failed to read uploaded files"); |
| 341 | + toast.error(translations.toast.failedToReadFiles[lang]); |
278 | 342 | return; |
279 | 343 | } |
280 | 344 |
|
|
336 | 400 | }; |
337 | 401 |
|
338 | 402 | const context: TreeContext = { |
| 403 | + getLang: () => lang, |
339 | 404 | getSelectedIds: () => selectedIds, |
340 | 405 | getExpandedIds: () => expandedIds, |
341 | 406 | getDraggedId: () => draggedId, |
|
362 | 427 | {onCopy} |
363 | 428 | {onMove} |
364 | 429 | {canRemove} |
| 430 | + {lang} |
| 431 | + dir="auto" |
365 | 432 | class={className} |
366 | 433 | {style} |
367 | 434 | ondragenter={handleDragEnterOrOver} |
|
405 | 472 | <AlertDialog.Cancel |
406 | 473 | class="inline-flex h-10 items-center justify-center rounded bg-gray-200 px-6 text-sm font-medium hover:bg-gray-300 focus-visible:outline-2 focus-visible:outline-current active:scale-95" |
407 | 474 | > |
408 | | - Cancel |
| 475 | + {dialogCancelLabel} |
409 | 476 | </AlertDialog.Cancel> |
410 | 477 | </div> |
411 | 478 | </div> |
|
0 commit comments