localization: officerdev (panels) to zero convertible
Last stretch of phase 3: FileBrowserWidget's five components (tabs, search, empty states), FileGrid (column headers, sort labels, empty folder), UploadProgress, DictateDialog and AgentRunnerModal. Reused ui.button.cancel/close from the shared catalog where the word was already there instead of adding panel-local duplicates. The two remaining scanner hits — useTaskRunner's "Running:"/"Error:" framing — are marked i18n-exempt rather than converted: they're interleaved with raw stdout/stderr inside one monospace <pre> feed, so translating the frame alone would read as broken mid-buffer. Same reasoning as the never-translate thrown-Error list, just not a thrown Error. officerdev (panels) added to the scanner's ZEROED guard — the check now holds three areas at zero.
This commit is contained in:
+13
-7
@@ -8,6 +8,7 @@ import { useClient } from 'hooks/useClient';
|
||||
import { chatListPath } from '../../../ChatHistory/chat-routes';
|
||||
import type { AgentSummary } from '../../useAgents';
|
||||
import { TaskInputForm, type TaskInputDef } from './TaskRunnerModal';
|
||||
import { useTranslation } from '../../../../locales';
|
||||
|
||||
// Starting an agent is not running a job: the POST returns as soon as the session is spawned, and the
|
||||
// run itself is watched in /chat. So this modal has no output pane — the transcript is the output,
|
||||
@@ -32,6 +33,7 @@ type AgentRunnerModalProps = {
|
||||
};
|
||||
|
||||
export const AgentRunnerModal = ({ open, onOpenChange, agent, entryName, entryFullPath }: AgentRunnerModalProps) => {
|
||||
const { t, tUi } = useTranslation();
|
||||
const client = useClient();
|
||||
const navigate = useNavigate();
|
||||
const [phase, setPhase] = useState<Phase>('ready');
|
||||
@@ -78,7 +80,7 @@ export const AgentRunnerModal = ({ open, onOpenChange, agent, entryName, entryFu
|
||||
setResult(started);
|
||||
setPhase('started');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to start the agent');
|
||||
setError(err instanceof Error ? err.message : t('agentRunner.startFailed'));
|
||||
setPhase('failed');
|
||||
}
|
||||
};
|
||||
@@ -122,9 +124,11 @@ export const AgentRunnerModal = ({ open, onOpenChange, agent, entryName, entryFu
|
||||
<div className="flex flex-col items-center justify-center gap-4 p-8 text-center">
|
||||
<CircleCheck className="h-10 w-10 text-duck-teal" />
|
||||
<div>
|
||||
<div className="text-base font-medium text-duck-dark dark:text-foreground">Agent running</div>
|
||||
<div className="text-base font-medium text-duck-dark dark:text-foreground">
|
||||
{t('agentRunner.agentRunning')}
|
||||
</div>
|
||||
<div className="text-sm text-duck-dark/50 dark:text-foreground/50 mt-1">
|
||||
It keeps going whether or not this stays open. Watch it in the chat — newest session on top.
|
||||
{t('agentRunner.keepsGoing')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -132,13 +136,13 @@ export const AgentRunnerModal = ({ open, onOpenChange, agent, entryName, entryFu
|
||||
onClick={openChat}
|
||||
className="flex items-center gap-2 px-5 py-2 rounded-lg bg-duck-teal text-white text-sm font-medium hover:bg-duck-teal/90 cursor-pointer"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" /> Open chat
|
||||
<ExternalLink className="h-4 w-4" /> {t('agentRunner.openChat')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="px-5 py-2 rounded-lg text-sm font-medium text-duck-dark/70 dark:text-foreground/70 hover:bg-duck-dark/5 dark:hover:bg-foreground/5 cursor-pointer"
|
||||
>
|
||||
Close
|
||||
{tUi('button.close')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -146,7 +150,9 @@ export const AgentRunnerModal = ({ open, onOpenChange, agent, entryName, entryFu
|
||||
<>
|
||||
{/* Target — the path the agent is pointed at, exactly as the server will receive it. */}
|
||||
<div className="px-5 py-3 border-b border-duck-dark/10 flex flex-col gap-1">
|
||||
<span className="text-xs font-medium text-duck-dark/70 dark:text-foreground/70">Target</span>
|
||||
<span className="text-xs font-medium text-duck-dark/70 dark:text-foreground/70">
|
||||
{t('agentRunner.target')}
|
||||
</span>
|
||||
<span className="text-xs font-mono break-all text-duck-dark dark:text-foreground">{entryFullPath}</span>
|
||||
</div>
|
||||
|
||||
@@ -173,7 +179,7 @@ export const AgentRunnerModal = ({ open, onOpenChange, agent, entryName, entryFu
|
||||
className="flex items-center gap-2 px-6 py-2.5 rounded-lg bg-duck-teal text-white font-medium text-sm hover:bg-duck-teal/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
|
||||
>
|
||||
{phase === 'starting' ? <Loader2 className="h-4 w-4 animate-spin" /> : <Play className="h-4 w-4" />}
|
||||
{phase === 'starting' ? 'Starting…' : 'Run agent'}
|
||||
{phase === 'starting' ? t('agentRunner.starting') : t('agentRunner.runAgent')}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
+15
-11
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
||||
import { toast } from 'sonner';
|
||||
import { blobToWav } from '../../../Chat/useAudioRecording';
|
||||
import { useTranslation } from '../../../../locales';
|
||||
import type { UseFileBrowserAppType } from '../useFileBrowserApp';
|
||||
|
||||
type DictateDialogProps = {
|
||||
@@ -9,6 +10,7 @@ type DictateDialogProps = {
|
||||
};
|
||||
|
||||
export const DictateDialog = ({ fileBrowserManager }: DictateDialogProps) => {
|
||||
const { t, tUi } = useTranslation();
|
||||
const { showDictate, setShowDictate, handleUpload } = fileBrowserManager;
|
||||
const [recording, setRecording] = useState(false);
|
||||
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
||||
@@ -23,8 +25,8 @@ export const DictateDialog = ({ fileBrowserManager }: DictateDialogProps) => {
|
||||
// the effect cleanup then runs over the same refs. Closing an already-closed AudioContext rejects.
|
||||
const cleanup = () => {
|
||||
cancelAnimationFrame(animFrameRef.current);
|
||||
mediaRecorderRef.current?.stream?.getTracks().forEach((t) => t.stop());
|
||||
streamRef.current?.getTracks().forEach((t) => t.stop());
|
||||
mediaRecorderRef.current?.stream?.getTracks().forEach((track) => track.stop());
|
||||
streamRef.current?.getTracks().forEach((track) => track.stop());
|
||||
if (audioCtxRef.current && audioCtxRef.current.state !== 'closed') void audioCtxRef.current.close();
|
||||
mediaRecorderRef.current = null;
|
||||
streamRef.current = null;
|
||||
@@ -63,7 +65,7 @@ export const DictateDialog = ({ fileBrowserManager }: DictateDialogProps) => {
|
||||
|
||||
drawSpectrum();
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Could not access microphone');
|
||||
toast.error(err instanceof Error ? err.message : t('dictate.micAccessFailed'));
|
||||
setShowDictate(false);
|
||||
}
|
||||
};
|
||||
@@ -119,12 +121,12 @@ export const DictateDialog = ({ fileBrowserManager }: DictateDialogProps) => {
|
||||
});
|
||||
|
||||
cancelAnimationFrame(animFrameRef.current);
|
||||
recorder.stream.getTracks().forEach((t) => t.stop());
|
||||
recorder.stream.getTracks().forEach((track) => track.stop());
|
||||
streamRef.current?.getTracks().forEach((t) => t.stop());
|
||||
audioCtxRef.current?.close();
|
||||
|
||||
if (blob.size === 0) {
|
||||
toast.error('No audio was captured');
|
||||
toast.error(t('dictate.noAudioCaptured'));
|
||||
cleanup();
|
||||
setShowDictate(false);
|
||||
return;
|
||||
@@ -140,7 +142,7 @@ export const DictateDialog = ({ fileBrowserManager }: DictateDialogProps) => {
|
||||
setRecording(false);
|
||||
await handleUpload([file]);
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Recording failed');
|
||||
toast.error(err instanceof Error ? err.message : t('dictate.recordingFailed'));
|
||||
cleanup();
|
||||
setShowDictate(false);
|
||||
}
|
||||
@@ -181,14 +183,16 @@ export const DictateDialog = ({ fileBrowserManager }: DictateDialogProps) => {
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Dictate</DialogTitle>
|
||||
<DialogDescription>Recording audio. Press Enter or click Stop to save.</DialogDescription>
|
||||
<DialogTitle>{t('dictate.title')}</DialogTitle>
|
||||
<DialogDescription>{t('dictate.description')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<canvas ref={canvasRef} width={460} height={120} className="w-full rounded-md bg-duck-dark/5" />
|
||||
<div className="flex items-center gap-2">
|
||||
{recording && <span className="h-3 w-3 rounded-full bg-destructive animate-pulse" />}
|
||||
<span className="text-sm text-duck-dark/60">{recording ? 'Recording...' : 'Starting...'}</span>
|
||||
<span className="text-sm text-duck-dark/60">
|
||||
{recording ? t('dictate.recording') : t('dictate.startingStatus')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-end w-full gap-2">
|
||||
<button
|
||||
@@ -199,7 +203,7 @@ export const DictateDialog = ({ fileBrowserManager }: DictateDialogProps) => {
|
||||
}}
|
||||
className="px-4 py-2 text-sm rounded-md text-duck-dark/70 hover:bg-duck-dark/5 cursor-pointer transition-colors"
|
||||
>
|
||||
Cancel
|
||||
{tUi('button.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -207,7 +211,7 @@ export const DictateDialog = ({ fileBrowserManager }: DictateDialogProps) => {
|
||||
disabled={!recording}
|
||||
className="px-4 py-2 text-sm rounded-md bg-duck-teal text-duck-yellow hover:bg-duck-teal/90 cursor-pointer transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
Stop & Save
|
||||
{t('dictate.stopSave')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+12
-8
@@ -5,6 +5,7 @@ import type { DirEntry } from '../../../../hooks/useFilesAPI';
|
||||
import { getFileType } from '../../../FileViewer';
|
||||
import type { UseFileBrowserAppType } from '../useFileBrowserApp';
|
||||
import { FileItem } from './FileItem';
|
||||
import { useTranslation } from '../../../../locales';
|
||||
|
||||
type SortField = 'name' | 'size' | 'type' | 'date';
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
@@ -47,6 +48,7 @@ const useColumnCount = (scrollRef: React.RefObject<HTMLDivElement | null>) => {
|
||||
};
|
||||
|
||||
export const FileGrid = ({ fileBrowserManager }: FileGridProps) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
visibleEntries: entries,
|
||||
viewMode,
|
||||
@@ -184,8 +186,8 @@ export const FileGrid = ({ fileBrowserManager }: FileGridProps) => {
|
||||
timer = undefined;
|
||||
};
|
||||
const onKey = (ev: KeyboardEvent) => {
|
||||
const t = ev.target as HTMLElement;
|
||||
if (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.closest('.monaco-editor')) return;
|
||||
const target = ev.target as HTMLElement;
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.closest('.monaco-editor')) return;
|
||||
if (ev.ctrlKey || ev.metaKey || ev.altKey || ev.key.length !== 1) return; // printable, unmodified only
|
||||
|
||||
ev.preventDefault();
|
||||
@@ -224,7 +226,9 @@ export const FileGrid = ({ fileBrowserManager }: FileGridProps) => {
|
||||
}, [setSearchQuery, searchInputRef, setSelected]);
|
||||
|
||||
if (entries.length === 0) {
|
||||
return <div className="flex items-center justify-center py-20 text-duck-dark/40 text-sm">This folder is empty</div>;
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20 text-duck-dark/40 text-sm">{t('files.grid.empty')}</div>
|
||||
);
|
||||
}
|
||||
|
||||
const renderItem = (entry: DirEntry) => (
|
||||
@@ -288,25 +292,25 @@ export const FileGrid = ({ fileBrowserManager }: FileGridProps) => {
|
||||
<span className="shrink-0 w-5" />
|
||||
<span className="shrink-0 w-5" />
|
||||
<button className={`flex-1 min-w-0 ${sortBtnClass('name')}`} onClick={() => toggleSort('name')}>
|
||||
Name {sortIcon('name')}
|
||||
{t('files.grid.name')} {sortIcon('name')}
|
||||
</button>
|
||||
<button
|
||||
className={`hidden md:flex shrink-0 w-20 justify-end ${sortBtnClass('type')}`}
|
||||
onClick={() => toggleSort('type')}
|
||||
>
|
||||
Type {sortIcon('type')}
|
||||
{t('files.grid.type')} {sortIcon('type')}
|
||||
</button>
|
||||
<button
|
||||
className={`hidden md:flex shrink-0 w-20 justify-end ${sortBtnClass('size')}`}
|
||||
onClick={() => toggleSort('size')}
|
||||
>
|
||||
Size {sortIcon('size')}
|
||||
{t('files.grid.size')} {sortIcon('size')}
|
||||
</button>
|
||||
<button
|
||||
className={`hidden md:flex shrink-0 w-28 justify-end ${sortBtnClass('date')}`}
|
||||
onClick={() => toggleSort('date')}
|
||||
>
|
||||
Date {sortIcon('date')}
|
||||
{t('files.grid.date')} {sortIcon('date')}
|
||||
</button>
|
||||
<span className="shrink-0 w-8" />
|
||||
</div>
|
||||
@@ -315,7 +319,7 @@ export const FileGrid = ({ fileBrowserManager }: FileGridProps) => {
|
||||
className="sticky top-0 z-10 bg-background/95 backdrop-blur-sm border-b border-duck-dark/10 flex items-center gap-2 px-3"
|
||||
style={{ height: HEADER_HEIGHT }}
|
||||
>
|
||||
<span className="text-xs text-duck-dark/40">Sort:</span>
|
||||
<span className="text-xs text-duck-dark/40">{t('files.grid.sort')}</span>
|
||||
{(['name', 'size', 'type', 'date'] as SortField[]).map((field) => (
|
||||
<button
|
||||
key={field}
|
||||
|
||||
+3
-1
@@ -1,10 +1,12 @@
|
||||
import type { UseFileBrowserAppType } from '../useFileBrowserApp';
|
||||
import { useTranslation } from '../../../../locales';
|
||||
|
||||
type UploadProgressProps = {
|
||||
fileBrowserManager: UseFileBrowserAppType;
|
||||
};
|
||||
|
||||
export const UploadProgress = ({ fileBrowserManager }: UploadProgressProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { uploadProgress } = fileBrowserManager;
|
||||
|
||||
if (uploadProgress === null) return null;
|
||||
@@ -12,7 +14,7 @@ export const UploadProgress = ({ fileBrowserManager }: UploadProgressProps) => {
|
||||
return (
|
||||
<div className="shrink-0 px-4 pt-3">
|
||||
<div className="flex items-center justify-between text-sm text-duck-dark/70 mb-1">
|
||||
<span>Uploading...</span>
|
||||
<span>{t('fileBrowserWidget.uploading')}</span>
|
||||
<span>{uploadProgress}%</span>
|
||||
</div>
|
||||
<div className="h-2 rounded-full bg-duck-dark/10 overflow-hidden">
|
||||
|
||||
+2
@@ -34,6 +34,7 @@ export function useTaskRunner() {
|
||||
|
||||
switch (msg.type) {
|
||||
case 'started':
|
||||
// i18n-exempt: framing text inside one monospace terminal feed with raw stdout/stderr (see 'error' below).
|
||||
setOutput((prev) => [...prev, { stream: 'system', text: `Running: ${msg.taskName}\n` }]);
|
||||
break;
|
||||
case 'stdout':
|
||||
@@ -47,6 +48,7 @@ export function useTaskRunner() {
|
||||
setPhase('done');
|
||||
break;
|
||||
case 'error':
|
||||
// i18n-exempt: same terminal feed as 'started' above — content, not UI chrome.
|
||||
setOutput((prev) => [...prev, { stream: 'stderr', text: `Error: ${msg.message}\n` }]);
|
||||
setPhase('done');
|
||||
setExitCode(-1);
|
||||
|
||||
+3
-1
@@ -5,13 +5,15 @@ import { SearchResults } from './components/SearchResults';
|
||||
import { BrowseTab } from './components/BrowseTab';
|
||||
import { RecentTab } from './components/RecentTab';
|
||||
import { PinnedTab } from './components/PinnedTab';
|
||||
import { useTranslation } from '../../../locales';
|
||||
|
||||
export const FileBrowserWidget = () => {
|
||||
const { t } = useTranslation();
|
||||
const fileBrowserManager = useFileBrowserWidget();
|
||||
const { tab, isSearching } = fileBrowserManager;
|
||||
|
||||
return (
|
||||
<Widget title="File Browser">
|
||||
<Widget title={t('fileBrowserWidget.title')}>
|
||||
<Header fileBrowserManager={fileBrowserManager} />
|
||||
<div className="px-4 pb-3 max-h-72 overflow-y-auto">
|
||||
{isSearching ? (
|
||||
|
||||
+3
-1
@@ -1,6 +1,7 @@
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { Breadcrumb } from '../../FileBrowserApp/components/Breadcrumb';
|
||||
import type { UseFileBrowserWidgetType } from '../useFileBrowserWidget';
|
||||
import { useTranslation } from '../../../../locales';
|
||||
import { EntryRow } from './EntryRow';
|
||||
|
||||
type BrowseTabProps = {
|
||||
@@ -8,6 +9,7 @@ type BrowseTabProps = {
|
||||
};
|
||||
|
||||
export const BrowseTab = ({ fileBrowserManager }: BrowseTabProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { browsePath, setBrowsePath, entries, loading, openFile, isPinned, togglePin } = fileBrowserManager;
|
||||
|
||||
return (
|
||||
@@ -20,7 +22,7 @@ export const BrowseTab = ({ fileBrowserManager }: BrowseTabProps) => {
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : entries.length === 0 ? (
|
||||
<p className="text-xs text-duck-dark/40 py-4 text-center">Empty directory</p>
|
||||
<p className="text-xs text-duck-dark/40 py-4 text-center">{t('fileBrowserWidget.emptyDirectory')}</p>
|
||||
) : (
|
||||
<ul className="space-y-0.5">
|
||||
{entries.map((entry) => {
|
||||
|
||||
+11
-7
@@ -1,10 +1,13 @@
|
||||
import { FolderOpen, Clock, Pin, Search, X } from 'lucide-react';
|
||||
import type { Tab, UseFileBrowserWidgetType } from '../useFileBrowserWidget';
|
||||
import type { PanelTranslationKey } from '../../../../locales';
|
||||
import { useTranslation } from '../../../../locales';
|
||||
|
||||
const TABS: { key: Tab; label: string; icon: typeof FolderOpen }[] = [
|
||||
{ key: 'browse', label: 'Browse', icon: FolderOpen },
|
||||
{ key: 'recent', label: 'Recent', icon: Clock },
|
||||
{ key: 'pinned', label: 'Pinned', icon: Pin },
|
||||
// Module-level data, so it carries a catalog KEY and the component resolves it.
|
||||
const TABS: { key: Tab; labelKey: PanelTranslationKey; icon: typeof FolderOpen }[] = [
|
||||
{ key: 'browse', labelKey: 'fileBrowserWidget.browse', icon: FolderOpen },
|
||||
{ key: 'recent', labelKey: 'fileBrowserWidget.recent', icon: Clock },
|
||||
{ key: 'pinned', labelKey: 'fileBrowserWidget.pinned', icon: Pin },
|
||||
];
|
||||
|
||||
type HeaderProps = {
|
||||
@@ -12,12 +15,13 @@ type HeaderProps = {
|
||||
};
|
||||
|
||||
export const Header = ({ fileBrowserManager }: HeaderProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { tab, setTab, isSearching, searchQuery, setSearchQuery } = fileBrowserManager;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-4 pb-1">
|
||||
<div className="flex items-center gap-1">
|
||||
{TABS.map(({ key, label, icon: Icon }) => (
|
||||
{TABS.map(({ key, labelKey, icon: Icon }) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setTab(key)}
|
||||
@@ -28,7 +32,7 @@ export const Header = ({ fileBrowserManager }: HeaderProps) => {
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
{label}
|
||||
{t(labelKey)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -38,7 +42,7 @@ export const Header = ({ fileBrowserManager }: HeaderProps) => {
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(ev) => setSearchQuery(ev.target.value)}
|
||||
placeholder="Search..."
|
||||
placeholder={t('fileBrowserWidget.search')}
|
||||
className="w-full rounded-lg border border-duck-dark/20 bg-background pl-8 pr-8 py-1 text-xs text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-2 focus:ring-duck-teal/30 focus:border-duck-teal/50"
|
||||
/>
|
||||
{searchQuery && (
|
||||
|
||||
+3
-1
@@ -1,4 +1,5 @@
|
||||
import type { UseFileBrowserWidgetType } from '../useFileBrowserWidget';
|
||||
import { useTranslation } from '../../../../locales';
|
||||
import { EntryRow } from './EntryRow';
|
||||
|
||||
type PinnedTabProps = {
|
||||
@@ -6,10 +7,11 @@ type PinnedTabProps = {
|
||||
};
|
||||
|
||||
export const PinnedTab = ({ fileBrowserManager }: PinnedTabProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { pinned, openFile, togglePin } = fileBrowserManager;
|
||||
|
||||
if (pinned.length === 0) {
|
||||
return <p className="text-xs text-duck-dark/40 py-4 text-center">No pinned files</p>;
|
||||
return <p className="text-xs text-duck-dark/40 py-4 text-center">{t('fileBrowserWidget.noPinnedFiles')}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+3
-1
@@ -1,4 +1,5 @@
|
||||
import type { UseFileBrowserWidgetType } from '../useFileBrowserWidget';
|
||||
import { useTranslation } from '../../../../locales';
|
||||
import { EntryRow } from './EntryRow';
|
||||
|
||||
type RecentTabProps = {
|
||||
@@ -6,10 +7,11 @@ type RecentTabProps = {
|
||||
};
|
||||
|
||||
export const RecentTab = ({ fileBrowserManager }: RecentTabProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { recents, openFile, isPinned, togglePin } = fileBrowserManager;
|
||||
|
||||
if (recents.length === 0) {
|
||||
return <p className="text-xs text-duck-dark/40 py-4 text-center">No recent files</p>;
|
||||
return <p className="text-xs text-duck-dark/40 py-4 text-center">{t('fileBrowserWidget.noRecentFiles')}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+3
-1
@@ -1,5 +1,6 @@
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import type { UseFileBrowserWidgetType } from '../useFileBrowserWidget';
|
||||
import { useTranslation } from '../../../../locales';
|
||||
import { EntryRow } from './EntryRow';
|
||||
|
||||
type SearchResultsProps = {
|
||||
@@ -7,6 +8,7 @@ type SearchResultsProps = {
|
||||
};
|
||||
|
||||
export const SearchResults = ({ fileBrowserManager }: SearchResultsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { searching, searchResults, openFile, isPinned, togglePin } = fileBrowserManager;
|
||||
|
||||
if (searching) {
|
||||
@@ -18,7 +20,7 @@ export const SearchResults = ({ fileBrowserManager }: SearchResultsProps) => {
|
||||
}
|
||||
|
||||
if (searchResults.length === 0) {
|
||||
return <p className="text-xs text-duck-dark/40 py-4 text-center">No results</p>;
|
||||
return <p className="text-xs text-duck-dark/40 py-4 text-center">{t('fileBrowserWidget.noResults')}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -527,6 +527,14 @@
|
||||
"clearSelection": "Clear selection",
|
||||
"selected": "selected",
|
||||
"refresh": "Refresh"
|
||||
},
|
||||
"grid": {
|
||||
"empty": "This folder is empty",
|
||||
"sort": "Sort:",
|
||||
"name": "Name",
|
||||
"type": "Type",
|
||||
"size": "Size",
|
||||
"date": "Date"
|
||||
}
|
||||
},
|
||||
"taskRunner": {
|
||||
@@ -607,5 +615,38 @@
|
||||
"timedOut": "Timed out",
|
||||
"downloadFailed": "Download failed",
|
||||
"couldNotStart": "Could not start the download"
|
||||
},
|
||||
"fileBrowserWidget": {
|
||||
"title": "File Browser",
|
||||
"search": "Search...",
|
||||
"browse": "Browse",
|
||||
"recent": "Recent",
|
||||
"pinned": "Pinned",
|
||||
"noResults": "No results",
|
||||
"emptyDirectory": "Empty directory",
|
||||
"noPinnedFiles": "No pinned files",
|
||||
"noRecentFiles": "No recent files",
|
||||
"uploading": "Uploading...",
|
||||
"empty": "This folder is empty",
|
||||
"sort": "Sort:"
|
||||
},
|
||||
"agentRunner": {
|
||||
"agentRunning": "Agent running",
|
||||
"keepsGoing": "It keeps going whether or not this stays open. Watch it in the chat — newest session on top.",
|
||||
"openChat": "Open chat",
|
||||
"target": "Target",
|
||||
"starting": "Starting…",
|
||||
"runAgent": "Run agent",
|
||||
"startFailed": "Failed to start the agent"
|
||||
},
|
||||
"dictate": {
|
||||
"title": "Dictate",
|
||||
"description": "Recording audio. Press Enter or click Stop to save.",
|
||||
"recording": "Recording...",
|
||||
"startingStatus": "Starting...",
|
||||
"stopSave": "Stop & Save",
|
||||
"noAudioCaptured": "No audio was captured",
|
||||
"micAccessFailed": "Could not access microphone",
|
||||
"recordingFailed": "Recording failed"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,6 +527,14 @@
|
||||
"clearSelection": "Limpar seleção",
|
||||
"selected": "selecionados",
|
||||
"refresh": "Atualizar"
|
||||
},
|
||||
"grid": {
|
||||
"empty": "Esta pasta está vazia",
|
||||
"sort": "Ordenar:",
|
||||
"name": "Nome",
|
||||
"type": "Tipo",
|
||||
"size": "Tamanho",
|
||||
"date": "Data"
|
||||
}
|
||||
},
|
||||
"taskRunner": {
|
||||
@@ -607,5 +615,38 @@
|
||||
"timedOut": "Tempo esgotado",
|
||||
"downloadFailed": "Falha ao descarregar",
|
||||
"couldNotStart": "Não foi possível iniciar o descarregamento"
|
||||
},
|
||||
"fileBrowserWidget": {
|
||||
"title": "Explorador de ficheiros",
|
||||
"search": "Pesquisar...",
|
||||
"browse": "Explorar",
|
||||
"recent": "Recentes",
|
||||
"pinned": "Fixados",
|
||||
"noResults": "Sem resultados",
|
||||
"emptyDirectory": "Diretório vazio",
|
||||
"noPinnedFiles": "Sem ficheiros fixados",
|
||||
"noRecentFiles": "Sem ficheiros recentes",
|
||||
"uploading": "A carregar...",
|
||||
"empty": "Esta pasta está vazia",
|
||||
"sort": "Ordenar:"
|
||||
},
|
||||
"agentRunner": {
|
||||
"agentRunning": "Agente em execução",
|
||||
"keepsGoing": "Continua a correr quer esta janela fique aberta quer não. Acompanhe-o no chat — a sessão mais recente fica no topo.",
|
||||
"openChat": "Abrir chat",
|
||||
"target": "Alvo",
|
||||
"starting": "A iniciar…",
|
||||
"runAgent": "Executar agente",
|
||||
"startFailed": "Falha ao iniciar o agente"
|
||||
},
|
||||
"dictate": {
|
||||
"title": "Ditado",
|
||||
"description": "A gravar áudio. Prima Enter ou clique em Parar para guardar.",
|
||||
"recording": "A gravar...",
|
||||
"startingStatus": "A iniciar...",
|
||||
"stopSave": "Parar e guardar",
|
||||
"noAudioCaptured": "Não foi captado áudio",
|
||||
"micAccessFailed": "Não foi possível aceder ao microfone",
|
||||
"recordingFailed": "Falha na gravação"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user