diff --git a/scripts/i18n/scan-hardcoded.ts b/scripts/i18n/scan-hardcoded.ts index c36cbf1e..21636ead 100755 --- a/scripts/i18n/scan-hardcoded.ts +++ b/scripts/i18n/scan-hardcoded.ts @@ -52,7 +52,10 @@ const Q = String.raw`(?:"([^"\n]{2,120})"|'([^'\n]{2,120})')`; */ const PATTERNS: { kind: string; rx: RegExp; neverTranslate?: true }[] = [ // >Some visible words< - { kind: 'jsx text', rx: />\s*([A-Z][A-Za-z0-9][^<>{}\n]{2,100}?)\s*`, so an arrow return type does not read as JSX text: `(id: string) => + // Promise` was reported as the string "Promise". + { kind: 'jsx text', rx: /(?\s*([A-Z][A-Za-z0-9][^<>{}\n]{2,100}?)\s*
@@ -142,7 +144,7 @@ function DetailBar({ ) : ( @@ -129,7 +129,7 @@ export const DirPickerModal = ({ open, onClose, onSelect }: DirPickerModalProps) onClick={() => setCreating(true)} className="flex items-center gap-1.5 text-xs opacity-60 hover:opacity-100 cursor-pointer" > - New folder + {t('chatHistory.newFolder')} {/* Shown even at zero, so the control's state is always legible: a folder with nothing hidden and one with everything hidden must not look identical. */} @@ -139,7 +139,11 @@ export const DirPickerModal = ({ open, onClose, onSelect }: DirPickerModalProps) className="flex items-center gap-1.5 text-xs opacity-60 hover:opacity-100 cursor-pointer" > {showHidden ? : } - {showHidden ? 'Hide hidden' : `Show hidden${hiddenCount > 0 ? ` (${hiddenCount})` : ''}`} + {showHidden + ? t('chatHistory.hideHidden') + : hiddenCount > 0 + ? t('chatHistory.showHiddenCount', { count: hiddenCount }) + : t('chatHistory.showHidden')}
)} @@ -151,7 +155,7 @@ export const DirPickerModal = ({ open, onClose, onSelect }: DirPickerModalProps) onClick={onClose} className="rounded-md px-3 py-1.5 text-sm opacity-60 hover:opacity-100 cursor-pointer" > - Cancel + {tUi('button.cancel')} diff --git a/src/workspaces/officerdev/src/apps/ChatHistory/NotifyMenu.tsx b/src/workspaces/officerdev/src/apps/ChatHistory/NotifyMenu.tsx index 3017c2ed..918602bc 100644 --- a/src/workspaces/officerdev/src/apps/ChatHistory/NotifyMenu.tsx +++ b/src/workspaces/officerdev/src/apps/ChatHistory/NotifyMenu.tsx @@ -1,13 +1,15 @@ import { Bell, BellOff, Check } from 'lucide-react'; import * as Dropdown from '@/components/ui/dropdown-menu'; import type { NotifyPreference } from 'state/useClaudeSessions'; +import type { PanelTranslationKey } from '../../locales'; +import { useTranslation } from '../../locales'; const { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } = Dropdown; -const OPTIONS: { value: NotifyPreference; label: string }[] = [ - { value: 'every_turn', label: 'Every end of turn' }, - { value: 'input_needed', label: 'Only when input is needed' }, - { value: 'none', label: 'None' }, +const OPTIONS: { value: NotifyPreference; labelKey: PanelTranslationKey }[] = [ + { value: 'every_turn', labelKey: 'chatHistory.notify.everyTurn' }, + { value: 'input_needed', labelKey: 'chatHistory.notify.inputNeeded' }, + { value: 'none', labelKey: 'chatHistory.notify.none' }, ]; /** @@ -40,6 +42,7 @@ type NotifyMenuProps = { * variant, which nothing here uses. */ export const NotifyMenu = ({ preference, onChange }: NotifyMenuProps) => { + const { t } = useTranslation(); const pick = (value: NotifyPreference) => { onChange(value); // Lazy, first-opt-in request — the only place this fires. A denial or an already-decided answer is a @@ -55,8 +58,10 @@ export const NotifyMenu = ({ preference, onChange }: NotifyMenuProps) => { @@ -184,10 +187,10 @@ export const SessionList = () => { @@ -209,25 +212,25 @@ export const SessionList = () => { <> {latest.error ? ( void latest.refetch()} className="cursor-pointer rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground" > - Try again + {t('chatHistory.tryAgain')} } /> ) : latest.isLoading && latest.sessions.length === 0 ? ( // Loading and empty used to be the same grey line of text, so a slow read looked like an // account with no history. - + ) : latest.sessions.length === 0 ? ( ) : ( @@ -277,12 +280,13 @@ export const SessionList = () => { */ type LiveKind = 'agents' | 'commanders'; -const LIVE_TABS: { value: LiveKind; label: string }[] = [ - { value: 'agents', label: 'Agents' }, - { value: 'commanders', label: 'Commanders' }, +const LIVE_TABS: { value: LiveKind; labelKey: PanelTranslationKey }[] = [ + { value: 'agents', labelKey: 'chatHistory.tabs.agents' }, + { value: 'commanders', labelKey: 'chatHistory.tabs.commanders' }, ]; const LiveView = ({ activeId, home }: { activeId?: string; home: string | null }) => { + const { t } = useTranslation(); const { live: sessions, isLoading, error, refetch } = useLiveSessions(); const [kind, setKind] = useLocalStorageState(LIVE_KIND_KEY, 'agents'); @@ -297,20 +301,20 @@ const LiveView = ({ activeId, home }: { activeId?: string; home: string | null } if (error) { return ( void refetch()} className="cursor-pointer rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground" > - Try again + {t('chatHistory.tryAgain')} } /> ); } - if (isLoading && sessions.length === 0) return ; + if (isLoading && sessions.length === 0) return ; return (
@@ -328,7 +332,7 @@ const LiveView = ({ activeId, home }: { activeId?: string; home: string | null } value={tab.value} className="flex-1 cursor-pointer rounded-md border border-transparent px-2 py-1 text-xs font-medium leading-normal ring-0 transition-all hover:bg-foreground/5 hover:text-foreground focus-visible:ring-0 data-[state=active]:border-border data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm" > - {tab.label} + {t(tab.labelKey)} 0 ? 'text-success' : 'opacity-50'}`}>({count}) ); diff --git a/src/workspaces/officerdev/src/apps/ChatHistory/SessionRow.tsx b/src/workspaces/officerdev/src/apps/ChatHistory/SessionRow.tsx index 64e5fdb2..0da99c55 100644 --- a/src/workspaces/officerdev/src/apps/ChatHistory/SessionRow.tsx +++ b/src/workspaces/officerdev/src/apps/ChatHistory/SessionRow.tsx @@ -5,6 +5,8 @@ import { DataRow, MetaItem, RelativeTime } from '@/components/Data'; import { errorText } from 'helpers/error-text'; import type { ClaudeSessionSummary, NotifyPreference } from 'state/useClaudeSessions'; import { chatSessionPath } from './chat-routes'; +import { useTranslation } from '../../locales'; +import { Untranslated } from '@/components/Untranslated'; /* One conversation, rendered the same way in all three tabs. @@ -26,9 +28,13 @@ import { chatSessionPath } from './chat-routes'; */ export const HarnessBadge = ({ harness }: { harness?: 'claude' | 'opencode' }) => harness === 'opencode' ? ( - OpenCode + + OpenCode + ) : ( - Claude + + Claude + ); /** @@ -113,6 +119,7 @@ export function useSessionRowActions(actions: { renameSession: (id: string, title: string) => Promise; deleteSession: (id: string) => Promise; }) { + const { t } = useTranslation(); const [editingId, setEditingId] = useState(null); const [editValue, setEditValue] = useState(''); const [confirmingId, setConfirmingId] = useState(null); @@ -134,7 +141,7 @@ export function useSessionRowActions(actions: { try { await actions.renameSession(id, title); } catch (err) { - toast.error(`Could not rename the session: ${errorText(err)}`); + toast.error(t('chatHistory.renameFailed', { reason: errorText(err) })); } }, [editingId, editValue, actions]); @@ -144,7 +151,7 @@ export function useSessionRowActions(actions: { try { await actions.deleteSession(id); } catch (err) { - toast.error(`Could not delete the session: ${errorText(err)}`); + toast.error(t('chatHistory.deleteFailed', { reason: errorText(err) })); } }, [actions], @@ -188,6 +195,7 @@ export const SessionRow = ({ actions, activeRef, }: SessionRowProps) => { + const { t, tUi } = useTranslation(); const isActive = activeId === session.id; const isEditing = actions.editingId === session.id; const isConfirming = actions.confirmingId === session.id; @@ -209,15 +217,15 @@ export const SessionRow = ({ if (ev.key === 'Escape') actions.setEditingId(null); }} onBlur={() => void actions.commitRename()} - aria-label="Session title" + aria-label={t('chatHistory.sessionTitle')} className="min-w-0 flex-1 border-b border-primary/40 bg-transparent text-sm outline-none" /> @@ -226,8 +234,8 @@ export const SessionRow = ({ onMouseDown={(ev) => ev.preventDefault()} onClick={() => actions.setEditingId(null)} className="cursor-pointer p-1 opacity-50 hover:opacity-100" - title="Cancel" - aria-label="Cancel rename" + title={tUi('button.cancel')} + aria-label={t('chatHistory.cancelRename')} > @@ -279,21 +287,23 @@ export const SessionRow = ({ {/* Say the size of it. Deleting a merged chain removes every part, and a row that looks like one conversation giving no warning that it is three files is exactly the surprise this prevents. */} - {session.partCount && session.partCount > 1 ? `Delete all ${session.partCount}?` : 'Delete?'} + {session.partCount && session.partCount > 1 + ? t('chatHistory.deleteAll', { count: session.partCount }) + : 'Delete?'} @@ -304,16 +314,16 @@ export const SessionRow = ({ diff --git a/src/workspaces/officerdev/src/apps/ChatHistory/SessionTree.tsx b/src/workspaces/officerdev/src/apps/ChatHistory/SessionTree.tsx index 06eb3f63..61066cd0 100644 --- a/src/workspaces/officerdev/src/apps/ChatHistory/SessionTree.tsx +++ b/src/workspaces/officerdev/src/apps/ChatHistory/SessionTree.tsx @@ -7,6 +7,7 @@ import type { LiveSession } from 'state/useLiveSessions'; import { chatNewPath } from './chat-routes'; import { SessionRow, shortenPath, type SessionRowActions, useScrollActiveIntoView } from './SessionRow'; import { useSelectedChatSession } from '../../channels'; +import { useTranslation } from '../../locales'; /* Directories as an accordion, each expanding to its own conversations. @@ -36,6 +37,7 @@ type DirectoryNodeProps = { }; const DirectoryNode = ({ pwd, home, expanded, onToggle, live, activeId, actions }: DirectoryNodeProps) => { + const { t } = useTranslation(); const navigate = useNavigate(); const [, setSelected] = useSelectedChatSession(); const activeRef = useScrollActiveIntoView(); @@ -104,8 +106,8 @@ const DirectoryNode = ({ pwd, home, expanded, onToggle, live, activeId, actions