diff --git a/scripts/i18n/scan-hardcoded.ts b/scripts/i18n/scan-hardcoded.ts index 35597d11..4e067781 100755 --- a/scripts/i18n/scan-hardcoded.ts +++ b/scripts/i18n/scan-hardcoded.ts @@ -74,9 +74,20 @@ const PATTERNS: { kind: string; rx: RegExp; neverTranslate?: true }[] = [ ), }, // Thrown Errors are for whoever is reading a terminal at 4am. They stay in one language. - { kind: 'thrown errors', rx: new RegExp(String.raw`new Error\(\s*` + Q, 'g'), neverTranslate: true }, + // + // Matches a template literal as well as a quoted one: `new Error(\`Whisper returned ${res.status}\`)` + // was falling through to `template str` and so counted as convertible, which is the opposite of true. + { + kind: 'thrown errors', + rx: new RegExp(String.raw`new Error\(\s*(?:` + Q + String.raw`|\`([^\`]{2,120})\`)`, 'g'), + neverTranslate: true, + }, // `Failed to create ${kind}` — needs interpolation, so it costs more to convert. - { kind: 'template str', rx: /`([A-Z][^`\n]{4,100}\$\{[^`]*)`/g }, + // + // The lookbehind hands a thrown Error to the category above instead of counting it here as well: a + // string matched by two patterns is counted twice, and it stayed convertible, so the area could never + // reach zero however it was treated. + { kind: 'template str', rx: /(? { + const { t } = useTranslation(); const [naming, setNaming] = useState(false); const [name, setName] = useState(''); const [error, setError] = useState(null); @@ -69,9 +71,9 @@ export const AgentIdentityBar = ({ panel }: AgentIdentityBarProps) => { return (
- This panel's agent no longer exists. + {t('chat.agent.gone')}
); @@ -87,7 +89,7 @@ export const AgentIdentityBar = ({ panel }: AgentIdentityBarProps) => { onClick={() => setNaming(true)} > - Name this agent + {t('chat.agent.name')} ); diff --git a/src/workspaces/officerdev/src/apps/Chat/ChatHeader.tsx b/src/workspaces/officerdev/src/apps/Chat/ChatHeader.tsx index 49caa94a..4ae402d6 100644 --- a/src/workspaces/officerdev/src/apps/Chat/ChatHeader.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/ChatHeader.tsx @@ -11,6 +11,7 @@ import { useChatPwds, useClaudeSessions } from 'state/useClaudeSessions'; import { useWorkspace, cwdToPath } from '../../components/Workspace'; import { usePanelSession } from './use-panel-session'; import type { ChatMessage } from './types'; +import { useTranslation } from '../../locales'; export type ChatSessionSelection = { sessionId: string | null; @@ -33,6 +34,7 @@ export type ChatSessionSelection = { const title = (s: { title: string }) => s.title || '(untitled)'; export const ChatHeader = ({ panelId }: { panelId?: string }) => { + const { t } = useTranslation(); const { workspace, cwd } = useWorkspace(); /** @@ -57,7 +59,7 @@ export const ChatHeader = ({ panelId }: { panelId?: string }) => { const [draft, setDraft] = useState(''); const current = sessions.find((s) => s.id === sessionId); - const label = current ? title(current) : sessionId ? 'Conversation' : 'New chat'; + const label = current ? title(current) : sessionId ? 'Conversation' : t('chat.newChat'); const commitRename = async (id: string) => { const next = draft.trim(); @@ -80,7 +82,7 @@ export const ChatHeader = ({ panelId }: { panelId?: string }) => { names it, because only the harness can decide that. */} setSessionId(null)} className="cursor-pointer"> - New chat + {t('chat.newChat')} {sessions.length > 0 && } @@ -124,7 +126,7 @@ export const ChatHeader = ({ panelId }: { panelId?: string }) => { {/* Siblings of the label, never nested inside it — these mutate, they do not select. */} diff --git a/src/workspaces/officerdev/src/apps/Chat/components/BackgroundTaskTray.tsx b/src/workspaces/officerdev/src/apps/Chat/components/BackgroundTaskTray.tsx index 664624c3..d16ace61 100644 --- a/src/workspaces/officerdev/src/apps/Chat/components/BackgroundTaskTray.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/components/BackgroundTaskTray.tsx @@ -11,6 +11,7 @@ import { useBackgroundTasks, useTaskDetail, type BackgroundTask } from '../useBa import { SubagentTrace } from './ToolActivity'; import { CodeSurface } from './CodeSurface'; import { markUserBackgroundJob, markStopRequested } from '../../../background-job-origin'; +import { useTranslation } from '../../../locales'; type BackgroundTaskTrayProps = { messages: ChatMessage[]; @@ -35,6 +36,7 @@ function statusTone(status: BackgroundTask['status']): Tone { * tail of a backgrounded shell's log, both read from the file Claude Code streams the task into. */ export const BackgroundTaskTray = ({ messages, sessionId }: BackgroundTaskTrayProps) => { + const { t } = useTranslation(); const { tasks, pinned, unpinned, running, clearable, togglePin, dismiss, dismissFinished } = useBackgroundTasks(messages); const [openId, setOpenId] = useState(null); @@ -189,7 +191,7 @@ export const BackgroundTaskTray = ({ messages, sessionId }: BackgroundTaskTrayPr )} {pinned.length > 0 && ( - 0}> + 0}> {pinned.map((task) => ( ))} @@ -204,8 +206,8 @@ export const BackgroundTaskTray = ({ messages, sessionId }: BackgroundTaskTrayPr )} {running && onStop && ( @@ -425,7 +429,7 @@ const TaskPanel = ({ title={ killOffered ? 'It ignored the request to stop. Kill it — no chance to clean up, release a port or take children with it.' - : 'Stop this job' + : t('chat.tasks.stopJob') } className={`flex shrink-0 cursor-pointer items-center gap-1 rounded border px-2 py-1 text-xs transition-colors ${ killOffered @@ -434,14 +438,14 @@ const TaskPanel = ({ }`} > {killOffered ? : } - {killOffered ? 'Kill' : 'Stop'} + {killOffered ? 'Kill' : t('chat.composer.stop')} )} diff --git a/src/workspaces/officerdev/src/apps/Chat/components/InputArea.tsx b/src/workspaces/officerdev/src/apps/Chat/components/InputArea.tsx index 600fdb6b..264257c5 100644 --- a/src/workspaces/officerdev/src/apps/Chat/components/InputArea.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/components/InputArea.tsx @@ -13,6 +13,7 @@ import { AttachmentList } from './AttachmentList'; import { AttachButton } from './AttachButton'; import { WebpageDialog } from './WebpageDialog'; import { BackgroundTaskTray } from './BackgroundTaskTray'; +import { useTranslation } from '../../../locales'; type InputAreaProps = { manager: UseEmbeddableChatType; @@ -21,6 +22,7 @@ type InputAreaProps = { }; export const InputArea = ({ manager, disabled }: InputAreaProps) => { + const { t } = useTranslation(); const [loginOpen, setLoginOpen] = useState(false); // Only for a commander, and only once it has an id. Everything a commander's loop needs in order to be // stoppable and deletable hangs off these two paths — see api/command-hq/spawned.ts. @@ -88,7 +90,7 @@ export const InputArea = ({ manager, disabled }: InputAreaProps) => { > {drop.isTarget && (
- Drop images to attach + {t('chat.attach.dropImages')}
)} {commandFeedback && ( @@ -158,7 +160,7 @@ export const InputArea = ({ manager, disabled }: InputAreaProps) => { onClick={stopGeneration} variant="destructive" size="icon" - title="Stop" + title={t('chat.composer.stop')} className="shrink-0 h-7 w-7 md:h-9 md:w-9 cursor-pointer" > @@ -177,7 +179,7 @@ export const InputArea = ({ manager, disabled }: InputAreaProps) => { }} disabled={!input.trim() || !isConnected} size="icon" - title={isGenerating ? 'Queue — sends when the current turn finishes' : 'Send'} + title={isGenerating ? 'Queue — sends when the current turn finishes' : t('chat.composer.send')} className={`shrink-0 h-7 w-7 md:h-9 md:w-9 cursor-pointer disabled:opacity-40 ${ isGenerating ? 'bg-busy hover:bg-busy/90 text-busy-foreground' : 'bg-duck-teal hover:bg-duck-teal/90' }`} @@ -307,6 +309,7 @@ const QueuedList = ({ onRemove: (id: string) => void; onSendNow: () => void; }) => { + const { t } = useTranslation(); if (queued.length === 0) return null; return ( @@ -316,7 +319,7 @@ const QueuedList = ({ )}
@@ -455,27 +458,30 @@ const ModelSwitch = ({ message }: { message: Extract }) => ( -
-
- - - {message.ok ? ( - message.enabled ? ( - 'Plan mode on — research only' +const PlanModeSwitch = ({ message }: { message: Extract }) => { + const { t } = useTranslation(); + return ( +
+
+ + + {message.ok ? ( + message.enabled ? ( + 'Plan mode on — research only' + ) : ( + 'Plan mode off' + ) ) : ( - 'Plan mode off' - ) - ) : ( - <> - Plan mode unchanged - · {message.error ?? 'the agent refused'} - - )} - -
-
-); + <> + {t('chat.message.planUnchanged')} + · {message.error ?? 'the agent refused'} + + )} + +
+
+ ); +}; /** * A plan, with the two buttons the agent is waiting on. @@ -496,6 +502,7 @@ const PlanCard = ({ message: Extract; onDecide?: (requestId: string, approve: boolean, feedback?: string) => void; }) => { + const { t } = useTranslation(); const [feedback, setFeedback] = useState(''); const [rejecting, setRejecting] = useState(false); const decided = message.approved !== undefined; @@ -535,7 +542,7 @@ const PlanCard = ({ autoFocus value={feedback} onChange={(ev) => setFeedback(ev.target.value)} - placeholder="What should change? The agent revises the plan from this." + placeholder={t('chat.message.reviseHint')} rows={2} className="w-full resize-none rounded border border-border bg-background px-2 py-1.5 text-xs outline-none focus:border-primary/50" /> @@ -564,7 +571,7 @@ const PlanCard = ({ className="flex cursor-pointer items-center gap-1 rounded bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground transition-opacity hover:opacity-90" > - Approve + {t('chat.message.approve')}
@@ -579,17 +586,20 @@ const PlanCard = ({ * render nothing at all, because the streaming bubble bailed on empty text. The transcript looked frozen * and the only evidence the turn was alive was the send button having become a stop button. */ -const ThinkingDots = () => ( - - {[0, 150, 300].map((delay) => ( - - ))} - -); +const ThinkingDots = () => { + const { t } = useTranslation(); + return ( + + {[0, 150, 300].map((delay) => ( + + ))} + + ); +}; type StreamingBubbleProps = { text: string; diff --git a/src/workspaces/officerdev/src/apps/Chat/components/MessageList.tsx b/src/workspaces/officerdev/src/apps/Chat/components/MessageList.tsx index 0f8284b0..1964952c 100644 --- a/src/workspaces/officerdev/src/apps/Chat/components/MessageList.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/components/MessageList.tsx @@ -8,6 +8,7 @@ import { buildRows } from './turn-rows'; import { useFoldExit } from './fold-exit'; import { ToolOpenProvider } from './tool-open'; import { TurnEnd, TurnFold } from './TurnFold'; +import { useTranslation } from '../../../locales'; const OVERSCAN = 5; @@ -16,6 +17,7 @@ type MessageListProps = { }; export const MessageList = ({ manager }: MessageListProps) => { + const { t } = useTranslation(); const { messages, historicalCount, @@ -115,7 +117,7 @@ export const MessageList = ({ manager }: MessageListProps) => { manager.emptyState ) : (
- Send a message to start + {t('chat.composer.startHint')}
))} {rows.length > 0 && ( @@ -175,7 +177,7 @@ export const MessageList = ({ manager }: MessageListProps) => { {showJumpToBottom && ( )} diff --git a/src/workspaces/officerdev/src/apps/Chat/components/ToolActivity.tsx b/src/workspaces/officerdev/src/apps/Chat/components/ToolActivity.tsx index 2e061ef8..c063b658 100644 --- a/src/workspaces/officerdev/src/apps/Chat/components/ToolActivity.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/components/ToolActivity.tsx @@ -6,6 +6,7 @@ import { diffStat, editDiff } from './line-diff'; import { CodeSurface } from './CodeSurface'; import { CopyButton } from './CopyButton'; import { useToolOpen } from './tool-open'; +import { useTranslation } from '../../../locales'; type ToolMessage = Extract; @@ -184,6 +185,7 @@ function langForPath(path: unknown): string { } export const ToolActivity = ({ message, defaultOpen }: ToolActivityProps) => { + const { t } = useTranslation(); const Icon = toolIcons[message.toolName] ?? Wrench; const { head, tail } = splitSummary(getToolSummary(message.toolName, message.toolInput)); const pending = message.output === undefined; @@ -266,7 +268,9 @@ export const ToolActivity = ({ message, defaultOpen }: ToolActivityProps) => { {children.length > 0 && (
-
Subagent
+
+ {t('chat.message.subagent')} +
)} @@ -274,7 +278,7 @@ export const ToolActivity = ({ message, defaultOpen }: ToolActivityProps) => { {message.output !== undefined && (
- {isError ? Output — error : 'Output'} + {isError ? {t('chat.message.outputError')} : 'Output'}
( -
-
- - - Turn finished · no closing message - -
-
-); +export const TurnEnd = () => { + const { t } = useTranslation(); + return ( +
+
+ + + {t('chat.turnFinished')} + +
+
+ ); +}; diff --git a/src/workspaces/officerdev/src/apps/Chat/components/UsagePopover.tsx b/src/workspaces/officerdev/src/apps/Chat/components/UsagePopover.tsx index 3af91bb1..2f11aa1a 100644 --- a/src/workspaces/officerdev/src/apps/Chat/components/UsagePopover.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/components/UsagePopover.tsx @@ -6,6 +6,7 @@ import type { UsageAccount } from '../useClaudeUsage'; import { currentLimit, formatAge, formatReset, formatTokens, useClaudeUsage, weekdayLabel } from '../useClaudeUsage'; import type { SwitchableAccount } from '../useAnthropicAccounts'; import { useAnthropicAccounts } from '../useAnthropicAccounts'; +import { useTranslation } from '../../../locales'; /** * Claude Code's own usage display, on the harness pill — for every account, not just the one signed in. @@ -43,36 +44,40 @@ const Section = ({ title, children }: { title: string; children: React.ReactNode
); -const Limits = ({ account, now }: { account: UsageAccount; now: number }) => ( -
-
- {account.limits.map((stored) => { - // What the stored figure means NOW — a window that has rolled over since it was measured reads as - // empty rather than as whatever it was when the account was last used. - const limit = currentLimit(stored, now); - const resets = formatReset(limit.resetsAt, now); - return ( -
-
- {limit.label} - {Math.round(limit.percent)}% +const Limits = ({ account, now }: { account: UsageAccount; now: number }) => { + const { t } = useTranslation(); + return ( +
+
+ {account.limits.map((stored) => { + // What the stored figure means NOW — a window that has rolled over since it was measured reads as + // empty rather than as whatever it was when the account was last used. + const limit = currentLimit(stored, now); + const resets = formatReset(limit.resetsAt, now); + return ( +
+
+ {limit.label} + {Math.round(limit.percent)}% +
+ + {resets && Resets in {resets}}
- - {resets && Resets in {resets}} -
- ); - })} -
-
-); + ); + })} +
+ + ); +}; const Days = ({ account }: { account: UsageAccount }) => { + const { t } = useTranslation(); // Every bar is scaled to the busiest day, so the shape of the week is readable even in a quiet one. A // fixed scale would flatten a whole week of ordinary work into nothing. const peak = Math.max(1, ...account.days.map((d) => d.tokens)); return ( -
+
{account.days.map((day, index) => { const today = index === account.days.length - 1; @@ -100,13 +105,14 @@ const Days = ({ account }: { account: UsageAccount }) => { }; const Models = ({ account }: { account: UsageAccount }) => { + const { t } = useTranslation(); // Four, because the tail is a long list of models that answered once. The heaviest is the scale, the same // way the week scales to its busiest day. const models = account.models.slice(0, 4); const peak = Math.max(1, ...models.map((m) => m.total)); return ( -
+
{models.map((model) => (
- @@ -342,9 +349,7 @@ export function UsagePopover({ label, harness }: UsagePopoverProps) { {/* Said on the account being LOOKED AT rather than as a count somewhere else. The numbers below are real and the reset times are still correct; the only thing missing is the ability to act on them, and naming the fix beats naming the problem. */} - {viewing && ( -
No saved sign-in — run /login to use this one
- )} + {viewing &&
{t('chat.usage.noSignIn')}
} {/* How old this reading is, and it earns its line only when it is not live. @@ -358,7 +363,7 @@ export function UsagePopover({ label, harness }: UsagePopoverProps) {
)} {selected?.current && isRefreshing && ( -
Updating…
+
{t('chat.usage.updating')}
)}
@@ -386,7 +391,7 @@ export function UsagePopover({ label, harness }: UsagePopoverProps) { still leaves real token history below it. */} {isError && (
- Couldn't read usage on this machine. + {t('chat.usage.unreadable')}
)} {selected?.limitsProblem && selected.current && ( @@ -394,7 +399,9 @@ export function UsagePopover({ label, harness }: UsagePopoverProps) { )} {isLoading && !selected && ( -
Loading…
+
+ {t('chat.usage.loading')} +
)} {selected && ( diff --git a/src/workspaces/officerdev/src/apps/Chat/components/WebpageDialog.tsx b/src/workspaces/officerdev/src/apps/Chat/components/WebpageDialog.tsx index 9eccfd11..d6a25220 100644 --- a/src/workspaces/officerdev/src/apps/Chat/components/WebpageDialog.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/components/WebpageDialog.tsx @@ -2,6 +2,7 @@ import type { FormEvent } from 'react'; import { useState } from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; +import { useTranslation } from '../../../locales'; type WebpageDialogProps = { open: boolean; @@ -10,6 +11,7 @@ type WebpageDialogProps = { }; export function WebpageDialog({ open, onOpenChange, onSubmit }: WebpageDialogProps) { + const { t } = useTranslation(); const [urlInput, setUrlInput] = useState(''); const handleSubmit = (ev?: FormEvent) => { @@ -25,8 +27,8 @@ export function WebpageDialog({ open, onOpenChange, onSubmit }: WebpageDialogPro - Attach Webpage - Enter a URL to scrape and attach as context. + {t('chat.attach.webpageTitle')} + {t('chat.attach.webpageHint')}
- Attach + {t('chat.attach.attach')}
diff --git a/src/workspaces/officerdev/src/locales/en-GB.json b/src/workspaces/officerdev/src/locales/en-GB.json index e6e91a39..3d043fa1 100644 --- a/src/workspaces/officerdev/src/locales/en-GB.json +++ b/src/workspaces/officerdev/src/locales/en-GB.json @@ -315,5 +315,114 @@ "newChatTitle": "New chat", "renameHint": "Click to rename this chat", "hideHidden": "Hide hidden" + }, + "chat": { + "newChat": "New chat", + "rename": "Rename", + "deleteQ": "Delete “{{title}}”?", + "duplicateAgent": "There is already an agent named “{{name}}” on this dashboard", + "composerPlaceholder": "What do you want to work on now?", + "turnFinished": "Turn finished · no closing message", + "job": { + "title": "Background job", + "runsIn": "Runs in", + "command": "Command — run exactly this", + "prompt": "Prompt — describe it, the agent works it out", + "watch": "Watch for something", + "watchHint": "In prose. The agent will ask before arming if anything is unclear.", + "handBack": "Exit and hand back to the agent, then re-arm", + "keepGoing": "Keep going — act each time and carry on", + "stopAfter": "Stop after", + "start": "Start", + "watchExample": "watch our Gitea repos; on any new push, pull it and push to the GitHub mirror", + "startJob": "Start a background job or watcher" + }, + "tasks": { + "restart": "Restart", + "pinned": "Pinned", + "clearFinished": "Clear all finished", + "clearFinishedTasks": "Clear all finished tasks", + "stopJob": "Stop this job", + "dismiss": "Dismiss", + "rerun": "Stop it and run the same command again", + "collapse": "Collapse", + "collapseDetail": "Collapse task detail", + "stopNamed": "Stop {{label}}", + "dismissNamed": "Dismiss {{label}}", + "status": "Task {{status}}." + }, + "usage": { + "noSignIn": "No saved sign-in — run /login to use this one", + "updating": "Updating…", + "unreadable": "Couldn’t read usage on this machine.", + "loading": "Loading…", + "limits": "Limits", + "byDay": "Tokens by day", + "byModel": "Tokens by model", + "title": "Usage and limits", + "modelBreakdown": "In {{input}} · out {{output}} · cache read {{cacheRead}} · cache write {{cacheWrite}}", + "switchTo": "Switch to {{account}}" + }, + "attach": { + "image": "Image", + "textFile": "Text File", + "webpageUrl": "Webpage URL", + "attach": "Attach", + "readFailed": "Could not read {{name}}: {{reason}}", + "unknownError": "unknown error", + "webpageTitle": "Attach Webpage", + "webpageHint": "Enter a URL to scrape and attach as context.", + "scrapeFailed": "Failed to scrape webpage", + "uploadFailed": "Failed to upload image", + "dropImages": "Drop images to attach" + }, + "signIn": { + "title": "Sign in to Claude", + "done": "Signed in. New turns use these credentials — no restart needed.", + "asking": "Asking the CLI for a sign-in link…", + "openHint": "Open this, authorise, and paste back the code it gives you.", + "authorise": "Authorise in claude.com", + "copyLink": "Copy the link, to open it in another browser or on your phone", + "pasteCode": "paste the code" + }, + "message": { + "copyCode": "Copy the code", + "copyMarkdown": "Copy as a markdown block", + "showMore": "Show more ({{count}} more lines)", + "subagent": "Subagent", + "outputError": "Output — error", + "contextCleared": "Context cleared — nothing above this is in memory", + "interrupted": "Interrupted by user", + "agentRestarted": "Agent restarted — turn cut off", + "retry": "Retry", + "planUnchanged": "Plan mode unchanged", + "approve": "Approve", + "frontmatter": "Frontmatter", + "systemPrompt": "System prompt", + "reviseHint": "What should change? The agent revises the plan from this.", + "waiting": "Waiting for a response", + "narrationFailed": "The narration was generated but would not play.", + "readAloudFailed": "Could not read this message aloud: {{reason}}", + "compacted": "Context compacted", + "compactedAuto": "Context compacted automatically", + "answerSent": "Answer sent — waiting for the agent", + "other": "Other..." + }, + "composer": { + "stop": "Stop", + "sendIntoTurn": "Send these into the turn that is already running", + "removeFromQueue": "Remove from queue", + "send": "Send", + "startHint": "Send a message to start", + "jumpToLatest": "Jump to latest message", + "noAudio": "No audio was captured" + }, + "agent": { + "gone": "This panel’s agent no longer exists.", + "forget": "Forget it", + "name": "Name this agent", + "peers": "Peers: {{names}}" + }, + "copy": "Copy" } } diff --git a/src/workspaces/officerdev/src/locales/pt-PT.json b/src/workspaces/officerdev/src/locales/pt-PT.json index 68aec700..ab459cf9 100644 --- a/src/workspaces/officerdev/src/locales/pt-PT.json +++ b/src/workspaces/officerdev/src/locales/pt-PT.json @@ -315,5 +315,114 @@ "newChatTitle": "Novo chat", "renameHint": "Clique para renomear este chat", "hideHidden": "Ocultar ocultas" + }, + "chat": { + "newChat": "Novo chat", + "rename": "Renomear", + "deleteQ": "Eliminar “{{title}}”?", + "duplicateAgent": "Já existe um agente chamado “{{name}}” neste painel", + "composerPlaceholder": "Em que quer trabalhar agora?", + "turnFinished": "Turno terminado · sem mensagem de fecho", + "job": { + "title": "Trabalho em segundo plano", + "runsIn": "Executa em", + "command": "Comando — executar exatamente isto", + "prompt": "Prompt — descreva-o e o agente descobre como", + "watch": "Vigiar algo", + "watchHint": "Em texto corrido. O agente pergunta antes de armar se algo não estiver claro.", + "handBack": "Sair e devolver ao agente, depois rearmar", + "keepGoing": "Continuar — agir de cada vez e prosseguir", + "stopAfter": "Parar depois de", + "start": "Iniciar", + "watchExample": "vigiar os nossos repositórios Gitea; em cada novo push, obtê-lo e enviar para o espelho no GitHub", + "startJob": "Iniciar um trabalho em segundo plano ou um vigilante" + }, + "tasks": { + "restart": "Reiniciar", + "pinned": "Fixado", + "clearFinished": "Limpar todos os terminados", + "clearFinishedTasks": "Limpar todas as tarefas terminadas", + "stopJob": "Parar este trabalho", + "dismiss": "Descartar", + "rerun": "Pará-lo e executar o mesmo comando novamente", + "collapse": "Recolher", + "collapseDetail": "Recolher o detalhe da tarefa", + "stopNamed": "Parar {{label}}", + "dismissNamed": "Descartar {{label}}", + "status": "Tarefa {{status}}." + }, + "usage": { + "noSignIn": "Sem sessão guardada — execute /login para usar esta", + "updating": "A atualizar…", + "unreadable": "Não foi possível ler o consumo nesta máquina.", + "loading": "A carregar…", + "limits": "Limites", + "byDay": "Tokens por dia", + "byModel": "Tokens por modelo", + "title": "Consumo e limites", + "modelBreakdown": "Entrada {{input}} · saída {{output}} · leitura de cache {{cacheRead}} · escrita de cache {{cacheWrite}}", + "switchTo": "Mudar para {{account}}" + }, + "attach": { + "image": "Imagem", + "textFile": "Ficheiro de texto", + "webpageUrl": "URL de página web", + "attach": "Anexar", + "readFailed": "Não foi possível ler {{name}}: {{reason}}", + "unknownError": "erro desconhecido", + "webpageTitle": "Anexar página web", + "webpageHint": "Introduza um URL para extrair e anexar como contexto.", + "scrapeFailed": "Falha ao extrair a página web", + "uploadFailed": "Falha ao carregar a imagem", + "dropImages": "Largue imagens para anexar" + }, + "signIn": { + "title": "Iniciar sessão no Claude", + "done": "Sessão iniciada. Os novos turnos usam estas credenciais — não é preciso reiniciar.", + "asking": "A pedir à CLI um link de início de sessão…", + "openHint": "Abra isto, autorize e cole o código que lhe for dado.", + "authorise": "Autorizar em claude.com", + "copyLink": "Copiar o link, para o abrir noutro navegador ou no telemóvel", + "pasteCode": "cole o código" + }, + "message": { + "copyCode": "Copiar o código", + "copyMarkdown": "Copiar como bloco markdown", + "showMore": "Mostrar mais ({{count}} linhas)", + "subagent": "Subagente", + "outputError": "Saída — erro", + "contextCleared": "Contexto limpo — nada acima disto está em memória", + "interrupted": "Interrompido pelo utilizador", + "agentRestarted": "Agente reiniciado — turno cortado", + "retry": "Tentar novamente", + "planUnchanged": "Modo de plano inalterado", + "approve": "Aprovar", + "frontmatter": "Frontmatter", + "systemPrompt": "Prompt de sistema", + "reviseHint": "O que deve mudar? O agente revê o plano a partir disto.", + "waiting": "À espera de resposta", + "narrationFailed": "A narração foi gerada mas não foi possível reproduzi-la.", + "readAloudFailed": "Não foi possível ler esta mensagem em voz alta: {{reason}}", + "compacted": "Contexto compactado", + "compactedAuto": "Contexto compactado automaticamente", + "answerSent": "Resposta enviada — à espera do agente", + "other": "Outro..." + }, + "composer": { + "stop": "Parar", + "sendIntoTurn": "Enviar isto para o turno que já está a correr", + "removeFromQueue": "Remover da fila", + "send": "Enviar", + "startHint": "Envie uma mensagem para começar", + "jumpToLatest": "Ir para a mensagem mais recente", + "noAudio": "Não foi captado áudio" + }, + "agent": { + "gone": "O agente deste painel já não existe.", + "forget": "Esquecê-lo", + "name": "Dar nome a este agente", + "peers": "Pares: {{names}}" + }, + "copy": "Copiar" } }