From 7fd4d6c98e18d56f91147e0c34fb615d7f83415c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 23 Aug 2026 15:56:59 +0000 Subject: [PATCH] file browser actions keyed (171 -> 124) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useFileBrowserApp.ts, the largest single file in the phase at 47 hits — and contrary to what I predicted, all 47 were convertible. I expected roughly half to be content and it was none: every one is a toast the reader sees, and the file NAMES in them are interpolated parameters inside a translated frame, which is where content belongs. Worth recording because the guess was wrong in the useful direction. Six hand-rolled plurals (`item${n !== 1 ? 's' : ''}` and friends) are real plural entries now, checked at 0, 1 and 2 in both locales. Portuguese reorders these — "1 item eliminado" rather than "Deleted 1 item" — which a frame with the count spliced in could not have expressed. --- .../FileBrowserApp/useFileBrowserApp.ts | 96 ++++++++++--------- .../officerdev/src/locales/en-GB.json | 58 +++++++++++ .../officerdev/src/locales/pt-PT.json | 58 +++++++++++ 3 files changed, 165 insertions(+), 47 deletions(-) diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/useFileBrowserApp.ts b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/useFileBrowserApp.ts index bf700605..5d06f769 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/useFileBrowserApp.ts +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/useFileBrowserApp.ts @@ -14,6 +14,7 @@ import { useAuth } from 'hooks/useAuth'; import { useFilesRefresh } from '../../../channels'; import { useNewDashboardDraft } from '../../Dashboards/useNewDashboardDraft'; import { copyToClipboard, canReadClipboard } from 'helpers/clipboard'; +import { useTranslation } from '../../../locales'; /** * The old query param. Kept only to recognise and redirect links made before 2026-08-15 — the folder @@ -22,6 +23,7 @@ import { copyToClipboard, canReadClipboard } from 'helpers/clipboard'; export const FILES_PATH_PARAM = 'path'; export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPath = false, panelId?: string) => { + const { t } = useTranslation(); const { user } = useAuth(); const navigate = useNavigate(); const location = useLocation(); @@ -186,7 +188,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa // transient, and bouncing someone out of a deep folder over one bad response would be worse than // the error they can already see. if (firstListing.current && currentPathRef.current !== basePath) { - toast.error('That folder is gone — back to the start'); + toast.error(t('files.folderGone')); setCurrentPath(basePath); rememberFolder({ path: basePath }); } else { @@ -227,7 +229,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa const data = await filesRef.current.search(q, currentPath); setSearchResults(data.results); } catch { - toast.error('Search failed'); + toast.error(t('files.searchFailed')); setSearchResults(null); } finally { setSearching(false); @@ -305,7 +307,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa if (selected.size > 0) { const paths = selectedPaths(); setClipboard({ paths, mode: 'copy' }); - toast.success(`Copied ${paths.length} item${paths.length !== 1 ? 's' : ''}`); + toast.success(t('files.copiedItems', { count: paths.length })); } return; } @@ -315,7 +317,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa if (selected.size > 0) { const paths = selectedPaths(); setClipboard({ paths, mode: 'cut' }); - toast.success(`Cut ${paths.length} item${paths.length !== 1 ? 's' : ''}`); + toast.success(t('files.cutItems', { count: paths.length })); } return; } @@ -429,9 +431,9 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa try { await files.createDir(dirPath); await refresh(); - toast.success(`Created folder "${name}"`); + toast.success(t('files.createdFolder', { name })); } catch { - toast.error('Failed to create folder'); + toast.error(t('files.createFolderFailed')); } }; @@ -463,9 +465,9 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa try { await files.writeFile(filePath, ''); await refresh(); - toast.success(`Created "${name}"`); + toast.success(t('files.created', { name })); } catch { - toast.error('Failed to create file'); + toast.error(t('files.createFileFailed')); } }; @@ -475,9 +477,9 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa try { await files.uploadFiles(currentPath, fileList, (pct) => setUploadProgress(pct)); await refresh(); - toast.success(`Uploaded ${count} file${count !== 1 ? 's' : ''}`); + toast.success(t('files.uploadedFiles', { count })); } catch { - toast.error('Failed to upload files'); + toast.error(t('files.uploadFailed')); } finally { setUploadProgress(null); } @@ -488,9 +490,9 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa try { await files.rename(path, newName); await refresh(); - toast.success(`Renamed to "${newName}"`); + toast.success(t('files.renamedTo', { name: newName })); } catch { - toast.error('Failed to rename'); + toast.error(t('files.renameFailed')); } }; @@ -499,7 +501,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa handleDeleteSelected(); return; } - if (!confirm(`Delete "${entry.name}"?`)) return; + if (!confirm(t('files.deleteQ', { name: entry.name }))) return; const path = entryPath(entry.name); try { await files.remove(path); @@ -509,25 +511,25 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa return next; }); await refresh(); - toast.success(`Deleted "${entry.name}"`); + toast.success(t('files.deleted', { name: entry.name })); } catch { - toast.error('Failed to delete'); + toast.error(t('files.deleteFailed')); } }; const handleDeleteSelected = async () => { const names = Array.from(selected); if (names.length === 0) return; - if (!confirm(`Delete ${names.length} item${names.length !== 1 ? 's' : ''}?`)) return; + if (!confirm(t('files.deleteItemsQ', { count: names.length }))) return; try { for (const name of names) { await files.remove(entryPath(name)); } setSelected(new Set()); await refresh(); - toast.success(`Deleted ${names.length} item${names.length !== 1 ? 's' : ''}`); + toast.success(t('files.deletedItems', { count: names.length })); } catch { - toast.error('Failed to delete some items'); + toast.error(t('files.deleteSomeFailed')); await refresh(); } }; @@ -560,7 +562,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa try { await files.download(paths); } catch { - toast.error('Failed to download'); + toast.error(t('files.downloadFailed')); } }; @@ -570,7 +572,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa try { await files.download(paths); } catch { - toast.error('Failed to download'); + toast.error(t('files.downloadFailed')); } }; @@ -596,7 +598,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa const paths = selected.size > 1 && selected.has(entry.name) ? selectedPaths() : [entryPath(entry.name)]; // Newline-separated, which is what a shell, an editor and a chat box all accept as a list of paths. copyToClipboard(paths.map((p) => `~${p}`).join('\n')); - toast.success(paths.length > 1 ? `${paths.length} paths copied` : 'Path copied'); + toast.success(paths.length > 1 ? `${paths.length} paths copied` : t('files.pathCopied')); }; /** @@ -606,20 +608,20 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa */ const handleCopyAbsPath = (path: string) => { copyToClipboard(`~${path}`); - toast.success('Path copied'); + toast.success(t('files.pathCopied')); }; const handleDownloadPath = async (path: string) => { try { await files.download([path]); } catch { - toast.error('Failed to download'); + toast.error(t('files.downloadFailed')); } }; const handleCopyCurrentPath = () => { copyToClipboard(`~${currentPath}`); - toast.success('Path copied'); + toast.success(t('files.pathCopied')); }; const handleChatHere = () => { @@ -638,7 +640,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa const handleReadAloud = async (entry: DirEntry) => { const filePath = entryPath(entry.name); - const toastId = toast.loading('Generating speech audio...'); + const toastId = toast.loading(t('files.generatingSpeech')); try { const { audioPath } = await files.tts(filePath, { saveNextTo: true }); toast.dismiss(toastId); @@ -650,20 +652,20 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa ephemeral: relativeToFolder(currentPath, audioPath), }); } catch { - toast.error('Failed to generate speech audio', { id: toastId }); + toast.error(t('files.speechFailed'), { id: toastId }); } }; const handleExtract = async (entry: DirEntry) => { const filePath = entryPath(entry.name); - const toastId = toast.loading('Extracting archive...'); + const toastId = toast.loading(t('files.extracting')); try { const { extractedPath } = await files.extract(filePath); const folderName = extractedPath.split('/').pop() ?? extractedPath; - toast.success(`Extracted to "${folderName}"`, { id: toastId }); + toast.success(t('files.extractedTo', { name: folderName }), { id: toastId }); await refresh(); } catch { - toast.error('Failed to extract archive', { id: toastId }); + toast.error(t('files.extractFailed'), { id: toastId }); } }; @@ -674,9 +676,9 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa try { await files.gitClone(url, currentPath); await refresh(); - toast.success('Repository cloned'); + toast.success(t('files.cloned')); } catch { - toast.error('Failed to clone repository'); + toast.error(t('files.cloneFailed')); } finally { setCloning(false); setCloneUrl(''); @@ -695,22 +697,22 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa try { await files.copy([entryPath(entry.name)], currentPath); await refresh(); - toast.success(`Duplicated "${entry.name}"`); + toast.success(t('files.duplicated', { name: entry.name })); } catch { - toast.error('Failed to duplicate'); + toast.error(t('files.duplicateFailed')); } }; /** Zip the selection (or the clicked entry) into an archive beside it, and keep it. */ const handleCompress = async (entry: DirEntry) => { const paths = selected.size > 1 && selected.has(entry.name) ? selectedPaths() : [entryPath(entry.name)]; - const toastId = toast.loading('Compressing...'); + const toastId = toast.loading(t('files.compressing')); try { const { name } = await files.compress(paths); await refresh(); - toast.success(`Created "${name}"`, { id: toastId }); + toast.success(t('files.created', { name }), { id: toastId }); } catch { - toast.error('Failed to compress', { id: toastId }); + toast.error(t('files.compressFailed'), { id: toastId }); } }; @@ -731,9 +733,9 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa setClipboard(null); } await refresh(); - toast.success(`Pasted into "${entry.name}"`); + toast.success(t('files.pastedInto', { name: entry.name })); } catch { - toast.error('Failed to paste'); + toast.error(t('files.pasteFailed')); } }; @@ -741,13 +743,13 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa const handleGitCloneUrl = async (url: string) => { const trimmed = url.trim(); if (!trimmed) return; - const toastId = toast.loading('Cloning...'); + const toastId = toast.loading(t('files.cloning')); try { await files.gitClone(trimmed, currentPath); await refresh(); - toast.success('Repository cloned', { id: toastId }); + toast.success(t('files.cloned'), { id: toastId }); } catch { - toast.error('Failed to clone repository', { id: toastId }); + toast.error(t('files.cloneFailed'), { id: toastId }); } }; @@ -765,21 +767,21 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa const path = entryPath(entry.name); const wasPinned = isPinned(path); togglePin(path, entry.name); - toast.success(wasPinned ? `Unpinned "${entry.name}"` : `Pinned "${entry.name}"`); + toast.success(wasPinned ? t('files.unpinned', { name: entry.name }) : t('files.pinned', { name: entry.name })); }; const handleCut = () => { const paths = selected.size > 0 ? selectedPaths() : []; if (paths.length === 0) return; setClipboard({ paths, mode: 'cut' }); - toast.success(`Cut ${paths.length} item${paths.length !== 1 ? 's' : ''}`); + toast.success(t('files.cutItems', { count: paths.length })); }; const handleCopy = () => { const paths = selected.size > 0 ? selectedPaths() : []; if (paths.length === 0) return; setClipboard({ paths, mode: 'copy' }); - toast.success(`Copied ${paths.length} item${paths.length !== 1 ? 's' : ''}`); + toast.success(t('files.copiedItems', { count: paths.length })); }; const handlePaste = async () => { @@ -792,9 +794,9 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa setClipboard(null); } await refresh(); - toast.success(`Pasted ${clipboard.paths.length} item${clipboard.paths.length !== 1 ? 's' : ''}`); + toast.success(t('files.pastedItems', { count: clipboard.paths.length })); } catch { - toast.error('Failed to paste'); + toast.error(t('files.pasteFailed')); } return; } @@ -804,7 +806,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa // is no way to pull clipboard contents on demand. Pasting an image has to come from a real paste // event instead. Refuse clearly rather than throwing "Cannot read properties of undefined". if (!canReadClipboard()) { - toast.error('Pasting from the clipboard needs a secure context (https). Drag the file in instead.'); + toast.error(t('files.pasteNeedsSecure')); return; } diff --git a/src/workspaces/officerdev/src/locales/en-GB.json b/src/workspaces/officerdev/src/locales/en-GB.json index cc07201b..984ec862 100644 --- a/src/workspaces/officerdev/src/locales/en-GB.json +++ b/src/workspaces/officerdev/src/locales/en-GB.json @@ -427,5 +427,63 @@ "peers": "Peers: {{names}}" }, "copy": "Copy" + }, + "files": { + "folderGone": "That folder is gone — back to the start", + "searchFailed": "Search failed", + "createFolderFailed": "Failed to create folder", + "createFileFailed": "Failed to create file", + "uploadFailed": "Failed to upload files", + "renameFailed": "Failed to rename", + "deleteFailed": "Failed to delete", + "deleteSomeFailed": "Failed to delete some items", + "downloadFailed": "Failed to download", + "pathCopied": "Path copied", + "generatingSpeech": "Generating speech audio…", + "speechFailed": "Failed to generate speech audio", + "extracting": "Extracting archive…", + "extractFailed": "Failed to extract archive", + "cloned": "Repository cloned", + "cloneFailed": "Failed to clone repository", + "cloning": "Cloning…", + "duplicateFailed": "Failed to duplicate", + "compressing": "Compressing…", + "compressFailed": "Failed to compress", + "pasteFailed": "Failed to paste", + "pasteNeedsSecure": "Pasting from the clipboard needs a secure context (https). Drag the file in instead.", + "copiedItems": { + "one": "Copied {{count}} item", + "other": "Copied {{count}} items" + }, + "cutItems": { + "one": "Cut {{count}} item", + "other": "Cut {{count}} items" + }, + "pastedItems": { + "one": "Pasted {{count}} item", + "other": "Pasted {{count}} items" + }, + "uploadedFiles": { + "one": "Uploaded {{count}} file", + "other": "Uploaded {{count}} files" + }, + "deleteItemsQ": { + "one": "Delete {{count}} item?", + "other": "Delete {{count}} items?" + }, + "deletedItems": { + "one": "Deleted {{count}} item", + "other": "Deleted {{count}} items" + }, + "createdFolder": "Created folder “{{name}}”", + "created": "Created “{{name}}”", + "renamedTo": "Renamed to “{{name}}”", + "deleteQ": "Delete “{{name}}”?", + "deleted": "Deleted “{{name}}”", + "extractedTo": "Extracted to “{{name}}”", + "duplicated": "Duplicated “{{name}}”", + "pastedInto": "Pasted into “{{name}}”", + "unpinned": "Unpinned “{{name}}”", + "pinned": "Pinned “{{name}}”" } } diff --git a/src/workspaces/officerdev/src/locales/pt-PT.json b/src/workspaces/officerdev/src/locales/pt-PT.json index 65221e51..2734f110 100644 --- a/src/workspaces/officerdev/src/locales/pt-PT.json +++ b/src/workspaces/officerdev/src/locales/pt-PT.json @@ -427,5 +427,63 @@ "peers": "Pares: {{names}}" }, "copy": "Copiar" + }, + "files": { + "folderGone": "Essa pasta desapareceu — de volta ao início", + "searchFailed": "A pesquisa falhou", + "createFolderFailed": "Falha ao criar a pasta", + "createFileFailed": "Falha ao criar o ficheiro", + "uploadFailed": "Falha ao carregar os ficheiros", + "renameFailed": "Falha ao renomear", + "deleteFailed": "Falha ao eliminar", + "deleteSomeFailed": "Falha ao eliminar alguns itens", + "downloadFailed": "Falha ao descarregar", + "pathCopied": "Caminho copiado", + "generatingSpeech": "A gerar áudio de fala…", + "speechFailed": "Falha ao gerar o áudio de fala", + "extracting": "A extrair o arquivo…", + "extractFailed": "Falha ao extrair o arquivo", + "cloned": "Repositório clonado", + "cloneFailed": "Falha ao clonar o repositório", + "cloning": "A clonar…", + "duplicateFailed": "Falha ao duplicar", + "compressing": "A comprimir…", + "compressFailed": "Falha ao comprimir", + "pasteFailed": "Falha ao colar", + "pasteNeedsSecure": "Colar da área de transferência exige um contexto seguro (https). Arraste o ficheiro para aqui em vez disso.", + "copiedItems": { + "one": "{{count}} item copiado", + "other": "{{count}} itens copiados" + }, + "cutItems": { + "one": "{{count}} item cortado", + "other": "{{count}} itens cortados" + }, + "pastedItems": { + "one": "{{count}} item colado", + "other": "{{count}} itens colados" + }, + "uploadedFiles": { + "one": "{{count}} ficheiro carregado", + "other": "{{count}} ficheiros carregados" + }, + "deleteItemsQ": { + "one": "Eliminar {{count}} item?", + "other": "Eliminar {{count}} itens?" + }, + "deletedItems": { + "one": "{{count}} item eliminado", + "other": "{{count}} itens eliminados" + }, + "createdFolder": "Pasta “{{name}}” criada", + "created": "“{{name}}” criado", + "renamedTo": "Renomeado para “{{name}}”", + "deleteQ": "Eliminar “{{name}}”?", + "deleted": "“{{name}}” eliminado", + "extractedTo": "Extraído para “{{name}}”", + "duplicated": "“{{name}}” duplicado", + "pastedInto": "Colado em “{{name}}”", + "unpinned": "“{{name}}” removido dos fixados", + "pinned": "“{{name}}” fixado" } }