From 68a0a8e326855df050abcca4cfbffb08f94f42d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 23 Aug 2026 15:43:56 +0000 Subject: [PATCH] chat panel keyed, first pass (280 -> 203) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bulk of the chat UI: composer, background-job dialog, task list, usage popover, attachments, the Claude sign-in flow and the message bubbles. background-job-prompt.ts is exempt in full — the module builds the brief the MODEL executes, and translating it would change what the agent does. Two marked lines, both with the reason. QuestionActivity.test.tsx now renders inside an I18nProvider with the real panel catalogue. It asserted on 'Other...' and would otherwise have seen the dot-path; wrapping keeps it asserting the English a person actually reads. Same fix the DataTable test needed in phase 1. Scanner, two counting bugs, each delta verified: - `new Error(`…`)` with a template literal fell through to 'template str' and was counted as convertible — the opposite of true. Now it lands in thrown errors. - and it was then matched by BOTH patterns, so it was counted twice while staying convertible; the template-string pattern now declines what a thrown Error owns. --- scripts/i18n/scan-hardcoded.ts | 15 ++- .../src/apps/Chat/AgentIdentityBar.tsx | 8 +- .../officerdev/src/apps/Chat/ChatHeader.tsx | 8 +- .../apps/Chat/ChatLauncher/ChatLauncher.tsx | 7 +- .../src/apps/Chat/ChatPanelWrapper.tsx | 4 +- .../Chat/EmbeddableChat/EmbeddableChat.tsx | 3 +- .../src/apps/Chat/background-job-prompt.ts | 4 +- .../src/apps/Chat/components/AttachButton.tsx | 10 +- .../Chat/components/BackgroundJobDialog.tsx | 27 ++--- .../Chat/components/BackgroundTaskTray.tsx | 26 +++-- .../src/apps/Chat/components/ChatImage.tsx | 4 +- .../Chat/components/ClaudeLoginDialog.tsx | 18 +-- .../src/apps/Chat/components/CodeSurface.tsx | 10 +- .../src/apps/Chat/components/CopyButton.tsx | 4 +- .../src/apps/Chat/components/InputArea.tsx | 13 ++- .../apps/Chat/components/MessageBubble.tsx | 90 ++++++++------- .../src/apps/Chat/components/MessageList.tsx | 6 +- .../apps/Chat/components/ModelSelector.tsx | 4 +- .../Chat/components/QuestionActivity.test.tsx | 98 ++++++++++------ .../apps/Chat/components/QuestionActivity.tsx | 8 +- .../src/apps/Chat/components/ToolActivity.tsx | 8 +- .../src/apps/Chat/components/TurnFold.tsx | 24 ++-- .../src/apps/Chat/components/UsagePopover.tsx | 67 ++++++----- .../apps/Chat/components/WebpageDialog.tsx | 8 +- .../officerdev/src/locales/en-GB.json | 109 ++++++++++++++++++ .../officerdev/src/locales/pt-PT.json | 109 ++++++++++++++++++ 26 files changed, 504 insertions(+), 188 deletions(-) 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" } }