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:
2026-08-23 16:36:48 +00:00
parent ad0fd12262
commit a6b4bc4170
14 changed files with 161 additions and 40 deletions
+8 -1
View File
@@ -34,8 +34,15 @@ const AREAS = [
* 'officer-web (shell)' landed 2026-08-23 (phase 2). It holds no never-translate hits of its own; what it
* does hold is ~30 `<Untranslated reason="foreign-ui-label">` labels quoted from Google's console, which
* `stripUntranslated` skips for the same reason.
*
* 'officerdev (panels)' landed 2026-08-23 (phase 3). It holds nine `new Error()` messages for whoever is
* reading a terminal, not a user — same never-translate shape as 'shared components'. Two more strings
* are marked `i18n-exempt` rather than never-translate: `useTaskRunner.ts`'s "Running:"/"Error:" framing
* is interleaved with raw stdout/stderr inside one monospace `<pre>` feed, so translating the frame alone
* would read as broken mid-buffer — the same reasoning as the never-translate list, reached by a
* different mechanism because it's not a thrown Error.
*/
const ZEROED: string[] = ['shared components', 'officer-web (shell)'];
const ZEROED: string[] = ['shared components', 'officer-web (shell)', 'officerdev (panels)'];
const SKIP = /node_modules|\.gen\.|[/\\]locales[/\\]|[/\\]vendor[/\\]|\.test\.|\.d\.ts$/;
@@ -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>
</>
@@ -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>
@@ -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}
@@ -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">
@@ -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);
@@ -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 ? (
@@ -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) => {
@@ -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 && (
@@ -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 (
@@ -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 (
@@ -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"
}
}