file browser actions keyed (171 -> 124)

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.
This commit is contained in:
2026-08-23 15:56:59 +00:00
parent 736a45e1ff
commit 7fd4d6c98e
3 changed files with 165 additions and 47 deletions
@@ -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;
}
@@ -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}}”"
}
}
@@ -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"
}
}