diff --git a/src/workspaces/officerdev/src/apps/Chat/AgentIdentityBar.tsx b/src/workspaces/officerdev/src/apps/Chat/AgentIdentityBar.tsx index 8f50717a..79646716 100644 --- a/src/workspaces/officerdev/src/apps/Chat/AgentIdentityBar.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/AgentIdentityBar.tsx @@ -16,7 +16,7 @@ type AgentIdentityBarProps = { panel: UseAgentPanelType }; * the two apart is to notice that the conversation did not reset. */ export const AgentIdentityBar = ({ panel }: AgentIdentityBarProps) => { - const { t } = useTranslation(); + const { t, tUi } = useTranslation(); const [naming, setNaming] = useState(false); const [name, setName] = useState(''); const [error, setError] = useState(null); @@ -58,7 +58,7 @@ export const AgentIdentityBar = ({ panel }: AgentIdentityBarProps) => { variant="ghost" className="h-6 w-6 p-0" onClick={() => setNaming(false)} - aria-label="Cancel" + aria-label={tUi('button.cancel')} > @@ -103,7 +103,7 @@ export const AgentIdentityBar = ({ panel }: AgentIdentityBarProps) => { {panel.agent.name} p.name).join(', ')}` : undefined} + title={peers.length ? t('chat.agent.peers', { names: peers.map((p) => p.name).join(', ') }) : undefined} > {peers.length === 0 ? 'no peers yet' diff --git a/src/workspaces/officerdev/src/apps/Chat/ChatHeader.tsx b/src/workspaces/officerdev/src/apps/Chat/ChatHeader.tsx index 4ae402d6..e8d9ac87 100644 --- a/src/workspaces/officerdev/src/apps/Chat/ChatHeader.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/ChatHeader.tsx @@ -34,7 +34,7 @@ export type ChatSessionSelection = { const title = (s: { title: string }) => s.title || '(untitled)'; export const ChatHeader = ({ panelId }: { panelId?: string }) => { - const { t } = useTranslation(); + const { t, tUi } = useTranslation(); const { workspace, cwd } = useWorkspace(); /** @@ -109,10 +109,15 @@ export const ChatHeader = ({ panelId }: { panelId?: string }) => { }} className="min-w-0 flex-1 rounded border border-duck-dark/20 bg-transparent px-1.5 py-0.5 text-xs" /> - - @@ -138,11 +143,11 @@ export const ChatHeader = ({ panelId }: { panelId?: string }) => { )} diff --git a/src/workspaces/officerdev/src/apps/Chat/components/MessageBubble.tsx b/src/workspaces/officerdev/src/apps/Chat/components/MessageBubble.tsx index 964da252..4500cf77 100644 --- a/src/workspaces/officerdev/src/apps/Chat/components/MessageBubble.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/components/MessageBubble.tsx @@ -86,7 +86,7 @@ const CollapsibleBlock = ({ label, content }: { label: string; content: string } type TtsState = 'idle' | 'loading' | 'playing'; const ReadAloudButton = ({ id, text }: { id: string; text: string }) => { - const { t } = useTranslation(); + const { t, tUi } = useTranslation(); const [state, setState] = useState('idle'); const audioRef = useRef(null); const api = useFilesAPI('user-data'); @@ -122,7 +122,7 @@ const ReadAloudButton = ({ id, text }: { id: string; text: string }) => { // the icon comes back, the button looks broken rather than failed. Most often this is the TTS // service being down, which is worth saying out loud. setState('idle'); - toast.error(`Could not read this message aloud: ${errorText(err)}`); + toast.error(t('chat.message.readAloudFailed', { reason: errorText(err) })); } }; @@ -343,6 +343,7 @@ export const MessageBubble = ({ message, onAnswer, onRetry, onDecidePlan, defaul * both sides discarded the taskId that relates them. */ const TaskActivity = ({ message }: { message: Extract }) => { + const { t, tUi } = useTranslation(); const { status, summary, description, taskType } = message; const Icon = status === 'completed' ? Check : status === 'failed' ? X : status === 'stopped' ? CircleSlash : Clock; const tone: Tone = @@ -357,7 +358,7 @@ const TaskActivity = ({ message }: { message: Extract{taskType}}
- {status ? summary || `Task ${status}.` : 'Running in the background…'} + {status ? summary || t('chat.tasks.status', { status }) : 'Running in the background…'}
@@ -384,6 +385,7 @@ const formatTokens = (n: number): string => (n >= 1000 ? `${Math.round(n / 1000) * the sidecar measured — which is the one a replay in another tab gets. */ const CompactActivity = ({ message }: { message: Extract }) => { + const { t, tUi } = useTranslation(); const { done, trigger, preTokens, durationMs } = message; const startedAt = useRef(Date.now()); const [elapsed, setElapsed] = useState(0); @@ -405,7 +407,11 @@ const CompactActivity = ({ message }: { message: Extract - {done ? `Context compacted${trigger === 'manual' ? '' : ' automatically'}` : 'Compacting the conversation…'} + {done + ? trigger === 'manual' + ? t('chat.message.compacted') + : t('chat.message.compactedAuto') + : 'Compacting the conversation…'} {detail && · {detail}}
@@ -502,7 +508,7 @@ const PlanCard = ({ message: Extract; onDecide?: (requestId: string, approve: boolean, feedback?: string) => void; }) => { - const { t } = useTranslation(); + const { t, tUi } = useTranslation(); const [feedback, setFeedback] = useState(''); const [rejecting, setRejecting] = useState(false); const decided = message.approved !== undefined; @@ -554,7 +560,7 @@ const PlanCard = ({ onClick={() => setRejecting(false)} className="cursor-pointer rounded px-2 py-1 text-xs text-muted-foreground hover:text-foreground" > - Cancel + {tUi('button.cancel')} )} )} diff --git a/src/workspaces/officerdev/src/apps/Chat/components/UsagePopover.tsx b/src/workspaces/officerdev/src/apps/Chat/components/UsagePopover.tsx index 2f11aa1a..b2245fff 100644 --- a/src/workspaces/officerdev/src/apps/Chat/components/UsagePopover.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/components/UsagePopover.tsx @@ -118,7 +118,12 @@ const Models = ({ account }: { account: UsageAccount }) => {
{/* The bar sits BEHIND the row rather than beside it — the model names are long and a separate track would leave no room for either. */} @@ -287,8 +292,10 @@ export function UsagePopover({ label, harness }: UsagePopoverProps) { title: a.current ? `${a.emailAccount} — signed in` : a.usable - ? `Switch to ${a.emailAccount}${a.orgName ? ` (${a.orgName})` : ''}` - : `${a.emailAccount} — sign-in expired, run /login`, + ? a.orgName + ? t('chat.usage.switchToOrg', { account: a.emailAccount, org: a.orgName }) + : t('chat.usage.switchTo', { account: a.emailAccount }) + : t('chat.usage.signInExpired', { account: a.emailAccount }), })), ...archived.map((a) => ({ key: `archived:${a.email}`, diff --git a/src/workspaces/officerdev/src/apps/Chat/useAttachments.ts b/src/workspaces/officerdev/src/apps/Chat/useAttachments.ts index d1ea0fb2..4f59bb26 100644 --- a/src/workspaces/officerdev/src/apps/Chat/useAttachments.ts +++ b/src/workspaces/officerdev/src/apps/Chat/useAttachments.ts @@ -2,12 +2,14 @@ import { useState } from 'react'; import { toast } from 'sonner'; import { useClient } from 'hooks/useClient'; import type { Attachment } from './types'; +import { useTranslation } from '../../locales'; type UseAttachmentsParams = { sessionId?: string | null; }; export function useAttachments(params?: UseAttachmentsParams) { + const { t } = useTranslation(); const client = useClient(); const [attachments, setAttachments] = useState([]); @@ -33,7 +35,7 @@ export function useAttachments(params?: UseAttachmentsParams) { ); } catch { setAttachments((prev) => prev.filter((_, i) => i !== idx)); - toast.error('Failed to scrape webpage'); + toast.error(t('chat.attach.scrapeFailed')); } }; @@ -58,7 +60,7 @@ export function useAttachments(params?: UseAttachmentsParams) { ); } catch { setAttachments((prev) => prev.filter((_, i) => i !== idx)); - toast.error('Failed to upload image'); + toast.error(t('chat.attach.uploadFailed')); } }; diff --git a/src/workspaces/officerdev/src/apps/Chat/useAudioRecording.ts b/src/workspaces/officerdev/src/apps/Chat/useAudioRecording.ts index c14434d6..1602af9b 100644 --- a/src/workspaces/officerdev/src/apps/Chat/useAudioRecording.ts +++ b/src/workspaces/officerdev/src/apps/Chat/useAudioRecording.ts @@ -1,7 +1,9 @@ import { useState, useRef, useEffect } from 'react'; import { toast } from 'sonner'; +import { useTranslation } from '../../locales'; export function useAudioRecording(onTranscription: (text: string) => void) { + const { t } = useTranslation(); const [recording, setRecording] = useState(false); const [transcribing, setTranscribing] = useState(false); const mediaRecorderRef = useRef(null); @@ -16,7 +18,7 @@ export function useAudioRecording(onTranscription: (text: string) => void) { try { if (recorder.state === 'inactive') { - recorder.stream.getTracks().forEach((t) => t.stop()); + recorder.stream.getTracks().forEach((track) => track.stop()); return; } @@ -30,10 +32,10 @@ export function useAudioRecording(onTranscription: (text: string) => void) { recorder.stop(); }); - recorder.stream.getTracks().forEach((t) => t.stop()); + recorder.stream.getTracks().forEach((track) => track.stop()); if (blob.size === 0) { - toast.error('No audio was captured'); + toast.error(t('chat.composer.noAudio')); return; } @@ -61,7 +63,7 @@ export function useAudioRecording(onTranscription: (text: string) => void) { setTranscribing(false); } } catch (err) { - recorder.stream?.getTracks().forEach((t) => t.stop()); + recorder.stream?.getTracks().forEach((track) => track.stop()); chunksRef.current = []; toast.error(err instanceof Error ? err.message : 'Recording failed'); } @@ -94,7 +96,7 @@ export function useAudioRecording(onTranscription: (text: string) => void) { const recorder = mediaRecorderRef.current; if (!recorder) return; if (recorder.state !== 'inactive') recorder.stop(); - recorder.stream?.getTracks().forEach((t) => t.stop()); + recorder.stream?.getTracks().forEach((track) => track.stop()); mediaRecorderRef.current = null; chunksRef.current = []; }, diff --git a/src/workspaces/officerdev/src/locales/en-GB.json b/src/workspaces/officerdev/src/locales/en-GB.json index 3d043fa1..7e710025 100644 --- a/src/workspaces/officerdev/src/locales/en-GB.json +++ b/src/workspaces/officerdev/src/locales/en-GB.json @@ -361,7 +361,9 @@ "byModel": "Tokens by model", "title": "Usage and limits", "modelBreakdown": "In {{input}} · out {{output}} · cache read {{cacheRead}} · cache write {{cacheWrite}}", - "switchTo": "Switch to {{account}}" + "switchTo": "Switch to {{account}}", + "switchToOrg": "Switch to {{account}} ({{org}})", + "signInExpired": "{{account}} — sign-in expired, run /login" }, "attach": { "image": "Image", diff --git a/src/workspaces/officerdev/src/locales/pt-PT.json b/src/workspaces/officerdev/src/locales/pt-PT.json index ab459cf9..39c521de 100644 --- a/src/workspaces/officerdev/src/locales/pt-PT.json +++ b/src/workspaces/officerdev/src/locales/pt-PT.json @@ -361,7 +361,9 @@ "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}}" + "switchTo": "Mudar para {{account}}", + "switchToOrg": "Mudar para {{account}} ({{org}})", + "signInExpired": "{{account}} — sessão expirada, execute /login" }, "attach": { "image": "Imagem",