refactor(i18n): read render-path translations from useTranslate in smaller component areas (activity through maintenance) (#39424)

Applies pnpm i18n:reactive-translate --helpers. Output is unchanged: t delegates
to translate() and helpers default the threaded parameter.

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-11-07 21:20:59 -08:00
committed by GitHub
co-authored by OrcaWin
parent 261bc0f2be
commit efeee83b2c
130 changed files with 1475 additions and 1634 deletions
+1 -101
View File
@@ -50,110 +50,17 @@
11 src/renderer/src/components/UpdateErrorCardContent.tsx
3 src/renderer/src/components/WorktreeBaseFallbackDialog.tsx
3 src/renderer/src/components/ZoomOverlay.tsx
4 src/renderer/src/components/activity/ActivityTitlebarControls.tsx
5 src/renderer/src/components/activity/activity-scope-filter-controls.tsx
11 src/renderer/src/components/activity/activity-thread-context-menu.tsx
6 src/renderer/src/components/activity/activity-thread-detail-pane.tsx
3 src/renderer/src/components/activity/activity-thread-hover-card-summary.tsx
2 src/renderer/src/components/activity/activity-thread-hover-card.tsx
1 src/renderer/src/components/activity/activity-thread-list-pane.tsx
2 src/renderer/src/components/activity/activity-thread-list-resize-handle.tsx
12 src/renderer/src/components/activity/activity-thread-list-toolbar.tsx
13 src/renderer/src/components/activity/activity-thread-options-menu.tsx
6 src/renderer/src/components/activity/activity-thread-row.tsx
1 src/renderer/src/components/activity/activity-thread-virtual-row.tsx
21 src/renderer/src/components/agent-session-continuation/AgentSessionContinuationDialog.tsx
1 src/renderer/src/components/agent-session-continuation/AgentSessionContinuationMenuItem.tsx
2 src/renderer/src/components/agent/AgentLaunchPromptComposer.tsx
9 src/renderer/src/components/agent/AgentLaunchSetupPanel.tsx
6 src/renderer/src/components/artifacts/ArtifactActions.tsx
2 src/renderer/src/components/artifacts/ArtifactCollection.tsx
1 src/renderer/src/components/artifacts/ArtifactDetailDrawer.tsx
3 src/renderer/src/components/artifacts/ArtifactDetailHeader.tsx
1 src/renderer/src/components/artifacts/ArtifactListRow.tsx
3 src/renderer/src/components/artifacts/ArtifactListSearchField.tsx
6 src/renderer/src/components/artifacts/ArtifactListTableHeader.tsx
2 src/renderer/src/components/artifacts/ArtifactListToolbar.tsx
2 src/renderer/src/components/artifacts/ArtifactPreview.tsx
16 src/renderer/src/components/artifacts/ArtifactPublishButton.tsx
6 src/renderer/src/components/artifacts/ArtifactPublishedLinkPanel.tsx
5 src/renderer/src/components/artifacts/ArtifactsPage.tsx
1 src/renderer/src/components/artifacts/ArtifactsPageSkeleton.tsx
17 src/renderer/src/components/artifacts/ArtifactsPageStates.tsx
5 src/renderer/src/components/automations/use-external-automation-actions.ts
10 src/renderer/src/components/browser-extension-install-dialog.tsx
4 src/renderer/src/components/browser-webauthn-account-dialog.tsx
8 src/renderer/src/components/cmd-j/PaletteCreateWorktreeRow.tsx
3 src/renderer/src/components/cmd-j/PaletteFilterChips.tsx
9 src/renderer/src/components/cmd-j/PaletteFilterFieldOptions.tsx
6 src/renderer/src/components/cmd-j/PaletteFilterMenu.tsx
1 src/renderer/src/components/cmd-j/palette-live-status.tsx
3 src/renderer/src/components/confirmation-dialog.tsx
2 src/renderer/src/components/contextual-tours/ContextualTourControl.tsx
7 src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx
2 src/renderer/src/components/contextual-tours/ContextualTourProgressDots.tsx
13 src/renderer/src/components/crash-report/CrashReportDialogSurface.tsx
3 src/renderer/src/components/dashboard-popout/AgentDashboardFilterChips.tsx
10 src/renderer/src/components/dashboard-popout/AgentDashboardToolbar.tsx
4 src/renderer/src/components/dashboard-popout/AgentKanbanBoard.tsx
5 src/renderer/src/components/dashboard-popout/AgentKanbanCard.tsx
2 src/renderer/src/components/dashboard-popout/AgentTerminalDialog.tsx
1 src/renderer/src/components/dashboard/AgentDashboardDrawer.tsx
10 src/renderer/src/components/dashboard/AgentDashboardExperimentalSetting.tsx
10 src/renderer/src/components/dashboard/AgentDashboardSettingsMenu.tsx
2 src/renderer/src/components/dashboard/DashboardAgentChildDisclosure.tsx
4 src/renderer/src/components/dashboard/DashboardAgentRow.tsx
3 src/renderer/src/components/dashboard/DashboardAgentRowMessage.tsx
9 src/renderer/src/components/dashboard/DashboardAgentRowTrailingControls.tsx
1 src/renderer/src/components/dictation/DictationController.tsx
7 src/renderer/src/components/dictation/DictationIndicator.tsx
12 src/renderer/src/components/diff-comments/DiffCommentCard.tsx
9 src/renderer/src/components/diff-comments/DiffCommentDraftCard.tsx
7 src/renderer/src/components/diff-comments/DiffCommentPopover.tsx
3 src/renderer/src/components/diff-comments/DiffCommentPopover.tsx
1 src/renderer/src/components/diff-comments/useDiffCommentDecorator.tsx
2 src/renderer/src/components/editor/markdown-preview/render-tree/use-markdown-preview-components.tsx
3 src/renderer/src/components/editor/review-note-delivery/ReviewNotesSendMenuContent.tsx
1 src/renderer/src/components/emulator-pane/EmulatorPane.tsx
9 src/renderer/src/components/emulator-pane/MobileEmulatorAgentSetupGuide.tsx
3 src/renderer/src/components/emulator-pane/MobileEmulatorAgentSetupGuideSteps.tsx
5 src/renderer/src/components/emulator-pane/MobileEmulatorTabIntroCallout.tsx
10 src/renderer/src/components/emulator-pane/emulator-device-picker.tsx
9 src/renderer/src/components/emulator-pane/emulator-pane-toolbar.tsx
5 src/renderer/src/components/emulator-pane/emulator-screen-stream-content.tsx
2 src/renderer/src/components/file-actions/OpenInContextSubmenu.tsx
5 src/renderer/src/components/floating-terminal/FloatingTerminalEmptyState.tsx
3 src/renderer/src/components/floating-terminal/FloatingTerminalIconContextMenu.tsx
8 src/renderer/src/components/floating-terminal/FloatingTerminalOrchestrationDialog.tsx
4 src/renderer/src/components/floating-terminal/FloatingTerminalToggleButton.tsx
8 src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.tsx
13 src/renderer/src/components/github/CommentCodeContext.tsx
3 src/renderer/src/components/github/CommentReactions.tsx
1 src/renderer/src/components/github/GitHubMarkdownComposer.tsx
4 src/renderer/src/components/github/IssueSourceSelector.tsx
3 src/renderer/src/components/github/PRAssigneesPanel.tsx
9 src/renderer/src/components/github/PRFilterDropdowns.tsx
4 src/renderer/src/components/github/PRFilterPickers.tsx
26 src/renderer/src/components/github/PRFilterSections.tsx
5 src/renderer/src/components/github/PRViewedCheckbox.tsx
1 src/renderer/src/components/github/github-markdown-composer-preview-pane.tsx
2 src/renderer/src/components/github/github-markdown-composer-tabbar.tsx
3 src/renderer/src/components/github/github-markdown-image-input.tsx
7 src/renderer/src/components/github/github-rate-limit-display.tsx
1 src/renderer/src/components/github/use-github-image-paste.ts
2 src/renderer/src/components/github/use-github-image-uploads.ts
10 src/renderer/src/components/gitlab-item-dialog-parts.tsx
1 src/renderer/src/components/gitlab-item-dialog/gitlab-conversation-tab.tsx
18 src/renderer/src/components/gitlab-item-dialog/gitlab-description-tab.tsx
7 src/renderer/src/components/gitlab-item-dialog/gitlab-files-tab.tsx
16 src/renderer/src/components/gitlab-item-dialog/gitlab-item-dialog-view.tsx
2 src/renderer/src/components/gitlab-item-dialog/gitlab-pipeline-tab.tsx
9 src/renderer/src/components/gitlab/gitlab-rate-limit-display.tsx
23 src/renderer/src/components/integrations/source-control/bitbucket-credentials-dialog.tsx
21 src/renderer/src/components/integrations/source-control/bitbucket-integration-card.tsx
25 src/renderer/src/components/integrations/source-control/cli-source-control-integration-cards.tsx
28 src/renderer/src/components/integrations/source-control/token-source-control-integration-cards.tsx
20 src/renderer/src/components/integrations/task-trackers/jira-integration-card.tsx
16 src/renderer/src/components/integrations/task-trackers/linear-integration-card.tsx
1 src/renderer/src/components/jira-attachment-image.tsx
5 src/renderer/src/components/jira-connect-credential-guide.tsx
30 src/renderer/src/components/jira-connect-dialog.tsx
@@ -182,15 +89,8 @@
9 src/renderer/src/components/linear-project-view-surfaces.tsx
8 src/renderer/src/components/linear-scope-selector.tsx
5 src/renderer/src/components/linear-workspace-picker.tsx
8 src/renderer/src/components/link-actions/LinkActionPopover.tsx
14 src/renderer/src/components/link-routing-preference-dialog.tsx
1 src/renderer/src/components/local-only-menu-hint.tsx
4 src/renderer/src/components/machine-identity/MachineNameField.tsx
17 src/renderer/src/components/macos-update-requirement/MacosUpdateRequirementDialog.tsx
12 src/renderer/src/components/maintenance/update-card/UpdateAvailableCardContent.tsx
6 src/renderer/src/components/maintenance/update-card/UpdateCardStateContent.tsx
1 src/renderer/src/components/maintenance/update-card/UpdateCheckFeedback.tsx
11 src/renderer/src/components/maintenance/update-card/UpdateDownloadCardContent.tsx
2 src/renderer/src/components/managed-runtime/ManagedRuntimeActionsMenu.tsx
3 src/renderer/src/components/managed-runtime/ManagedRuntimeInstallRow.tsx
5 src/renderer/src/components/managed-runtime/ManagedRuntimeRemoveDialog.tsx
@@ -5,9 +5,10 @@ import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useActivityUnreadCount } from './useActivityUnreadCount'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
export function ActivityTitlebarControls(): React.JSX.Element {
const t = useTranslate()
const unreadCount = useActivityUnreadCount()
const closeActivityPage = useAppStore((s) => s.closeActivityPage)
@@ -25,7 +26,7 @@ export function ActivityTitlebarControls(): React.JSX.Element {
variant="ghost"
size="icon-xs"
onClick={closeActivityPage}
aria-label={translate(
aria-label={t(
'auto.components.activity.ActivityTitlebarControls.dc708f3eff',
'Close agents'
)}
@@ -34,22 +35,18 @@ export function ActivityTitlebarControls(): React.JSX.Element {
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate(
'auto.components.activity.ActivityTitlebarControls.dc708f3eff',
'Close agents'
)}
{t('auto.components.activity.ActivityTitlebarControls.dc708f3eff', 'Close agents')}
</TooltipContent>
</Tooltip>
<Bell className="size-3.5 shrink-0 text-muted-foreground" />
<span className="truncate text-xs font-medium">
{translate('auto.components.activity.ActivityTitlebarControls.d6a8de3934', 'agents')}
{t('auto.components.activity.ActivityTitlebarControls.d6a8de3934', 'agents')}
</span>
<Badge variant="secondary" className="h-5 px-1.5 text-[11px] font-normal">
{translate(
'auto.components.activity.ActivityTitlebarControls.unreadCount',
'{{count}} unread',
{ count: unreadCount, defaultValue_one: '{{count}} unread' }
)}
{t('auto.components.activity.ActivityTitlebarControls.unreadCount', '{{count}} unread', {
count: unreadCount,
defaultValue_one: '{{count}} unread'
})}
</Badge>
</div>
</div>
@@ -21,6 +21,7 @@ import { tabFromWorktreeAttributedStatusEntry } from '../sidebar/workspace-card/
import { agentEntrySessionStartedAt as effectiveWorktreeAgentRowStartedAt } from '../../../../shared/agent-session/lifecycle/agent-session-start-time'
import type { BuildActivityEventsArgs } from './activity-event-builder'
import { standaloneActivityWorktree } from './activity-standalone-worktree'
import type { Translate } from '@/i18n/use-translate'
export type ActivityTabContext = { worktreeId: string; tab: TerminalTab }
export type ActivityEventOwner = { worktree: Worktree; repo: Repo | null; knownWorktree: boolean }
@@ -141,7 +142,8 @@ export function resolveActivityEventOwner(
entry: AgentStatusEntry,
terminalPtyId: string | null | undefined,
tabHostIndex: ActivityTabHostIndex,
ownerCache: Map<string, ActivityEventOwner>
ownerCache: Map<string, ActivityEventOwner>,
t?: Translate
): ActivityEventOwner {
const executionHostId = resolveActivityExecutionHostId(
context,
@@ -160,7 +162,7 @@ export function resolveActivityEventOwner(
const worktree =
resolvedWorktree ??
mappedWorktree ??
standaloneActivityWorktree(context.worktreeId, executionHostId)
standaloneActivityWorktree(context.worktreeId, executionHostId, t)
let repo =
executionHostId && args.repos
? findIndexedRepoOwnerForHost(args.repos, worktree.repoId, executionHostId)
@@ -29,6 +29,7 @@ import {
resolveActivityEventOwner,
type ActivityEventOwner
} from './activity-event-builder-context'
import type { Translate } from '@/i18n/use-translate'
export { createActivityEventBuildCache, type ActivityEventBuildCache, newestActivityHistoryEntries }
@@ -52,7 +53,8 @@ export type BuildActivityEventsArgs = {
export function buildActivityEvents(
args: BuildActivityEventsArgs,
cache?: ActivityEventBuildCache
cache?: ActivityEventBuildCache,
t?: Translate
): {
events: ActivityEvent[]
liveAgentByPaneKey: Record<string, ActivityLiveAgentSnapshot>
@@ -103,7 +105,8 @@ export function buildActivityEvents(
entry,
context.tab.ptyId,
tabHostIndex,
ownerCache
ownerCache,
t
)
const orchestration = args.runtimeAgentOrchestrationByPaneKey?.[paneKey]
// Only fresh raw live state proves working; history and orchestration enrichment cannot establish liveness.
@@ -1,7 +1,7 @@
import React from 'react'
import { useAppStore } from '@/store'
import { DropdownMenuCheckboxItem, DropdownMenuItem } from '@/components/ui/dropdown-menu'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import SidebarRepositoryFilterSection from '@/components/sidebar/worktree-list/controls/SidebarRepositoryFilterSection'
import { SidebarHostScopeMenuSection } from '@/components/sidebar/worktree-list/controls/SidebarHostScopeMenuSection'
import {
@@ -12,6 +12,7 @@ import { useSidebarHostScopeOptions } from '@/components/sidebar/worktree-list/c
/** Persisted Agents scope is separate from workspace navigation filters; the parent owns the Filters label/separator. */
export function ActivityScopeFilterMenuItems(): React.JSX.Element {
const t = useTranslate()
const agentsVisibleHostIds = useAppStore((s) => s.agentsVisibleHostIds)
const setAgentsVisibleHostIds = useAppStore((s) => s.setAgentsVisibleHostIds)
const agentsFilterRepoIds = useAppStore((s) => s.agentsFilterRepoIds)
@@ -42,7 +43,7 @@ export function ActivityScopeFilterMenuItems(): React.JSX.Element {
onCheckedChange={(checked) => setHideAutomationGenerated(checked === true)}
onSelect={(event) => event.preventDefault()}
>
{translate(
{t(
'auto.components.activity.ActivityScopeFilterControls.hideAutomationCreated',
'Hide automation-created'
)}
@@ -52,7 +53,7 @@ export function ActivityScopeFilterMenuItems(): React.JSX.Element {
onCheckedChange={(checked) => setHideCliCreated(checked === true)}
onSelect={(event) => event.preventDefault()}
>
{translate(
{t(
'auto.components.activity.ActivityScopeFilterControls.hideCliCreated',
'Hide CLI-created'
)}
@@ -62,12 +63,12 @@ export function ActivityScopeFilterMenuItems(): React.JSX.Element {
checked={hideFromOtherDevices}
onCheckedChange={(checked) => setHideFromOtherDevices(checked === true)}
onSelect={(event) => event.preventDefault()}
aria-label={translate(
aria-label={t(
'auto.components.activity.ActivityScopeFilterControls.hideOtherClientsAria',
'Hide agents in workspaces created from other Orca clients on shared remote servers'
)}
>
{translate(
{t(
'auto.components.activity.ActivityScopeFilterControls.hideOtherClients',
'Hide other-client agents'
)}
@@ -75,7 +76,7 @@ export function ActivityScopeFilterMenuItems(): React.JSX.Element {
) : null}
{showHostScopeControls ? (
<SidebarHostScopeMenuSection
hostVisibilityLabel={getSidebarHostVisibilityLabel(agentsVisibleHostIds, hostOptions)}
hostVisibilityLabel={getSidebarHostVisibilityLabel(agentsVisibleHostIds, hostOptions, t)}
hostOptions={hostOptions}
preserveWorkspaceBoardOpen={false}
// Why: the section only calls this to reset to "all hosts".
@@ -95,7 +96,7 @@ export function ActivityScopeFilterMenuItems(): React.JSX.Element {
setAgentsFilterRepoIds([])
}}
>
{translate(
{t(
'auto.components.activity.ActivityScopeFilterControls.resetScope',
'Show all hosts and projects'
)}
@@ -1,4 +1,5 @@
import { i18n, translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { getTranslationResourceVersion } from '@/i18n/translation-resource-version'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants'
import type { Worktree } from '../../../../shared/worktree/types'
@@ -12,18 +13,13 @@ let cachedDisplayNameScope: string | undefined
function buildStandaloneActivityWorktree(
worktreeId: string,
executionHostId?: ExecutionHostId
executionHostId?: ExecutionHostId,
t: Translate = translate
): Worktree {
const displayName =
worktreeId === FLOATING_TERMINAL_WORKTREE_ID
? translate(
'auto.components.activity.standaloneWorktree.floatingTerminal',
'Floating terminal'
)
: translate(
'auto.components.activity.standaloneWorktree.standaloneTerminal',
'Standalone terminal'
)
? t('auto.components.activity.standaloneWorktree.floatingTerminal', 'Floating terminal')
: t('auto.components.activity.standaloneWorktree.standaloneTerminal', 'Standalone terminal')
return {
id: worktreeId,
...(executionHostId ? { hostId: executionHostId } : {}),
@@ -49,7 +45,8 @@ function buildStandaloneActivityWorktree(
/** Return a stable synthetic worktree for terminal-only activity. */
export function standaloneActivityWorktree(
worktreeId: string,
executionHostId?: ExecutionHostId
executionHostId?: ExecutionHostId,
t?: Translate
): Worktree {
const displayNameScope = `${i18n.language}\0${getTranslationResourceVersion()}`
if (cachedDisplayNameScope !== displayNameScope) {
@@ -62,7 +59,7 @@ export function standaloneActivityWorktree(
if (standaloneActivityWorktrees.size >= STANDALONE_ACTIVITY_WORKTREES_CAP) {
standaloneActivityWorktrees.clear()
}
worktree = buildStandaloneActivityWorktree(worktreeId, executionHostId)
worktree = buildStandaloneActivityWorktree(worktreeId, executionHostId, t)
standaloneActivityWorktrees.set(cacheKey, worktree)
}
return worktree
@@ -9,6 +9,7 @@ import {
ContextMenuTrigger
} from '@/components/ui/context-menu'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { CLOSE_ALL_CONTEXT_MENUS_EVENT } from '@/lib/close-all-context-menus'
import { formatClipboardFilePath } from '@/lib/clipboard-file-path'
import { getWorktreeRowLocalPathOpenOwner } from '@/lib/worktree-row-open-in-owner'
@@ -25,12 +26,13 @@ type CopyTarget = { key: string; label: string; value: string }
export function getActivityThreadCopyTargets(
thread: AgentPaneThread,
hasWorkspace: boolean
hasWorkspace: boolean,
t: Translate = translate
): CopyTarget[] {
const title: CopyTarget = {
key: 'title',
label: translate('auto.components.activity.ActivityThreadContextMenu.copyTitle', 'Copy Title'),
value: activityThreadRowCopy(thread).taskTitle
label: t('auto.components.activity.ActivityThreadContextMenu.copyTitle', 'Copy Title'),
value: activityThreadRowCopy(thread, t).taskTitle
}
// Why gated: synthetic floating/standalone worktrees have no path.
if (!hasWorkspace || !thread.worktree.path) {
@@ -38,7 +40,7 @@ export function getActivityThreadCopyTargets(
}
const path: CopyTarget = {
key: 'path',
label: translate('auto.components.activity.ActivityThreadContextMenu.copyPath', 'Copy Path'),
label: t('auto.components.activity.ActivityThreadContextMenu.copyPath', 'Copy Path'),
value: thread.worktree.path
}
// Same order as the workspace menu: Copy Path, then the name.
@@ -85,6 +87,7 @@ export function ActivityThreadContextMenu({
/** Receives whether the menu is open, so the row can keep its preview out of the way. */
children: (menuOpen: boolean) => React.ReactElement
}): React.JSX.Element {
const t = useTranslate()
const [menuOpen, setMenuOpen] = React.useState(false)
// Why a snapshot: the pointerdown on a portaled item clears the list selection before onSelect.
const [targets, setTargets] = React.useState<readonly AgentPaneThread[]>([thread])
@@ -112,7 +115,7 @@ export function ActivityThreadContextMenu({
{/* Why no focus restore: refocusing the row would reopen its hover preview and pin it open. */}
<ContextMenuContent className="w-52" onCloseAutoFocus={(event) => event.preventDefault()}>
<ContextMenuLabel>
{translate('auto.components.activity.ActivityThreadContextMenu.agentSection', 'Agent')}
{t('auto.components.activity.ActivityThreadContextMenu.agentSection', 'Agent')}
</ContextMenuLabel>
{targets.length > 1 ? (
<ActivityThreadBulkMenuItems
@@ -154,23 +157,21 @@ function ActivityThreadSingleMenuItems({
onMarkRead: (thread: AgentPaneThread) => void
onMarkUnread: (thread: AgentPaneThread) => void
}): React.JSX.Element {
const t = useTranslate()
return (
<>
<ContextMenuItem onSelect={() => onOpen(thread)}>
<PanelRight className="size-3.5" />
{translate('auto.components.activity.ActivityThreadContextMenu.open', 'Open')}
{t('auto.components.activity.ActivityThreadContextMenu.open', 'Open')}
</ContextMenuItem>
{canJump ? (
<ContextMenuItem onSelect={() => onJump(thread)}>
<ExternalLink className="size-3.5" />
{translate(
'auto.components.activity.ActivityThreadContextMenu.goToWorkspace',
'Go to Workspace'
)}
{t('auto.components.activity.ActivityThreadContextMenu.goToWorkspace', 'Go to Workspace')}
</ContextMenuItem>
) : null}
<ContextMenuSeparator />
{getActivityThreadCopyTargets(thread, canJump).map((target) => (
{getActivityThreadCopyTargets(thread, canJump, t).map((target) => (
<ContextMenuItem
key={target.key}
onSelect={() => void window.api.ui.writeClipboardText(formatCopyTarget(thread, target))}
@@ -186,21 +187,15 @@ function ActivityThreadSingleMenuItems({
>
{thread.unread ? <BellOff className="size-3.5" /> : <Bell className="size-3.5" />}
{thread.unread
? translate('auto.components.activity.ActivityThreadContextMenu.markRead', 'Mark Read')
: translate(
'auto.components.activity.ActivityThreadContextMenu.markUnread',
'Mark Unread'
)}
? t('auto.components.activity.ActivityThreadContextMenu.markRead', 'Mark Read')
: t('auto.components.activity.ActivityThreadContextMenu.markUnread', 'Mark Unread')}
</ContextMenuItem>
{isClearableActivityThread(thread) ? (
<>
<ContextMenuSeparator />
<ContextMenuItem onSelect={() => clearActivityThread(thread)}>
<X className="size-3.5" />
{translate(
'auto.components.activity.ActivityThreadContextMenu.clear',
'Clear from List'
)}
{t('auto.components.activity.ActivityThreadContextMenu.clear', 'Clear from List')}
</ContextMenuItem>
</>
) : null}
@@ -220,6 +215,7 @@ function ActivityThreadBulkMenuItems({
onMarkManyRead: (threads: readonly AgentPaneThread[]) => void
onMarkManyUnread: (threads: readonly AgentPaneThread[]) => void
}): React.JSX.Element {
const t = useTranslate()
const unread = targets.filter((target) => target.unread)
const read = targets.filter((target) => !target.unread && canMarkUnread(target))
const clearable = targets.filter(isClearableActivityThread)
@@ -229,7 +225,7 @@ function ActivityThreadBulkMenuItems({
{unread.length > 0 ? (
<ContextMenuItem onSelect={() => onMarkManyRead(unread)}>
<BellOff className="size-3.5" />
{translate(
{t(
'auto.components.activity.ActivityThreadContextMenu.markManyRead',
'Mark {{count}} Agents Read',
{ count: unread.length, defaultValue_one: 'Mark {{count}} Agent Read' }
@@ -239,15 +235,12 @@ function ActivityThreadBulkMenuItems({
<ContextMenuItem disabled={read.length === 0} onSelect={() => onMarkManyUnread(read)}>
<Bell className="size-3.5" />
{read.length > 0
? translate(
? t(
'auto.components.activity.ActivityThreadContextMenu.markManyUnread',
'Mark {{count}} Agents Unread',
{ count: read.length, defaultValue_one: 'Mark {{count}} Agent Unread' }
)
: translate(
'auto.components.activity.ActivityThreadContextMenu.markUnread',
'Mark Unread'
)}
: t('auto.components.activity.ActivityThreadContextMenu.markUnread', 'Mark Unread')}
</ContextMenuItem>
)}
<ContextMenuSeparator />
@@ -257,15 +250,12 @@ function ActivityThreadBulkMenuItems({
>
<X className="size-3.5" />
{clearable.length > 0
? translate(
? t(
'auto.components.activity.ActivityThreadContextMenu.clearMany',
'Clear {{count}} Agents from List',
{ count: clearable.length, defaultValue_one: 'Clear {{count}} Agent from List' }
)
: translate(
'auto.components.activity.ActivityThreadContextMenu.clear',
'Clear from List'
)}
: t('auto.components.activity.ActivityThreadContextMenu.clear', 'Clear from List')}
</ContextMenuItem>
</>
)
@@ -14,6 +14,7 @@ import {
threadAgentStateLabel
} from './activity-thread-presentation'
import type { ActivityThreadGroup, AgentPaneThread } from './activity-thread-types'
import { useIntlLocale, useTranslate } from '@/i18n/use-translate'
export { ActivityThreadOptionsMenu } from './activity-thread-options-menu'
@@ -26,9 +27,10 @@ export function EventTime({
compact?: boolean
className?: string
}): React.JSX.Element {
const intlLocale = useIntlLocale()
// Memoized rows ignore unrelated writes; refresh this leaf from the visibility-gated 30s clock, matching WorktreeCardAgents.
const now = useNow(30_000)
const absolute = formatAbsoluteDate(timestamp)
const absolute = formatAbsoluteDate(timestamp, intlLocale)
return (
<Tooltip>
<TooltipTrigger asChild>
@@ -42,7 +44,9 @@ export function EventTime({
aria-label={absolute}
onClick={(event) => event.stopPropagation()}
>
{compact ? formatShortTimeAgo(timestamp, now) : formatRelativeTime(timestamp, now)}
{compact
? formatShortTimeAgo(timestamp, now)
: formatRelativeTime(timestamp, now, intlLocale)}
</button>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={6}>
@@ -71,8 +75,9 @@ export function ThreadAgentStateIndicator({
}: {
thread: AgentPaneThread
}): React.JSX.Element {
const t = useTranslate()
const state = activityThreadStatusId(thread)
const label = threadAgentStateLabel(thread)
const label = threadAgentStateLabel(thread, t)
return (
<Tooltip>
<TooltipTrigger asChild>
@@ -2,7 +2,7 @@ import React from 'react'
import { MessageSquareText, TerminalSquare } from 'lucide-react'
import { AgentTypeIcon } from '@/lib/agent-type-icon'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { EventRepoBadge, ThreadAgentStateIndicator } from './activity-thread-controls'
import type { ActivityTerminalPortalSlotId, AgentPaneThread } from './activity-thread-types'
@@ -33,6 +33,7 @@ export function ActivityThreadDetailPane({
showTerminalLoadingLabel: boolean
visibleThreadCount: number
}): React.JSX.Element {
const t = useTranslate()
return (
<section className="min-w-0 flex-1 overflow-hidden">
{selectedThread ? (
@@ -67,11 +68,11 @@ export function ActivityThreadDetailPane({
<div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-2 p-4 text-sm text-muted-foreground">
<TerminalSquare className="size-7" />
{selectedWorktreeAvailable
? translate(
? t(
'auto.components.activity.ActivityPrototypePage.afdc2139a8',
'Agent terminal closed. Open a new terminal in this workspace to continue.'
)
: translate(
: t(
'auto.components.activity.ActivityPrototypePage.22b22034bc',
'Standalone terminal unavailable in Activity.'
)}
@@ -111,7 +112,7 @@ export function ActivityThreadDetailPane({
<div className="ml-3 mt-3 inline-flex items-center gap-2 rounded-md border border-border bg-background/85 px-2 py-1 text-xs text-muted-foreground shadow-xs">
<span className="h-3 w-1.5 rounded-sm bg-muted-foreground/70" />
<span>
{translate(
{t(
'auto.components.activity.ActivityPrototypePage.8de7c5beaa',
'Terminal unavailable'
)}
@@ -121,7 +122,7 @@ export function ActivityThreadDetailPane({
<div className="ml-3 mt-3 inline-flex items-center gap-2 rounded-md border border-border bg-background/85 px-2 py-1 text-xs text-muted-foreground shadow-xs">
<span className="h-3 w-1.5 animate-pulse rounded-sm bg-muted-foreground/70" />
<span>
{translate(
{t(
'auto.components.activity.ActivityPrototypePage.1b633f5c1e',
'Connecting terminal...'
)}
@@ -139,15 +140,12 @@ export function ActivityThreadDetailPane({
{visibleThreadCount === 0 ? (
<>
<MessageSquareText className="size-7" />
{translate(
'auto.components.activity.ActivityPrototypePage.e3db9892f6',
'No activity yet.'
)}
{t('auto.components.activity.ActivityPrototypePage.e3db9892f6', 'No activity yet.')}
</>
) : (
<>
<TerminalSquare className="size-7" />
{translate(
{t(
'auto.components.activity.ActivityPrototypePage.cf780197a1',
'Select an agent to view its activity'
)}
@@ -1,5 +1,6 @@
import type { AgentDotState } from '@/components/AgentStateDot'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { formatAgentTypeLabel } from '../../../../shared/agent-status/presentation/agent-type-label'
import { getAgentRowPrimaryText } from '../../../../shared/agent-status/presentation/agent-row-primary-text'
import { getActivityThreadWorkspaceTitle } from '@/lib/activity-thread-display'
@@ -37,7 +38,8 @@ function activityStatusRank(thread: AgentPaneThread): number {
export function getActivityThreadGroup(
thread: AgentPaneThread,
groupBy: ActivityGroupBy
groupBy: ActivityGroupBy,
t: Translate = translate
): { key: string; label: string; state?: AgentDotState } {
if (groupBy === 'none') {
return { key: 'all', label: '' }
@@ -45,17 +47,14 @@ export function getActivityThreadGroup(
if (groupBy === 'status') {
// The key is the glyph every row in the group draws, so it heads the group too.
const status = activityThreadStatusId(thread)
return { key: status, label: threadAgentStateLabel(thread), state: status }
return { key: status, label: threadAgentStateLabel(thread, t), state: status }
}
if (groupBy === 'project') {
return thread.repo
? { key: `project:${thread.repo.id}`, label: thread.repo.displayName }
: {
key: 'project:unknown',
label: translate(
'auto.components.activity.ActivityPrototypePage.5651b216c6',
'Unknown project'
)
label: t('auto.components.activity.ActivityPrototypePage.5651b216c6', 'Unknown project')
}
}
if (groupBy === 'worktree') {
@@ -66,7 +65,8 @@ export function getActivityThreadGroup(
export function buildActivityThreadGroups(
threads: AgentPaneThread[],
groupBy: ActivityGroupBy
groupBy: ActivityGroupBy,
t?: Translate
): ActivityThreadGroup[] {
if (groupBy === 'none') {
return threads.length > 0 ? [{ key: 'all', label: '', threads }] : []
@@ -74,7 +74,7 @@ export function buildActivityThreadGroups(
const groups: ActivityThreadGroup[] = []
const groupIndexByKey = new Map<string, number>()
for (const thread of threads) {
const group = getActivityThreadGroup(thread, groupBy)
const group = getActivityThreadGroup(thread, groupBy, t)
const existingIndex = groupIndexByKey.get(group.key)
if (existingIndex === undefined) {
groups.push({ ...group, threads: [thread] })
@@ -8,6 +8,7 @@ import { formatAgentTypeLabel } from '../../../../shared/agent-status/presentati
import { getWorktreeGitIdentityDisplay } from '../../../../shared/worktree/git-identity-display'
import { jumpToWorktreeFromSidebar } from '@/lib/worktree-jump-navigation'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { formatClipboardFilePath } from '@/lib/clipboard-file-path'
import { getWorktreeRowLocalPathOpenOwner } from '@/lib/worktree-row-open-in-owner'
import { cn } from '@/lib/utils'
@@ -43,6 +44,7 @@ export function ActivityThreadHoverCardSummary({
onJumpToWorkspace?: (event: React.MouseEvent) => void
canJumpToWorkspace?: boolean
}): React.JSX.Element {
const t = useTranslate()
const { worktree, repo } = thread
const sshTargetLabels = useAppStore((s) => s.sshTargetLabels)
const runtimeEnvironments = useAppStore((s) => s.runtimeEnvironments)
@@ -69,12 +71,9 @@ export function ActivityThreadHoverCardSummary({
return getExecutionHostLabel(executionHostId)
}, [executionHostId, hostLabelOverrides, parsedHost, runtimeEnvironments, sshTargetLabels])
const branchIdentityDisplay = useMemo(() => getWorktreeGitIdentityDisplay(worktree), [worktree])
const { taskTitle, needsAttention } = activityThreadRowCopy(thread)
const { taskTitle, needsAttention } = activityThreadRowCopy(thread, t)
const workspaceTitle = getActivityThreadWorkspaceTitle(worktree)
const copyPathLabel = translate(
'auto.components.activity.ActivityThreadHoverCard.copyPath',
'Copy path'
)
const copyPathLabel = t('auto.components.activity.ActivityThreadHoverCard.copyPath', 'Copy path')
const isKnownWorktree = useAppStore((s) =>
Boolean(s.getKnownWorktreeById(worktree.id, executionHostId))
@@ -144,7 +143,7 @@ export function ActivityThreadHoverCardSummary({
needsAttention ? 'text-agent-question-text' : 'text-muted-foreground'
)}
>
{threadAgentStateLabel(thread)}
{threadAgentStateLabel(thread, t)}
</span>
<span className="text-muted-foreground/40">•</span>
<EventTime timestamp={thread.latestTimestamp} compact />
@@ -168,14 +167,11 @@ export function ActivityThreadHoverCardSummary({
<WorktreeCardDetailSection>
<DetailHeader
icon={<FolderGit2 className="size-3 text-muted-foreground" />}
label={translate(
'auto.components.activity.ActivityThreadHoverCard.workspace',
'Workspace'
)}
label={t('auto.components.activity.ActivityThreadHoverCard.workspace', 'Workspace')}
actions={
canJump ? (
<MetadataActionIcon
label={translate(
label={t(
'auto.components.activity.ActivityThreadHoverCard.jumpToWorkspace',
'Jump to workspace'
)}
@@ -2,7 +2,7 @@ import React, { useCallback } from 'react'
import { Pencil, StickyNote } from 'lucide-react'
import { HoverCardContent } from '@/components/ui/hover-card'
import { SelectedTextCopyMenu } from '@/components/SelectedTextCopyMenu'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import CommentMarkdown from '../sidebar/CommentMarkdown'
import {
DetailHeader,
@@ -99,6 +99,7 @@ function ActivityThreadHoverCardContent({
onJumpToWorkspace?: (thread: AgentPaneThread) => void
canJumpToWorkspace?: boolean
}): React.JSX.Element {
const t = useTranslate()
const { worktree, repo } = thread
const foundation = useWorktreeCardFoundation({ worktree, repo: repo ?? undefined })
const review = useWorktreeCardReviewDetails({
@@ -260,13 +261,10 @@ function ActivityThreadHoverCardContent({
<WorktreeCardDetailSection>
<DetailHeader
icon={<StickyNote className="size-3 text-muted-foreground" />}
label={translate('auto.components.sidebar.WorktreeCardMeta.93cbea12c2', 'Notes')}
label={t('auto.components.sidebar.WorktreeCardMeta.93cbea12c2', 'Notes')}
actions={
<MetadataActionIcon
label={translate(
'auto.components.sidebar.WorktreeCardMeta.c7fa72ead0',
'Edit notes'
)}
label={t('auto.components.sidebar.WorktreeCardMeta.c7fa72ead0', 'Edit notes')}
onClick={foundation.handleEditComment}
>
<Pencil className="size-3" />
@@ -7,7 +7,7 @@ import {
type Range
} from '@tanstack/react-virtual'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { ActivityThreadListToolbar } from './activity-thread-list-toolbar'
import {
getActiveStickyHeaderIndex,
@@ -118,6 +118,7 @@ export function ActivityThreadListPane({
/** Optional view-local scroll memory; updated without triggering React renders. */
scrollTopRef?: React.MutableRefObject<number>
}): React.JSX.Element {
const t = useTranslate()
const [internalCollapsedGroupKeys, setInternalCollapsedGroupKeys] = useState<Set<string>>(
() => new Set()
)
@@ -386,7 +387,7 @@ export function ActivityThreadListPane({
</div>
{visibleThreadCount === 0 ? (
<div className="px-3 py-8 text-center text-xs text-muted-foreground">
{translate(
{t(
'auto.components.activity.ActivityPrototypePage.7cd632006b',
'No agent activity matches these filters.'
)}
@@ -1,6 +1,6 @@
import type React from 'react'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
export function ActivityThreadListResizeHandle({
isResizing,
@@ -9,16 +9,14 @@ export function ActivityThreadListResizeHandle({
isResizing?: boolean
onResizeStart?: React.MouseEventHandler<HTMLDivElement>
}): React.JSX.Element {
const t = useTranslate()
return (
<div
aria-label={translate(
aria-label={t(
'auto.components.activity.ActivityPrototypePage.443690186e',
'Resize activity thread list'
)}
title={translate(
'auto.components.activity.ActivityPrototypePage.866083500b',
'Drag to resize'
)}
title={t('auto.components.activity.ActivityPrototypePage.866083500b', 'Drag to resize')}
className={cn(
'group absolute -right-1.5 top-0 z-20 flex h-full w-3 cursor-col-resize items-stretch justify-center',
isResizing && 'bg-ring/10'
@@ -12,7 +12,7 @@ import {
import { Toggle } from '@/components/ui/toggle'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { ActivityThreadOptionsMenu } from './activity-thread-controls'
import type {
ActivityGroupBy,
@@ -60,6 +60,7 @@ export function ActivityThreadListToolbar({
showOptionsMenu: boolean
showInlineActions?: boolean
}): React.JSX.Element | null {
const t = useTranslate()
const showToolbar = showFilterControls || showOptionsMenu
if (!showToolbar) {
return null
@@ -76,7 +77,7 @@ export function ActivityThreadListToolbar({
ref={activityFilterInputRef}
value={query}
onChange={(event) => onQueryChange(event.target.value)}
placeholder={translate(
placeholder={t(
'auto.components.activity.ActivityPrototypePage.795cbf26e2',
'Filter...'
)}
@@ -91,7 +92,7 @@ export function ActivityThreadListToolbar({
variant="ghost"
size="icon-xs"
className="absolute right-0.5 top-1/2 -translate-y-1/2 size-5 p-0 text-muted-foreground hover:text-foreground"
aria-label={translate(
aria-label={t(
'auto.components.sidebar.WorkspaceKanbanSearchField.3b7ea51793',
'Clear search'
)}
@@ -113,7 +114,7 @@ export function ActivityThreadListToolbar({
<SelectTrigger
size="sm"
className="h-7 w-[116px] shrink-0 px-2 text-[11px]"
aria-label={translate(
aria-label={t(
'auto.components.activity.ActivityPrototypePage.groupAgentActivityBy',
'Group agent activity by'
)}
@@ -122,25 +123,19 @@ export function ActivityThreadListToolbar({
</SelectTrigger>
<SelectContent align="end">
<SelectItem value="none">
{translate('auto.components.activity.ActivityPrototypePage.none', 'None')}
{t('auto.components.activity.ActivityPrototypePage.none', 'None')}
</SelectItem>
<SelectItem value="status">
{translate('auto.components.activity.ActivityPrototypePage.4a3986b200', 'Status')}
{t('auto.components.activity.ActivityPrototypePage.4a3986b200', 'Status')}
</SelectItem>
<SelectItem value="project">
{translate(
'auto.components.activity.ActivityPrototypePage.8c3b621ddf',
'Project'
)}
{t('auto.components.activity.ActivityPrototypePage.8c3b621ddf', 'Project')}
</SelectItem>
<SelectItem value="worktree">
{translate(
'auto.components.activity.ActivityPrototypePage.b29191b3e0',
'Worktree'
)}
{t('auto.components.activity.ActivityPrototypePage.b29191b3e0', 'Worktree')}
</SelectItem>
<SelectItem value="agent">
{translate('auto.components.activity.ActivityPrototypePage.f6396e1f85', 'Agent')}
{t('auto.components.activity.ActivityPrototypePage.f6396e1f85', 'Agent')}
</SelectItem>
</SelectContent>
</Select>
@@ -158,7 +153,7 @@ export function ActivityThreadListToolbar({
? '!border border-primary/30 !bg-primary/10 !text-primary/90 shadow-xs hover:!bg-primary/15 hover:!text-primary'
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
)}
aria-label={translate(
aria-label={t(
'auto.components.activity.ActivityPrototypePage.d1a88df9a8',
'Show unread threads only'
)}
@@ -167,7 +162,7 @@ export function ActivityThreadListToolbar({
</Toggle>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate(
{t(
'auto.components.activity.ActivityPrototypePage.d1a88df9a8',
'Show unread threads only'
)}
@@ -203,10 +198,7 @@ export function ActivityThreadListToolbar({
>
<CheckCheck className="size-3.5" />
<span>
{translate(
'auto.components.activity.ActivityPrototypePage.023ff75afe',
'Mark all read'
)}
{t('auto.components.activity.ActivityPrototypePage.023ff75afe', 'Mark all read')}
</span>
</Button>
) : null}
@@ -221,7 +213,7 @@ export function ActivityThreadListToolbar({
>
<Trash2 className="size-3.5" />
<span>
{translate(
{t(
'auto.components.activity.ActivityPrototypePage.clearCompleted',
'Clear completed'
)}
@@ -17,6 +17,7 @@ import {
} from '@/components/ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { OptionsFilterCountBadge } from '@/components/sidebar/worktree-list/controls/options-filter-count-badge'
import {
ActivityScopeFilterMenuItems,
@@ -32,18 +33,18 @@ const GROUP_BY_OPTIONS = [
'agent'
] as const satisfies readonly ActivityGroupBy[]
function getActivityGroupByLabel(groupBy: ActivityGroupBy): string {
function getActivityGroupByLabel(groupBy: ActivityGroupBy, t: Translate = translate): string {
switch (groupBy) {
case 'none':
return translate('auto.components.activity.ActivityPrototypePage.none', 'None')
return t('auto.components.activity.ActivityPrototypePage.none', 'None')
case 'status':
return translate('auto.components.activity.ActivityPrototypePage.4a3986b200', 'Status')
return t('auto.components.activity.ActivityPrototypePage.4a3986b200', 'Status')
case 'project':
return translate('auto.components.activity.ActivityPrototypePage.8c3b621ddf', 'Project')
return t('auto.components.activity.ActivityPrototypePage.8c3b621ddf', 'Project')
case 'worktree':
return translate('auto.components.activity.ActivityPrototypePage.b29191b3e0', 'Worktree')
return t('auto.components.activity.ActivityPrototypePage.b29191b3e0', 'Worktree')
case 'agent':
return translate('auto.components.activity.ActivityPrototypePage.f6396e1f85', 'Agent')
return t('auto.components.activity.ActivityPrototypePage.f6396e1f85', 'Agent')
}
}
@@ -78,12 +79,13 @@ export function ActivityThreadOptionsMenu({
unreadOnly?: boolean
onUnreadOnlyChange?: (unreadOnly: boolean) => void
}): React.JSX.Element {
const t = useTranslate()
const skipCloseAutoFocusRef = React.useRef(false)
const activeFilterCount =
useActivityScopeFilterCount() + (onUnreadOnlyChange && unreadOnly ? 1 : 0)
const optionsLabel =
activeFilterCount > 0
? translate(
? t(
'auto.components.activity.ActivityPrototypePage.threadListOptionsActiveFilters',
'Thread list options ({{count}} filters active)',
{
@@ -91,10 +93,7 @@ export function ActivityThreadOptionsMenu({
defaultValue_one: 'Thread list options ({{count}} filter active)'
}
)
: translate(
'auto.components.activity.ActivityPrototypePage.db8a1878b5',
'Thread list options'
)
: t('auto.components.activity.ActivityPrototypePage.db8a1878b5', 'Thread list options')
return (
<DropdownMenu>
@@ -115,7 +114,7 @@ export function ActivityThreadOptionsMenu({
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{activeFilterCount > 0
? translate(
? t(
'auto.components.activity.ActivityPrototypePage.activityOptionsFilterCount',
'Activity options ({{count}} filters)',
{
@@ -123,7 +122,7 @@ export function ActivityThreadOptionsMenu({
defaultValue_one: 'Activity options ({{count}} filter)'
}
)
: translate(
: t(
'auto.components.activity.ActivityPrototypePage.activityOptions',
'Activity options'
)}
@@ -142,7 +141,7 @@ export function ActivityThreadOptionsMenu({
}}
>
<DropdownMenuLabel>
{translate('auto.components.activity.ActivityPrototypePage.filtersSection', 'Filters')}
{t('auto.components.activity.ActivityPrototypePage.filtersSection', 'Filters')}
</DropdownMenuLabel>
{onUnreadOnlyChange ? (
<DropdownMenuCheckboxItem
@@ -150,10 +149,7 @@ export function ActivityThreadOptionsMenu({
onCheckedChange={(checked) => onUnreadOnlyChange(checked === true)}
onSelect={(event) => event.preventDefault()}
>
{translate(
'auto.components.activity.ActivityPrototypePage.showUnreadOnly',
'Show unread only'
)}
{t('auto.components.activity.ActivityPrototypePage.showUnreadOnly', 'Show unread only')}
</DropdownMenuCheckboxItem>
) : null}
{onShowChildAgentsChange ? (
@@ -162,7 +158,7 @@ export function ActivityThreadOptionsMenu({
onCheckedChange={(checked) => onShowChildAgentsChange(checked === true)}
onSelect={(event) => event.preventDefault()}
>
{translate(
{t(
'auto.components.activity.ActivityPrototypePage.showChildAgents',
'Show child agents'
)}
@@ -171,20 +167,17 @@ export function ActivityThreadOptionsMenu({
<ActivityScopeFilterMenuItems />
<DropdownMenuSeparator />
<DropdownMenuLabel>
{translate('auto.components.activity.ActivityPrototypePage.viewSection', 'View')}
{t('auto.components.activity.ActivityPrototypePage.viewSection', 'View')}
</DropdownMenuLabel>
{groupBy && onGroupByChange ? (
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<span className="flex flex-1 items-center justify-between gap-3">
<span>
{translate(
'auto.components.activity.ActivityPrototypePage.770d458144',
'Group by'
)}
{t('auto.components.activity.ActivityPrototypePage.770d458144', 'Group by')}
</span>
<span className="min-w-0 truncate text-[11px] font-medium text-muted-foreground">
{getActivityGroupByLabel(groupBy)}
{getActivityGroupByLabel(groupBy, t)}
</span>
</span>
</DropdownMenuSubTrigger>
@@ -199,7 +192,7 @@ export function ActivityThreadOptionsMenu({
value={value}
onSelect={(event) => event.preventDefault()}
>
{getActivityGroupByLabel(value)}
{getActivityGroupByLabel(value, t)}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
@@ -211,7 +204,7 @@ export function ActivityThreadOptionsMenu({
onCheckedChange={(checked) => onCompactModeChange(checked === true)}
onSelect={(event) => event.preventDefault()}
>
{translate('auto.components.activity.ActivityPrototypePage.f70e4bec47', 'Compact mode')}
{t('auto.components.activity.ActivityPrototypePage.f70e4bec47', 'Compact mode')}
</DropdownMenuCheckboxItem>
{onShowSearchChange ? (
<DropdownMenuCheckboxItem
@@ -222,7 +215,7 @@ export function ActivityThreadOptionsMenu({
onShowSearchChange(show)
}}
>
{translate('auto.components.activity.ActivityPrototypePage.showSearch', 'Show search')}
{t('auto.components.activity.ActivityPrototypePage.showSearch', 'Show search')}
</DropdownMenuCheckboxItem>
) : null}
{onMarkAllThreadsRead || onClearCompleted ? (
@@ -232,10 +225,7 @@ export function ActivityThreadOptionsMenu({
<DropdownMenuItem onSelect={onMarkAllThreadsRead} disabled={!hasUnreadThreads}>
<CheckCheck className="size-3.5 text-muted-foreground" />
<span>
{translate(
'auto.components.activity.ActivityPrototypePage.023ff75afe',
'Mark all read'
)}
{t('auto.components.activity.ActivityPrototypePage.023ff75afe', 'Mark all read')}
</span>
</DropdownMenuItem>
) : null}
@@ -243,7 +233,7 @@ export function ActivityThreadOptionsMenu({
<DropdownMenuItem onSelect={onClearCompleted} disabled={!hasCompletedThreads}>
<Trash2 className="size-3.5 text-muted-foreground" />
<span>
{translate(
{t(
'auto.components.activity.ActivityPrototypePage.clearCompleted',
'Clear completed'
)}
@@ -13,6 +13,7 @@ import {
} from '@/lib/activity-thread-display'
import { formatUiRelativeTime } from '@/i18n/relative-time-format'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import type {
AgentStatusEntry,
AgentStatusState
@@ -36,12 +37,16 @@ const ABSOLUTE_DATE_OPTIONS: Intl.DateTimeFormatOptions = {
minute: '2-digit'
}
export function formatAbsoluteDate(timestamp: number): string {
return uiDateTimeFormat(ABSOLUTE_DATE_OPTIONS).format(new Date(timestamp))
export function formatAbsoluteDate(timestamp: number, intlLocale?: string): string {
return uiDateTimeFormat(ABSOLUTE_DATE_OPTIONS, intlLocale).format(new Date(timestamp))
}
export function formatRelativeTime(timestamp: number, now = Date.now()): string {
return formatUiRelativeTime(timestamp - now)
export function formatRelativeTime(
timestamp: number,
now = Date.now(),
intlLocale?: string
): string {
return formatUiRelativeTime(timestamp - now, intlLocale)
}
function truncatePreservingSurrogates(value: string, maxLength: number): string {
@@ -189,46 +194,40 @@ function threadCurrentState(
)
}
export function threadAgentStateLabel(thread: AgentPaneThread): string {
export function threadAgentStateLabel(thread: AgentPaneThread, t: Translate = translate): string {
// Literal translation keys/fallbacks register catalog references and avoid bundling every state string at boot.
switch (activityThreadStatusId(thread)) {
case 'working':
return translate('auto.components.activity.ActivityPrototypePage.state.working', 'Working')
return t('auto.components.activity.ActivityPrototypePage.state.working', 'Working')
case 'monitoring':
return translate(
return t(
'auto.components.activity.ActivityPrototypePage.state.monitoring',
'Monitoring background tasks'
)
case 'blocked':
return translate('auto.components.activity.ActivityPrototypePage.state.blocked', 'Blocked')
return t('auto.components.activity.ActivityPrototypePage.state.blocked', 'Blocked')
case 'waiting':
return translate(
'auto.components.activity.ActivityPrototypePage.state.waiting',
'Waiting for input'
)
return t('auto.components.activity.ActivityPrototypePage.state.waiting', 'Waiting for input')
case 'interrupted':
return translate('auto.components.activity.ActivityPrototypePage.interrupted', 'Interrupted')
return t('auto.components.activity.ActivityPrototypePage.interrupted', 'Interrupted')
case 'failed':
return translate('auto.components.activity.ActivityPrototypePage.state.failed', 'Failed')
return t('auto.components.activity.ActivityPrototypePage.state.failed', 'Failed')
case 'done':
return translate('auto.components.activity.ActivityPrototypePage.state.done', 'Done')
return t('auto.components.activity.ActivityPrototypePage.state.done', 'Done')
case 'idle':
return translate('auto.components.activity.ActivityPrototypePage.state.idle', 'Idle')
return t('auto.components.activity.ActivityPrototypePage.state.idle', 'Idle')
case 'unverifiable':
return translate(
return t(
'auto.components.activity.ActivityPrototypePage.state.unverifiable',
'No recent update'
)
case 'unconfirmed':
return translate(
return t(
'auto.components.activity.ActivityPrototypePage.state.unconfirmed',
'Couldn’t confirm'
)
case 'permission':
return translate(
'auto.components.activity.ActivityPrototypePage.state.permission',
'Needs attention'
)
return t('auto.components.activity.ActivityPrototypePage.state.permission', 'Needs attention')
}
}
@@ -254,7 +253,10 @@ function previewDuplicatesIdentity(preview: string, title: string, workspace: st
return normalized === normalizeScanLabel(title) || normalized === normalizeScanLabel(workspace)
}
export function activityThreadRowCopy(thread: AgentPaneThread): ActivityThreadRowCopy {
export function activityThreadRowCopy(
thread: AgentPaneThread,
t?: Translate
): ActivityThreadRowCopy {
const workspaceLabel = getActivityThreadWorkspaceTitle(thread.worktree)
const taskTitle = thread.paneTitle.trim() || workspaceLabel
const renderedPreview = activityThreadResponseRenderPreview({
@@ -277,7 +279,7 @@ export function activityThreadRowCopy(thread: AgentPaneThread): ActivityThreadRo
if (state !== 'done' && state !== 'idle') {
return {
taskTitle,
statusLine: threadAgentStateLabel(thread),
statusLine: threadAgentStateLabel(thread, t),
statusKind: 'state',
needsAttention,
workspaceLabel
@@ -5,7 +5,7 @@ import { formatAgentTypeLabel } from '../../../../shared/agent-status/presentati
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { useUiLanguageRevision } from '@/i18n/use-ui-language-revision'
import '../sidebar/workspace-card/activity/worktree-card-empty-inputs'
import { FilledBellIcon } from '../sidebar/workspace-card/identity/worktree-card-glyphs'
@@ -86,9 +86,10 @@ export const ActivityThreadRow = React.memo(function ActivityThreadRow({
showJumpAction?: boolean
previewSuppressed?: boolean
}): React.JSX.Element {
const t = useTranslate()
useUiLanguageRevision()
const { taskTitle, statusLine, statusKind, needsAttention, workspaceLabel } =
activityThreadRowCopy(thread)
activityThreadRowCopy(thread, t)
const showMarkdownStatus = statusKind === 'message'
const agentLabel = formatAgentTypeLabel(thread.agentType)
@@ -176,7 +177,7 @@ export const ActivityThreadRow = React.memo(function ActivityThreadRow({
<TooltipProvider delayDuration={800} skipDelayDuration={0}>
{canJump && showJumpAction ? (
<ActivityThreadRowAction
label={translate(
label={t(
'auto.components.activity.ActivityPrototypePage.4616ea39fd',
'Jump to workspace'
)}
@@ -187,7 +188,7 @@ export const ActivityThreadRow = React.memo(function ActivityThreadRow({
) : null}
{onClear ? (
<ActivityThreadRowAction
label={translate(
label={t(
'auto.components.activity.ActivityThreadRow.clearNotification',
'Clear notification'
)}
@@ -212,7 +213,7 @@ export const ActivityThreadRow = React.memo(function ActivityThreadRow({
}}
onMouseDown={(event) => event.stopPropagation()}
className="flex size-4 shrink-0 cursor-pointer items-center justify-center rounded hover:bg-accent/80 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
aria-label={translate(
aria-label={t(
'auto.components.activity.ActivityPrototypePage.markThreadRead',
'Mark thread as read'
)}
@@ -224,7 +225,7 @@ export const ActivityThreadRow = React.memo(function ActivityThreadRow({
</button>
</TooltipTrigger>
<TooltipContent side="left">
{translate(
{t(
'auto.components.activity.ActivityPrototypePage.markThreadRead',
'Mark thread as read'
)}
@@ -247,7 +248,7 @@ export const ActivityThreadRow = React.memo(function ActivityThreadRow({
'hover:bg-accent/80 active:scale-95',
'focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring'
)}
aria-label={translate(
aria-label={t(
'auto.components.activity.ActivityPrototypePage.59b131fbd9',
'Mark thread unread'
)}
@@ -256,7 +257,7 @@ export const ActivityThreadRow = React.memo(function ActivityThreadRow({
</button>
</TooltipTrigger>
<TooltipContent side="left">
{translate(
{t(
'auto.components.activity.ActivityPrototypePage.59b131fbd9',
'Mark thread unread'
)}
@@ -1,5 +1,5 @@
import type React from 'react'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { clearActivityThread, isClearableActivityThread } from './activity-clear-completed'
import { ActivityStatusGroupHeader } from './activity-thread-controls'
import { ActivityThreadContextMenu } from './activity-thread-context-menu'
@@ -46,11 +46,12 @@ export function ActivityThreadVirtualRow({
compactMode: boolean
showJumpAction: boolean
}): React.JSX.Element {
const t = useTranslate()
if (item.type === 'header') {
return (
<div
role="group"
aria-label={translate(
aria-label={t(
'auto.components.activity.ActivityPrototypePage.a2b4437bfb',
'{{value0}} activity',
{ value0: item.group.label }
@@ -7,6 +7,7 @@ import type { AgentChildRowModel } from '../../../shared/agent-status/presentati
import { agentStateLabel } from '@/components/AgentStateDot'
import { sayBackgroundTaskTranslated } from '@/components/native-chat/tools/tasks/background-task-words-text'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { agentNoUpdateLabel } from '../../../shared/agent-status/presentation/agent-row-decay-state'
export type AgentChildRowText = {
@@ -32,11 +33,14 @@ export function agentChildRowDetailText(row: AgentChildRowModel, now: number): s
}
/** The line beneath a full-width row: what the child said, or that it ended; '' otherwise. */
export function agentChildRowMessageLine(row: AgentChildRowModel): string {
export function agentChildRowMessageLine(
row: AgentChildRowModel,
t: Translate = translate
): string {
if (row.detail?.kind === 'message') {
return row.detail.text
}
return row.detail?.kind === 'ended' ? translate('components.agentChildRow.ended', 'Ended') : ''
return row.detail?.kind === 'ended' ? t('components.agentChildRow.ended', 'Ended') : ''
}
/** The row's name, or its state when the child reported none. */
@@ -28,7 +28,7 @@ import {
SelectTrigger,
SelectValue
} from '@/components/ui/select'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { getAgentCatalog, getAgentLabel } from '@/lib/agent-catalog'
import {
buildAgentSessionContinuationPrompt,
@@ -63,6 +63,7 @@ export function AgentSessionContinuationDialog({
onOpenChange,
onStarted
}: AgentSessionContinuationDialogProps): React.JSX.Element {
const t = useTranslate()
const workspace = request ? worktreeLaunchWorkspace(request.worktreeId) : null
const abortRef = useRef<AbortController | null>(null)
const settings = useAppStore((state) => state.settings)
@@ -241,13 +242,10 @@ export function AgentSessionContinuationDialog({
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-sm">
<MessageSquarePlus className="size-4" />
{translate(
'components.agentSessionContinuation.dialogTitle',
'Hand Off to Another Agent'
)}
{t('components.agentSessionContinuation.dialogTitle', 'Hand Off to Another Agent')}
</DialogTitle>
<DialogDescription className="text-xs">
{translate(
{t(
'components.agentSessionContinuation.dialogDescription',
'Start a fresh Agent session from this stopping point. The original session stays unchanged.'
)}
@@ -258,11 +256,11 @@ export function AgentSessionContinuationDialog({
<div className="min-w-0 rounded-md border border-border bg-muted/30 px-3 py-2">
<div className="truncate text-xs font-medium">
{sourceName ||
translate('components.agentSessionContinuation.untitledSession', 'Current session')}
t('components.agentSessionContinuation.untitledSession', 'Current session')}
</div>
<div className="mt-0.5 min-h-4 text-[11px] text-muted-foreground">
{sourceAgentLabel
? translate(
? t(
'components.agentSessionContinuation.originalAgent',
'Original Agent: {{agent}}',
{ agent: sourceAgentLabel }
@@ -273,7 +271,7 @@ export function AgentSessionContinuationDialog({
<div className="min-w-0 space-y-1.5">
<label className="text-xs font-medium">
{translate('components.agentSessionContinuation.agent', 'Agent')}
{t('components.agentSessionContinuation.agent', 'Agent')}
</label>
<AgentLaunchPicker
draftScope={draftScope}
@@ -290,21 +288,21 @@ export function AgentSessionContinuationDialog({
<div className="min-h-4">
{preparing ? (
<p className="text-[11px] text-muted-foreground">
{translate(
{t(
'components.agentSessionContinuation.preparingContext',
'Preparing saved chat context…'
)}
</p>
) : detecting ? (
<p className="text-[11px] text-muted-foreground">
{translate(
{t(
'components.agentSessionContinuation.detectingAgents',
'Detecting Agents on this workspace host…'
)}
</p>
) : detectionFailed ? (
<p className="text-[11px] text-destructive">
{translate(
{t(
'components.agentSessionContinuation.detectionFailed',
'Could not detect Agents on this workspace host.'
)}
@@ -312,12 +310,12 @@ export function AgentSessionContinuationDialog({
) : destinationModel.state === 'settled' && rows.length === 0 ? (
<p className="text-[11px] text-muted-foreground">
{onlySourceFamilyOffered && sourceAgentLabel
? translate(
? t(
'components.agentSessionContinuation.onlySourceFamilyDetected',
'Only Agents sharing the usage limit with {{agent}} are available on this workspace host. Enable or install a different Agent.',
{ agent: sourceAgentLabel }
)
: translate(
: t(
'components.agentSessionContinuation.noAgents',
'No enabled Agents were detected on this workspace host.'
)}
@@ -328,7 +326,7 @@ export function AgentSessionContinuationDialog({
<div className="min-w-0 space-y-1.5">
<label className="text-xs font-medium">
{translate('components.agentSessionContinuation.context', 'Context')}
{t('components.agentSessionContinuation.context', 'Context')}
</label>
<Select
disabled={controlsDisabled}
@@ -344,16 +342,13 @@ export function AgentSessionContinuationDialog({
</SelectTrigger>
<SelectContent>
<SelectItem value="focused">
{translate(
{t(
'components.agentSessionContinuation.modeFocused',
'Focused handoff (Recommended)'
)}
</SelectItem>
<SelectItem value="full" disabled={!hasFullContext}>
{translate(
'components.agentSessionContinuation.modeFull',
'Full session transcript'
)}
{t('components.agentSessionContinuation.modeFull', 'Full session transcript')}
</SelectItem>
</SelectContent>
</Select>
@@ -362,20 +357,20 @@ export function AgentSessionContinuationDialog({
? null
: !hasFullContext
? request?.source.sourceKind === 'chat'
? translate(
? t(
'components.agentSessionContinuation.modeLoadedDescription',
'Uses recent messages currently loaded in this chat and the current workspace.'
)
: translate(
: t(
'components.agentSessionContinuation.modeCapturedDescription',
'Uses captured text and the current workspace.'
)
: contextMode === 'focused'
? translate(
? t(
'components.agentSessionContinuation.modeFocusedDescription',
'Uses the latest status and current workspace, reading older transcript details only when needed.'
)
: translate(
: t(
'components.agentSessionContinuation.modeFullDescription',
'Asks the new Agent to read the complete saved session before continuing. This can take longer and use significant context, plan usage, or API credits.'
)}
@@ -385,7 +380,7 @@ export function AgentSessionContinuationDialog({
<div className="min-h-4 text-[11px] text-muted-foreground">
{workingDirectory ? (
<>
{translate('components.agentSessionContinuation.startsIn', 'Starts in:')}{' '}
{t('components.agentSessionContinuation.startsIn', 'Starts in:')}{' '}
<span className="break-all font-mono text-foreground/80">{workingDirectory}</span>
</>
) : null}
@@ -394,7 +389,7 @@ export function AgentSessionContinuationDialog({
<DialogFooter>
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
{translate('components.native-chat.question.cancel', 'Cancel')}
{t('components.native-chat.question.cancel', 'Cancel')}
</Button>
<Button
type="button"
@@ -404,8 +399,8 @@ export function AgentSessionContinuationDialog({
>
{showStarting ? <Loader2 className="size-3.5 animate-spin" /> : null}
{starting
? translate('components.agentSessionContinuation.starting', 'Starting…')
: translate('components.agentSessionContinuation.startSession', 'Start New Session')}
? t('components.agentSessionContinuation.starting', 'Starting…')
: t('components.agentSessionContinuation.startSession', 'Start New Session')}
</Button>
</DialogFooter>
</DialogContent>
@@ -1,6 +1,6 @@
import { MessageSquarePlus } from 'lucide-react'
import { DropdownMenuItem } from '@/components/ui/dropdown-menu'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
type AgentSessionContinuationMenuItemProps = {
disabled?: boolean
@@ -11,13 +11,11 @@ export function AgentSessionContinuationMenuItem({
onSelect,
disabled = false
}: AgentSessionContinuationMenuItemProps): React.JSX.Element {
const t = useTranslate()
return (
<DropdownMenuItem disabled={disabled} onSelect={onSelect}>
<MessageSquarePlus />
{translate(
'components.agentSessionContinuation.handOffToAnotherAgent',
'Hand Off to Another Agent'
)}
{t('components.agentSessionContinuation.handOffToAnotherAgent', 'Hand Off to Another Agent')}
</DropdownMenuItem>
)
}
@@ -8,7 +8,7 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { copyArtifactLink, openArtifactInBrowser } from './artifact-link-actions'
type ArtifactActionsProps = {
@@ -22,14 +22,15 @@ export function ArtifactActions({
item,
onDelete
}: ArtifactActionsProps): React.JSX.Element {
const t = useTranslate()
return (
<div
className="flex shrink-0 items-center gap-2"
aria-label={translate('auto.components.artifacts.actions', 'Artifact actions')}
aria-label={t('auto.components.artifacts.actions', 'Artifact actions')}
>
<Button size="sm" onClick={() => void copyArtifactLink(item.shareUrl)}>
<Copy />
{translate('auto.components.artifacts.copyLink', 'Copy link')}
{t('auto.components.artifacts.copyLink', 'Copy link')}
</Button>
<Tooltip>
<TooltipTrigger asChild>
@@ -38,13 +39,13 @@ export function ArtifactActions({
size="icon-sm"
className="text-muted-foreground hover:text-foreground"
onClick={() => openArtifactInBrowser(item.shareUrl)}
aria-label={translate('auto.components.artifacts.openInBrowser', 'Open in browser')}
aria-label={t('auto.components.artifacts.openInBrowser', 'Open in browser')}
>
<ExternalLink />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate('auto.components.artifacts.openInBrowser', 'Open in browser')}
{t('auto.components.artifacts.openInBrowser', 'Open in browser')}
</TooltipContent>
</Tooltip>
<DropdownMenu>
@@ -55,7 +56,7 @@ export function ArtifactActions({
size="icon-sm"
className="text-muted-foreground hover:text-foreground"
disabled={deleting}
aria-label={translate(
aria-label={t(
'auto.components.artifacts.ArtifactActions.more',
'More artifact actions'
)}
@@ -70,7 +71,7 @@ export function ArtifactActions({
onSelect={() => onDelete(item)}
>
<Trash2 className="size-3.5" />
{translate('auto.components.artifacts.ArtifactsPage.deleteArtifact', 'Delete artifact')}
{t('auto.components.artifacts.ArtifactsPage.deleteArtifact', 'Delete artifact')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -2,7 +2,7 @@ import { useMemo, useState } from 'react'
import { Loader2 } from 'lucide-react'
import type { ArtifactListItem } from '../../../../shared/artifacts'
import { Button } from '@/components/ui/button'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
import { clampArtifactListSearchQuery, filterArtifactsBySearchQuery } from './artifact-list-search'
import { ArtifactListRows } from './ArtifactListRows'
@@ -33,6 +33,7 @@ export function ArtifactCollection({
onRefresh: () => void
isRefreshing: boolean
}): React.JSX.Element {
const t = useTranslate()
const [query, setQuery] = useState('')
// Clamp before state/filtering so multi-MB pastes cannot reach either.
const onQueryChange = (next: string): void => setQuery(clampArtifactListSearchQuery(next))
@@ -67,7 +68,7 @@ export function ArtifactCollection({
/>
) : (
<p className="px-3 py-6 text-center text-sm text-muted-foreground">
{translate('auto.components.artifacts.ArtifactCollection.noMatches', 'No matches')}
{t('auto.components.artifacts.ArtifactCollection.noMatches', 'No matches')}
</p>
)}
{hasMore ? (
@@ -81,7 +82,7 @@ export function ArtifactCollection({
onClick={loadMore}
>
{loadingMore ? <Loader2 className="animate-spin" /> : null}
{translate('auto.components.artifacts.ArtifactCollection.loadMore', 'Load more')}
{t('auto.components.artifacts.ArtifactCollection.loadMore', 'Load more')}
</Button>
</div>
) : null}
@@ -1,7 +1,7 @@
import * as VisuallyHidden from 'radix-ui/visually-hidden'
import type { ArtifactListItem } from '../../../../shared/artifacts'
import { Sheet, SheetContent, SheetDescription, SheetTitle } from '@/components/ui/sheet'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { artifactName } from './artifact-display-labels'
import { ArtifactDetailHeader } from './ArtifactDetailHeader'
import { ArtifactPreview } from './ArtifactPreview'
@@ -17,6 +17,7 @@ export function ArtifactDetailDrawer({
onClose: () => void
onDelete: (item: ArtifactListItem) => void
}): React.JSX.Element {
const t = useTranslate()
return (
<Sheet open={item !== null} onOpenChange={(open) => !open && onClose()}>
<SheetContent
@@ -29,7 +30,7 @@ export function ArtifactDetailDrawer({
<div className="flex h-full min-h-0 flex-col">
<VisuallyHidden.Root asChild>
<SheetDescription>
{translate(
{t(
'auto.components.artifacts.ArtifactDetailDrawer.description',
'Preview and manage this shared artifact.'
)}
@@ -3,7 +3,7 @@ import { Globe, X } from 'lucide-react'
import type { ArtifactListItem } from '../../../../shared/artifacts'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { translate } from '@/i18n/i18n'
import { useIntlLocale, useTranslate } from '@/i18n/use-translate'
import { ArtifactActions } from './ArtifactActions'
import {
formatArtifactExpiry,
@@ -24,6 +24,8 @@ export function ArtifactDetailHeader({
onClose: () => void
onDelete: (target: ArtifactListItem) => void
}): React.JSX.Element {
const t = useTranslate()
const intlLocale = useIntlLocale()
return (
// Inset actions below the fixed Windows/Linux controls overlay so they stay clickable.
<div className="flex flex-wrap items-start justify-between gap-3 border-b border-border/50 px-4 py-3 pr-[max(1rem,var(--window-controls-width,0px))]">
@@ -36,14 +38,14 @@ export function ArtifactDetailHeader({
<Globe className="size-3 shrink-0 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate(
{t(
'auto.components.artifacts.ArtifactDetailHeader.publicLink',
'Anyone with this link can view it'
)}
</TooltipContent>
</Tooltip>
<span className="sr-only">
{translate(
{t(
'auto.components.artifacts.ArtifactDetailHeader.publicLink',
'Anyone with this link can view it'
)}
@@ -51,8 +53,9 @@ export function ArtifactDetailHeader({
<p className="truncate font-mono text-xs text-muted-foreground">{item.shareUrl}</p>
</div>
<p className="truncate text-[11px] text-muted-foreground">
{formatArtifactUpdatedAt(item.artifact.updatedAt)} ·{' '}
{formatByteSize(item.artifact.byteSize)} · {formatArtifactExpiry(item.artifact.expiresAt)}
{formatArtifactUpdatedAt(item.artifact.updatedAt, t, intlLocale)} ·{' '}
{formatByteSize(item.artifact.byteSize)} ·{' '}
{formatArtifactExpiry(item.artifact.expiresAt, t, intlLocale)}
</p>
</div>
<div className="flex shrink-0 items-center gap-2">
@@ -62,7 +65,7 @@ export function ArtifactDetailHeader({
variant="ghost"
size="icon-sm"
onClick={onClose}
aria-label={translate('auto.components.artifacts.ArtifactDetailHeader.close', 'Close')}
aria-label={t('auto.components.artifacts.ArtifactDetailHeader.close', 'Close')}
>
<X />
</Button>
@@ -18,6 +18,7 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { translate } from '@/i18n/i18n'
import { useIntlLocale, useTranslate, type Translate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
import { isPortaledRowMenuClick, isRowActivationKey } from '@/lib/list-row-interaction'
import {
@@ -49,24 +50,25 @@ type ArtifactRowAction = {
function artifactRowActions(
item: ArtifactListItem,
deleting: boolean,
deleteArtifact: (item: ArtifactListItem) => void
deleteArtifact: (item: ArtifactListItem) => void,
t: Translate = translate
): readonly ArtifactRowAction[] {
return [
{
key: 'copy',
label: translate('auto.components.artifacts.copyLink', 'Copy link'),
label: t('auto.components.artifacts.copyLink', 'Copy link'),
icon: Copy,
onSelect: () => void copyArtifactLink(item.shareUrl)
},
{
key: 'open',
label: translate('auto.components.artifacts.openInBrowser', 'Open in browser'),
label: t('auto.components.artifacts.openInBrowser', 'Open in browser'),
icon: ExternalLink,
onSelect: () => openArtifactInBrowser(item.shareUrl)
},
{
key: 'delete',
label: translate('auto.components.artifacts.ArtifactsPage.deleteArtifact', 'Delete artifact'),
label: t('auto.components.artifacts.ArtifactsPage.deleteArtifact', 'Delete artifact'),
icon: Trash2,
onSelect: () => deleteArtifact(item),
destructive: true,
@@ -91,12 +93,14 @@ export function ArtifactListRow({
selectArtifact: (slug: string) => void
deleteArtifact: (item: ArtifactListItem) => void
}): React.JSX.Element {
const t = useTranslate()
const intlLocale = useIntlLocale()
const name = artifactName(item)
const typeLabel = artifactTypeLabel(item)
const updatedLabel = formatArtifactUpdatedCompact(item.artifact.updatedAt)
const expiryLabel = formatArtifactExpiryCompact(item.artifact.expiresAt)
const typeLabel = artifactTypeLabel(item, t)
const updatedLabel = formatArtifactUpdatedCompact(item.artifact.updatedAt, t, intlLocale)
const expiryLabel = formatArtifactExpiryCompact(item.artifact.expiresAt, t, intlLocale)
const sizeLabel = formatByteSize(item.artifact.byteSize)
const rowActions = artifactRowActions(item, deleting, deleteArtifact)
const rowActions = artifactRowActions(item, deleting, deleteArtifact, t)
return (
// Non-modal, so scrolling is never blocked: windowing unmounting an open menu mid-scroll reads as dismissal.
@@ -148,7 +152,7 @@ export function ArtifactListRow({
variant="ghost"
size="icon-xs"
className="size-7 text-muted-foreground"
aria-label={translate('auto.components.artifacts.actions', 'Artifact actions')}
aria-label={t('auto.components.artifacts.actions', 'Artifact actions')}
onClick={(event) => event.stopPropagation()}
>
<MoreHorizontal className="size-4" />
@@ -2,7 +2,7 @@ import { useRef } from 'react'
import { Search, X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
export function ArtifactListSearchField({
@@ -16,6 +16,7 @@ export function ArtifactListSearchField({
onClear: () => void
className?: string
}): React.JSX.Element {
const t = useTranslate()
const inputRef = useRef<HTMLInputElement>(null)
const hasText = query !== ''
@@ -27,11 +28,11 @@ export function ArtifactListSearchField({
type="text"
autoFocus
value={query}
aria-label={translate(
aria-label={t(
'auto.components.artifacts.ArtifactListSearchField.label',
'Search artifacts'
)}
placeholder={translate(
placeholder={t(
'auto.components.artifacts.ArtifactListSearchField.placeholder',
'Search...'
)}
@@ -56,10 +57,7 @@ export function ArtifactListSearchField({
variant="ghost"
size="icon-xs"
className="absolute right-1 top-1/2 -translate-y-1/2"
aria-label={translate(
'auto.components.artifacts.ArtifactListSearchField.clear',
'Clear search'
)}
aria-label={t('auto.components.artifacts.ArtifactListSearchField.clear', 'Clear search')}
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
onClear()
@@ -1,22 +1,19 @@
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { ARTIFACTS_TABLE_GRID_CLASS } from './artifacts-table-layout'
import { LIST_TABLE_HEADER_CLASS } from '@/lib/list-table-layout'
export function ArtifactListTableHeader(): React.JSX.Element {
const t = useTranslate()
return (
<div className={cn(ARTIFACTS_TABLE_GRID_CLASS, LIST_TABLE_HEADER_CLASS)}>
<span>{translate('auto.components.artifacts.ArtifactListTableHeader.name', 'Name')}</span>
<span>{translate('auto.components.artifacts.ArtifactListTableHeader.type', 'Type')}</span>
<span>{translate('auto.components.artifacts.ArtifactListTableHeader.size', 'Size')}</span>
<span>
{translate('auto.components.artifacts.ArtifactListTableHeader.updated', 'Updated')}
</span>
<span>
{translate('auto.components.artifacts.ArtifactListTableHeader.expires', 'Expires')}
</span>
<span>{t('auto.components.artifacts.ArtifactListTableHeader.name', 'Name')}</span>
<span>{t('auto.components.artifacts.ArtifactListTableHeader.type', 'Type')}</span>
<span>{t('auto.components.artifacts.ArtifactListTableHeader.size', 'Size')}</span>
<span>{t('auto.components.artifacts.ArtifactListTableHeader.updated', 'Updated')}</span>
<span>{t('auto.components.artifacts.ArtifactListTableHeader.expires', 'Expires')}</span>
<span className="sr-only">
{translate('auto.components.artifacts.ArtifactListTableHeader.actions', 'Actions')}
{t('auto.components.artifacts.ArtifactListTableHeader.actions', 'Actions')}
</span>
</div>
)
@@ -1,7 +1,7 @@
import { RefreshCw } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
import { ArtifactListSearchField } from './ArtifactListSearchField'
@@ -16,6 +16,7 @@ export function ArtifactListToolbar({
onRefresh: () => void
isRefreshing: boolean
}): React.JSX.Element {
const t = useTranslate()
return (
<div className="flex shrink-0 items-center gap-2">
<ArtifactListSearchField
@@ -30,7 +31,7 @@ export function ArtifactListToolbar({
type="button"
variant="ghost"
size="icon-sm"
aria-label={translate('auto.components.artifacts.ArtifactsPage.refresh', 'Refresh')}
aria-label={t('auto.components.artifacts.ArtifactsPage.refresh', 'Refresh')}
onClick={onRefresh}
disabled={isRefreshing}
className="shrink-0 border border-border bg-background shadow-none hover:bg-muted/50"
@@ -39,7 +40,7 @@ export function ArtifactListToolbar({
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate('auto.components.artifacts.ArtifactsPage.refresh', 'Refresh')}
{t('auto.components.artifacts.ArtifactsPage.refresh', 'Refresh')}
</TooltipContent>
</Tooltip>
</div>
@@ -3,6 +3,7 @@ import { AlertCircle, Loader2 } from 'lucide-react'
import { ORCA_BROWSER_GUEST_WEB_PREFERENCES_ATTRIBUTE } from '../../../../shared/browser-guest-web-preferences'
import { moveFocusToRendererBeforeWebviewDetach } from '@/components/browser-pane/host-guest/webview-registry'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
type PreviewState = 'loading' | 'ready' | 'unavailable'
const ARTIFACT_PREVIEW_LOAD_TIMEOUT_MS = 20_000
@@ -61,6 +62,7 @@ function attachArtifactWebview({
}
export function ArtifactPreview({ shareUrl }: { shareUrl: string }): React.JSX.Element {
const t = useTranslate()
const containerRef = useRef<HTMLDivElement>(null)
const [state, setState] = useState<PreviewState>('loading')
@@ -151,10 +153,10 @@ export function ArtifactPreview({ shareUrl }: { shareUrl: string }): React.JSX.E
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-2 bg-editor-surface px-6 text-center">
<AlertCircle className="size-6 text-muted-foreground" />
<p className="text-sm font-medium">
{translate('auto.components.artifacts.previewUnavailable', 'Preview unavailable')}
{t('auto.components.artifacts.previewUnavailable', 'Preview unavailable')}
</p>
<p className="max-w-sm text-xs text-muted-foreground">
{translate(
{t(
'auto.components.artifacts.previewUnavailableDescription',
'Open this artifact in your browser to view it.'
)}
@@ -4,7 +4,7 @@ import type { ArtifactWriteRequest } from '../../../../shared/artifacts'
import { Button } from '@/components/ui/button'
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
import { useAppStore } from '@/store'
import { ArtifactPublishedLinkPanel } from './ArtifactPublishedLinkPanel'
@@ -44,6 +44,7 @@ export function ArtifactPublishButton({
onOpenChange: (open: boolean) => void
}
)): React.JSX.Element {
const t = useTranslate()
const [ownOpen, setOwnOpen] = useState(false)
const open = controlledOpen ?? ownOpen
const setOpen = onOpenChange ?? setOwnOpen
@@ -122,10 +123,7 @@ export function ArtifactPublishButton({
openSettingsPage()
}
const label = translate(
'auto.components.artifacts.ArtifactPublishButton.a4a49da6af',
'Share as artifact'
)
const label = t('auto.components.artifacts.ArtifactPublishButton.a4a49da6af', 'Share as artifact')
return (
<Popover open={open} onOpenChange={(nextOpen) => !busy && setOpen(nextOpen)}>
{anchorRef ? (
@@ -175,18 +173,15 @@ export function ArtifactPublishButton({
>
<div className="space-y-1 border-b border-border/60 px-4 py-3.5">
<h3 className="text-sm font-semibold">
{translate(
'auto.components.artifacts.ArtifactPublishButton.confirmTitle',
'Share as artifact'
)}
{t('auto.components.artifacts.ArtifactPublishButton.confirmTitle', 'Share as artifact')}
</h3>
<p className="text-xs leading-5 text-muted-foreground">
{publishedLink
? translate(
? t(
'auto.components.artifacts.ArtifactPublishButton.publishedDescription',
'Anyone with this link can view the shared file.'
)
: translate(
: t(
'auto.components.artifacts.ArtifactPublishButton.confirmDescription',
'This publishes the current file at a link anyone with the URL can view.'
)}
@@ -198,13 +193,13 @@ export function ArtifactPublishButton({
<div className="flex items-center justify-between gap-3">
<div className="min-w-0 space-y-0.5">
<p className="text-xs font-medium">
{translate(
{t(
'auto.components.artifacts.ArtifactPublishButton.accountTitle',
'Orca account'
)}
</p>
<p className="text-[11px] leading-4 text-muted-foreground">
{translate(
{t(
'auto.components.artifacts.ArtifactPublishButton.accountDescription',
'Sign in to create and manage this link.'
)}
@@ -216,11 +211,11 @@ export function ArtifactPublishButton({
disabled={authStatus?.configured !== true}
>
{authStatus?.state === 'reconnect-required'
? translate(
? t(
'auto.components.artifacts.ArtifactPublishButton.signInAgain',
'Sign in again'
)
: translate('auto.components.artifacts.ArtifactPublishButton.signIn', 'Sign in')}
: t('auto.components.artifacts.ArtifactPublishButton.signIn', 'Sign in')}
</OrcaSignInButton>
</div>
) : null}
@@ -230,13 +225,13 @@ export function ArtifactPublishButton({
<div className={cn('space-y-2', !signedIn && 'border-t border-border/60 pt-3')}>
<div className="space-y-0.5">
<p className="text-xs font-medium">
{translate(
{t(
'auto.components.artifacts.ArtifactPublishButton.publishingOffTitle',
'Artifact sharing is off'
)}
</p>
<p className="text-[11px] leading-4 text-muted-foreground">
{translate(
{t(
'auto.components.artifacts.ArtifactPublishButton.publishingOffDescription',
'Learn about public links and enable sharing in Settings.'
)}
@@ -249,7 +244,7 @@ export function ArtifactPublishButton({
className="w-full"
onClick={openArtifactsSettings}
>
{translate(
{t(
'auto.components.artifacts.ArtifactPublishButton.openSettings',
'Open Artifacts settings'
)}
@@ -261,7 +256,7 @@ export function ArtifactPublishButton({
{checkingLink ? (
<div className="flex items-center justify-center gap-2 py-2 text-xs text-muted-foreground">
<Loader2 className="size-3.5 animate-spin" />
{translate(
{t(
'auto.components.artifacts.ArtifactPublishButton.checkingLink',
'Checking for an existing link…'
)}
@@ -269,7 +264,7 @@ export function ArtifactPublishButton({
) : currentLookup?.status === 'error' ? (
<div className="space-y-2">
<p className="text-xs leading-5 text-muted-foreground">
{translate(
{t(
'auto.components.artifacts.ArtifactPublishButton.checkFailed',
'Could not check for an existing link.'
)}
@@ -281,7 +276,7 @@ export function ArtifactPublishButton({
className="w-full"
onClick={() => setLookupRevision((current) => current + 1)}
>
{translate('auto.components.artifacts.ArtifactPublishButton.tryAgain', 'Try again')}
{t('auto.components.artifacts.ArtifactPublishButton.tryAgain', 'Try again')}
</Button>
</div>
) : publishedLink ? (
@@ -302,8 +297,8 @@ export function ArtifactPublishButton({
>
{publishing ? <Loader2 className="animate-spin" /> : <Share2 />}
{publishing
? translate('auto.components.artifacts.ArtifactPublishButton.sharing', 'Sharing…')
: translate(
? t('auto.components.artifacts.ArtifactPublishButton.sharing', 'Sharing…')
: t(
'auto.components.artifacts.ArtifactPublishButton.sharePublicLink',
'Generate link'
)}
@@ -2,7 +2,7 @@ import { useCallback, useRef, useState } from 'react'
import { Check, Copy, ExternalLink, Loader2, RefreshCw } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { copyArtifactLink, openArtifactInBrowser } from './artifact-link-actions'
export function ArtifactPublishedLinkPanel({
@@ -16,6 +16,7 @@ export function ArtifactPublishedLinkPanel({
sharingEnabled: boolean
onUpdate: () => void
}): React.JSX.Element {
const t = useTranslate()
const [copied, setCopied] = useState(false)
const copiedResetTimerRef = useRef<number | null>(null)
const mountedRef = useRef(false)
@@ -52,8 +53,8 @@ export function ArtifactPublishedLinkPanel({
}
const copyLabel = copied
? translate('auto.components.artifacts.copySuccess', 'Artifact link copied')
: translate('auto.components.artifacts.ArtifactPublishedLinkPanel.copyLink', 'Copy link')
? t('auto.components.artifacts.copySuccess', 'Artifact link copied')
: t('auto.components.artifacts.ArtifactPublishedLinkPanel.copyLink', 'Copy link')
return (
<div ref={setPanelRef} className="space-y-3">
@@ -89,7 +90,7 @@ export function ArtifactPublishedLinkPanel({
size="icon-xs"
className="text-muted-foreground hover:text-foreground"
onClick={() => openArtifactInBrowser(shareUrl)}
aria-label={translate(
aria-label={t(
'auto.components.artifacts.ArtifactPublishedLinkPanel.openLink',
'Open link'
)}
@@ -98,10 +99,7 @@ export function ArtifactPublishedLinkPanel({
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>
{translate(
'auto.components.artifacts.ArtifactPublishedLinkPanel.openLink',
'Open link'
)}
{t('auto.components.artifacts.ArtifactPublishedLinkPanel.openLink', 'Open link')}
</TooltipContent>
</Tooltip>
</div>
@@ -117,11 +115,8 @@ export function ArtifactPublishedLinkPanel({
>
{publishing ? <Loader2 className="animate-spin" /> : <RefreshCw />}
{publishing
? translate(
'auto.components.artifacts.ArtifactPublishedLinkPanel.updating',
'Updating…'
)
: translate(
? t('auto.components.artifacts.ArtifactPublishedLinkPanel.updating', 'Updating…')
: t(
'auto.components.artifacts.ArtifactPublishedLinkPanel.update',
'Update shared content'
)}
@@ -4,7 +4,7 @@ import { useConfirmationDialog } from '@/components/confirmation-dialog-context'
import { persistConfirmationSkipPreference } from '@/components/confirmation-skip-preference'
import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call'
import { useAppStore } from '@/store'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { ArtifactCollection } from './ArtifactCollection'
import { ArtifactDetailDrawer } from './ArtifactDetailDrawer'
import { ArtifactsPageSkeleton } from './ArtifactsPageSkeleton'
@@ -18,6 +18,7 @@ import { artifactAccountIdentity, useArtifactPagination } from './useArtifactPag
const LOCAL_RUNTIME = { kind: 'local' } as const
export default function ArtifactsPage(): React.JSX.Element {
const t = useTranslate()
const closePage = useAppStore((state) => state.closeArtifactsPage)
const authStatus = useAppStore((state) => state.orcaProfileAuthStatus)
const refreshAuth = useAppStore((state) => state.refreshCurrentOrcaProfileAuth)
@@ -97,13 +98,13 @@ export default function ArtifactsPage(): React.JSX.Element {
const name = item.artifact.title || item.artifact.originalFileName || item.artifact.slug
if (!settings?.skipDeleteArtifactConfirm) {
const accepted = await confirm({
title: translate('auto.components.artifacts.ArtifactsPage.deleteTitle', 'Delete artifact?'),
description: translate(
title: t('auto.components.artifacts.ArtifactsPage.deleteTitle', 'Delete artifact?'),
description: t(
'auto.components.artifacts.ArtifactsPage.deleteDescription',
'“{{name}}” will no longer be available at its public link.',
{ name }
),
confirmLabel: translate('auto.components.artifacts.ArtifactsPage.delete', 'Delete'),
confirmLabel: t('auto.components.artifacts.ArtifactsPage.delete', 'Delete'),
confirmVariant: 'destructive',
dontAskAgain: {
onConfirmed: () =>
@@ -148,7 +149,7 @@ export default function ArtifactsPage(): React.JSX.Element {
console.error('Failed to delete artifact:', deleteError)
if (requestedAccountIsCurrent()) {
setError(
translate(
t(
'auto.components.artifacts.ArtifactsPage.deleteFailed',
'Could not delete the artifact.'
)
@@ -175,7 +176,7 @@ export default function ArtifactsPage(): React.JSX.Element {
}
>
<h1 className="truncate text-base font-semibold leading-8">
{translate('auto.components.artifacts.ArtifactsPage.title', 'Artifacts')}
{t('auto.components.artifacts.ArtifactsPage.title', 'Artifacts')}
</h1>
</header>
@@ -1,5 +1,5 @@
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { ARTIFACTS_TABLE_GRID_CLASS } from './artifacts-table-layout'
import { LIST_TABLE_CONTAINER_CLASS, LIST_TABLE_HEADER_CLASS } from '@/lib/list-table-layout'
@@ -17,16 +17,14 @@ const TABLE_ROW_SKELETONS = [
] as const
export function ArtifactsPageSkeleton(): React.JSX.Element {
const t = useTranslate()
return (
<div
className="flex min-h-0 flex-1 flex-col gap-4 overflow-hidden px-3 pb-4 md:px-5"
// Why: aria-label on a roleless div is not exposed to screen readers.
role="status"
aria-busy="true"
aria-label={translate(
'auto.components.artifacts.ArtifactsPageSkeleton.loading',
'Loading artifacts'
)}
aria-label={t('auto.components.artifacts.ArtifactsPageSkeleton.loading', 'Loading artifacts')}
>
<div className="flex shrink-0 items-center gap-2">
<SkeletonBar className="h-8 w-56 shrink-0 rounded-md" />
@@ -1,6 +1,6 @@
import { ArrowRight, Files, Loader2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { OrcaSignInButton, OrcaSignInWaitNotice } from '../OrcaSignInProgress'
export function ArtifactsPageErrorBanner({
@@ -12,11 +12,12 @@ export function ArtifactsPageErrorBanner({
loading: boolean
onRetry: () => void
}): React.JSX.Element {
const t = useTranslate()
return (
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-destructive/30 bg-destructive/10 px-3 py-2 md:px-5">
<p className="min-w-0 flex-1 text-xs text-destructive">{error}</p>
<Button type="button" variant="outline" size="xs" disabled={loading} onClick={onRetry}>
{translate('auto.components.artifacts.ArtifactsPage.retry', 'Retry')}
{t('auto.components.artifacts.ArtifactsPage.retry', 'Retry')}
</Button>
</div>
)
@@ -31,28 +32,26 @@ export function ArtifactsPageAuthState({
configured: boolean
onOpenAccountSettings: () => void
}): React.JSX.Element {
const t = useTranslate()
return (
<div className="flex min-h-72 flex-1 flex-col items-center justify-center gap-3 px-5 py-5 text-center md:px-8">
<Files className="size-8 text-muted-foreground" />
<div className="space-y-1">
<h2 className="text-sm font-semibold">
{needsReconnect
? translate(
'auto.components.artifacts.ArtifactsPage.reconnectHeading',
'Sign in to Orca again'
)
: translate(
? t('auto.components.artifacts.ArtifactsPage.reconnectHeading', 'Sign in to Orca again')
: t(
'auto.components.artifacts.ArtifactsPage.shareSignInHeading',
'Sign in to share artifacts'
)}
</h2>
<p className="max-w-sm text-xs leading-5 text-muted-foreground">
{needsReconnect
? translate(
? t(
'auto.components.artifacts.ArtifactsPage.reconnectCopy',
'Sign in again to view and manage the artifacts shared through your account.'
)
: translate(
: t(
'auto.components.artifacts.ArtifactsPage.shareSignInCopy',
'Use your Orca account to upload artifacts and manage their public links.'
)}
@@ -62,24 +61,21 @@ export function ArtifactsPageAuthState({
<>
<OrcaSignInButton size="sm">
{needsReconnect
? translate(
'auto.components.artifacts.ArtifactsPage.signInAgainAction',
'Sign in again'
)
: translate('auto.components.artifacts.ArtifactsPage.signIn', 'Sign in to Orca')}
? t('auto.components.artifacts.ArtifactsPage.signInAgainAction', 'Sign in again')
: t('auto.components.artifacts.ArtifactsPage.signIn', 'Sign in to Orca')}
</OrcaSignInButton>
<OrcaSignInWaitNotice className="justify-center" />
</>
) : (
<div className="flex flex-col items-center gap-2">
<p className="max-w-sm text-xs leading-5 text-muted-foreground">
{translate(
{t(
'auto.components.artifacts.ArtifactsPage.unconfiguredCopy',
'Orca account sign-in is not configured on this machine yet.'
)}
</p>
<Button variant="outline" size="sm" onClick={onOpenAccountSettings}>
{translate(
{t(
'auto.components.artifacts.ArtifactsPage.openAccountSettings',
'Open account settings'
)}
@@ -104,34 +100,32 @@ export function ArtifactsPageEmptyState({
onLoadMore: () => void
onOpenArtifactsSettings: () => void
}): React.JSX.Element {
const t = useTranslate()
return (
<div className="flex flex-1 flex-col items-center justify-center gap-2 px-5 py-5 text-center md:px-8">
<Files className="size-8 text-muted-foreground" />
<h2 className="text-sm font-semibold">
{hasMore
? translate(
? t(
'auto.components.artifacts.ArtifactsPage.moreAvailable',
'More artifacts are available'
)
: publishingBlocked
? translate(
'auto.components.artifacts.ArtifactsPage.publishingOff',
'Publishing is turned off'
)
: translate('auto.components.artifacts.ArtifactsPage.empty', 'No shared artifacts')}
? t('auto.components.artifacts.ArtifactsPage.publishingOff', 'Publishing is turned off')
: t('auto.components.artifacts.ArtifactsPage.empty', 'No shared artifacts')}
</h2>
<p className="max-w-sm text-xs leading-5 text-muted-foreground">
{hasMore
? translate(
? t(
'auto.components.artifacts.ArtifactsPage.moreAvailableCopy',
'Load the next page to continue.'
)
: publishingBlocked
? translate(
? t(
'auto.components.artifacts.ArtifactsPage.publishingOffCopy',
'Nothing on this device can create a public artifact link yet. Allow publishing in Settings → Artifacts, then share from an open HTML or Markdown file or ask your agent.'
)
: translate(
: t(
'auto.components.artifacts.ArtifactsPage.emptyCopy',
'Open an HTML or Markdown file and select Share as artifact, or ask your agent to share it.'
)}
@@ -144,7 +138,7 @@ export function ArtifactsPageEmptyState({
className="mt-1"
onClick={onOpenArtifactsSettings}
>
{translate(
{t(
'auto.components.artifacts.ArtifactsPage.openArtifactsSettings',
'Open Settings → Artifacts'
)}
@@ -160,7 +154,7 @@ export function ArtifactsPageEmptyState({
onClick={onLoadMore}
>
{loadingMore ? <Loader2 className="animate-spin" /> : null}
{translate('auto.components.artifacts.ArtifactCollection.loadMore', 'Load more')}
{t('auto.components.artifacts.ArtifactCollection.loadMore', 'Load more')}
</Button>
) : null}
</div>
@@ -1,5 +1,6 @@
import type { ArtifactListItem } from '../../../../shared/artifacts'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { formatUiRelativeTime, formatUiRelativeTimeFromDate } from '@/i18n/relative-time-format'
export function artifactName(item: ArtifactListItem): string {
@@ -16,50 +17,67 @@ export function formatByteSize(value: number): string {
return `${(value / (1024 * 1024)).toFixed(1)} MB`
}
export function formatArtifactUpdatedAt(value: string): string {
return translate('auto.components.artifacts.updatedAt', 'Updated {{when}}', {
when: formatArtifactUpdatedCompact(value)
export function formatArtifactUpdatedAt(
value: string,
t: Translate = translate,
intlLocale?: string
): string {
return t('auto.components.artifacts.updatedAt', 'Updated {{when}}', {
when: formatArtifactUpdatedCompact(value, t, intlLocale)
})
}
export function formatArtifactUpdatedCompact(value: string): string {
export function formatArtifactUpdatedCompact(
value: string,
t: Translate = translate,
intlLocale?: string
): string {
return formatUiRelativeTimeFromDate(
value,
translate('auto.components.artifacts.updatedRecently', 'recently')
t('auto.components.artifacts.updatedRecently', 'recently'),
intlLocale
)
}
/** Phrased from the stored timestamp alone — never a claim about server-side state. */
export function formatArtifactExpiry(value: string): string {
export function formatArtifactExpiry(
value: string,
t: Translate = translate,
intlLocale?: string
): string {
const expiresAt = new Date(value)
if (Number.isNaN(expiresAt.getTime())) {
return translate('auto.components.artifacts.expiryUnknown', 'Expiry unknown')
return t('auto.components.artifacts.expiryUnknown', 'Expiry unknown')
}
const remainingMs = expiresAt.getTime() - Date.now()
return remainingMs <= 0
? translate('auto.components.artifacts.expired', 'Link expired')
: translate('auto.components.artifacts.expires', 'Link expires {{when}}', {
when: formatUiRelativeTime(remainingMs)
? t('auto.components.artifacts.expired', 'Link expired')
: t('auto.components.artifacts.expires', 'Link expires {{when}}', {
when: formatUiRelativeTime(remainingMs, intlLocale)
})
}
export function formatArtifactExpiryCompact(value: string): string {
export function formatArtifactExpiryCompact(
value: string,
t: Translate = translate,
intlLocale?: string
): string {
const expiresAt = new Date(value)
if (Number.isNaN(expiresAt.getTime())) {
return translate('auto.components.artifacts.expiryUnknown', 'Expiry unknown')
return t('auto.components.artifacts.expiryUnknown', 'Expiry unknown')
}
const remainingMs = expiresAt.getTime() - Date.now()
return remainingMs <= 0
? translate('auto.components.artifacts.expiredCompact', 'Expired')
: formatUiRelativeTime(remainingMs)
? t('auto.components.artifacts.expiredCompact', 'Expired')
: formatUiRelativeTime(remainingMs, intlLocale)
}
export function artifactTypeLabel(item: ArtifactListItem): string {
export function artifactTypeLabel(item: ArtifactListItem, t: Translate = translate): string {
if (item.artifact.sourceContentType === 'text/markdown') {
return translate('auto.components.artifacts.typeMarkdown', 'Markdown')
return t('auto.components.artifacts.typeMarkdown', 'Markdown')
}
if (item.artifact.sourceContentType === 'text/html') {
return translate('auto.components.artifacts.typeHtml', 'HTML')
return t('auto.components.artifacts.typeHtml', 'HTML')
}
return item.artifact.sourceContentType
}
@@ -1,16 +1,17 @@
import type { ArtifactListItem } from '../../../../shared/artifacts'
import { isClipboardTextByteLengthOverLimit } from '../../../../shared/clipboard-text'
import { artifactName, artifactTypeLabel } from './artifact-display-labels'
import type { Translate } from '../../i18n/use-translate'
/** Pasted queries above this are clamped so filtering never runs on unbounded input. */
export const ARTIFACT_LIST_SEARCH_QUERY_MAX_BYTES = 2 * 1024
export function artifactSearchHaystack(item: ArtifactListItem): string {
export function artifactSearchHaystack(item: ArtifactListItem, t?: Translate): string {
return [
artifactName(item),
item.artifact.originalFileName,
item.artifact.slug,
artifactTypeLabel(item)
artifactTypeLabel(item, t)
]
.filter((value): value is string => Boolean(value))
.join('\n')
@@ -44,12 +45,13 @@ export function artifactMatchesSearchQuery(item: ArtifactListItem, query: string
export function filterArtifactsBySearchQuery(
artifacts: readonly ArtifactListItem[],
query: string
query: string,
t?: Translate
): readonly ArtifactListItem[] {
// Why: normalize once per filter, not once per artifact.
const activeQuery = activeArtifactListSearchQuery(query)
if (activeQuery === null) {
return artifacts
}
return artifacts.filter((item) => artifactSearchHaystack(item).includes(activeQuery))
return artifacts.filter((item) => artifactSearchHaystack(item, t).includes(activeQuery))
}
@@ -6,7 +6,7 @@ import { CommandItem } from '@/components/ui/command'
import { CREATE_WORKTREE_ITEM_ID } from '@/lib/workspace/search/worktrees/worktree-palette-create-action'
import type { CmdJTaskUrlCreatePreview } from '@/lib/workspace/search/worktrees/worktree-palette-task-url-match'
import type { LinearIssue } from '../../../../shared/linear/issue-types'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
function TaskUrlProviderIcon({
provider
@@ -41,21 +41,20 @@ export function PaletteCreateWorktreeRow({
taskUrlPreview: CmdJTaskUrlCreatePreview | null
onSelect: () => void
}): React.JSX.Element {
const t = useTranslate()
const showLinearPreview = linearPending || linearIssue !== null
const linearPreviewLabel = linearIssue
? translate(
? t(
'worktreeJumpPalette.linearIssue.createLabel',
'Create worktree from Linear issue {{value0}}: {{value1}}',
{ value0: linearIssue.identifier, value1: linearIssue.title }
)
: linearPending && showLinearLoadingFeedback
? translate(
'worktreeJumpPalette.linearIssue.loadingLabel',
'Loading Linear issue {{value0}}',
{ value0: linearIdentifier ?? '' }
)
? t('worktreeJumpPalette.linearIssue.loadingLabel', 'Loading Linear issue {{value0}}', {
value0: linearIdentifier ?? ''
})
: linearPending
? translate(
? t(
'worktreeJumpPalette.linearIssue.pendingLabel',
'Create worktree from Linear issue {{value0}}',
{ value0: linearIdentifier ?? '' }
@@ -116,11 +115,11 @@ export function PaletteCreateWorktreeRow({
</div>
<div className="mt-0.5 truncate text-[12px] text-muted-foreground">
{linearIssue || !showLinearLoadingFeedback
? translate(
? t(
'worktreeJumpPalette.linearIssue.createHint',
'Create worktree from Linear issue'
)
: translate('worktreeJumpPalette.linearIssue.loadingHint', 'Loading Linear issue…')}
: t('worktreeJumpPalette.linearIssue.loadingHint', 'Loading Linear issue…')}
</div>
</div>
</>
@@ -140,14 +139,12 @@ export function PaletteCreateWorktreeRow({
</div>
<div className="mt-0.5 truncate text-[12px] text-muted-foreground">
{taskUrlPreview.loading && showTaskUrlLoadingFeedback
? translate('worktreeJumpPalette.taskUrl.loadingHint', 'Loading {{value0}}…', {
? t('worktreeJumpPalette.taskUrl.loadingHint', 'Loading {{value0}}…', {
value0: taskUrlPreview.kindLabel
})
: translate(
'worktreeJumpPalette.taskUrl.createHint',
'Create worktree from {{value0}}',
{ value0: taskUrlPreview.kindLabel }
)}
: t('worktreeJumpPalette.taskUrl.createHint', 'Create worktree from {{value0}}', {
value0: taskUrlPreview.kindLabel
})}
</div>
</div>
</>
@@ -158,11 +155,9 @@ export function PaletteCreateWorktreeRow({
</div>
<div className="min-w-0 flex-1">
<div className="text-[14px] font-semibold tracking-[-0.01em] text-foreground">
{translate(
'auto.components.WorktreeJumpPalette.95be6587d3',
'Create worktree "{{value0}}"',
{ value0: createWorktreeName }
)}
{t('auto.components.WorktreeJumpPalette.95be6587d3', 'Create worktree "{{value0}}"', {
value0: createWorktreeName
})}
</div>
</div>
</>
@@ -1,6 +1,6 @@
import React, { useMemo } from 'react'
import { ListFilter, X } from 'lucide-react'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import type { PaletteFilterModel } from './palette-filter-options'
import {
EMPTY_PALETTE_FILTER,
@@ -21,6 +21,7 @@ export default function PaletteFilterChips({
filter: PaletteFilterState
onFilterChange: (next: PaletteFilterState) => void
}): React.JSX.Element | null {
const t = useTranslate()
const chips = useMemo<Chip[]>(() => {
const hostLabels = new Map(model.hosts.map((host) => [host.id, host.label]))
const repositoryLabels = new Map(
@@ -49,9 +50,7 @@ export default function PaletteFilterChips({
<div className="mx-3 mt-2 flex items-center gap-1.5 pl-3.5 text-[11px] text-muted-foreground">
{/* Why: a fixed anchor the eye can find at one filter, where a bare muted line vanishes. */}
<ListFilter className="size-3 shrink-0" aria-hidden="true" />
<span className="sr-only">
{translate('worktreeJumpPalette.filter.scopedTo', 'Scoped to')}
</span>
<span className="sr-only">{t('worktreeJumpPalette.filter.scopedTo', 'Scoped to')}</span>
{/* Why: horizontal scroll keeps every chip reachable without a hard +N dead-end. */}
<div className="scrollbar-sleek flex min-w-0 flex-1 items-center gap-1 overflow-x-auto">
{chips.map((chip, index) => (
@@ -64,13 +63,9 @@ export default function PaletteFilterChips({
<button
type="button"
onClick={() => onFilterChange(togglePaletteFilterValue(filter, chip.field, chip.id))}
aria-label={translate(
'worktreeJumpPalette.filter.removeChip',
'Remove filter {{value0}}',
{
value0: chip.label
}
)}
aria-label={t('worktreeJumpPalette.filter.removeChip', 'Remove filter {{value0}}', {
value0: chip.label
})}
className="group flex h-6 max-w-[140px] shrink-0 items-center gap-1 rounded-sm px-1.5 text-muted-foreground transition-colors hover:text-foreground focus-visible:bg-accent focus-visible:text-foreground"
>
<span className="min-w-0 truncate">{chip.label}</span>
@@ -88,7 +83,7 @@ export default function PaletteFilterChips({
onClick={() => onFilterChange(EMPTY_PALETTE_FILTER)}
className="ml-1 mr-4 shrink-0 rounded-md px-1.5 py-0.5 hover:bg-accent hover:text-foreground"
>
{translate('worktreeJumpPalette.filter.clearAll', 'Clear all')}
{t('worktreeJumpPalette.filter.clearAll', 'Clear all')}
</button>
</div>
)
@@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useVirtualizer } from '@tanstack/react-virtual'
import { Check, ChevronLeft, SearchIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import type { PaletteFilterOption } from './palette-filter-options'
import type { PaletteFilterField } from './palette-filter'
import {
@@ -79,6 +79,7 @@ export function PaletteFilterFieldOptions({
onClearField: () => void
onSelectAllMatching: (ids: readonly string[]) => void
}): React.JSX.Element {
const t = useTranslate()
const selected = useMemo(() => new Set(group.selected), [group.selected])
const normalizedQuery = optionQuery.trim().toLowerCase()
const rankMode: FilterOptionRankMode = group.field === 'host' ? 'registry' : 'popularity'
@@ -189,17 +190,17 @@ export function PaletteFilterFieldOptions({
const searchPlaceholder =
group.field === 'host'
? translate('worktreeJumpPalette.filter.searchHosts', 'Filter hosts...')
: translate('worktreeJumpPalette.filter.searchProjects', 'Filter projects...')
? t('worktreeJumpPalette.filter.searchHosts', 'Filter hosts...')
: t('worktreeJumpPalette.filter.searchProjects', 'Filter projects...')
const emptyLabel =
group.field === 'host'
? translate('worktreeJumpPalette.filter.noHosts', 'No matching hosts')
: translate('worktreeJumpPalette.filter.noProjects', 'No matching projects')
? t('worktreeJumpPalette.filter.noHosts', 'No matching hosts')
: t('worktreeJumpPalette.filter.noProjects', 'No matching projects')
// Translate complete field-specific labels; lowercasing nouns breaks languages such as German.
const clearLabel =
group.field === 'host'
? translate('worktreeJumpPalette.filter.clearHosts', 'Clear hosts')
: translate('worktreeJumpPalette.filter.clearProjects', 'Clear projects')
? t('worktreeJumpPalette.filter.clearHosts', 'Clear hosts')
: t('worktreeJumpPalette.filter.clearProjects', 'Clear projects')
const canSelectAll = ranked.unselectedCount > 0
const canClear = ranked.selectedCount > 0
@@ -214,7 +215,7 @@ export function PaletteFilterFieldOptions({
className="flex h-7 items-center gap-1 rounded-md px-1.5 text-[12px] text-muted-foreground hover:bg-accent hover:text-foreground"
>
<ChevronLeft className="size-3.5" aria-hidden="true" />
{translate('worktreeJumpPalette.filter.back', 'Back')}
{t('worktreeJumpPalette.filter.back', 'Back')}
</button>
<span className="min-w-0 flex-1 truncate pr-2 text-[12px] font-medium text-foreground">
{group.heading}
@@ -242,7 +243,7 @@ export function PaletteFilterFieldOptions({
{ranked.selectedCollapsed ? (
<div className="flex items-center justify-between gap-2 border-b border-border/55 px-2.5 py-1.5">
<span className="text-[11px] text-muted-foreground">
{translate(
{t(
'worktreeJumpPalette.filter.selectedCollapsed',
'{{value0}} selected — remove via chips or Clear',
{ value0: ranked.selectedCount }
@@ -305,7 +306,7 @@ export function PaletteFilterFieldOptions({
onClick={() => onSelectAllMatching(ranked.unselectedIds)}
className="rounded-md px-1.5 py-0.5 text-[11px] text-muted-foreground hover:bg-accent hover:text-foreground"
>
{translate(
{t(
'worktreeJumpPalette.filter.selectAllMatching',
'Select all matching ({{value0}})',
{ value0: ranked.unselectedCount }
@@ -3,7 +3,7 @@ import { ChevronRight, ListFilter, X } from 'lucide-react'
import { Command, CommandGroup, CommandItem, CommandList } from '@/components/ui/command'
import { Button } from '@/components/ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { useUiLanguageRevision } from '@/i18n/use-ui-language-revision'
import type { PaletteFilterModel } from './palette-filter-options'
import {
@@ -72,6 +72,7 @@ export default function PaletteFilterMenu({
onRequestInputFocus: () => void
portalContainer: HTMLElement | null
}): React.JSX.Element | null {
const t = useTranslate()
const languageRevision = useUiLanguageRevision()
const [open, setOpen] = useState(false)
const [activeField, setActiveField] = useState<PaletteFilterField | null>(null)
@@ -83,7 +84,7 @@ export default function PaletteFilterMenu({
if (model.hosts.length > 1) {
entries.push({
field: 'host',
heading: translate('worktreeJumpPalette.filter.hosts', 'Hosts'),
heading: t('worktreeJumpPalette.filter.hosts', 'Hosts'),
options: model.hosts,
selected: filter.hostIds
})
@@ -92,14 +93,14 @@ export default function PaletteFilterMenu({
entries.push({
field: 'repository',
// "Projects" is the user-facing term for repository-granular choices; see filter.emptySubtitle.
heading: translate('worktreeJumpPalette.filter.projects', 'Projects'),
heading: t('worktreeJumpPalette.filter.projects', 'Projects'),
options: model.repositories,
selected: filter.repoIds
})
}
return entries
// oxlint-disable-next-line react-hooks/exhaustive-deps -- translate() reads the UI language implicitly.
}, [filter.hostIds, filter.repoIds, model.hosts, model.repositories, languageRevision])
}, [filter.hostIds, filter.repoIds, model.hosts, model.repositories, languageRevision, t])
const activeGroup =
activeField == null ? null : (groups.find((group) => group.field === activeField) ?? null)
@@ -163,12 +164,12 @@ export default function PaletteFilterMenu({
type="button"
variant={active ? 'secondary' : 'outline'}
size="xs"
aria-label={translate('worktreeJumpPalette.filter.trigger', 'Filter results')}
aria-label={t('worktreeJumpPalette.filter.trigger', 'Filter results')}
data-active={active ? 'true' : undefined}
className="ml-2"
>
<ListFilter className="size-3.5" aria-hidden="true" />
<span>{translate('worktreeJumpPalette.filter.label', 'Filter')}</span>
<span>{t('worktreeJumpPalette.filter.label', 'Filter')}</span>
{active ? (
<span className="rounded-full bg-primary/85 px-1.5 text-[10px] font-semibold tabular-nums text-primary-foreground">
{selectionCount}
@@ -210,7 +211,7 @@ export default function PaletteFilterMenu({
{active ? (
<div className="flex items-center justify-between border-t border-border/55 px-3 py-2">
<span className="text-[11px] text-muted-foreground">
{translate('worktreeJumpPalette.filter.activeCount', '{{value0}} active', {
{t('worktreeJumpPalette.filter.activeCount', '{{value0}} active', {
value0: selectionCount
})}
</span>
@@ -220,7 +221,7 @@ export default function PaletteFilterMenu({
className="flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] text-muted-foreground hover:bg-accent hover:text-foreground"
>
<X className="size-3" aria-hidden="true" />
{translate('worktreeJumpPalette.filter.clearAll', 'Clear all')}
{t('worktreeJumpPalette.filter.clearAll', 'Clear all')}
</button>
</div>
) : null}
@@ -30,7 +30,7 @@ import {
terminalTabHasUnreadActivity,
type TerminalTabAttentionBadge
} from '@/components/tab-bar/terminal-tab-activity-status'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { AGENT_STATUS_STALE_AFTER_MS } from '../../../../shared/agent-status/contracts/agent-status-freshness'
import type { AgentStatusEntry } from '../../../../shared/agent-status/contracts/agent-status-types'
@@ -237,6 +237,7 @@ export function PaletteRecentTabStatusDot({
row: RecentWorkspaceTabRow | null
fallback: React.ReactNode
}): React.JSX.Element {
const t = useTranslate()
const live = useLiveStatus()
const terminalTabId = row?.terminalTab?.id
const status: WorktreeStatus | null =
@@ -258,10 +259,7 @@ export function PaletteRecentTabStatusDot({
const statusLabel =
badge === 'unread'
? // Reuse the translated tab-bar label for the same unread signal.
translate(
'auto.components.tab.bar.TerminalTabLeadingIcon.7ab2964bea',
'Unread agent completion'
)
t('auto.components.tab.bar.TerminalTabLeadingIcon.7ab2964bea', 'Unread agent completion')
: getWorktreeStatusLabel(badge)
// Why: the outer hit target owns the tooltip because the overlaid pip ignores pointer events.
return (
@@ -3,7 +3,7 @@ import { Switch } from '@/components/ui/switch'
import { useAppStore } from '@/store'
import type { ContextualTourStepControl } from '../../../../shared/contextual-tours'
import { CONTEXTUAL_TOUR_ENABLE_AUTO_WORKSPACE_NAME_EVENT } from './contextual-tour-composer-events'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
export function ContextualTourControl({
control
@@ -29,6 +29,7 @@ export function toggleAutoRenameBranchFromWork(args: {
}
function AutoRenameBranchFromWorkControl(): JSX.Element {
const t = useTranslate()
const enabled = useAppStore((s) => s.settings?.autoRenameBranchFromWork === true)
const updateSettings = useAppStore((s) => s.updateSettings)
@@ -37,7 +38,7 @@ function AutoRenameBranchFromWorkControl(): JSX.Element {
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<div className="text-xs font-medium text-foreground">
{translate(
{t(
'auto.components.contextual.tours.ContextualTourControl.731c5573df',
'Auto-name from first message'
)}
@@ -45,7 +46,7 @@ function AutoRenameBranchFromWorkControl(): JSX.Element {
</div>
<Switch
checked={enabled}
aria-label={translate(
aria-label={t(
'auto.components.contextual.tours.ContextualTourControl.186eecc34f',
'Auto-name workspace from first agent message'
)}
@@ -27,7 +27,7 @@ import {
watchContextualTourFloatingPosition,
type ContextualTourFloatingPosition
} from './contextual-tour-floating-position'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router'
import { hasVisibleOverlay } from '@/lib/visible-overlay'
import { getMeasurableContextualTourTarget } from './contextual-tour-gate'
@@ -111,6 +111,7 @@ export function ContextualTourOverlaySurface({
onStepAction,
onOverlayKeyDownCapture
}: ContextualTourOverlaySurfaceProps): JSX.Element {
const t = useTranslate()
const arrowRef = useRef<SVGSVGElement | null>(null)
const [floatingPosition, setFloatingPosition] = useState<ContextualTourFloatingPosition | null>(
null
@@ -133,11 +134,8 @@ export function ContextualTourOverlaySurface({
const defaultPrimaryAction = {
kind: renderState.isLastStep ? 'complete' : 'next',
label: renderState.isLastStep
? translate('auto.components.contextual.tours.ContextualTourOverlaySurface.complete', 'Done')
: translate(
'auto.components.contextual.tours.contextual.tour.overlay.measurement.38b3155418',
'Next'
)
? t('auto.components.contextual.tours.ContextualTourOverlaySurface.complete', 'Done')
: t('auto.components.contextual.tours.contextual.tour.overlay.measurement.38b3155418', 'Next')
} satisfies ContextualTourStepAction
const primaryAction =
renderState.primaryAction ?? (renderState.hidePrimaryAction ? null : defaultPrimaryAction)
@@ -201,11 +199,11 @@ export function ContextualTourOverlaySurface({
data-contextual-tour-dismiss=""
aria-label={
renderState.isLastStep
? translate(
? t(
'auto.components.contextual.tours.ContextualTourOverlaySurface.d974f32a83',
'Dismiss tour'
)
: translate(
: t(
'auto.components.contextual.tours.ContextualTourOverlaySurface.4f86e2a10b',
'Skip tour'
)
@@ -217,9 +215,7 @@ export function ContextualTourOverlaySurface({
</Button>
<h2 className="flex items-center gap-2 pr-6 text-sm font-semibold tracking-tight text-foreground">
{renderState.badge === 'new' ? (
<Badge variant="secondary">
{translate('components.contextualTours.badgeNew', 'New')}
</Badge>
<Badge variant="secondary">{t('components.contextualTours.badgeNew', 'New')}</Badge>
) : null}
<span className="min-w-0">{renderState.title}</span>
</h2>
@@ -236,14 +232,14 @@ export function ContextualTourOverlaySurface({
type="button"
variant="ghost"
size="xs"
aria-label={translate(
aria-label={t(
'auto.components.contextual.tours.ContextualTourOverlaySurface.4a9568f773',
'Back'
)}
onClick={onBack}
>
<ArrowLeft />
{translate(
{t(
'auto.components.contextual.tours.ContextualTourOverlaySurface.4a9568f773',
'Back'
)}
@@ -1,6 +1,6 @@
import type { JSX } from 'react'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
export function ContextualTourProgressDots({
current,
@@ -9,6 +9,7 @@ export function ContextualTourProgressDots({
current: number
total: number
}): JSX.Element {
const t = useTranslate()
if (total <= 1) {
return <span aria-hidden="true" className="h-1.5 w-4" />
}
@@ -19,7 +20,7 @@ export function ContextualTourProgressDots({
aria-valuemin={1}
aria-valuemax={total}
aria-valuenow={current}
aria-label={translate(
aria-label={t(
'auto.components.contextual.tours.ContextualTourProgressDots.dcd6e6b03e',
'Step {{value0}} of {{value1}}',
{ value0: current, value1: total }
@@ -45,7 +46,7 @@ export function ContextualTourProgressDots({
})}
</span>
<span className="whitespace-nowrap text-[11px] font-medium leading-none text-muted-foreground">
{translate(
{t(
'auto.components.contextual.tours.ContextualTourProgressDots.stepOfTotal',
'{{current}} of {{total}}',
{ current, total }
@@ -21,7 +21,8 @@ import {
type CrashReportRecord
} from '../../../../shared/crash-reporting'
import type { GitHubViewer } from '../../../../shared/github/pull-request-types'
import { getIntlLocale, translate } from '@/i18n/i18n'
import { translate } from '@/i18n/i18n'
import { useIntlLocale, useTranslate, type Translate } from '@/i18n/use-translate'
import {
CRASH_REPORT_SUBMIT_FAILURE_TOAST_ID,
getCrashReportCopySubmissionFailure,
@@ -30,74 +31,68 @@ import {
} from './crash-report-submit-notice'
import { useCrashReportCopy } from './use-crash-report-copy'
function formatSummary(report: CrashReportRecord): string {
function formatSummary(report: CrashReportRecord, t: Translate = translate): string {
if (isReactErrorBoundaryReport(report)) {
const surface = typeof report.details.surface === 'string' ? report.details.surface : null
return surface
? translate(
? t(
'auto.components.crash.report.CrashReportDialogSurface.renderErrorIn',
'React render error in {{surface}}',
{ surface }
)
: translate(
'auto.components.crash.report.CrashReportDialogSurface.renderError',
'React render error'
)
: t('auto.components.crash.report.CrashReportDialogSurface.renderError', 'React render error')
}
return `${report.processType} ${report.reason}${
report.exitCode === null ? '' : ` (exit ${report.exitCode})`
}`
}
function getDialogTitle(report: CrashReportRecord | null): string {
function getDialogTitle(report: CrashReportRecord | null, t: Translate = translate): string {
if (!report) {
return translate(
'auto.components.crash.report.CrashReportDialogSurface.reportTitle',
'Report a crash'
)
return t('auto.components.crash.report.CrashReportDialogSurface.reportTitle', 'Report a crash')
}
return report && isReactErrorBoundaryReport(report)
? translate(
? t(
'auto.components.crash.report.CrashReportDialogSurface.uiErrorTitle',
'Orca hit a recoverable UI error'
)
: translate(
: t(
'auto.components.crash.report.CrashReportDialogSurface.closedTitle',
'Orca closed unexpectedly'
)
}
function getDialogDescription(report: CrashReportRecord | null): string {
function getDialogDescription(report: CrashReportRecord | null, t: Translate = translate): string {
if (!report) {
return translate(
return t(
'auto.components.crash.report.CrashReportDialogSurface.reportDescription',
'Send a privacy-safe crash report. Recent redacted diagnostic logs are included when available.'
)
}
return report && isReactErrorBoundaryReport(report)
? translate(
? t(
'auto.components.crash.report.CrashReportDialogSurface.uiErrorDescription',
'Send a privacy-safe diagnostic report to help us understand the failed UI surface.'
)
: translate(
: t(
'auto.components.crash.report.CrashReportDialogSurface.closedDescription',
'Send a privacy-safe diagnostic report to help us understand what happened.'
)
}
function getNotesPlaceholder(report: CrashReportRecord | null): string {
function getNotesPlaceholder(report: CrashReportRecord | null, t: Translate = translate): string {
if (!report) {
return translate(
return t(
'auto.components.crash.report.CrashReportDialogSurface.reportNotesPlaceholder',
'Optional: what happened?'
)
}
return report && isReactErrorBoundaryReport(report)
? translate(
? t(
'auto.components.crash.report.CrashReportDialogSurface.uiErrorNotesPlaceholder',
'Optional: what were you doing before this UI error?'
)
: translate(
: t(
'auto.components.crash.report.CrashReportDialogSurface.closedNotesPlaceholder',
'Optional: what were you doing before Orca closed?'
)
@@ -118,6 +113,8 @@ export function CrashReportDialogSurface({
onOpenChange,
onReportChange
}: CrashReportDialogSurfaceProps): React.JSX.Element {
const t = useTranslate()
const intlLocale = useIntlLocale()
const mountedRef = useMountedRef()
const [notes, setNotes] = useState('')
const [includeDiagnosticLogs, setIncludeDiagnosticLogs] = useState(true)
@@ -230,10 +227,7 @@ export function CrashReportDialogSurface({
toast.warning(warningNotice.title, { description: warningNotice.description })
} else {
toast.success(
translate(
'auto.components.crash.report.CrashReportDialog.8e24fe4f75',
'Crash report sent.'
)
t('auto.components.crash.report.CrashReportDialog.8e24fe4f75', 'Crash report sent.')
)
}
onOpenChange(false)
@@ -270,26 +264,28 @@ export function CrashReportDialogSurface({
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-sm">
<AlertTriangle className="size-4 text-destructive" />
{getDialogTitle(report)}
{getDialogTitle(report, t)}
</DialogTitle>
<DialogDescription className="text-xs">{getDialogDescription(report)}</DialogDescription>
<DialogDescription className="text-xs">
{getDialogDescription(report, t)}
</DialogDescription>
</DialogHeader>
<div className="min-w-0 space-y-3">
{report ? (
<>
<div className="rounded-md border border-border/70 bg-muted/30 p-3 text-xs">
<div className="font-medium text-foreground">{formatSummary(report)}</div>
<div className="font-medium text-foreground">{formatSummary(report, t)}</div>
<div className="mt-1 text-muted-foreground">
{new Date(report.createdAt).toLocaleString(getIntlLocale())} · {report.platform}{' '}
{new Date(report.createdAt).toLocaleString(intlLocale)} · {report.platform}{' '}
{report.arch} ·{' '}
{translate('auto.components.crash.report.CrashReportDialog.835037edc9', 'Orca')}{' '}
{t('auto.components.crash.report.CrashReportDialog.835037edc9', 'Orca')}{' '}
{report.appVersion}
</div>
</div>
<div className="min-w-0 space-y-1.5">
<div className="text-[11px] font-medium text-muted-foreground">
{translate(
{t(
'auto.components.crash.report.CrashReportDialog.6d3ebe216a',
'Diagnostic text'
)}
@@ -302,11 +298,11 @@ export function CrashReportDialogSurface({
) : (
<div className="rounded-md border border-border/70 bg-muted/30 p-3 text-xs text-muted-foreground">
{loading
? translate(
? t(
'auto.components.crash.report.CrashReportDialog.765591798d',
'Checking for crash reports...'
)
: translate(
: t(
'auto.components.crash.report.CrashReportDialog.ead6fc0510',
'No automatic crash report was captured. You can still send details and include recent diagnostic logs when available.'
)}
@@ -319,15 +315,15 @@ export function CrashReportDialogSurface({
rows={4}
// Keep the UI and formatter on the same input budget.
maxLength={MAX_USER_NOTES_LENGTH}
placeholder={getNotesPlaceholder(report)}
placeholder={getNotesPlaceholder(report, t)}
className="min-h-24 w-full rounded-md border border-border bg-background px-3 py-2 text-sm outline-none ring-offset-background placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
/>
<div
aria-hidden="true"
className="text-right text-[11px] tabular-nums text-muted-foreground"
>
{notes.length.toLocaleString(getIntlLocale())} /{' '}
{MAX_USER_NOTES_LENGTH.toLocaleString(getIntlLocale())}
{notes.length.toLocaleString(intlLocale)} /{' '}
{MAX_USER_NOTES_LENGTH.toLocaleString(intlLocale)}
</div>
</div>
<div className="flex items-start gap-2 rounded-md border border-border/70 bg-muted/20 p-3">
@@ -340,13 +336,13 @@ export function CrashReportDialogSurface({
/>
<div className="space-y-1">
<Label htmlFor="crash-report-attach-diagnostics" className="text-xs">
{translate(
{t(
'auto.components.crash.report.CrashReportDialog.b082f27490',
'Attach recent diagnostic logs'
)}
</Label>
<div className="text-xs leading-5 text-muted-foreground">
{translate(
{t(
'auto.components.crash.report.CrashReportDialog.e59f0b9427',
'Sends a capped redacted log bundle with the report.'
)}
@@ -364,7 +360,7 @@ export function CrashReportDialogSurface({
disabled={loading}
>
<Clipboard className="size-3.5" />
{translate('auto.components.crash.report.CrashReportDialog.50b00dc327', 'Copy Details')}
{t('auto.components.crash.report.CrashReportDialog.50b00dc327', 'Copy Details')}
</Button>
<Button
type="button"
@@ -373,11 +369,11 @@ export function CrashReportDialogSurface({
onClick={handleDismiss}
disabled={submitting}
>
{translate('auto.components.crash.report.CrashReportDialog.88fea8e84e', "Don't Send")}
{t('auto.components.crash.report.CrashReportDialog.88fea8e84e', "Don't Send")}
</Button>
<Button type="button" size="sm" onClick={handleSubmit} disabled={loading || submitting}>
<Send className="size-3.5" />
{translate('auto.components.crash.report.CrashReportDialog.b4951cd27c', 'Send Report')}
{t('auto.components.crash.report.CrashReportDialog.b4951cd27c', 'Send Report')}
</Button>
</DialogFooter>
</DialogContent>
@@ -1,6 +1,6 @@
import { X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import type { DashboardFilters, DashboardReviewFilter } from './agent-board-filtering'
type FilterLabel = { id: string; label: string }
@@ -12,13 +12,14 @@ function ActiveChip({
label: string
onRemove: () => void
}): React.JSX.Element {
const t = useTranslate()
return (
<span className="inline-flex h-[22px] items-center gap-1 rounded-full border border-border bg-muted/55 pr-1 pl-2 text-[11px]">
{label}
<button
type="button"
onClick={onRemove}
aria-label={translate('dashboardPopout.filters.remove', 'Remove {{label}} filter', {
aria-label={t('dashboardPopout.filters.remove', 'Remove {{label}} filter', {
label
})}
className="rounded-full text-muted-foreground hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
@@ -48,10 +49,11 @@ export function AgentDashboardFilterChips({
onReviewToggle: (id: DashboardReviewFilter) => void
onClear: () => void
}): React.JSX.Element {
const t = useTranslate()
return (
<div className="flex shrink-0 flex-wrap items-center gap-1 border-b border-border px-3 py-2">
<span className="mr-0.5 text-[10px] font-semibold uppercase tracking-[0.05em] text-muted-foreground">
{translate('dashboardPopout.filters.active', 'Filters')}
{t('dashboardPopout.filters.active', 'Filters')}
</span>
{filters.projects.map((id) => (
<ActiveChip
@@ -80,7 +82,7 @@ export function AgentDashboardFilterChips({
onClick={onClear}
className="h-[22px] px-1 text-[11px] text-muted-foreground"
>
{translate('dashboardPopout.filters.clear', 'Clear')}
{t('dashboardPopout.filters.clear', 'Clear')}
</Button>
</div>
)
@@ -10,7 +10,7 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { Input } from '@/components/ui/input'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
import { getWorkspaceStatusVisualMeta } from '../sidebar/workspace-status/presentation/workspace-status'
import type { DashboardCard, DashboardFilterOptions } from '../../../../shared/dashboard-snapshot'
@@ -55,6 +55,7 @@ export function AgentDashboardToolbar({
searchInputRef,
filterControl
}: AgentDashboardToolbarProps): React.JSX.Element {
const t = useTranslate()
const isMac = navigator.userAgent.includes('Mac')
const projects = projectOptions(cards, filterOptions?.projects)
const statuses = workspaceStatusOptions(cards, filterOptions?.workspaceStatuses)
@@ -76,7 +77,7 @@ export function AgentDashboardToolbar({
onFiltersChange({ projects: [], workspaceStatuses: [], reviewStates: [] })
}
const reviewLabel = (id: DashboardReviewFilter): string =>
translate('dashboardPopout.filters.reviewChip', 'Review: {{state}}', {
t('dashboardPopout.filters.reviewChip', 'Review: {{state}}', {
state: reviewStateLabel(id)
})
@@ -89,11 +90,11 @@ export function AgentDashboardToolbar({
ref={searchInputRef}
value={query}
onChange={(event) => onQueryChange(event.target.value)}
placeholder={translate(
placeholder={t(
'dashboardPopout.search.placeholder',
'Search worktree, project, or agent…'
)}
aria-label={translate('dashboardPopout.search.label', 'Search agents')}
aria-label={t('dashboardPopout.search.label', 'Search agents')}
className="h-7 bg-muted/55 pr-16 pl-7 text-xs"
/>
{query ? (
@@ -101,7 +102,7 @@ export function AgentDashboardToolbar({
variant="ghost"
size="icon-xs"
onClick={() => onQueryChange('')}
aria-label={translate('dashboardPopout.search.clear', 'Clear search')}
aria-label={t('dashboardPopout.search.clear', 'Clear search')}
className="absolute top-1/2 right-0.5 -translate-y-1/2 text-muted-foreground"
>
<X className="size-3" />
@@ -117,7 +118,7 @@ export function AgentDashboardToolbar({
</div>
{!filterControl && (query || activeCount > 0) ? (
<span className="shrink-0 text-[11px] tabular-nums text-muted-foreground">
{translate('dashboardPopout.search.results', '{{shown}} of {{total}} shown', {
{t('dashboardPopout.search.results', '{{shown}} of {{total}} shown', {
shown: filteredCount,
total: cards.length
})}
@@ -135,7 +136,7 @@ export function AgentDashboardToolbar({
)}
>
<Filter className="size-3" />
{translate('dashboardPopout.filters.label', 'Filter')}
{t('dashboardPopout.filters.label', 'Filter')}
{activeCount > 0 ? (
<span className="rounded-full bg-foreground px-1.5 py-px text-[10px] leading-none text-background">
{activeCount}
@@ -146,7 +147,7 @@ export function AgentDashboardToolbar({
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-64" sideOffset={6}>
<DropdownMenuLabel>
{translate('dashboardPopout.filters.project', 'Project')}
{t('dashboardPopout.filters.project', 'Project')}
</DropdownMenuLabel>
{projects.map((option) => (
<DropdownMenuCheckboxItem
@@ -161,14 +162,17 @@ export function AgentDashboardToolbar({
))}
<DropdownMenuSeparator />
<DropdownMenuLabel>
{translate('dashboardPopout.filters.workspaceStatus', 'Workspace status')}
{t('dashboardPopout.filters.workspaceStatus', 'Workspace status')}
</DropdownMenuLabel>
{statuses.map((option) => {
const meta = getWorkspaceStatusVisualMeta({
id: option.id,
label: option.label,
color: option.color
})
const meta = getWorkspaceStatusVisualMeta(
{
id: option.id,
label: option.label,
color: option.color
},
t
)
return (
<DropdownMenuCheckboxItem
key={option.id}
@@ -184,7 +188,7 @@ export function AgentDashboardToolbar({
})}
<DropdownMenuSeparator />
<DropdownMenuLabel>
{translate('dashboardPopout.filters.reviewStatus', 'PR / MR status')}
{t('dashboardPopout.filters.reviewStatus', 'PR / MR status')}
</DropdownMenuLabel>
{REVIEW_OPTIONS.map((option) => (
<DropdownMenuCheckboxItem
@@ -193,7 +197,7 @@ export function AgentDashboardToolbar({
onCheckedChange={() => toggleReview(option)}
onSelect={(event) => event.preventDefault()}
>
<span>{reviewStateLabel(option)}</span>
<span>{reviewStateLabel(option, t)}</span>
<FilterOptionCount count={reviewCounts.get(option) ?? 0} />
</DropdownMenuCheckboxItem>
))}
@@ -204,7 +208,7 @@ export function AgentDashboardToolbar({
className="text-muted-foreground"
>
<X className="size-3.5" />
{translate('dashboardPopout.filters.clearAll', 'Clear all filters')}
{t('dashboardPopout.filters.clearAll', 'Clear all filters')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -20,6 +20,7 @@ import {
} from './agent-board-filtering'
import './agent-board-transitions.css'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import type { AgentSubjectReadIntent } from '@/attention/agent-subject-read-actions'
/** Relay acknowledgments to the main renderer; optional calls tolerate dev-HMR preload skew until restart. */
@@ -32,16 +33,16 @@ function revealAgentViaPopoutRelay(args: AgentRevealArgs): void {
void window.api.dashboard.revealAgent?.(args)
}
function bucketLabel(bucket: DashboardBucket): string {
function bucketLabel(bucket: DashboardBucket, t: Translate = translate): string {
switch (bucket) {
case 'attention':
return translate('dashboardPopout.bucket.attention', 'Needs You')
return t('dashboardPopout.bucket.attention', 'Needs You')
case 'working':
return translate('dashboardPopout.bucket.working', 'Working')
return t('dashboardPopout.bucket.working', 'Working')
case 'done':
return translate('dashboardPopout.bucket.done', 'Done')
return t('dashboardPopout.bucket.done', 'Done')
case 'idle':
return translate('dashboardPopout.bucket.idle', 'Idle')
return t('dashboardPopout.bucket.idle', 'Idle')
}
}
@@ -75,7 +76,8 @@ function KanbanColumn({
now: number
onOpenTerminal: (card: DashboardCard) => void
}): React.JSX.Element {
const label = bucketLabel(bucket)
const t = useTranslate()
const label = bucketLabel(bucket, t)
return (
// Cards carry attention color; keep the column neutral to avoid duplicate signaling.
@@ -94,7 +96,7 @@ function KanbanColumn({
<div className="scrollbar-sleek flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto px-2 pb-2">
{cards.length === 0 ? (
<p className="px-1 py-2 text-[11px] text-muted-foreground">
{translate('dashboardPopout.bucket.empty', 'None')}
{t('dashboardPopout.bucket.empty', 'None')}
</p>
) : (
cards.map((card) => (
@@ -137,6 +139,7 @@ export function AgentKanbanBoard({
onBrowserForeground,
headerActions
}: AgentKanbanBoardProps): React.JSX.Element {
const t = useTranslate()
const visibleBuckets = useMemo(
() =>
DASHBOARD_BUCKET_ORDER.filter((bucket) => bucket !== 'idle' || snapshot.showIdle === true),
@@ -242,11 +245,9 @@ export function AgentKanbanBoard({
className={cn('relative flex flex-col bg-background text-foreground', containerClassName)}
>
<div className="flex shrink-0 items-center gap-2 border-b border-border px-4 py-2.5">
<h1 className="text-[13px] font-semibold">
{translate('dashboardPopout.title', 'Agents')}
</h1>
<h1 className="text-[13px] font-semibold">{t('dashboardPopout.title', 'Agents')}</h1>
<span className="text-[11px] text-muted-foreground">
{translate('dashboardPopout.total', '{{count}} total', {
{t('dashboardPopout.total', '{{count}} total', {
count: visibleCards.length
})}
</span>
@@ -257,7 +258,7 @@ export function AgentKanbanBoard({
<button
type="button"
onClick={onClose}
aria-label={translate('dashboardPopout.close', 'Close dashboard')}
aria-label={t('dashboardPopout.close', 'Close dashboard')}
className="rounded-sm p-1 text-muted-foreground opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
>
<XIcon className="size-4" />
@@ -20,22 +20,23 @@ import {
} from '../../../../shared/dashboard-snapshot'
import type { RepoIcon } from '../../../../shared/repo-icon'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { DashboardHostBadge } from './DashboardHostBadge'
function formatStartedAgo(startedAt: number, now: number): string {
function formatStartedAgo(startedAt: number, now: number, t: Translate = translate): string {
const seconds = Math.max(0, Math.floor((now - startedAt) / 1000))
if (seconds < 60) {
return translate('dashboardPopout.card.time.justNow', 'just now')
return t('dashboardPopout.card.time.justNow', 'just now')
}
const minutes = Math.floor(seconds / 60)
if (minutes < 60) {
return translate('dashboardPopout.card.time.minutes', '{{count}}m', { count: minutes })
return t('dashboardPopout.card.time.minutes', '{{count}}m', { count: minutes })
}
const hours = Math.floor(minutes / 60)
if (hours < 24) {
return translate('dashboardPopout.card.time.hours', '{{count}}h', { count: hours })
return t('dashboardPopout.card.time.hours', '{{count}}h', { count: hours })
}
return translate('dashboardPopout.card.time.days', '{{count}}d', {
return t('dashboardPopout.card.time.days', '{{count}}d', {
count: Math.floor(hours / 24)
})
}
@@ -45,8 +46,8 @@ function displayTimestamp(card: DashboardCard): number {
return card.finishedAt ?? card.startedAt
}
function formatSubagentCount(count: number): string {
return translate('dashboardPopout.card.subagents', '{{count}} subagents', {
function formatSubagentCount(count: number, t: Translate = translate): string {
return t('dashboardPopout.card.subagents', '{{count}} subagents', {
count,
defaultValue_one: '{{count}} subagent'
})
@@ -133,6 +134,7 @@ const REVIEW_PRESENTATION = {
} as const
function ReviewPill({ card }: { card: DashboardCard }): React.JSX.Element | null {
const t = useTranslate()
if (!card.review) {
return null
}
@@ -141,13 +143,13 @@ function ReviewPill({ card }: { card: DashboardCard }): React.JSX.Element | null
const title = (() => {
switch (card.review.state) {
case 'open':
return translate('dashboardPopout.card.review.open', 'Open review')
return t('dashboardPopout.card.review.open', 'Open review')
case 'draft':
return translate('dashboardPopout.card.review.draft', 'Draft review')
return t('dashboardPopout.card.review.draft', 'Draft review')
case 'merged':
return translate('dashboardPopout.card.review.merged', 'Merged review')
return t('dashboardPopout.card.review.merged', 'Merged review')
case 'closed':
return translate('dashboardPopout.card.review.closed', 'Closed review')
return t('dashboardPopout.card.review.closed', 'Closed review')
}
})()
return (
@@ -198,6 +200,7 @@ export const AgentKanbanCard = memo(
now,
onOpenTerminal
}: AgentKanbanCardProps): React.JSX.Element {
const t = useTranslate()
useTranslation()
const [subagentsOpen, setSubagentsOpen] = useState(false)
// Tint only attention and done so scan-worthy outcomes retain distinct meaning.
@@ -247,7 +250,7 @@ export const AgentKanbanCard = memo(
{card.lastUserMessage ? (
<div className="line-clamp-1 text-[11px] leading-snug text-muted-foreground">
<span className="font-medium text-foreground/45">
{translate('dashboardPopout.card.you', 'You')}
{t('dashboardPopout.card.you', 'You')}
</span>{' '}
{card.lastUserMessage}
</div>
@@ -286,7 +289,7 @@ export const AgentKanbanCard = memo(
<ChevronRight
className={cn('size-3 transition-transform', subagentsOpen && 'rotate-90')}
/>
{formatSubagentCount(card.subagents.length)}
{formatSubagentCount(card.subagents.length, t)}
</button>
{subagentsOpen ? (
<div className="ml-1 flex flex-col gap-1 border-l border-border pl-2">
@@ -332,7 +335,7 @@ export const AgentKanbanCard = memo(
<ReviewPill card={card} />
{displayTimestamp(card) > 0 ? (
<span className="ml-auto shrink-0 pl-1 tabular-nums">
{formatStartedAgo(displayTimestamp(card), now)}
{formatStartedAgo(displayTimestamp(card), now, t)}
</span>
) : null}
</button>
@@ -12,7 +12,7 @@ import {
} from '../../../../shared/dashboard-snapshot'
import { AgentTerminalPreview } from './AgentTerminalPreview'
import { terminalPreviewUnavailableMessage } from './terminal-preview-unavailable-message'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
/** Routing payload for focusing an agent's pane in the main window. */
@@ -41,6 +41,7 @@ function AgentTerminalFrame({
onReveal,
onBrowserForeground
}: AgentTerminalFrameProps): React.JSX.Element {
const t = useTranslate()
const reveal = (): void => {
onReveal({
repoId: card.repoId,
@@ -71,7 +72,7 @@ function AgentTerminalFrame({
onClick={() => onOpenChange(false)}
>
<XIcon className="size-4" />
<span className="sr-only">{translate('dashboardPopout.terminal.close', 'Close')}</span>
<span className="sr-only">{t('dashboardPopout.terminal.close', 'Close')}</span>
</Button>
</div>
{card.ptyId ? (
@@ -101,13 +102,13 @@ function AgentTerminalFrame({
/>
) : (
<div className="min-h-0 flex-1 px-2.5 pb-2 text-[11px] text-muted-foreground">
{terminalPreviewUnavailableMessage({ hostKind: card.hostKind })}
{terminalPreviewUnavailableMessage({ hostKind: card.hostKind }, t)}
</div>
)}
<div className="flex shrink-0 items-center gap-1.5 px-2.5 py-1.5">
<Button type="button" variant="outline" size="xs" className="ml-auto" onClick={reveal}>
<SquareArrowOutUpRight className="size-3" />
{translate('dashboardPopout.terminal.focusWorktree', 'Open worktree')}
{t('dashboardPopout.terminal.focusWorktree', 'Open worktree')}
</Button>
</div>
</>
@@ -35,6 +35,7 @@ import {
} from '@/components/link-actions/link-action-request'
import { installDashboardPreviewLinkInteraction } from './dashboard-preview-link-interaction'
import type { DashboardPreviewLinkSource } from './dashboard-preview-http-actions'
import { useTranslate } from '@/i18n/use-translate'
const PREVIEW_SCROLLBACK_ROWS = 24
// Preview snapshots bound history; pane scrollback would only cost memory.
@@ -64,6 +65,7 @@ export function AgentTerminalPreview({
linkSource?: DashboardPreviewLinkSource
onBrowserForeground?: () => void
}): React.JSX.Element {
const t = useTranslate()
const containerRef = useRef<HTMLDivElement>(null)
const terminalRef = useRef<Terminal | null>(null)
const [linkRequest, setLinkRequest] = useState<LinkActionRequest | null>(null)
@@ -383,7 +385,7 @@ export function AgentTerminalPreview({
) : null}
{ptyGone ? (
<div className="absolute inset-0 flex items-center justify-center px-2.5 py-8 text-center text-[11px] text-muted-foreground">
{terminalPreviewUnavailableMessage({ ptyId })}
{terminalPreviewUnavailableMessage({ ptyId }, t)}
</div>
) : null}
<div
@@ -1,6 +1,7 @@
import { Server } from 'lucide-react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
import type { DashboardCardHostKind } from '../../../../shared/dashboard-snapshot'
import { parseExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host'
@@ -25,23 +26,26 @@ function fallbackHostLabel(executionHostId: ExecutionHostId | undefined): string
return null
}
export function dashboardHostTooltipLabel({
hostKind,
executionHostId,
hostLabel
}: Pick<DashboardHostBadgeProps, 'hostKind' | 'executionHostId' | 'hostLabel'>): string | null {
export function dashboardHostTooltipLabel(
{
hostKind,
executionHostId,
hostLabel
}: Pick<DashboardHostBadgeProps, 'hostKind' | 'executionHostId' | 'hostLabel'>,
t: Translate = translate
): string | null {
if (hostKind !== 'ssh' && hostKind !== 'remote') {
return null
}
const label = hostLabel?.trim() || fallbackHostLabel(executionHostId)
if (hostKind === 'ssh') {
return label
? translate('dashboardPopout.host.sshNamed', 'SSH host · {{host}}', { host: label })
: translate('dashboardPopout.host.ssh', 'SSH host')
? t('dashboardPopout.host.sshNamed', 'SSH host · {{host}}', { host: label })
: t('dashboardPopout.host.ssh', 'SSH host')
}
return label
? translate('dashboardPopout.host.remoteNamed', 'Remote Orca host · {{host}}', { host: label })
: translate('dashboardPopout.host.remote', 'Remote Orca host')
? t('dashboardPopout.host.remoteNamed', 'Remote Orca host · {{host}}', { host: label })
: t('dashboardPopout.host.remote', 'Remote Orca host')
}
export function DashboardHostBadge({
@@ -52,7 +56,8 @@ export function DashboardHostBadge({
className,
iconClassName
}: DashboardHostBadgeProps): React.JSX.Element | null {
const tooltipLabel = dashboardHostTooltipLabel({ hostKind, executionHostId, hostLabel })
const t = useTranslate()
const tooltipLabel = dashboardHostTooltipLabel({ hostKind, executionHostId, hostLabel }, t)
if (!tooltipLabel) {
return null
}
@@ -1,4 +1,5 @@
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import type { DashboardCard, DashboardFilterOption } from '../../../../shared/dashboard-snapshot'
import type { DashboardReviewFilter } from './agent-board-filtering'
@@ -78,17 +79,17 @@ export const REVIEW_OPTIONS: readonly DashboardReviewFilter[] = [
'none'
]
export function reviewStateLabel(state: DashboardReviewFilter): string {
export function reviewStateLabel(state: DashboardReviewFilter, t: Translate = translate): string {
switch (state) {
case 'open':
return translate('dashboardPopout.filters.review.open', 'Open')
return t('dashboardPopout.filters.review.open', 'Open')
case 'draft':
return translate('dashboardPopout.filters.review.draft', 'Draft')
return t('dashboardPopout.filters.review.draft', 'Draft')
case 'merged':
return translate('dashboardPopout.filters.review.merged', 'Merged')
return t('dashboardPopout.filters.review.merged', 'Merged')
case 'closed':
return translate('dashboardPopout.filters.review.closed', 'Closed')
return t('dashboardPopout.filters.review.closed', 'Closed')
case 'none':
return translate('dashboardPopout.filters.review.none', 'No review')
return t('dashboardPopout.filters.review.none', 'No review')
}
}
@@ -1,24 +1,28 @@
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import type { DashboardCardHostKind } from '../../../../shared/dashboard-snapshot'
import { parseAppSshPtyId } from '../../../../shared/ssh-pty-id'
import { isRemoteRuntimePtyId } from '../../../../shared/remote-runtime/remote-runtime-pty-id'
/** Remote snapshot absence cannot prove pane exit because the client does not own the PTY; see docs/reference/ssh-execution-boundary.md. */
export function terminalPreviewUnavailableMessage(source: {
ptyId?: string | null
hostKind?: DashboardCardHostKind
}): string {
export function terminalPreviewUnavailableMessage(
source: {
ptyId?: string | null
hostKind?: DashboardCardHostKind
},
t: Translate = translate
): string {
const isRemote =
source.hostKind === 'ssh' ||
source.hostKind === 'remote' ||
isRemoteRuntimePtyId(source.ptyId) ||
(typeof source.ptyId === 'string' && parseAppSshPtyId(source.ptyId) !== null)
return isRemote
? translate(
? t(
'dashboardPopout.terminal.remotePreviewUnavailable',
'No preview for this remote session — open the workspace to view the terminal.'
)
: translate(
: t(
'dashboardPopout.terminal.previewUnavailable',
'No preview available — open the workspace to view this agent.'
)
@@ -17,7 +17,7 @@ import {
import { AgentDashboardSettingsMenu } from './AgentDashboardSettingsMenu'
import { useLiveDashboardSnapshot } from './useLiveDashboardSnapshot'
import { useAgentDashboardTelemetry } from '../dashboard-popout/use-agent-dashboard-telemetry'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { trackSidebarLiveWidth } from '@/components/sidebar/sidebar-live-width'
// Nested interactive overlays own Escape before the companion sheet; exclude the sheet’s own role=dialog marker.
@@ -94,6 +94,7 @@ export function AgentDashboardDrawer({
leftSidebarStyle,
statusBarVisible
}: AgentDashboardDrawerProps): React.JSX.Element {
const t = useTranslate()
const open = useAppStore((s) => s.agentDashboardDrawerOpen)
const setOpen = useAppStore((s) => s.setAgentDashboardDrawerOpen)
const sidebarOpen = useAppStore((s) => s.sidebarOpen)
@@ -243,7 +244,7 @@ export function AgentDashboardDrawer({
onPointerDownOutside={guardSidebarInteraction}
onInteractOutside={guardSidebarInteraction}
>
<SheetTitle className="sr-only">{translate('dashboardPopout.title', 'Agents')}</SheetTitle>
<SheetTitle className="sr-only">{t('dashboardPopout.title', 'Agents')}</SheetTitle>
{/* Closed SheetContent unmounts the live snapshot body, avoiding closed-path derivation. */}
<div ref={boardRef} className="flex min-h-0 flex-1 flex-col">
<AgentDashboardDrawerBody
@@ -1,5 +1,5 @@
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { Label } from '../ui/label'
import { SearchableSetting } from '../settings/section-layout/SearchableSetting'
import {
@@ -18,33 +18,28 @@ export function AgentDashboardExperimentalSetting({
settings,
updateSettings
}: AgentDashboardExperimentalSettingProps): React.JSX.Element {
const t = useTranslate()
const enabled = settings.experimentalAgentDashboardPopout === true
const mode = settings.experimentalAgentDashboardMode ?? 'in-window'
return (
<SearchableSetting
title={translate(
'auto.components.settings.ExperimentalPane.agentDashboard.title',
'Agent Dashboard'
)}
description={translate(
title={t('auto.components.settings.ExperimentalPane.agentDashboard.title', 'Agent Dashboard')}
description={t(
'auto.components.settings.ExperimentalPane.agentDashboard.description',
'Kanban board for monitoring agents across worktrees, in-window or as a pop-out.'
)}
keywords={getExperimentalSearchEntry().agentDashboard.keywords}
keywords={getExperimentalSearchEntry(t).agentDashboard.keywords}
className="space-y-3 py-3"
id="experimental-agent-dashboard"
>
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 shrink space-y-0.5">
<Label>
{translate(
'auto.components.settings.ExperimentalPane.agentDashboard.title',
'Agent Dashboard'
)}
{t('auto.components.settings.ExperimentalPane.agentDashboard.title', 'Agent Dashboard')}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.ExperimentalPane.agentDashboard.copy',
'Adds an Agent Dashboard entry to the left sidebar. Monitor agents that need you, are working, or are done, with optional idle agents.'
)}
@@ -52,7 +47,7 @@ export function AgentDashboardExperimentalSetting({
</div>
<SettingsSwitch
checked={enabled}
ariaLabel={translate(
ariaLabel={t(
'auto.components.settings.ExperimentalPane.agentDashboard.toggleLabel',
'Toggle Agent Dashboard'
)}
@@ -64,13 +59,10 @@ export function AgentDashboardExperimentalSetting({
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 shrink space-y-0.5">
<Label>
{translate(
'auto.components.settings.ExperimentalPane.agentDashboard.modeLabel',
'Open as'
)}
{t('auto.components.settings.ExperimentalPane.agentDashboard.modeLabel', 'Open as')}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.ExperimentalPane.agentDashboard.modeCopy',
'Show the dashboard as an in-window board beside the sidebar or a separate pop-out window.'
)}
@@ -79,7 +71,7 @@ export function AgentDashboardExperimentalSetting({
<SettingsSegmentedControl
value={mode}
onChange={(next) => updateSettings({ experimentalAgentDashboardMode: next })}
ariaLabel={translate(
ariaLabel={t(
'auto.components.settings.ExperimentalPane.agentDashboard.modeAriaLabel',
'Agent Dashboard open mode'
)}
@@ -87,14 +79,14 @@ export function AgentDashboardExperimentalSetting({
options={[
{
value: 'in-window',
label: translate(
label: t(
'auto.components.settings.ExperimentalPane.agentDashboard.modeInWindow',
'In-window'
)
},
{
value: 'popout',
label: translate(
label: t(
'auto.components.settings.ExperimentalPane.agentDashboard.modePopout',
'Pop-out'
)
@@ -13,7 +13,7 @@ import {
SettingsSwitch
} from '../settings/form-controls/SettingsFormControls'
import type { AgentDashboardMode } from '../../../../shared/ui-chrome-types'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { canOpenAgentDashboardPopout } from './agent-dashboard-open-mode'
type AgentDashboardSettingsMenuProps = {
@@ -28,6 +28,7 @@ export function AgentDashboardSettingsMenu({
onSwitchToPopout,
onOpenChange
}: AgentDashboardSettingsMenuProps): React.JSX.Element {
const t = useTranslate()
const mode = useAppStore((s) => s.settings?.experimentalAgentDashboardMode ?? 'in-window')
const showIdle = useAppStore((s) => s.settings?.experimentalAgentDashboardShowIdle === true)
const updateSettings = useAppStore((s) => s.updateSettings)
@@ -50,7 +51,7 @@ export function AgentDashboardSettingsMenu({
<Button
variant="ghost"
size="icon-xs"
aria-label={translate('dashboardPopout.settingsLabel', 'Agent Dashboard settings')}
aria-label={t('dashboardPopout.settingsLabel', 'Agent Dashboard settings')}
className="text-muted-foreground"
>
<Settings className="size-3.5" />
@@ -58,7 +59,7 @@ export function AgentDashboardSettingsMenu({
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}>
{translate('dashboardPopout.settingsTooltip', 'Board settings')}
{t('dashboardPopout.settingsTooltip', 'Board settings')}
</TooltipContent>
</Tooltip>
<DropdownMenuContent align="end" sideOffset={8} collisionPadding={8} className="w-72 p-2">
@@ -67,13 +68,13 @@ export function AgentDashboardSettingsMenu({
<div className="flex items-start justify-between gap-3 rounded-md px-1.5 py-1.5">
<span className="min-w-0 space-y-0.5">
<span className="block text-[12px] font-medium leading-4 text-foreground">
{translate(
{t(
'auto.components.settings.ExperimentalPane.agentDashboard.modeLabel',
'Open as'
)}
</span>
<span className="block text-[11px] leading-4 text-muted-foreground">
{translate(
{t(
'auto.components.settings.ExperimentalPane.agentDashboard.modeCopy',
'Show the dashboard as an in-window board beside the sidebar or a separate pop-out window.'
)}
@@ -84,7 +85,7 @@ export function AgentDashboardSettingsMenu({
<SettingsSegmentedControl
value={mode}
onChange={handleModeChange}
ariaLabel={translate(
ariaLabel={t(
'auto.components.settings.ExperimentalPane.agentDashboard.modeAriaLabel',
'Agent Dashboard open mode'
)}
@@ -93,14 +94,14 @@ export function AgentDashboardSettingsMenu({
options={[
{
value: 'in-window',
label: translate(
label: t(
'auto.components.settings.ExperimentalPane.agentDashboard.modeInWindow',
'In-window'
)
},
{
value: 'popout',
label: translate(
label: t(
'auto.components.settings.ExperimentalPane.agentDashboard.modePopout',
'Pop-out'
)
@@ -114,10 +115,10 @@ export function AgentDashboardSettingsMenu({
<div className="flex items-start justify-between gap-3 rounded-md px-1.5 py-1.5">
<span className="min-w-0 space-y-0.5">
<span className="block text-[12px] font-medium leading-4 text-foreground">
{translate('dashboardPopout.settings.showIdle', 'Show idle agents')}
{t('dashboardPopout.settings.showIdle', 'Show idle agents')}
</span>
<span className="block text-[11px] leading-4 text-muted-foreground">
{translate(
{t(
'dashboardPopout.settings.showIdleCopy',
'Include agents that have gone quiet for 30 minutes without reporting completion. Hidden by default.'
)}
@@ -130,7 +131,7 @@ export function AgentDashboardSettingsMenu({
experimentalAgentDashboardShowIdle: !showIdle
})
}}
ariaLabel={translate('dashboardPopout.settings.showIdle', 'Show idle agents')}
ariaLabel={t('dashboardPopout.settings.showIdle', 'Show idle agents')}
/>
</div>
</DropdownMenuContent>
@@ -2,7 +2,7 @@ import React, { useCallback } from 'react'
import { ChevronDown } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
type Props = {
childAgentCount?: number
@@ -17,6 +17,7 @@ export function DashboardAgentChildDisclosure({
onToggleChildAgents,
timestamp
}: Props) {
const t = useTranslate()
const hasChildDisclosure =
typeof childAgentCount === 'number' &&
childAgentCount > 0 &&
@@ -66,12 +67,12 @@ export function DashboardAgentChildDisclosure({
className="col-start-1 row-start-1 -mr-1.5"
aria-label={
childAgentsExpanded
? translate(
? t(
'auto.components.dashboard.DashboardAgentChildDisclosure.hideChildAgents',
'Hide {{count}} child agents',
{ count: childAgentCount, defaultValue_one: 'Hide {{count}} child agent' }
)
: translate(
: t(
'auto.components.dashboard.DashboardAgentChildDisclosure.showChildAgents',
'Show {{count}} child agents',
{ count: childAgentCount, defaultValue_one: 'Show {{count}} child agent' }
@@ -37,14 +37,15 @@ import {
agentChildRowNoUpdateLabel
} from '@/components/agent-child-row-text'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { useUiLanguageRevision } from '@/i18n/use-ui-language-revision'
function formatTimeAgo(ts: number, now: number): string {
function formatTimeAgo(ts: number, now: number, t: Translate = translate): string {
const delta = now - ts
if (delta < 60_000) {
return translate('auto.components.dashboard.DashboardAgentRow.justNow', 'just now')
return t('auto.components.dashboard.DashboardAgentRow.justNow', 'just now')
}
return translate('auto.components.dashboard.DashboardAgentRow.timeAgo', '{{duration}} ago', {
return t('auto.components.dashboard.DashboardAgentRow.timeAgo', '{{duration}} ago', {
duration: formatCompactDuration(delta)
})
}
@@ -119,6 +120,7 @@ const DashboardAgentRow = React.memo(function DashboardAgentRow({
sendTargetDisabledReason,
onSendTargetClick
}: Props) {
const t = useTranslate()
useUiLanguageRevision()
const view = useClaudeAgentRowView(agent)
const highlighted = view ? view.current && view.focused : isFocusedPane
@@ -173,7 +175,7 @@ const DashboardAgentRow = React.memo(function DashboardAgentRow({
const isMonitoring = agent.state === 'working' && agent.entry.workingMode === 'monitoring'
const isWorking = agent.state === 'working' && !isMonitoring
// Show tool details only for working/approval waits; ended or monitoring turns have stale last-tool metadata.
const stoppingLabel = agentRowStoppingLabel(agent.entry, agent.state)
const stoppingLabel = agentRowStoppingLabel(agent.entry, agent.state, t)
const showsTool = showsAgentToolPreview(agent.state) && !isMonitoring && stoppingLabel === null
const { presentation, dotStateOverride } = resolveRowProgramStatus(agent)
const toolName = showsTool ? (agent.entry.toolName?.trim() ?? '') : ''
@@ -183,7 +185,7 @@ const DashboardAgentRow = React.memo(function DashboardAgentRow({
: ''
// Why: a child row's message line is the model's, so a child that ended without an outcome says so.
const lastAssistantMessage = agent.childRow
? agentChildRowMessageLine(agent.childRow)
? agentChildRowMessageLine(agent.childRow, t)
: (agent.entry.lastAssistantMessage?.trim() ?? '')
const isInterrupted = agentVerdictDisplayMark(agent.entry) === 'interrupted'
const lineage = agent.lineage
@@ -192,7 +194,7 @@ const DashboardAgentRow = React.memo(function DashboardAgentRow({
const participatesInLineage = isLineageChild || lineageChildCount > 0
const identityTitle =
lineageChildCount > 0
? translate(
? t(
'auto.components.dashboard.DashboardAgentRow.dispatchedAgents',
'{{agentType}} - dispatched {{count}} agents',
{
@@ -212,20 +214,20 @@ const DashboardAgentRow = React.memo(function DashboardAgentRow({
// Unverifiable rows expose elapsed silence in their timestamp slot; do not hide their only content in a tooltip.
const noUpdateLabel = dotState === 'unverifiable' ? rowNoUpdateLabel(agent, now) : null
const startedTimeAgo = startedAt !== null ? formatTimeAgo(startedAt, now) : null
const doneTimeAgo = doneAt !== null ? formatTimeAgo(doneAt, now) : null
const startedTimeAgo = startedAt !== null ? formatTimeAgo(startedAt, now, t) : null
const doneTimeAgo = doneAt !== null ? formatTimeAgo(doneAt, now, t) : null
const relativeTimestamp = noUpdateLabel ?? doneTimeAgo ?? startedTimeAgo
const tsParts: string[] = noUpdateLabel ? [noUpdateLabel] : []
if (startedTimeAgo !== null) {
tsParts.push(
translate('auto.components.dashboard.DashboardAgentRow.startedAgo', 'started {{time}}', {
t('auto.components.dashboard.DashboardAgentRow.startedAgo', 'started {{time}}', {
time: startedTimeAgo
})
)
}
if (doneTimeAgo !== null) {
tsParts.push(
translate('auto.components.dashboard.DashboardAgentRow.doneAgo', 'done {{time}}', {
t('auto.components.dashboard.DashboardAgentRow.doneAgo', 'done {{time}}', {
time: doneTimeAgo
})
)
@@ -254,10 +256,7 @@ const DashboardAgentRow = React.memo(function DashboardAgentRow({
aria-current={view?.current ? 'true' : undefined}
aria-description={
opensClaudeParentConversation(agent)
? translate(
'auto.lib.claudeAgentRowView.openParentConversation',
'Open parent conversation'
)
? t('auto.lib.claudeAgentRowView.openParentConversation', 'Open parent conversation')
: undefined
}
data-agent-send-target={sendTargetStatus}
@@ -4,7 +4,7 @@ import {
CommentMarkdownAsync,
preloadCommentMarkdown
} from '@/components/sidebar/comment-markdown-lazy'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
type DashboardAgentRowMessageProps = {
expanded: boolean
@@ -21,6 +21,7 @@ export function DashboardAgentRowMessage({
stoppedByUser,
lastAssistantMessage
}: DashboardAgentRowMessageProps): React.JSX.Element | null {
const t = useTranslate()
// Warm the sidebar’s only boot-visible markdown chunk on mount rather than waiting for text.
useEffect(preloadCommentMarkdown, [])
// Reserve the collapsed message slot so incoming/cleared assistant text cannot shift row height.
@@ -37,20 +38,14 @@ export function DashboardAgentRowMessage({
className="shrink-0 text-[10px] leading-snug text-muted-foreground/80"
aria-label={
stoppedByUser
? translate(
? t(
'auto.components.dashboard.DashboardAgentRowMessage.1ec01cef03',
'Interrupted by user'
)
: translate(
'auto.components.activity.ActivityPrototypePage.interrupted',
'Interrupted'
)
: t('auto.components.activity.ActivityPrototypePage.interrupted', 'Interrupted')
}
>
{translate(
'auto.components.dashboard.DashboardAgentRowMessage.0a01046763',
'interrupted'
)}
{t('auto.components.dashboard.DashboardAgentRowMessage.0a01046763', 'interrupted')}
</span>
) : null}
{lastAssistantMessage ? (
@@ -1,6 +1,6 @@
import React, { useCallback } from 'react'
import { ChevronDown, Send, X } from 'lucide-react'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
import { DashboardAgentChildDisclosure } from './DashboardAgentChildDisclosure'
@@ -34,6 +34,7 @@ export function DashboardAgentRowTrailingControls({
onToggleExpanded,
onSendTargetClick
}: DashboardAgentRowTrailingControlsProps): React.JSX.Element {
const t = useTranslate()
// Nested controls must not activate the agent row or enclosing worktree card.
const stopMouseDown = useCallback((event: React.MouseEvent) => {
event.stopPropagation()
@@ -91,17 +92,14 @@ export function DashboardAgentRowTrailingControls({
'worktree-agent-send-target-button z-10 inline-flex h-5 items-center gap-1 rounded-md border px-1.5 text-[10px] font-medium leading-none transition-[background-color,border-color,color,opacity] whitespace-nowrap',
sendTargetStatus === 'sending' && 'cursor-progress opacity-75'
)}
aria-label={translate(
'auto.components.dashboard.DashboardAgentRow.0272969e28',
'Send to this agent'
)}
title={translate(
aria-label={t(
'auto.components.dashboard.DashboardAgentRow.0272969e28',
'Send to this agent'
)}
title={t('auto.components.dashboard.DashboardAgentRow.0272969e28', 'Send to this agent')}
>
<Send className="size-3" />
<span>{translate('auto.components.dashboard.DashboardAgentRow.912e136cd9', 'Send')}</span>
<span>{t('auto.components.dashboard.DashboardAgentRow.912e136cd9', 'Send')}</span>
</button>
)}
{!hasChildDisclosure && !sendTargetStatus && hideDismiss && relativeTimestamp !== null && (
@@ -135,11 +133,11 @@ export function DashboardAgentRowTrailingControls({
'can-hover:opacity-0 transition-opacity duration-150',
'group-hover/agent-row:opacity-100 focus-visible:opacity-100'
)}
aria-label={translate(
aria-label={t(
'auto.components.dashboard.DashboardAgentRow.b06e13fcf7',
'Dismiss agent'
)}
title={translate('auto.components.dashboard.DashboardAgentRow.5ae84475cc', 'Dismiss')}
title={t('auto.components.dashboard.DashboardAgentRow.5ae84475cc', 'Dismiss')}
>
<X className="size-3.5" />
</button>
@@ -156,11 +154,8 @@ export function DashboardAgentRowTrailingControls({
'can-hover:opacity-0 transition-opacity duration-150',
'group-hover/agent-row:opacity-100 focus-visible:opacity-100'
)}
aria-label={translate(
'auto.components.dashboard.DashboardAgentRow.b06e13fcf7',
'Dismiss agent'
)}
title={translate('auto.components.dashboard.DashboardAgentRow.5ae84475cc', 'Dismiss')}
aria-label={t('auto.components.dashboard.DashboardAgentRow.b06e13fcf7', 'Dismiss agent')}
title={t('auto.components.dashboard.DashboardAgentRow.5ae84475cc', 'Dismiss')}
>
<X className="size-3.5" />
</button>
@@ -186,14 +181,8 @@ export function DashboardAgentRowTrailingControls({
className="inline-flex shrink-0 items-center justify-center text-muted-foreground/60 hover:text-foreground"
aria-label={
expanded
? translate(
'auto.components.dashboard.DashboardAgentRow.a41fb5376e',
'Collapse details'
)
: translate(
'auto.components.dashboard.DashboardAgentRow.a743da52ff',
'Expand details'
)
? t('auto.components.dashboard.DashboardAgentRow.a41fb5376e', 'Collapse details')
: t('auto.components.dashboard.DashboardAgentRow.a743da52ff', 'Expand details')
}
aria-expanded={expanded}
>
@@ -50,6 +50,7 @@ import { buildDashboardSnapshotFilterOptions } from './dashboard-snapshot-filter
import { groupSubagentsByParentPaneKey } from './dashboard-subagent-cards'
import { selectDashboardOrchestration } from './dashboard-orchestration-selection'
import { dashboardRowBucketProjection, readAgentRowTurnRecords } from './dashboard-row-bucket'
import type { Translate } from '@/i18n/use-translate'
/** Keep builder inputs as a Pick so tests need only construct the store slices read here. */
export type DashboardSnapshotState = Pick<
@@ -95,7 +96,8 @@ export function buildDashboardSnapshot(
rowsCache?: WorktreeAgentRowsCache
/** Freshness token for rowsCache (agentStatusEpoch); required for the cache to be safe. */
rowsGeneration?: unknown
} = {}
} = {},
t?: Translate
): DashboardSnapshot {
const cards: DashboardCard[] = []
const workspaces: DashboardWorkspace[] | undefined =
@@ -141,7 +143,7 @@ export function buildDashboardSnapshot(
? groupSubagentsByParentPaneKey(rows)
: undefined
const context = includeCardDetails
? resolveDashboardCardContext(state, repo, worktree)
? resolveDashboardCardContext(state, repo, worktree, t)
: undefined
if (workspaces && workspaces.length < DASHBOARD_MAX_MAP_WORKSPACES) {
const hostMetadata = dashboardCardMapWorkspaceMetadata(
@@ -15,6 +15,7 @@ import {
getParentPrChecksGitHubPRCacheEntry
} from '../right-sidebar/hosted-review/folder-checks/parent-pr-checks-github-pr-cache'
import { canUseParentPrChecksHostedReviewCacheEntry } from '../right-sidebar/hosted-review/folder-checks/parent-pr-checks-hosted-review-cache'
import type { Translate } from '@/i18n/use-translate'
export type DashboardCardContextState = Partial<
Pick<AppState, 'hostedReviewCache' | 'prCache' | 'settings' | 'workspaceStatuses'>
@@ -39,7 +40,8 @@ function hasLinkedReview(worktree: Worktree): boolean {
function resolveReview(
state: DashboardCardContextState,
repo: Repo | null,
worktree: Worktree
worktree: Worktree,
t?: Translate
): DashboardCardReview | undefined {
if (!repo || !state.hostedReviewCache || !state.prCache || repo.kind === 'folder') {
return undefined
@@ -60,7 +62,7 @@ function resolveReview(
const hostedReview = hostedReviewEntry?.data
if (
hostedReview &&
canUseParentPrChecksHostedReviewCacheEntry(worktree, hostedReview, hostedReviewEntry)
canUseParentPrChecksHostedReviewCacheEntry(worktree, hostedReview, hostedReviewEntry, t)
) {
return { number: hostedReview.number, state: hostedReview.state }
}
@@ -79,14 +81,15 @@ function resolveReview(
export function resolveDashboardCardContext(
state: DashboardCardContextState,
repo: Repo | null,
worktree: Worktree
worktree: Worktree,
t?: Translate
): DashboardCardContext {
const statuses =
state.workspaceStatuses && state.workspaceStatuses.length > 0
? state.workspaceStatuses
: DEFAULT_WORKSPACE_STATUSES
const workspaceStatusId = getWorkspaceStatus(worktree, statuses)
const review = resolveReview(state, repo, worktree)
const review = resolveReview(state, repo, worktree, t)
return {
workspaceStatus:
statuses.find((status) => status.id === workspaceStatusId) ?? DEFAULT_WORKSPACE_STATUSES[0],
@@ -7,6 +7,7 @@ import { captureInsertionTarget } from './dictation-insertion-target'
import { createDictationTranscriptDelivery } from './dictation-transcript-delivery'
import { recordStoppedSession, waitForStoppedSession } from './dictation-stopped-sessions'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import {
showDictationDisabledToast,
showDictationNoModelToast,
@@ -30,6 +31,7 @@ import {
} from '../../../../shared/dictation-session-machine'
export function DictationController() {
const t = useTranslate()
const dictationState = useAppStore((s) => s.dictationState)
const setDictationState = useAppStore((s) => s.setDictationState)
const setPartialTranscript = useAppStore((s) => s.setPartialTranscript)
@@ -314,11 +316,9 @@ export function DictationController() {
recordStoppedSession(sessionId, stoppedSessionIdsRef, stoppedResolversRef),
onError: (sessionId, error) => {
toast.error(
translate(
'auto.components.dictation.DictationController.de136f1199',
'Speech error: {{value0}}',
{ value0: error }
)
t('auto.components.dictation.DictationController.de136f1199', 'Speech error: {{value0}}', {
value0: error
})
)
const { run } = dispatch({ type: 'abandon' })
stopCapture()
@@ -2,7 +2,7 @@ import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useShortcutKeyDetails } from '@/hooks/useShortcutLabel'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
import { useAppStore } from '@/store'
import { Square } from 'lucide-react'
@@ -12,6 +12,7 @@ import { useDictationMeter } from './dictation-meter-store'
// captureLive: the mic is recording (and buffering) while the speech engine is still starting.
export function DictationIndicator({ captureLive }: { captureLive: boolean }) {
const t = useTranslate()
const dictationState = useAppStore((state) => state.dictationState)
const partialTranscript = useAppStore((state) => state.partialTranscript)
const dictationMeter = useDictationMeter()
@@ -28,23 +29,20 @@ export function DictationIndicator({ captureLive }: { captureLive: boolean }) {
const isClipping = isListening && dictationMeter.isClipping
const isSpeaking = isListening && dictationMeter.isSpeaking && !isClipping
const lifecycleLabel = isListening
? translate('auto.components.dictation.DictationIndicator.3de5a129e7', 'Listening')
? t('auto.components.dictation.DictationIndicator.3de5a129e7', 'Listening')
: dictationState === 'starting'
? translate('auto.components.dictation.DictationIndicator.7f3660a7ba', 'Starting mic…')
: translate('auto.components.dictation.DictationIndicator.f082d0cb9d', 'Processing…')
? t('auto.components.dictation.DictationIndicator.7f3660a7ba', 'Starting mic…')
: t('auto.components.dictation.DictationIndicator.f082d0cb9d', 'Processing…')
const label = isClipping
? translate('auto.components.dictation.DictationIndicator.4977162383', 'Too loud')
? t('auto.components.dictation.DictationIndicator.4977162383', 'Too loud')
: isSpeaking
? translate('auto.components.dictation.DictationIndicator.25f2b7a6a5', 'Speaking')
? t('auto.components.dictation.DictationIndicator.25f2b7a6a5', 'Speaking')
: lifecycleLabel
const announcedLabel = isClipping ? label : lifecycleLabel
const canStop = dictationState !== 'stopping'
const showShortcut = !isHoldMode && shortcut.keys.length > 0
const transcript = partialTranscript.trim()
const stopLabel = translate(
'auto.components.dictation.DictationIndicator.335e1bc6cb',
'Stop dictation'
)
const stopLabel = t('auto.components.dictation.DictationIndicator.335e1bc6cb', 'Stop dictation')
return (
<div
@@ -70,7 +68,7 @@ export function DictationIndicator({ captureLive }: { captureLive: boolean }) {
</span>
{isLoadingModel ? (
<span aria-hidden className="min-w-0 truncate text-xs text-muted-foreground">
{translate(
{t(
'auto.components.dictation.DictationIndicator.loadingModel',
'Loading speech model…'
)}
@@ -4,7 +4,7 @@ import { useLayoutEffect, useRef, useState, type ReactNode } from 'react'
import { Button } from '@/components/ui/button'
import { getDiffCommentLineMetaLabel } from './diff-comment-line-meta-label'
import { useMountedRef } from '@/hooks/useMountedRef'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { useUiLanguageRevision } from '@/i18n/use-ui-language-revision'
import { openAppHttpLink } from '@/lib/browser/navigation/app-http-link-open'
@@ -51,6 +51,7 @@ export function DiffCommentCard({
onSubmitEdit,
headerActions
}: Props): React.JSX.Element {
const t = useTranslate()
// Zone cards live in separate React roots, so subscribe directly to language changes.
useUiLanguageRevision()
const [editing, setEditing] = useState(false)
@@ -138,12 +139,12 @@ export function DiffCommentCard({
const trimmedDraft = draft.trim()
const canSubmit = !submitting && trimmedDraft.length > 0 && trimmedDraft !== body
const lineLabel =
label === undefined ? getDiffCommentLineMetaLabel({ lineNumber, startLine }) : label
label === undefined ? getDiffCommentLineMetaLabel({ lineNumber, startLine }, t) : label
const metaText = [
author || translate('auto.components.diff.comments.DiffCommentCard.noteAuthorFallback', 'Note'),
author || t('auto.components.diff.comments.DiffCommentCard.noteAuthorFallback', 'Note'),
lineLabel,
createdAtLabel ||
(sentAt ? translate('auto.components.diff.comments.DiffCommentCard.sentMeta', 'sent') : null)
(sentAt ? t('auto.components.diff.comments.DiffCommentCard.sentMeta', 'sent') : null)
]
.filter(Boolean)
.join(' ')
@@ -191,11 +192,11 @@ export function DiffCommentCard({
<button
type="button"
className="orca-diff-comment-pill-btn"
title={translate(
title={t(
'auto.components.diff.comments.DiffCommentCard.508ee678a5',
'Open in browser'
)}
aria-label={translate(
aria-label={t(
'auto.components.diff.comments.DiffCommentCard.508ee678a5',
'Open in browser'
)}
@@ -205,7 +206,7 @@ export function DiffCommentCard({
openAppHttpLink(url, { event: ev })
}}
>
{translate('auto.components.diff.comments.DiffCommentCard.6978871a3d', 'Open')}
{t('auto.components.diff.comments.DiffCommentCard.6978871a3d', 'Open')}
</button>
{(onSubmitEdit || onDelete) && (
<span className="orca-diff-comment-pill-divider" />
@@ -217,11 +218,11 @@ export function DiffCommentCard({
<button
type="button"
className="orca-diff-comment-pill-btn"
title={translate(
title={t(
'auto.components.diff.comments.DiffCommentCard.cad3384faa',
'Edit note'
)}
aria-label={translate(
aria-label={t(
'auto.components.diff.comments.DiffCommentCard.cad3384faa',
'Edit note'
)}
@@ -240,11 +241,11 @@ export function DiffCommentCard({
<button
type="button"
className="orca-diff-comment-pill-btn orca-diff-comment-pill-btn-danger"
title={translate(
title={t(
'auto.components.diff.comments.DiffCommentCard.cce596969e',
'Delete note'
)}
aria-label={translate(
aria-label={t(
'auto.components.diff.comments.DiffCommentCard.cce596969e',
'Delete note'
)}
@@ -300,7 +301,7 @@ export function DiffCommentCard({
/>
<div className="orca-diff-comment-popover-footer">
<Button variant="ghost" size="sm" onClick={handleCancel} disabled={submitting}>
{translate('auto.components.diff.comments.DiffCommentCard.0203bed775', 'Cancel')}
{t('auto.components.diff.comments.DiffCommentCard.0203bed775', 'Cancel')}
</Button>
<Button
size="sm"
@@ -308,14 +309,11 @@ export function DiffCommentCard({
disabled={!canSubmit}
title={
submitting
? translate(
'auto.components.diff.comments.DiffCommentCard.bb0a55f856',
'Saving…'
)
? t('auto.components.diff.comments.DiffCommentCard.bb0a55f856', 'Saving…')
: undefined
}
>
{translate('auto.components.diff.comments.DiffCommentCard.109a791e7b', 'Save')}
{t('auto.components.diff.comments.DiffCommentCard.109a791e7b', 'Save')}
<CornerDownLeft className="ml-1 size-3 opacity-70" />
</Button>
</div>
@@ -6,7 +6,7 @@ import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from
import { CornerDownLeft } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { useMountedRef } from '@/hooks/useMountedRef'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import {
getCommentBodySubmitState,
hasBoundedCommentBodyText
@@ -48,6 +48,7 @@ export function DiffCommentDraftCard({
initialBody = '',
onBodyChange
}: DiffCommentDraftCardProps): React.JSX.Element {
const t = useTranslate()
const [body, setBody] = useState(initialBody)
const bodyRef = useRef(body)
const [submitting, setSubmitting] = useState(false)
@@ -81,16 +82,14 @@ export function DiffCommentDraftCard({
const labelId = useId()
const headerLabel =
startLine && startLine !== lineNumber
? translate(
? t(
'auto.components.diff.comments.DiffCommentPopover.c845170b3b',
'Lines {{value0}}-{{value1}}',
{ value0: startLine, value1: lineNumber }
)
: translate(
'auto.components.diff.comments.DiffCommentPopover.e05063cfc1',
'Line {{value0}}',
{ value0: lineNumber }
)
: t('auto.components.diff.comments.DiffCommentPopover.e05063cfc1', 'Line {{value0}}', {
value0: lineNumber
})
const { attachDrop } = images
const hasImageUpload = !!imageUploadAddress
@@ -171,7 +170,7 @@ export function DiffCommentDraftCard({
}
if (bodyState.status === 'too-large-leading-whitespace') {
toast.error(
translate(
t(
'auto.components.diff.comments.DiffCommentPopover.commentTooLarge',
'Comment is too large to submit safely.'
)
@@ -189,9 +188,7 @@ export function DiffCommentDraftCard({
} catch (err) {
console.error('Failed to submit diff comment draft:', err)
// A rejected save never reaches the caller's own failure toast, so report it here.
toast.error(
translate('auto.components.editor.diffCommentSaveFailed', 'Failed to save comment')
)
toast.error(t('auto.components.editor.diffCommentSaveFailed', 'Failed to save comment'))
if (mountedRef.current) {
setSubmitting(false)
}
@@ -244,10 +241,7 @@ export function DiffCommentDraftCard({
className="orca-diff-comment-popover-textarea"
placeholder={
placeholder ??
translate(
'auto.components.diff.comments.DiffCommentPopover.f6c449191e',
'Add note for the AI'
)
t('auto.components.diff.comments.DiffCommentPopover.f6c449191e', 'Add note for the AI')
}
value={images.value}
onPaste={images.onPaste}
@@ -265,29 +259,23 @@ export function DiffCommentDraftCard({
{/* Footer */}
<div className="orca-diff-comment-popover-footer">
<Button type="button" variant="ghost" size="sm" onClick={onCancel} disabled={submitting}>
{translate('auto.components.diff.comments.DiffCommentPopover.2b3ce6d394', 'Cancel')}
{t('auto.components.diff.comments.DiffCommentPopover.2b3ce6d394', 'Cancel')}
</Button>
<Button
type="button"
size="sm"
aria-label={
submitLabel ??
translate('auto.components.diff.comments.DiffCommentPopover.5fa4bfebaa', 'Add note')
t('auto.components.diff.comments.DiffCommentPopover.5fa4bfebaa', 'Add note')
}
onClick={() => void handleSubmit()}
disabled={!canSubmit}
>
{submitting
? (submittingLabel ??
translate(
'auto.components.diff.comments.DiffCommentPopover.5e9b3d0c2e',
'Adding...'
))
t('auto.components.diff.comments.DiffCommentPopover.5e9b3d0c2e', 'Adding...'))
: (submitLabel ??
translate(
'auto.components.diff.comments.DiffCommentPopover.5fa4bfebaa',
'Add note'
))}
t('auto.components.diff.comments.DiffCommentPopover.5fa4bfebaa', 'Add note'))}
{!submitting && <CornerDownLeft className="ml-1 size-3 opacity-70" />}
</Button>
</div>
@@ -9,6 +9,7 @@ import {
hasBoundedCommentBodyText
} from '@/lib/comment-body-submit-state'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { installOpenDraftAddReviewNoteGuard } from '../editor/editor-shortcuts'
import { resolveDiffCommentPopoverTop } from './diff-comment-popover-position'
@@ -55,6 +56,7 @@ export function DiffCommentPopover({
onCancel,
onSubmit
}: Props): React.JSX.Element {
const t = useTranslate()
const [body, setBody] = useState('')
// Why: mirror the draft into a ref so the mousedown listener reads it fresh without re-registering each keystroke.
const bodyRef = useRef(body)
@@ -160,7 +162,7 @@ export function DiffCommentPopover({
}
if (bodyState.status === 'too-large-leading-whitespace') {
toast.error(
translate(
t(
'auto.components.diff.comments.DiffCommentPopover.commentTooLarge',
'Comment is too large to submit safely.'
)
@@ -194,12 +196,12 @@ export function DiffCommentPopover({
<div id={labelId} className="orca-diff-comment-popover-label">
{title ??
(startLine && startLine !== lineNumber
? translate(
? t(
'auto.components.diff.comments.DiffCommentPopover.c845170b3b',
'Lines {{value0}}-{{value1}}',
{ value0: startLine, value1: lineNumber }
)
: translate(
: t(
'auto.components.diff.comments.DiffCommentPopover.e05063cfc1',
'Line {{value0}}',
{ value0: lineNumber }
@@ -233,7 +235,7 @@ export function DiffCommentPopover({
/>
<div className="orca-diff-comment-popover-footer">
<Button variant="ghost" size="sm" onClick={onCancel}>
{translate('auto.components.diff.comments.DiffCommentPopover.2b3ce6d394', 'Cancel')}
{t('auto.components.diff.comments.DiffCommentPopover.2b3ce6d394', 'Cancel')}
</Button>
<Button size="sm" onClick={handleSubmit} disabled={submitting || !canSubmitComment}>
{submitting ? submittingLabel : submitLabel}
@@ -1,15 +1,17 @@
import type { DiffComment } from '../../../../shared/diff-comment-types'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
// Card meta text, e.g. "Note line 4 sent"; agent prompts keep the English getDiffCommentLineLabel.
export function getDiffCommentLineMetaLabel(
comment: Pick<DiffComment, 'lineNumber' | 'startLine'>
comment: Pick<DiffComment, 'lineNumber' | 'startLine'>,
t: Translate = translate
): string {
if (comment.startLine !== undefined && comment.startLine !== comment.lineNumber) {
return translate('diffComments.lineRangeMeta', 'lines {{start}}-{{end}}', {
return t('diffComments.lineRangeMeta', 'lines {{start}}-{{end}}', {
start: comment.startLine,
end: comment.lineNumber
})
}
return translate('diffComments.lineMeta', 'line {{line}}', { line: comment.lineNumber })
return t('diffComments.lineMeta', 'line {{line}}', { line: comment.lineNumber })
}
@@ -6,7 +6,7 @@ import { MobileEmulatorAgentSetupGuideLayer } from './MobileEmulatorAgentSetupGu
import { useEmulatorPaneSession } from './use-emulator-pane-session'
import { scrcpyDeviceIdFromStreamUrl } from './emulator-scrcpy-stream-url'
import { useEmulatorInputCheck } from './use-emulator-input-check'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { useUiLanguageRevision } from '@/i18n/use-ui-language-revision'
type EmulatorPaneProps = {
@@ -17,6 +17,7 @@ type EmulatorPaneProps = {
}
export default function EmulatorPane({ tab, worktreeId, isActive = true }: EmulatorPaneProps) {
const t = useTranslate()
useUiLanguageRevision()
const {
devices,
@@ -73,7 +74,7 @@ export default function EmulatorPane({ tab, worktreeId, isActive = true }: Emula
{selectionRequired && (
<p role="status" className="border-b border-border px-3 py-2 text-xs text-muted-foreground">
{emulatorOpeningMessage(selectionRequired)}
{emulatorOpeningMessage(selectionRequired, t)}
</p>
)}
{visibleError ? (
@@ -92,10 +93,7 @@ export default function EmulatorPane({ tab, worktreeId, isActive = true }: Emula
<MobileEmulatorAgentSetupGuideLayer isActive={isActive}>
{!isLive && !loading ? (
<p className="mb-4 text-center text-xs text-muted-foreground">
{translate(
'auto.components.emulator.pane.EmulatorPane.59b08fa031',
'No emulator connected'
)}
{t('auto.components.emulator.pane.EmulatorPane.59b08fa031', 'No emulator connected')}
</p>
) : null}
<EmulatorDeviceFrame
@@ -4,7 +4,7 @@ import { useAppStore } from '@/store'
import { Button } from '../ui/button'
import { MobileEmulatorAgentSetupGuideSteps } from './MobileEmulatorAgentSetupGuideSteps'
import type { useMobileEmulatorAgentSetupState } from './use-mobile-emulator-agent-setup-state'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
type MobileEmulatorAgentSetupGuideProps = {
setup: ReturnType<typeof useMobileEmulatorAgentSetupState>
@@ -13,6 +13,7 @@ type MobileEmulatorAgentSetupGuideProps = {
export function MobileEmulatorAgentSetupGuide({
setup
}: MobileEmulatorAgentSetupGuideProps): React.JSX.Element {
const t = useTranslate()
const dismissMobileEmulatorAgentSetup = useAppStore((s) => s.dismissMobileEmulatorAgentSetup)
const openSettingsPage = useAppStore((s) => s.openSettingsPage)
const openSettingsTarget = useAppStore((s) => s.openSettingsTarget)
@@ -32,7 +33,7 @@ export function MobileEmulatorAgentSetupGuide({
return (
<div
role="region"
aria-label={translate(
aria-label={t(
'auto.components.emulator.pane.MobileEmulatorAgentSetupGuide.2fda9ff015',
'Set up agent control'
)}
@@ -42,7 +43,7 @@ export function MobileEmulatorAgentSetupGuide({
<p className="min-w-0 flex-1 text-[11px] leading-4 text-muted-foreground">
{setup.setupComplete ? (
<span className="font-medium text-foreground">
{translate(
{t(
'auto.components.emulator.pane.MobileEmulatorAgentSetupGuide.0ac0fef514',
'Agent control is ready.'
)}
@@ -50,12 +51,12 @@ export function MobileEmulatorAgentSetupGuide({
) : (
<>
<span className="font-medium text-foreground">
{translate(
{t(
'auto.components.emulator.pane.MobileEmulatorAgentSetupGuide.2bdfff8763',
'Agent control (optional).'
)}{' '}
</span>
{translate(
{t(
'auto.components.emulator.pane.MobileEmulatorAgentSetupGuide.installSkillPrompt',
'Install the Orca CLI skill when you want agents to drive this simulator.'
)}
@@ -72,10 +73,7 @@ export function MobileEmulatorAgentSetupGuide({
className="h-6 px-2.5 text-[11px]"
onClick={dismiss}
>
{translate(
'auto.components.emulator.pane.MobileEmulatorAgentSetupGuide.d10ae98046',
'Done'
)}
{t('auto.components.emulator.pane.MobileEmulatorAgentSetupGuide.d10ae98046', 'Done')}
</Button>
) : (
<>
@@ -86,7 +84,7 @@ export function MobileEmulatorAgentSetupGuide({
className="h-6 px-2 text-[11px] text-muted-foreground"
onClick={dismiss}
>
{translate(
{t(
'auto.components.emulator.pane.MobileEmulatorAgentSetupGuide.3756cbeca7',
'Not now'
)}
@@ -100,11 +98,11 @@ export function MobileEmulatorAgentSetupGuide({
onClick={() => setExpanded((value) => !value)}
>
{expanded
? translate(
? t(
'auto.components.emulator.pane.MobileEmulatorAgentSetupGuide.6d950431d2',
'Hide'
)
: translate(
: t(
'auto.components.emulator.pane.MobileEmulatorAgentSetupGuide.ebceac65a4',
'Set up'
)}
@@ -124,7 +122,7 @@ export function MobileEmulatorAgentSetupGuide({
onClick={openSettings}
className="text-[11px] text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
>
{translate(
{t(
'auto.components.emulator.pane.MobileEmulatorAgentSetupGuide.3f003507f4',
'Open full setup in Settings'
)}
@@ -2,7 +2,7 @@ import { useAppStore } from '@/store'
import { ORCA_CLI_SKILL_NAME } from '@/lib/agent-feature-skill-names'
import { AgentSkillSetupPanel } from '../skills/setup/AgentSkillSetupPanel'
import type { useMobileEmulatorAgentSetupState } from './use-mobile-emulator-agent-setup-state'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
type MobileEmulatorAgentSetupGuideStepsProps = {
setup: ReturnType<typeof useMobileEmulatorAgentSetupState>
@@ -11,12 +11,13 @@ type MobileEmulatorAgentSetupGuideStepsProps = {
export function MobileEmulatorAgentSetupGuideSteps({
setup
}: MobileEmulatorAgentSetupGuideStepsProps): React.JSX.Element {
const t = useTranslate()
const recordFeatureInteraction = useAppStore((s) => s.recordFeatureInteraction)
return (
<div className="py-2.5">
<p className="text-sm font-medium">
{translate(
{t(
'auto.components.emulator.pane.MobileEmulatorAgentSetupGuideSteps.21f5687c07',
'Orca CLI skill'
)}
@@ -25,11 +26,11 @@ export function MobileEmulatorAgentSetupGuideSteps({
variant="inline"
hideHeader
className="min-w-0"
title={translate(
title={t(
'auto.components.emulator.pane.MobileEmulatorAgentSetupGuideSteps.21f5687c07',
'Orca CLI skill'
)}
description={translate(
description={t(
'auto.components.emulator.pane.MobileEmulatorAgentSetupGuideSteps.64fb057667',
'Teaches agents the orca emulator commands for this worktree.'
)}
@@ -1,10 +1,11 @@
import { X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { useMobileEmulatorTabIntroActions } from './use-mobile-emulator-tab-intro-actions'
export function MobileEmulatorTabIntroCallout(): React.JSX.Element {
const t = useTranslate()
const { keepIntro, hideIntro, dismissIntro } = useMobileEmulatorTabIntroActions()
return (
@@ -14,7 +15,7 @@ export function MobileEmulatorTabIntroCallout(): React.JSX.Element {
onPointerDown={(event) => event.preventDefault()}
>
<p className="min-w-0 flex-1 text-[11px] leading-4 text-foreground/85">
{translate(
{t(
'auto.components.emulator.pane.MobileEmulatorTabIntroCallout.5789936d9a',
'Preview iOS simulators while agents drive the screen.'
)}
@@ -27,10 +28,7 @@ export function MobileEmulatorTabIntroCallout(): React.JSX.Element {
className="h-6 px-2 text-[11px]"
onClick={keepIntro}
>
{translate(
'auto.components.emulator.pane.MobileEmulatorTabIntroCallout.8014b4b80b',
'Keep'
)}
{t('auto.components.emulator.pane.MobileEmulatorTabIntroCallout.8014b4b80b', 'Keep')}
</Button>
<Button
type="button"
@@ -39,10 +37,7 @@ export function MobileEmulatorTabIntroCallout(): React.JSX.Element {
className="h-6 px-2 text-[11px] text-muted-foreground"
onClick={hideIntro}
>
{translate(
'auto.components.emulator.pane.MobileEmulatorTabIntroCallout.6e051a40b7',
'Hide'
)}
{t('auto.components.emulator.pane.MobileEmulatorTabIntroCallout.6e051a40b7', 'Hide')}
</Button>
<Tooltip>
<TooltipTrigger asChild>
@@ -50,7 +45,7 @@ export function MobileEmulatorTabIntroCallout(): React.JSX.Element {
type="button"
variant="ghost"
size="icon-xs"
aria-label={translate(
aria-label={t(
'auto.components.emulator.pane.MobileEmulatorTabIntroCallout.1924982130',
'Dismiss'
)}
@@ -61,10 +56,7 @@ export function MobileEmulatorTabIntroCallout(): React.JSX.Element {
</Button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}>
{translate(
'auto.components.emulator.pane.MobileEmulatorTabIntroCallout.1924982130',
'Dismiss'
)}
{t('auto.components.emulator.pane.MobileEmulatorTabIntroCallout.1924982130', 'Dismiss')}
</TooltipContent>
</Tooltip>
</div>
@@ -10,7 +10,7 @@ import {
CommandItem,
CommandList
} from '@/components/ui/command'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import type { SimulatorDeviceRow } from './emulator-pane-types'
import { emulatorDeviceDescription, groupEmulatorDevices } from './emulator-device-picker-groups'
@@ -22,13 +22,14 @@ type Props = {
}
export function EmulatorDevicePicker({ devices, selectedUdid, disabled, onSelectDevice }: Props) {
const t = useTranslate()
const [open, setOpen] = useState(false)
const [search, setSearch] = useState('')
const [otherOpen, setOtherOpen] = useState(false)
const [unavailableOpen, setUnavailableOpen] = useState(false)
const groups = groupEmulatorDevices(devices, selectedUdid)
const selected = devices.find((device) => device.udid === selectedUdid)
const chooseLabel = translate('emulator.picker.choose', 'Choose emulator')
const chooseLabel = t('emulator.picker.choose', 'Choose emulator')
const duplicateKeys = new Map<string, number>()
for (const device of devices) {
const key = `${device.name} ${emulatorDeviceDescription(device)}`
@@ -39,10 +40,10 @@ export function EmulatorDevicePicker({ devices, selectedUdid, disabled, onSelect
const duplicate = (duplicateKeys.get(`${device.name} ${description}`) ?? 0) > 1
const status =
device.state === 'Booted'
? translate('emulator.picker.running', 'Running')
? t('emulator.picker.running', 'Running')
: device.state === 'Booting'
? translate('emulator.picker.starting', 'Starting')
: translate('emulator.picker.stopped', 'Stopped')
? t('emulator.picker.starting', 'Starting')
: t('emulator.picker.stopped', 'Stopped')
return (
<CommandItem
key={device.udid}
@@ -67,7 +68,7 @@ export function EmulatorDevicePicker({ devices, selectedUdid, disabled, onSelect
{device.isAvailable === false && (
<div className="text-xs text-muted-foreground">
{device.availabilityError ||
translate('emulator.picker.runtimeUnavailable', 'Runtime unavailable')}
t('emulator.picker.runtimeUnavailable', 'Runtime unavailable')}
</div>
)}
</div>
@@ -106,12 +107,12 @@ export function EmulatorDevicePicker({ devices, selectedUdid, disabled, onSelect
<CommandInput
value={search}
onValueChange={setSearch}
placeholder={translate('emulator.picker.search', 'Search devices…')}
placeholder={t('emulator.picker.search', 'Search devices…')}
/>
<CommandList className="max-h-80">
<CommandEmpty>{translate('emulator.picker.empty', 'No matching devices')}</CommandEmpty>
<CommandEmpty>{t('emulator.picker.empty', 'No matching devices')}</CommandEmpty>
{groups.used.length > 0 && (
<CommandGroup heading={translate('emulator.picker.used', 'Used devices')}>
<CommandGroup heading={t('emulator.picker.used', 'Used devices')}>
{groups.used.map(renderDevice)}
</CommandGroup>
)}
@@ -125,7 +126,7 @@ export function EmulatorDevicePicker({ devices, selectedUdid, disabled, onSelect
selection="palette"
>
<ChevronRight className={otherOpen ? 'rotate-90' : ''} />
{translate('emulator.picker.other', 'Other devices on this computer')} (
{t('emulator.picker.other', 'Other devices on this computer')} (
{groups.other.length})
</CommandItem>
)}
@@ -142,7 +143,7 @@ export function EmulatorDevicePicker({ devices, selectedUdid, disabled, onSelect
selection="palette"
>
<ChevronRight className={unavailableOpen ? 'rotate-90' : ''} />
{translate('emulator.picker.unavailable', 'Unavailable runtimes')} (
{t('emulator.picker.unavailable', 'Unavailable runtimes')} (
{groups.unavailable.length})
</CommandItem>
)}
@@ -1,21 +1,25 @@
import type { EmulatorSelectionRequired } from '../../../../shared/emulator-opening-preferences'
import { translate } from '@/i18n/i18n'
export function emulatorOpeningMessage(reason: EmulatorSelectionRequired): string {
import type { Translate } from '@/i18n/use-translate'
export function emulatorOpeningMessage(
reason: EmulatorSelectionRequired,
t: Translate = translate
): string {
switch (reason) {
case 'missing':
return translate(
return t(
'emulator.opening.missing',
'The saved device is no longer on this computer. Choose another device.'
)
case 'unavailable':
return translate(
return t(
'emulator.opening.unavailable',
'The saved device runtime is unavailable. Choose another device or install its runtime.'
)
case 'ask':
return translate('emulator.opening.askPrompt', 'Choose a device to open.')
return t('emulator.opening.askPrompt', 'Choose a device to open.')
case 'first-use':
return translate(
return t(
'emulator.opening.firstUse',
'Choose your first device. Orca will remember successful selections on this computer.'
)
@@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import type { SimulatorDeviceRow } from './emulator-pane-types'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
type EmulatorPaneToolbarProps = {
displayName: string
@@ -31,6 +31,7 @@ export function EmulatorPaneToolbar({
onHome,
onRotate
}: EmulatorPaneToolbarProps) {
const t = useTranslate()
// Show preview/control connection state rather than CoreSimulator boot state.
const statusLabel = isLive ? 'Connected' : loading ? 'Working…' : 'Not connected'
const subtleStatus = isLive || loading
@@ -67,22 +68,19 @@ export function EmulatorPaneToolbar({
className="h-7 gap-1 px-2 text-xs"
onClick={onRotate}
disabled={!isLive || loading}
aria-label={translate(
aria-label={t(
'auto.components.emulator.pane.emulator.pane.toolbar.6bd8dff42a',
'Rotate'
)}
>
<RotateCw className="size-3.5" />
<span className="hidden sm:inline">
{translate(
'auto.components.emulator.pane.emulator.pane.toolbar.6bd8dff42a',
'Rotate'
)}
{t('auto.components.emulator.pane.emulator.pane.toolbar.6bd8dff42a', 'Rotate')}
</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>
{translate('auto.components.emulator.pane.emulator.pane.toolbar.6bd8dff42a', 'Rotate')}
{t('auto.components.emulator.pane.emulator.pane.toolbar.6bd8dff42a', 'Rotate')}
</TooltipContent>
</Tooltip>
<Tooltip>
@@ -94,16 +92,13 @@ export function EmulatorPaneToolbar({
className="size-7"
onClick={onHome}
disabled={!isLive || loading}
aria-label={translate(
'auto.components.emulator.pane.emulator.pane.toolbar.e7a0d1897e',
'Home'
)}
aria-label={t('auto.components.emulator.pane.emulator.pane.toolbar.e7a0d1897e', 'Home')}
>
<Home className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>
{translate('auto.components.emulator.pane.emulator.pane.toolbar.e7a0d1897e', 'Home')}
{t('auto.components.emulator.pane.emulator.pane.toolbar.e7a0d1897e', 'Home')}
</TooltipContent>
</Tooltip>
{isLive ? (
@@ -116,7 +111,7 @@ export function EmulatorPaneToolbar({
className="size-7 text-muted-foreground hover:text-destructive"
onClick={onShutdown}
disabled={loading}
aria-label={translate(
aria-label={t(
'auto.components.emulator.pane.emulator.pane.toolbar.06e10d7356',
'Shut down emulator'
)}
@@ -125,7 +120,7 @@ export function EmulatorPaneToolbar({
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>
{translate(
{t(
'auto.components.emulator.pane.emulator.pane.toolbar.06e10d7356',
'Shut down emulator'
)}
@@ -145,14 +140,8 @@ export function EmulatorPaneToolbar({
}
>
{loading
? translate(
'auto.components.emulator.pane.emulator.pane.toolbar.868c0f2938',
'Working…'
)
: translate(
'auto.components.emulator.pane.emulator.pane.toolbar.81b3571a07',
'Connect'
)}
? t('auto.components.emulator.pane.emulator.pane.toolbar.868c0f2938', 'Working…')
: t('auto.components.emulator.pane.emulator.pane.toolbar.81b3571a07', 'Connect')}
</Button>
)}
</div>
@@ -1,16 +1,18 @@
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
export function getEmulatorScreenAriaLabel(
isLive: boolean,
keyboardCaptureActive: boolean
keyboardCaptureActive: boolean,
t: Translate = translate
): string | undefined {
if (!isLive) {
return undefined
}
return keyboardCaptureActive
? translate(
? t(
'auto.components.emulator.pane.emulator.device.frame.8f25ffaf8a',
'Emulator screen, keyboard captured. Press Escape to release.'
)
: translate('auto.components.emulator.pane.emulator.device.frame.9406c15775', 'Emulator screen')
: t('auto.components.emulator.pane.emulator.device.frame.9406c15775', 'Emulator screen')
}
@@ -3,7 +3,7 @@ import { useEffect, type CSSProperties } from 'react'
import { useEmulatorFrameStream } from './use-emulator-frame-stream'
import { useEmulatorVideoStream } from './use-emulator-video-stream'
import { scrcpyDeviceIdFromStreamUrl } from './emulator-scrcpy-stream-url'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import type { VisualStreamGeometry } from './emulator-device-frame-layout'
type StreamSize = {
@@ -34,6 +34,7 @@ export function EmulatorScreenStreamContent({
streamKey,
streamRotation = 0
}: EmulatorScreenStreamContentProps) {
const t = useTranslate()
const androidDeviceId = scrcpyDeviceIdFromStreamUrl(previewUrl)
const video = useEmulatorVideoStream(
@@ -66,7 +67,7 @@ export function EmulatorScreenStreamContent({
ref={video.canvasRef}
className={mediaClassName}
style={mediaStyle}
aria-label={translate(
aria-label={t(
'auto.components.emulator.pane.emulator.screen.stream.content.5ee64cd44e',
'Emulator screen'
)}
@@ -79,7 +80,7 @@ export function EmulatorScreenStreamContent({
<img
key={`${previewUrl}::${streamKey ?? ''}`}
src={frameStream.frameUrl}
alt={translate(
alt={t(
'auto.components.emulator.pane.emulator.screen.stream.content.5ee64cd44e',
'Emulator screen'
)}
@@ -107,7 +108,7 @@ export function EmulatorScreenStreamContent({
<>
<Loader2 className="size-6 animate-spin text-primary" />
<span className="text-xs">
{translate(
{t(
'auto.components.emulator.pane.emulator.screen.stream.content.5f818f12ab',
'Connecting emulator…'
)}
@@ -115,14 +116,14 @@ export function EmulatorScreenStreamContent({
</>
) : displayError ? (
<span className="px-6 text-center text-xs">
{translate(
{t(
'auto.components.emulator.pane.emulator.screen.stream.content.36841af608',
'Stream disconnected'
)}
</span>
) : (
<span className="px-6 text-center text-xs">
{translate(
{t(
'auto.components.emulator.pane.emulator.screen.stream.content.8b1a0d8694',
'Emulator preview'
)}
@@ -13,6 +13,7 @@ import type {
StreamSize,
VisualStreamGeometry
} from './emulator-device-frame-layout'
import { useTranslate } from '@/i18n/use-translate'
type EmulatorScreenSurfaceProps = {
frameLayout: DeviceFrameLayout | null
@@ -59,6 +60,7 @@ export function EmulatorScreenSurface({
streamKey,
streamRotation
}: EmulatorScreenSurfaceProps) {
const t = useTranslate()
return (
<div
className={cn(
@@ -83,7 +85,7 @@ export function EmulatorScreenSurface({
role={isLive ? 'application' : undefined}
tabIndex={isLive ? 0 : undefined}
aria-keyshortcuts={keyboardCaptureActive ? 'Escape' : undefined}
aria-label={getEmulatorScreenAriaLabel(isLive, keyboardCaptureActive)}
aria-label={getEmulatorScreenAriaLabel(isLive, keyboardCaptureActive, t)}
>
{/* Use the real streamed screen; extra in-screen chrome would duplicate iOS status bars and distort bezels. */}
<EmulatorScreenStreamContent
@@ -28,6 +28,7 @@ import { useEmulatorPaneLifecycle } from './use-emulator-pane-lifecycle'
import { useEmulatorPaneShutdown } from './use-emulator-pane-shutdown'
import { emulatorStartErrorText, noEmulatorDevicesError } from './emulator-pane-error-copy'
import { SIMULATOR_DEFAULT_TAB_LABEL, formatSimulatorTabLabel } from '@/lib/simulator-tab-label'
import { useTranslate } from '@/i18n/use-translate'
type UseEmulatorPaneSessionArgs = {
worktreeId: string
@@ -40,6 +41,7 @@ export function useEmulatorPaneSession({
tabId,
autoAttachOnMount
}: UseEmulatorPaneSessionArgs) {
const t = useTranslate()
const configuredDefaultUdid = useAppStore((state) =>
state.settings && getEmulatorOpeningMode(state.settings) === 'specific'
? (state.settings.mobileEmulatorDefaultDeviceUdid ?? null)
@@ -307,7 +309,7 @@ export function useEmulatorPaneSession({
sendGesture,
sendRotate,
visualOrientation,
displayName: formatSimulatorTabLabel(view.displayName),
displayName: formatSimulatorTabLabel(view.displayName, t),
previewUrl: view.previewUrl,
wsUrl: view.wsUrl,
streamKey: streamKey ?? undefined,
@@ -15,7 +15,7 @@ import { getWorkspaceEditorSshHost } from '@/lib/workspace-editor-ssh-target'
import { NO_OPEN_IN_APPLICATIONS } from '@/lib/open-in-application-selection'
import type { ExecutionHostId } from '../../../../shared/execution-host'
import type { OpenInApplication } from '../../../../shared/ui-chrome-types'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import {
getOpenInEntryAvailability,
openOpenInAppsSettings,
@@ -33,6 +33,7 @@ export function OpenInContextSubmenu({
workspaceId,
owner
}: OpenInContextSubmenuProps): React.JSX.Element {
const t = useTranslate()
const openInApplications = useAppStateValue(
(s) => s.settings?.openInApplications ?? NO_OPEN_IN_APPLICATIONS
)
@@ -64,7 +65,7 @@ export function OpenInContextSubmenu({
<ContextMenuSub>
<ContextMenuSubTrigger disabled={!path}>
<FolderOpen className="size-3.5" />
{translate('auto.components.sidebar.WorktreeOpenInMenu.8009ab69a6', 'Open in')}
{t('auto.components.sidebar.WorktreeOpenInMenu.8009ab69a6', 'Open in')}
</ContextMenuSubTrigger>
<ContextMenuSubContent className="w-52">
{openInApplications.map((application) => {
@@ -73,7 +74,8 @@ export function OpenInContextSubmenu({
openInRoute.connectionId,
openInRoute.runtimeEnvironmentId,
openInRoute.ownerUnresolved,
runtimeSshHost
runtimeSshHost,
t
)
return (
<ContextMenuItem
@@ -97,7 +99,7 @@ export function OpenInContextSubmenu({
})}
{openInApplications.length > 0 ? <ContextMenuSeparator /> : null}
<ContextMenuItem onSelect={openOpenInAppsSettings}>
{translate('auto.components.sidebar.WorktreeOpenInMenu.1417fd8380', 'Customize apps...')}
{t('auto.components.sidebar.WorktreeOpenInMenu.1417fd8380', 'Customize apps...')}
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
@@ -2,7 +2,7 @@ import { FileText, Globe, Minus, TerminalSquare } from 'lucide-react'
import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo'
import { Button } from '@/components/ui/button'
import type { ShortcutKeyComboDetails } from '@/hooks/useShortcutLabel'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
type FloatingTerminalEmptyStateProps = {
onNewTerminal: () => void
@@ -33,6 +33,7 @@ export function FloatingTerminalEmptyState({
openMarkdownShortcut,
closeShortcut
}: FloatingTerminalEmptyStateProps): React.JSX.Element {
const t = useTranslate()
return (
<div
className="absolute inset-0 flex items-center justify-center"
@@ -50,7 +51,7 @@ export function FloatingTerminalEmptyState({
>
<TerminalSquare className="size-3.5 opacity-90" />
<span className="truncate text-left leading-none">
{translate(
{t(
'auto.components.floating.terminal.FloatingTerminalPanel.3215fc73e9',
'New Terminal'
)}
@@ -66,7 +67,7 @@ export function FloatingTerminalEmptyState({
>
<FileText className="size-3.5 opacity-90" />
<span className="truncate text-left leading-none">
{translate(
{t(
'auto.components.floating.terminal.FloatingTerminalPanel.629528690b',
'New Markdown Note'
)}
@@ -81,7 +82,7 @@ export function FloatingTerminalEmptyState({
>
<FileText className="size-3.5 opacity-90" />
<span className="truncate text-left leading-none">
{translate(
{t(
'auto.components.floating.terminal.FloatingTerminalPanel.88ffb502e5',
'Open Markdown Note'
)}
@@ -97,7 +98,7 @@ export function FloatingTerminalEmptyState({
>
<Globe className="size-3.5 opacity-90" />
<span className="truncate text-left leading-none">
{translate(
{t(
'auto.components.floating.terminal.FloatingTerminalPanel.8b07759314',
'New Browser'
)}
@@ -113,10 +114,7 @@ export function FloatingTerminalEmptyState({
>
<Minus className="size-3.5 opacity-90" />
<span className="truncate text-left leading-none">
{translate(
'auto.components.floating.terminal.FloatingTerminalPanel.fc1042e92b',
'Minimize'
)}
{t('auto.components.floating.terminal.FloatingTerminalPanel.fc1042e92b', 'Minimize')}
</span>
<FloatingEmptyStateShortcut shortcut={closeShortcut} />
</Button>
@@ -9,7 +9,7 @@ import {
} from '@/components/ui/dropdown-menu'
import { useAppStore } from '@/store'
import type { FloatingTerminalTriggerLocation } from '../../../../shared/ui-chrome-types'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { useUiLanguageRevision } from '@/i18n/use-ui-language-revision'
type FloatingTerminalIconContextMenuProps = {
@@ -25,6 +25,7 @@ export function FloatingTerminalIconContextMenu({
className,
style
}: FloatingTerminalIconContextMenuProps): React.JSX.Element {
const t = useTranslate()
const languageRevision = useUiLanguageRevision()
const updateSettings = useAppStore((s) => s.updateSettings)
const [open, setOpen] = useState(false)
@@ -44,7 +45,7 @@ export function FloatingTerminalIconContextMenu({
if (currentLocation === 'floating-button') {
return {
icon: <PanelBottom className="size-3.5" />,
label: translate(
label: t(
'auto.components.floating.terminal.FloatingTerminalIconContextMenu.0ee79e0674',
'Move to Status Bar'
),
@@ -53,14 +54,14 @@ export function FloatingTerminalIconContextMenu({
}
return {
icon: <PanelTop className="size-3.5" />,
label: translate(
label: t(
'auto.components.floating.terminal.FloatingTerminalIconContextMenu.763f5fa2c1',
'Move to Floating Button'
),
location: 'floating-button' as const
}
// oxlint-disable-next-line react-hooks/exhaustive-deps -- translate() reads the UI language implicitly.
}, [currentLocation, languageRevision])
}, [currentLocation, languageRevision, t])
return (
<>
@@ -117,7 +118,7 @@ export function FloatingTerminalIconContextMenu({
}}
>
<EyeOff className="size-3.5" />
{translate(
{t(
'auto.components.floating.terminal.FloatingTerminalIconContextMenu.8e7d775287',
'Hide Floating Workspace'
)}
@@ -1,5 +1,6 @@
import { Button } from '@/components/ui/button'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
type FloatingTerminalOrchestrationCardProps = {
visible: boolean
@@ -7,11 +8,10 @@ type FloatingTerminalOrchestrationCardProps = {
onEnable: () => void
}
export function renderFloatingTerminalOrchestrationCard({
visible,
onDismiss,
onEnable
}: FloatingTerminalOrchestrationCardProps): React.JSX.Element | null {
export function renderFloatingTerminalOrchestrationCard(
{ visible, onDismiss, onEnable }: FloatingTerminalOrchestrationCardProps,
t: Translate = translate
): React.JSX.Element | null {
if (!visible) {
return null
}
@@ -23,13 +23,13 @@ export function renderFloatingTerminalOrchestrationCard({
<div className="space-y-2">
<div className="space-y-0.5">
<p className="text-sm font-medium">
{translate(
{t(
'auto.components.floating.terminal.FloatingTerminalPanel.2a3c5ddf5e',
'Enable orchestration'
)}
</p>
<p className="text-xs leading-5 text-muted-foreground">
{translate(
{t(
'auto.components.floating.terminal.FloatingTerminalPanel.8cf80db43b',
'Set up the Orca CLI and agent skill so agents can coordinate through Orca.'
)}
@@ -37,16 +37,10 @@ export function renderFloatingTerminalOrchestrationCard({
</div>
<div className="flex items-center gap-2">
<Button type="button" variant="ghost" size="sm" className="flex-1" onClick={onDismiss}>
{translate(
'auto.components.floating.terminal.FloatingTerminalPanel.adc281394d',
'Dismiss'
)}
{t('auto.components.floating.terminal.FloatingTerminalPanel.adc281394d', 'Dismiss')}
</Button>
<Button type="button" variant="default" size="sm" className="flex-1" onClick={onEnable}>
{translate(
'auto.components.floating.terminal.FloatingTerminalPanel.bbc177f98f',
'Enable'
)}
{t('auto.components.floating.terminal.FloatingTerminalPanel.bbc177f98f', 'Enable')}
</Button>
</div>
</div>
@@ -17,7 +17,7 @@ import {
import { useActiveProjectSkillRuntime } from '@/hooks/useActiveProjectSkillRuntime'
import { refreshSkillFreshness } from '@/hooks/useSkillFreshness'
import { useAppStore } from '@/store'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
type FloatingTerminalOrchestrationDialogProps = {
open: boolean
@@ -30,6 +30,7 @@ export function FloatingTerminalOrchestrationDialog({
onOpenChange,
onSetupStateChange
}: FloatingTerminalOrchestrationDialogProps): React.JSX.Element {
const t = useTranslate()
const activeSkillRuntime = useActiveProjectSkillRuntime()
const {
installed: orchestrationSkillDetected,
@@ -64,14 +65,14 @@ export function FloatingTerminalOrchestrationDialog({
{/* The panel hides its header, so the modal owns the title and status. */}
<div className="flex flex-wrap items-center gap-2 pr-6">
<DialogTitle>
{translate(
{t(
'auto.components.floating.terminal.FloatingTerminalOrchestrationDialog.543f325a14',
'Enable orchestration'
)}
</DialogTitle>
{orchestrationSkillLoading && !orchestrationSkillDetected ? (
<IntegrationStatusPill tone="neutral">
{translate(
{t(
'auto.components.floating.terminal.FloatingTerminalOrchestrationDialog.dfd021ce46',
'Checking...'
)}
@@ -81,7 +82,7 @@ export function FloatingTerminalOrchestrationDialog({
<SkillFreshnessStatusPill skillName={ORCHESTRATION_SKILL_NAME} />
) : (
<IntegrationStatusPill tone="connected">
{translate(
{t(
'auto.components.floating.terminal.FloatingTerminalOrchestrationDialog.630c0ac8c8',
'Installed'
)}
@@ -89,7 +90,7 @@ export function FloatingTerminalOrchestrationDialog({
)
) : (
<IntegrationStatusPill tone="attention">
{translate(
{t(
'auto.components.floating.terminal.FloatingTerminalOrchestrationDialog.05d7aabc20',
'Not installed'
)}
@@ -97,7 +98,7 @@ export function FloatingTerminalOrchestrationDialog({
)}
</div>
<DialogDescription className="sr-only">
{translate(
{t(
'auto.components.floating.terminal.FloatingTerminalOrchestrationDialog.6f0aed26b8',
'Install the Orca CLI and orchestration skill so agents can coordinate through Orca.'
)}
@@ -105,11 +106,11 @@ export function FloatingTerminalOrchestrationDialog({
</DialogHeader>
<AgentSkillSetupPanel
title={translate(
title={t(
'auto.components.floating.terminal.FloatingTerminalOrchestrationDialog.1cd3f8af64',
'Orchestration skill'
)}
description={translate(
description={t(
'auto.components.floating.terminal.FloatingTerminalOrchestrationDialog.f726054620',
'Enables agents to hand off context and coordinate work through Orca.'
)}
@@ -120,10 +121,7 @@ export function FloatingTerminalOrchestrationDialog({
installDisabled={Boolean(activeSkillRuntime.installDisabledReason)}
variant="inline"
hideHeader
installLabel={translate(
'auto.components.skills.SkillInstallDialog.39acb9e8f4',
'Install skill'
)}
installLabel={t('auto.components.skills.SkillInstallDialog.39acb9e8f4', 'Install skill')}
onBeforeInstall={() => {
useAppStore.getState().recordFeatureInteraction('agent-orchestration-setup')
}}
@@ -1,10 +1,12 @@
import { renderFloatingTerminalPanelSurface } from './FloatingTerminalPanelSurface'
import type { FloatingTerminalPanelProps } from './floating-terminal-panel-types'
import { useFloatingTerminalPanelController } from './use-floating-terminal-panel-controller'
import { useTranslate } from '../../i18n/use-translate'
export { FloatingTerminalToggleButton } from './FloatingTerminalToggleButton'
export function FloatingTerminalPanel(props: FloatingTerminalPanelProps): React.JSX.Element {
const t = useTranslate()
const surface = useFloatingTerminalPanelController(props)
return renderFloatingTerminalPanelSurface(surface)
return renderFloatingTerminalPanelSurface(surface, t)
}
@@ -11,66 +11,70 @@ import { FloatingTerminalOrchestrationDialog } from './FloatingTerminalOrchestra
import { FloatingTerminalResizeHandles } from './FloatingTerminalResizeHandles'
import { FloatingTerminalWindowControls } from './FloatingTerminalWindowControls'
import type { useFloatingTerminalPanelController } from './use-floating-terminal-panel-controller'
import type { Translate } from '@/i18n/use-translate'
const NO_ACTIVITY_TERMINAL_PORTALS: [] = []
export function renderFloatingTerminalPanelSurface({
open,
onOpenChange,
bounds,
maximized,
stagedBoundsRef,
setPanelNode,
commitUserBounds,
reportFloatingFocusFromTarget,
handleShortcutSurfaceKeyDown,
handleDragStart,
handleDragMove,
handleDragEnd,
handleTitlebarDoubleClick,
surface,
model,
terminalItems,
activeGroup,
activeTerminalId,
activeEditorUnifiedId,
activeBrowserId,
activeTab,
activeTabType,
expandedPaneByTabId,
closeFloatingItemConfirmed,
closeOthers,
closeToRight,
closeToLeft,
createFloatingTerminalTab,
createFloatingBrowserTab,
createFloatingMarkdownTab,
openFloatingMarkdownTab,
setTabCustomTitle,
setTabColor,
setTabPaneExpanded,
createBrowserTab,
closeAllFiles,
makePreviewFilePermanent,
pinFile,
toggleMaximized,
hasVisibleFloatingTabs,
managedBrowserCreationEnabled,
cwd,
panelViewportSettled,
focusPanelForShortcuts,
newTerminalShortcut,
newBrowserShortcut,
newMarkdownShortcut,
openMarkdownShortcut,
closeShortcut,
showOrchestrationSetup,
dismissOrchestrationSetup,
setOrchestrationDialogOpen,
previewUserBounds,
orchestrationDialogOpen,
refreshOrchestrationSetupVisibility
}: ReturnType<typeof useFloatingTerminalPanelController>): React.JSX.Element {
export function renderFloatingTerminalPanelSurface(
{
open,
onOpenChange,
bounds,
maximized,
stagedBoundsRef,
setPanelNode,
commitUserBounds,
reportFloatingFocusFromTarget,
handleShortcutSurfaceKeyDown,
handleDragStart,
handleDragMove,
handleDragEnd,
handleTitlebarDoubleClick,
surface,
model,
terminalItems,
activeGroup,
activeTerminalId,
activeEditorUnifiedId,
activeBrowserId,
activeTab,
activeTabType,
expandedPaneByTabId,
closeFloatingItemConfirmed,
closeOthers,
closeToRight,
closeToLeft,
createFloatingTerminalTab,
createFloatingBrowserTab,
createFloatingMarkdownTab,
openFloatingMarkdownTab,
setTabCustomTitle,
setTabColor,
setTabPaneExpanded,
createBrowserTab,
closeAllFiles,
makePreviewFilePermanent,
pinFile,
toggleMaximized,
hasVisibleFloatingTabs,
managedBrowserCreationEnabled,
cwd,
panelViewportSettled,
focusPanelForShortcuts,
newTerminalShortcut,
newBrowserShortcut,
newMarkdownShortcut,
openMarkdownShortcut,
closeShortcut,
showOrchestrationSetup,
dismissOrchestrationSetup,
setOrchestrationDialogOpen,
previewUserBounds,
orchestrationDialogOpen,
refreshOrchestrationSetupVisibility
}: ReturnType<typeof useFloatingTerminalPanelController>,
t?: Translate
): React.JSX.Element {
return (
// Stay above notifications but below modals; separate shadow and border shells to keep rounded corners clean.
<div
@@ -254,11 +258,14 @@ export function renderFloatingTerminalPanelSurface({
</div>
)}
</WorkspaceTabDragLayer>
{renderFloatingTerminalOrchestrationCard({
visible: showOrchestrationSetup && activeTabType === 'terminal',
onDismiss: dismissOrchestrationSetup,
onEnable: () => setOrchestrationDialogOpen(true)
})}
{renderFloatingTerminalOrchestrationCard(
{
visible: showOrchestrationSetup && activeTabType === 'terminal',
onDismiss: dismissOrchestrationSetup,
onEnable: () => setOrchestrationDialogOpen(true)
},
t
)}
{!maximized && (
<FloatingTerminalResizeHandles
bounds={bounds}
@@ -21,7 +21,7 @@ import {
type FloatingTerminalTriggerPosition,
type FloatingTerminalTriggerPositionSource
} from './floating-terminal-trigger-position'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
const FLOATING_TERMINAL_TRIGGER_DRAG_THRESHOLD = 4
@@ -64,6 +64,7 @@ export function FloatingTerminalToggleButton({
open: boolean
onToggle: () => void
}): React.JSX.Element {
const t = useTranslate()
// Memoized: subscribe so a language change still repaints its labels.
useTranslation()
const shortcutLabel = useShortcutLabel('floatingTerminal.toggle')
@@ -208,16 +209,16 @@ export function FloatingTerminalToggleButton({
data-floating-terminal-toggle
aria-label={
open
? translate(
? t(
'auto.components.floating.terminal.FloatingTerminalToggleButton.5785dd9148',
'Minimize floating workspace'
)
: showAttentionDot
? translate(
? t(
'auto.components.floating.terminal.FloatingTerminalToggleButton.4cb418b991',
'Show floating workspace, new activity'
)
: translate(
: t(
'auto.components.floating.terminal.FloatingTerminalToggleButton.3b04b065b5',
'Show floating workspace'
)
@@ -241,7 +242,7 @@ export function FloatingTerminalToggleButton({
</Button>
</TooltipTrigger>
<TooltipContent side="left" sideOffset={6}>
{translate(
{t(
'auto.components.floating.terminal.FloatingTerminalToggleButton.bfe7809a70',
'{{value0}} floating workspace ({{value1}})',
{ value0: open ? 'Minimize' : 'Show', value1: shortcutLabel }
@@ -12,7 +12,7 @@ import {
DEFAULT_DISABLED_TUI_AGENTS,
isTuiAgentEnabled
} from '../../../../shared/agent-launch/catalog/tui-agent-selection'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { useOptionalShortcutLabel } from '@/hooks/useShortcutLabel'
type FloatingTerminalWindowControlsProps = {
@@ -33,6 +33,7 @@ export function FloatingTerminalWindowControls({
onToggleMaximized,
onMinimize
}: FloatingTerminalWindowControlsProps): React.JSX.Element {
const t = useTranslate()
const defaultTuiAgent = useAppStore((s) => s.settings?.defaultTuiAgent ?? null)
const setActiveTabForWorktree = useAppStore((s) => s.setActiveTabForWorktree)
const activateTab = useAppStore((s) => s.activateTab)
@@ -90,7 +91,7 @@ export function FloatingTerminalWindowControls({
variant="outline"
size="icon-xs"
className={controlButtonClassName}
aria-label={translate(
aria-label={t(
'auto.components.floating.terminal.FloatingTerminalWindowControls.648352c51f',
'Open {{value0}} in floating workspace',
{ value0: defaultAgentLabel ?? defaultAgent }
@@ -101,7 +102,7 @@ export function FloatingTerminalWindowControls({
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate(
{t(
'auto.components.floating.terminal.FloatingTerminalWindowControls.648352c51f',
'Open {{value0}} in floating workspace',
{ value0: defaultAgentLabel ?? defaultAgent }
@@ -118,11 +119,11 @@ export function FloatingTerminalWindowControls({
className={controlButtonClassName}
aria-label={
maximized
? translate(
? t(
'auto.components.floating.terminal.FloatingTerminalWindowControls.1c79cba25d',
'Restore floating workspace'
)
: translate(
: t(
'auto.components.floating.terminal.FloatingTerminalWindowControls.3f4ca29961',
'Maximize floating workspace'
)
@@ -136,14 +137,14 @@ export function FloatingTerminalWindowControls({
<TooltipContent side="bottom" sideOffset={6}>
{maximized
? withShortcutHint(
translate(
t(
'auto.components.floating.terminal.FloatingTerminalWindowControls.b5686fee1e',
'Restore'
),
maximizeShortcutLabel
)
: withShortcutHint(
translate(
t(
'auto.components.floating.terminal.FloatingTerminalWindowControls.109870e023',
'Maximize'
),
@@ -158,7 +159,7 @@ export function FloatingTerminalWindowControls({
variant="outline"
size="icon-xs"
className={controlButtonClassName}
aria-label={translate(
aria-label={t(
'auto.components.floating.terminal.FloatingTerminalWindowControls.1bbaa0302f',
'Minimize floating workspace'
)}
@@ -169,7 +170,7 @@ export function FloatingTerminalWindowControls({
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{withShortcutHint(
translate(
t(
'auto.components.floating.terminal.FloatingTerminalWindowControls.2f6054342c',
'Minimize'
),
@@ -6,7 +6,7 @@ import { ButtonGroup } from '@/components/ui/button-group'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { detectLanguage } from '@/lib/language-detect'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import {
createCommentCodeContextExpansionState,
resolveCommentCodeContextExpansionState,
@@ -64,6 +64,7 @@ export function CommentCodeContext({
baseSha: string | undefined
loadPRFileContents: LoadPRFileContents
}): React.JSX.Element | null {
const t = useTranslate()
const [contents, setContents] = useState<GitHubPRFileContents | null>(null)
const [error, setError] = useState(false)
const [contextExpansionState, setContextExpansionState] = useState(() =>
@@ -158,7 +159,7 @@ export function CommentCodeContext({
return (
<div className="mb-3 flex items-center gap-2 rounded-md border border-border/40 bg-muted/20 px-3 py-2 text-[12px] text-muted-foreground">
<LoaderCircle className="size-3.5 animate-spin" />
{translate('auto.components.GitHubItemDialog.db61d76cd5', 'Loading code context…')}
{t('auto.components.GitHubItemDialog.db61d76cd5', 'Loading code context…')}
</div>
)
}
@@ -195,11 +196,11 @@ export function CommentCodeContext({
} = codeContext
const language = detectLanguage(comment.path)
const blockTooltip = shouldUseBlockRange
? translate(
? t(
'auto.components.github.CommentCodeContext.showSurroundingCodeBlock',
'Show surrounding code block'
)
: translate(
: t(
'auto.components.github.CommentCodeContext.showNearbyCodeContext',
'Show nearby code context'
)
@@ -212,17 +213,17 @@ export function CommentCodeContext({
<span className="shrink-0 font-mono">
L{from}
{to !== from
? translate('auto.components.GitHubItemDialog.d1c0dad471', '-L{{value0}}', {
? t('auto.components.GitHubItemDialog.d1c0dad471', '-L{{value0}}', {
value0: to
})
: ''}
</span>
{(from !== commentFrom || to !== commentTo) && (
<span className="shrink-0 font-mono text-muted-foreground/70">
{translate('auto.components.GitHubItemDialog.bd7be7b1fd', 'comment L')}
{t('auto.components.GitHubItemDialog.bd7be7b1fd', 'comment L')}
{commentFrom}
{commentTo !== commentFrom
? translate('auto.components.GitHubItemDialog.d1c0dad471', '-L{{value0}}', {
? t('auto.components.GitHubItemDialog.d1c0dad471', '-L{{value0}}', {
value0: commentTo
})
: ''}
@@ -231,10 +232,7 @@ export function CommentCodeContext({
</div>
<ButtonGroup
className="text-muted-foreground"
aria-label={translate(
'auto.components.GitHubItemDialog.d43736d09c',
'Code context controls'
)}
aria-label={t('auto.components.GitHubItemDialog.d43736d09c', 'Code context controls')}
>
{(contextBefore > 0 || contextAfter > 0) && (
<Tooltip>
@@ -248,7 +246,7 @@ export function CommentCodeContext({
setContextBefore(0)
setContextAfter(0)
}}
aria-label={translate(
aria-label={t(
'auto.components.GitHubItemDialog.b1574e8ac2',
'Reset code context'
)}
@@ -257,7 +255,7 @@ export function CommentCodeContext({
</Button>
</TooltipTrigger>
<TooltipContent>
{translate('auto.components.GitHubItemDialog.b1574e8ac2', 'Reset code context')}
{t('auto.components.GitHubItemDialog.b1574e8ac2', 'Reset code context')}
</TooltipContent>
</Tooltip>
)}
@@ -274,7 +272,7 @@ export function CommentCodeContext({
Math.min(current + CODE_CONTEXT_EXPAND_STEP, commentFrom - 1)
)
}
aria-label={translate(
aria-label={t(
'auto.components.GitHubItemDialog.307c98e8e3',
'Show {{value0}} more lines above',
{ value0: CODE_CONTEXT_EXPAND_STEP }
@@ -284,7 +282,7 @@ export function CommentCodeContext({
</Button>
</TooltipTrigger>
<TooltipContent>
{translate('auto.components.GitHubItemDialog.5664681624', 'Show more lines above')}
{t('auto.components.GitHubItemDialog.5664681624', 'Show more lines above')}
</TooltipContent>
</Tooltip>
<Tooltip>
@@ -300,7 +298,7 @@ export function CommentCodeContext({
Math.min(current + CODE_CONTEXT_EXPAND_STEP, totalLines - commentTo)
)
}
aria-label={translate(
aria-label={t(
'auto.components.GitHubItemDialog.showMoreLinesBelow',
'Show {{value0}} more lines below',
{ value0: CODE_CONTEXT_EXPAND_STEP }
@@ -310,7 +308,7 @@ export function CommentCodeContext({
</Button>
</TooltipTrigger>
<TooltipContent>
{translate('auto.components.GitHubItemDialog.06c06e58ba', 'Show more lines below')}
{t('auto.components.GitHubItemDialog.06c06e58ba', 'Show more lines below')}
</TooltipContent>
</Tooltip>
<Tooltip>
@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
import { GITHUB_REACTION_ORDER } from '@/lib/source-control/review/comments/pr-comment-reactions'
import type { GitHubReaction, GitHubReactionContent } from '../../../../shared/github/comment-types'
@@ -19,33 +20,33 @@ const REACTION_EMOJI: Record<GitHubReaction['content'], string> = {
eyes: '👀'
}
function reactionLabel(content: GitHubReactionContent): string {
function reactionLabel(content: GitHubReactionContent, t: Translate = translate): string {
switch (content) {
case '+1':
return translate('auto.components.github.CommentReactions.reaction.thumbsUp', 'thumbs up')
return t('auto.components.github.CommentReactions.reaction.thumbsUp', 'thumbs up')
case '-1':
return translate('auto.components.github.CommentReactions.reaction.thumbsDown', 'thumbs down')
return t('auto.components.github.CommentReactions.reaction.thumbsDown', 'thumbs down')
case 'laugh':
return translate('auto.components.github.CommentReactions.reaction.laugh', 'laugh')
return t('auto.components.github.CommentReactions.reaction.laugh', 'laugh')
case 'confused':
return translate('auto.components.github.CommentReactions.reaction.confused', 'confused')
return t('auto.components.github.CommentReactions.reaction.confused', 'confused')
case 'heart':
return translate('auto.components.github.CommentReactions.reaction.heart', 'heart')
return t('auto.components.github.CommentReactions.reaction.heart', 'heart')
case 'hooray':
return translate('auto.components.github.CommentReactions.reaction.hooray', 'hooray')
return t('auto.components.github.CommentReactions.reaction.hooray', 'hooray')
case 'rocket':
return translate('auto.components.github.CommentReactions.reaction.rocket', 'rocket')
return t('auto.components.github.CommentReactions.reaction.rocket', 'rocket')
case 'eyes':
return translate('auto.components.github.CommentReactions.reaction.eyes', 'eyes')
return t('auto.components.github.CommentReactions.reaction.eyes', 'eyes')
}
}
function reactionCountLabel(count: number, reaction: string): string {
return translate(
'auto.components.GitHubItemDialog.reactionCount',
'{{count}} {{reaction}} reactions',
{ count, reaction, defaultValue_one: '{{count}} {{reaction}} reaction' }
)
function reactionCountLabel(count: number, reaction: string, t: Translate = translate): string {
return t('auto.components.GitHubItemDialog.reactionCount', '{{count}} {{reaction}} reactions', {
count,
reaction,
defaultValue_one: '{{count}} {{reaction}} reaction'
})
}
export function CommentReactions({
@@ -60,6 +61,7 @@ export function CommentReactions({
reacted: boolean
) => Promise<boolean> | boolean
}): React.JSX.Element | null {
const t = useTranslate()
const visibleReactions = (reactions ?? []).filter((reaction) => reaction.count > 0)
const addReactionButtonRef = React.useRef<HTMLButtonElement>(null)
const pickerGroupRef = React.useRef<HTMLDivElement>(null)
@@ -95,10 +97,7 @@ export function CommentReactions({
}
}
const pickerLabel = translate(
'auto.components.github.CommentReactions.addReaction',
'Add reaction'
)
const pickerLabel = t('auto.components.github.CommentReactions.addReaction', 'Add reaction')
return (
<div className={cn('mt-2 flex flex-wrap items-center gap-1.5', className)}>
@@ -115,7 +114,7 @@ export function CommentReactions({
reaction.viewerHasReacted && 'border-ring bg-accent text-accent-foreground'
)}
aria-pressed={Boolean(reaction.viewerHasReacted)}
aria-label={reactionCountLabel(reaction.count, reactionLabel(reaction.content))}
aria-label={reactionCountLabel(reaction.count, reactionLabel(reaction.content, t), t)}
onClick={() =>
void changeReaction(
reaction.content,
@@ -131,7 +130,7 @@ export function CommentReactions({
) : (
<span
className="inline-flex h-6 items-center gap-1 rounded-full border border-border/60 bg-muted/35 px-2 text-[12px] leading-none text-foreground"
aria-label={reactionCountLabel(reaction.count, reactionLabel(reaction.content))}
aria-label={reactionCountLabel(reaction.count, reactionLabel(reaction.content, t), t)}
>
<span aria-hidden="true">{REACTION_EMOJI[reaction.content]}</span>
<span className="tabular-nums">{reaction.count}</span>
@@ -191,15 +190,15 @@ export function CommentReactions({
className={cn('text-lg', reacted && 'bg-accent text-accent-foreground')}
aria-label={
reacted
? translate(
? t(
'auto.components.github.CommentReactions.removeNamedReaction',
'Remove {{value0}} reaction',
{ value0: reactionLabel(content) }
{ value0: reactionLabel(content, t) }
)
: translate(
: t(
'auto.components.github.CommentReactions.addNamedReaction',
'Add {{value0}} reaction',
{ value0: reactionLabel(content) }
{ value0: reactionLabel(content, t) }
)
}
aria-pressed={reacted}
@@ -26,7 +26,7 @@ import {
import { GitHubMarkdownImageInput } from './github-markdown-image-input'
import { useImageInput } from '@/components/github/use-image-input'
import type { GitHubOwnerRepo } from '../../../../shared/github/pull-request-types'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { useAppStore } from '@/store'
import {
getRichMarkdownSpellcheckAttribute,
@@ -65,6 +65,7 @@ export function GitHubMarkdownComposer({
layout = 'stacked',
previewGithubRepo = null
}: GitHubMarkdownComposerProps): React.JSX.Element {
const t = useTranslate()
const rootRef = useRef<HTMLDivElement | null>(null)
const editorRef = useRef<Editor | null>(null)
const applyingExternalValueRef = useRef(false)
@@ -336,7 +337,7 @@ export function GitHubMarkdownComposer({
>
<Paperclip className="size-3.5 shrink-0" />
<span>
{translate(
{t(
'auto.components.github.GitHubMarkdownComposer.b7e4a1c902',
'Paste, drop, or click to add files'
)}
@@ -3,6 +3,7 @@ import type { GitHubOwnerRepo } from '../../../../shared/github/pull-request-typ
import RepoBadgeLabel from '@/components/repo/RepoBadgeLabel'
import { Trans } from 'react-i18next'
import { i18n, translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
import {
githubRepoIdentityKey,
@@ -21,10 +22,10 @@ export type IssueSourceIndicatorProps = {
}
// Name the resolved repository, not remote aliases that could imply a stable UI-to-git-config mapping.
function issueSourceTemplate(variant: 'list' | 'item'): string {
function issueSourceTemplate(variant: 'list' | 'item', t: Translate = translate): string {
return variant === 'item'
? translate('auto.components.github.IssueSourceIndicator.issueFromRepo', 'Issue from <repo/>')
: translate('auto.components.github.IssueSourceIndicator.issuesFromRepo', 'Issues from <repo/>')
? t('auto.components.github.IssueSourceIndicator.issueFromRepo', 'Issue from <repo/>')
: t('auto.components.github.IssueSourceIndicator.issuesFromRepo', 'Issues from <repo/>')
}
export function sameGitHubOwnerRepo(
@@ -46,6 +47,7 @@ export default function IssueSourceIndicator({
localRepo,
className
}: IssueSourceIndicatorProps): React.JSX.Element | null {
const t = useTranslate()
if (!issues || !prs) {
return null
}
@@ -57,7 +59,7 @@ export default function IssueSourceIndicator({
host && !isDefaultGitHubHost(host)
? `${host}/${issues.owner}/${issues.repo}`
: `${issues.owner}/${issues.repo}`
const template = issueSourceTemplate(variant)
const template = issueSourceTemplate(variant, t)
const label = template.replace('<repo/>', slug)
return (
<span
@@ -5,7 +5,8 @@ import { sameGitHubOwnerRepo } from '@/components/github/IssueSourceIndicator'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import { Trans } from 'react-i18next'
import { i18n, translate } from '@/i18n/i18n'
import { i18n } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
export type IssueSourceSelectorProps = {
/** Unset preferences behave as auto; untouched repos omit the stored key. */
@@ -54,6 +55,7 @@ export default function IssueSourceSelector({
suppressTooltip = false,
issueSource
}: IssueSourceSelectorProps): React.JSX.Element | null {
const t = useTranslate()
if (!origin || !upstream) {
return null
}
@@ -71,10 +73,7 @@ export default function IssueSourceSelector({
const group = (
<div
role="group"
aria-label={translate(
'auto.components.github.IssueSourceSelector.787c970baf',
'Issue source'
)}
aria-label={t('auto.components.github.IssueSourceSelector.787c970baf', 'Issue source')}
className={cn(
'inline-flex items-center overflow-hidden rounded border border-border/40',
className
@@ -94,7 +93,7 @@ export default function IssueSourceSelector({
>
{density === 'compact'
? 'U'
: translate('auto.components.github.IssueSourceSelector.30b2c9df91', 'Upstream')}
: t('auto.components.github.IssueSourceSelector.30b2c9df91', 'Upstream')}
</button>
<button
type="button"
@@ -113,7 +112,7 @@ export default function IssueSourceSelector({
>
{density === 'compact'
? 'O'
: translate('auto.components.github.IssueSourceSelector.51d1608920', 'Origin')}
: t('auto.components.github.IssueSourceSelector.51d1608920', 'Origin')}
</button>
</div>
)
@@ -132,7 +131,7 @@ export default function IssueSourceSelector({
<TooltipContent side="bottom" sideOffset={4} className="max-w-[260px]">
<Trans
i18n={i18n}
defaults={translate(
defaults={t(
'auto.components.github.IssueSourceSelector.showingIssuesFromRepo',
'Showing issues from <repo/>'
)}
@@ -4,7 +4,7 @@ import { LoaderCircle, Pencil } from 'lucide-react'
import { toast } from 'sonner'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { useAppStore } from '@/store'
import { useImmediateMutation } from '@/hooks/useIssueMetadata'
import { useRepoAssignees } from '@/hooks/useGitHubRepoMetadata'
@@ -34,6 +34,7 @@ export function PRAssigneesPanel({
sourceContext?: TaskSourceContext | null
onMutated: () => void
}): React.JSX.Element {
const t = useTranslate()
const [assigneePopoverOpen, setAssigneePopoverOpen] = useState(false)
const [localAssignees, setLocalAssignees] = useState<GitHubAssignableUser[]>(
() => item.assignees ?? []
@@ -176,16 +177,13 @@ export function PRAssigneesPanel({
return (
<section>
<div className="mb-2 flex items-center justify-between text-[11px] font-semibold uppercase tracking-[0.05em] text-muted-foreground">
<span>{translate('auto.components.GitHubItemDialog.83ac703dda', 'Assignees')}</span>
<span>{t('auto.components.GitHubItemDialog.83ac703dda', 'Assignees')}</span>
<Popover open={assigneePopoverOpen} onOpenChange={setAssigneePopoverOpen}>
<PopoverTrigger asChild>
<button
type="button"
disabled={!canEditAssignees || isPending('assignees') || repoAssignees.loading}
aria-label={translate(
'auto.components.GitHubItemDialog.76adcf5fe2',
'Edit assignees'
)}
aria-label={t('auto.components.GitHubItemDialog.76adcf5fe2', 'Edit assignees')}
className="rounded p-0.5 text-muted-foreground transition hover:bg-accent hover:text-foreground disabled:opacity-50"
>
{isPending('assignees') ? (
@@ -244,7 +242,7 @@ export function PRAssigneesPanel({
</div>
{localAssignees.length === 0 ? (
<div className="text-[12px] text-muted-foreground">
{translate('auto.components.GitHubItemDialog.c67de9e2fe', 'No one assigned')}
{t('auto.components.GitHubItemDialog.c67de9e2fe', 'No one assigned')}
</div>
) : (
<ul className="flex flex-col gap-1.5">
@@ -21,6 +21,7 @@ import type {
import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target'
import type { ParsedTaskQuery } from '../../../../shared/task-query'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
type Props = {
parsed: ParsedTaskQuery
@@ -35,8 +36,8 @@ type Props = {
export type { PRFilterChange } from '@/components/github/PRFilterSections'
function currentUserLabel(): string {
return translate('auto.components.github.PRFilterDropdowns.currentUser', 'Current user')
function currentUserLabel(t: Translate = translate): string {
return t('auto.components.github.PRFilterDropdowns.currentUser', 'Current user')
}
function userOptions(users: GitHubAssignableUser[]): PickerOption[] {
@@ -52,13 +53,14 @@ function ActivePill({
value: string
onClear: () => void
}): React.JSX.Element {
const t = useTranslate()
return (
<span className="inline-flex h-6 items-center gap-1 rounded-full border border-border/60 bg-muted/50 pl-2 pr-1 text-[11px] text-foreground">
<span className="text-muted-foreground">{label}:</span>
<span className="max-w-[160px] truncate font-medium">{value}</span>
<button
type="button"
aria-label={translate(
aria-label={t(
'auto.components.github.PRFilterDropdowns.8a2ffbf9b3',
'Remove {{value0}} filter',
{ value0: label }
@@ -80,6 +82,7 @@ export default function PRFilterDropdowns({
ownerTarget,
onChange
}: Props): React.JSX.Element {
const t = useTranslate()
const [openSection, setOpenSection] = useState<SectionKey | null>(null)
const [popoverOpen, setPopoverOpen] = useState(false)
const owner = primarySlug?.owner ?? null
@@ -141,12 +144,12 @@ export default function PRFilterDropdowns({
const statusPillValue = ((): string | null => {
const parts: string[] = []
if (parsed.state === 'closed' || parsed.state === 'merged') {
parts.push(filterStateLabel(parsed.state))
parts.push(filterStateLabel(parsed.state, t))
} else if (parsed.state === 'all') {
parts.push(translate('auto.components.github.PRFilterDropdowns.anyState', 'Any'))
parts.push(t('auto.components.github.PRFilterDropdowns.anyState', 'Any'))
}
if (parsed.draft) {
parts.push(filterDraftLabel())
parts.push(filterDraftLabel(t))
}
return parts.length > 0 ? parts.join(' · ') : null
})()
@@ -185,7 +188,7 @@ export default function PRFilterDropdowns({
)}
>
<ListFilter className="size-3.5" />
{translate('auto.components.github.PRFilterDropdowns.79c54552f7', 'Filters')}
{t('auto.components.github.PRFilterDropdowns.79c54552f7', 'Filters')}
{activeCount > 0 ? (
<span className="ml-0.5 rounded-full bg-muted px-1.5 text-[10px] font-medium text-foreground">
{activeCount}
@@ -246,22 +249,22 @@ export default function PRFilterDropdowns({
</Popover>
{statusPillValue ? (
<ActivePill
label={translate('auto.components.github.PRFilterDropdowns.13b3ac0a84', 'Status')}
label={t('auto.components.github.PRFilterDropdowns.13b3ac0a84', 'Status')}
value={statusPillValue}
onClear={() => onChange({ state: 'open', draft: false })}
/>
) : null}
{parsed.author ? (
<ActivePill
label={translate('auto.components.github.PRFilterDropdowns.01f3f3d161', 'Author')}
label={t('auto.components.github.PRFilterDropdowns.01f3f3d161', 'Author')}
value={parsed.author}
onClear={() => onChange({ author: null })}
/>
) : null}
{parsed.labels.length > 0 ? (
<ActivePill
label={translate('auto.components.github.PRFilterDropdowns.9d0f2eda6d', 'Label')}
value={filterLabelsValue(parsed.labels) ?? ''}
label={t('auto.components.github.PRFilterDropdowns.9d0f2eda6d', 'Label')}
value={filterLabelsValue(parsed.labels, t) ?? ''}
onClear={() => onChange({ labels: [] })}
/>
) : null}
@@ -269,8 +272,8 @@ export default function PRFilterDropdowns({
<ActivePill
label={
reviewerKind === 'reviewed-by'
? translate('auto.components.github.PRFilterDropdowns.7f1ba66c3e', 'Reviewed by')
: translate('auto.components.github.PRFilterDropdowns.b27b7e526c', 'Review from')
? t('auto.components.github.PRFilterDropdowns.7f1ba66c3e', 'Reviewed by')
: t('auto.components.github.PRFilterDropdowns.b27b7e526c', 'Review from')
}
value={reviewerActive}
onClear={() => onChange({ reviewer: null })}
@@ -278,7 +281,7 @@ export default function PRFilterDropdowns({
) : null}
{parsed.assignee ? (
<ActivePill
label={translate('auto.components.github.PRFilterDropdowns.979be3cf6b', 'Assignee')}
label={t('auto.components.github.PRFilterDropdowns.979be3cf6b', 'Assignee')}
value={parsed.assignee}
onClear={() => onChange({ assignee: null })}
/>
@@ -10,6 +10,7 @@ import {
import { cn } from '@/lib/utils'
import { Trans } from 'react-i18next'
import { i18n, translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { isClipboardTextByteLengthOverLimit } from '../../../../shared/clipboard-text'
export type PickerOption = { key: string; primary: string; secondary?: string }
@@ -51,31 +52,30 @@ export function filterPullRequestPickerOptions(
)
}
function pickerFallbackText({
loading,
queryTooLarge,
error,
emptyText
}: {
loading: boolean
queryTooLarge: boolean
error: string | null
emptyText?: string
}): string {
function pickerFallbackText(
{
loading,
queryTooLarge,
error,
emptyText
}: {
loading: boolean
queryTooLarge: boolean
error: string | null
emptyText?: string
},
t: Translate = translate
): string {
if (loading) {
return translate('auto.components.github.PRFilterPickers.loading', 'Loading…')
return t('auto.components.github.PRFilterPickers.loading', 'Loading…')
}
if (queryTooLarge) {
return translate(
return t(
'auto.components.github.PRFilterPickers.searchTextTooLarge',
'Search text is too large.'
)
}
return (
error ??
emptyText ??
translate('auto.components.github.PRFilterPickers.noMatches', 'No matches')
)
return error ?? emptyText ?? t('auto.components.github.PRFilterPickers.noMatches', 'No matches')
}
export function SingleSelectList({
@@ -100,6 +100,7 @@ export function SingleSelectList({
allowCustomValue?: boolean
onSelect: (value: string | null) => void
}): React.JSX.Element {
const t = useTranslate()
const [query, setQuery] = useState('')
const filtered = useMemo(() => filterPullRequestPickerOptions(options, query), [options, query])
const { queryTooLarge, trimmedQuery } = getPullRequestPickerQueryState(query)
@@ -109,11 +110,11 @@ export function SingleSelectList({
!queryTooLarge &&
!filtered.some((o) => o.key.toLowerCase() === trimmedQuery.toLowerCase())
const fallback = showCustom
? translate(
? t(
'auto.components.github.PRFilterPickers.pressEnterToUseValue',
'Press Enter to use the typed value.'
)
: pickerFallbackText({ loading, queryTooLarge, error, emptyText })
: pickerFallbackText({ loading, queryTooLarge, error, emptyText }, t)
return (
<Command shouldFilter={false}>
@@ -134,10 +135,7 @@ export function SingleSelectList({
<span className="min-w-0 truncate text-muted-foreground">
<Trans
i18n={i18n}
defaults={translate(
'auto.components.github.PRFilterPickers.useTypedValue',
'Use <value/>'
)}
defaults={t('auto.components.github.PRFilterPickers.useTypedValue', 'Use <value/>')}
components={{
value: <span className="font-medium text-foreground">{trimmedQuery}</span>
}}
@@ -151,7 +149,7 @@ export function SingleSelectList({
onSelect={() => onSelect(null)}
className="gap-2 px-3 py-1.5 text-xs text-muted-foreground"
>
{translate('auto.components.github.PRFilterPickers.472c12ae03', 'Clear')}
{t('auto.components.github.PRFilterPickers.472c12ae03', 'Clear')}
</CommandItem>
) : null}
{filtered.map((opt) => {
@@ -195,16 +193,20 @@ export function MultiSelectList({
emptyText?: string
onChange: (next: string[]) => void
}): React.JSX.Element {
const t = useTranslate()
const [query, setQuery] = useState('')
const filtered = useMemo(() => filterPullRequestPickerOptions(options, query), [options, query])
const selectedSet = useMemo(() => new Set(selected), [selected])
const { queryTooLarge } = getPullRequestPickerQueryState(query)
const fallback = pickerFallbackText({
loading,
queryTooLarge,
error,
emptyText
})
const fallback = pickerFallbackText(
{
loading,
queryTooLarge,
error,
emptyText
},
t
)
const toggle = (key: string): void => {
const next = new Set(selectedSet)
@@ -232,13 +234,9 @@ export function MultiSelectList({
onSelect={() => onChange([])}
className="gap-2 px-3 py-1.5 text-xs text-muted-foreground"
>
{translate(
'auto.components.github.PRFilterPickers.clearSelected',
'Clear ({{count}})',
{
count: selected.length
}
)}
{t('auto.components.github.PRFilterPickers.clearSelected', 'Clear ({{count}})', {
count: selected.length
})}
</CommandItem>
) : null}
{filtered.map((opt) => {
@@ -8,6 +8,7 @@ import {
import { cn } from '@/lib/utils'
import type { ParsedTaskQuery } from '../../../../shared/task-query'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
export type PRFilterChange = {
author?: string | null
@@ -20,42 +21,45 @@ export type PRFilterChange = {
export type SectionKey = 'status' | 'author' | 'label' | 'reviewer' | 'assignee'
export function filterStateLabel(state: 'open' | 'closed' | 'merged'): string {
export function filterStateLabel(
state: 'open' | 'closed' | 'merged',
t: Translate = translate
): string {
if (state === 'open') {
return translate('auto.components.github.PRFilterSections.d78b60b5c2', 'Open')
return t('auto.components.github.PRFilterSections.d78b60b5c2', 'Open')
}
if (state === 'closed') {
return translate('auto.components.github.PRFilterSections.0fd3249e2e', 'Closed')
return t('auto.components.github.PRFilterSections.0fd3249e2e', 'Closed')
}
return translate('auto.components.github.PRFilterSections.bd162b7d5a', 'Merged')
return t('auto.components.github.PRFilterSections.bd162b7d5a', 'Merged')
}
export function filterDraftLabel(): string {
return translate('auto.components.github.PRFilterSections.draft', 'Draft')
export function filterDraftLabel(t: Translate = translate): string {
return t('auto.components.github.PRFilterSections.draft', 'Draft')
}
export function filterLabelsValue(labels: string[]): string | null {
export function filterLabelsValue(labels: string[], t: Translate = translate): string | null {
if (labels.length === 0) {
return null
}
if (labels.length === 1) {
return labels[0]
}
return translate('auto.components.github.PRFilterSections.labelCount', '{{count}} labels', {
return t('auto.components.github.PRFilterSections.labelCount', '{{count}} labels', {
count: labels.length,
defaultValue_one: '{{count}} label'
})
}
function statusLabel(parsed: ParsedTaskQuery): string {
function statusLabel(parsed: ParsedTaskQuery, t: Translate = translate): string {
const parts: string[] = []
if (parsed.state === 'all') {
parts.push(translate('auto.components.github.PRFilterSections.allStates', 'All'))
parts.push(t('auto.components.github.PRFilterSections.allStates', 'All'))
} else if (parsed.state) {
parts.push(filterStateLabel(parsed.state))
parts.push(filterStateLabel(parsed.state, t))
}
if (parsed.draft) {
parts.push(filterDraftLabel())
parts.push(filterDraftLabel(t))
}
return parts.join(' · ')
}
@@ -69,38 +73,39 @@ function StatusSection({
kind: 'prs' | 'issues'
onSelect: (change: PRFilterChange) => void
}): React.JSX.Element {
const t = useTranslate()
const states: { key: 'open' | 'closed' | 'merged' | 'all'; label: string }[] =
kind === 'prs'
? [
{
key: 'open',
label: filterStateLabel('open')
label: filterStateLabel('open', t)
},
{
key: 'closed',
label: filterStateLabel('closed')
label: filterStateLabel('closed', t)
},
{
key: 'merged',
label: filterStateLabel('merged')
label: filterStateLabel('merged', t)
},
{
key: 'all',
label: translate('auto.components.github.PRFilterSections.2b2f019091', 'Any state')
label: t('auto.components.github.PRFilterSections.2b2f019091', 'Any state')
}
]
: [
{
key: 'open',
label: filterStateLabel('open')
label: filterStateLabel('open', t)
},
{
key: 'closed',
label: filterStateLabel('closed')
label: filterStateLabel('closed', t)
},
{
key: 'all',
label: translate('auto.components.github.PRFilterSections.2b2f019091', 'Any state')
label: t('auto.components.github.PRFilterSections.2b2f019091', 'Any state')
}
]
return (
@@ -120,7 +125,7 @@ function StatusSection({
<span>{s.label}</span>
{active ? (
<span className="text-[10px] text-muted-foreground">
{translate('auto.components.github.PRFilterSections.e0002f1eba', 'selected')}
{t('auto.components.github.PRFilterSections.e0002f1eba', 'selected')}
</span>
) : null}
</button>
@@ -138,6 +143,7 @@ function DraftToggle({
parsed: ParsedTaskQuery
onSelect: (change: PRFilterChange) => void
}): React.JSX.Element {
const t = useTranslate()
return (
<>
<div className="my-1 h-px bg-border" />
@@ -149,14 +155,14 @@ function DraftToggle({
parsed.draft && 'bg-muted/40 font-medium'
)}
>
<span>{translate('auto.components.github.PRFilterSections.b930de7194', 'Draft only')}</span>
<span>{t('auto.components.github.PRFilterSections.b930de7194', 'Draft only')}</span>
{parsed.draft ? (
<span className="text-[10px] text-muted-foreground">
{translate('auto.components.github.PRFilterSections.1e9b5244f2', 'on')}
{t('auto.components.github.PRFilterSections.1e9b5244f2', 'on')}
</span>
) : (
<span className="text-[10px] text-muted-foreground">
{translate('auto.components.github.PRFilterSections.f0cf6dd591', 'off')}
{t('auto.components.github.PRFilterSections.f0cf6dd591', 'off')}
</span>
)}
</button>
@@ -190,22 +196,23 @@ export function SectionMenu({
onPick: (s: SectionKey) => void
onClearAll: (() => void) | null
}): React.JSX.Element {
const status = statusLabel(parsed)
const t = useTranslate()
const status = statusLabel(parsed, t)
const rows: { key: SectionKey; label: string; value: string | null }[] = [
{
key: 'status',
label: translate('auto.components.github.PRFilterSections.764a0b4ce1', 'Status'),
label: t('auto.components.github.PRFilterSections.764a0b4ce1', 'Status'),
value: status || null
},
{
key: 'author',
label: translate('auto.components.github.PRFilterSections.24754c44ad', 'Author'),
label: t('auto.components.github.PRFilterSections.24754c44ad', 'Author'),
value: parsed.author
},
{
key: 'label',
label: translate('auto.components.github.PRFilterSections.b1d9fdea08', 'Label'),
value: filterLabelsValue(parsed.labels)
label: t('auto.components.github.PRFilterSections.b1d9fdea08', 'Label'),
value: filterLabelsValue(parsed.labels, t)
},
...(kind === 'prs'
? [
@@ -213,15 +220,15 @@ export function SectionMenu({
key: 'reviewer' as SectionKey,
label:
reviewerKind === 'reviewed-by'
? translate('auto.components.github.PRFilterDropdowns.7f1ba66c3e', 'Reviewed by')
: translate('auto.components.github.PRFilterDropdowns.b27b7e526c', 'Review from'),
? t('auto.components.github.PRFilterDropdowns.7f1ba66c3e', 'Reviewed by')
: t('auto.components.github.PRFilterDropdowns.b27b7e526c', 'Review from'),
value: reviewerActive
}
]
: []),
{
key: 'assignee',
label: translate('auto.components.github.PRFilterSections.ea3416d646', 'Assignee'),
label: t('auto.components.github.PRFilterSections.ea3416d646', 'Assignee'),
value: parsed.assignee
}
]
@@ -229,11 +236,8 @@ export function SectionMenu({
<div className="py-1 text-xs">
<div className="px-3 py-1.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
{kind === 'prs'
? translate(
'auto.components.github.PRFilterSections.filterPullRequests',
'Filter pull requests'
)
: translate('auto.components.github.PRFilterSections.filterIssues', 'Filter issues')}
? t('auto.components.github.PRFilterSections.filterPullRequests', 'Filter pull requests')
: t('auto.components.github.PRFilterSections.filterIssues', 'Filter issues')}
</div>
{rows.map((row) => (
<button
@@ -257,7 +261,7 @@ export function SectionMenu({
onClick={onClearAll}
className="w-full px-3 py-1.5 text-left text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
>
{translate('auto.components.github.PRFilterSections.30ebb6ca44', 'Clear all filters')}
{t('auto.components.github.PRFilterSections.30ebb6ca44', 'Clear all filters')}
</button>
</>
) : null}
@@ -296,6 +300,7 @@ export function SectionDetail({
onBack: () => void
onSelect: (change: PRFilterChange) => void
}): React.JSX.Element {
const t = useTranslate()
return (
<div>
<button
@@ -304,7 +309,7 @@ export function SectionDetail({
className="flex w-full items-center gap-1 border-b border-border px-3 py-1.5 text-[11px] text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
>
<ChevronRight className="size-3 rotate-180" />
{translate('auto.components.github.PRFilterSections.b69fa4fa20', 'Back')}
{t('auto.components.github.PRFilterSections.b69fa4fa20', 'Back')}
</button>
{section === 'status' ? (
<StatusSection parsed={parsed} kind={kind} onSelect={onSelect} />
@@ -315,11 +320,11 @@ export function SectionDetail({
activeValue={parsed.author}
loading={false}
error={null}
searchPlaceholder={translate(
searchPlaceholder={t(
'auto.components.github.PRFilterSections.filterOrTypeLogin',
'Filter or type a login...'
)}
emptyText={translate('auto.components.github.PRFilterSections.458ea3602b', 'No authors')}
emptyText={t('auto.components.github.PRFilterSections.458ea3602b', 'No authors')}
allowCustomValue
renderOption={(opt) => <UserOptionRow option={opt} />}
onSelect={(value) => onSelect({ author: value })}
@@ -331,11 +336,11 @@ export function SectionDetail({
activeValue={parsed.assignee}
loading={usersLoading}
error={usersError}
searchPlaceholder={translate(
searchPlaceholder={t(
'auto.components.github.PRFilterSections.filterOrTypeLogin',
'Filter or type a login...'
)}
emptyText={translate('auto.components.github.PRFilterSections.a00830d3f7', 'No users')}
emptyText={t('auto.components.github.PRFilterSections.a00830d3f7', 'No users')}
allowCustomValue
renderOption={(opt) => <UserOptionRow option={opt} />}
onSelect={(value) => onSelect({ assignee: value })}
@@ -347,11 +352,11 @@ export function SectionDetail({
selected={parsed.labels}
loading={labelsLoading}
error={labelsError}
searchPlaceholder={translate(
searchPlaceholder={t(
'auto.components.github.PRFilterSections.filterLabels',
'Filter labels...'
)}
emptyText={translate('auto.components.github.PRFilterSections.de26e2eb06', 'No labels')}
emptyText={t('auto.components.github.PRFilterSections.de26e2eb06', 'No labels')}
onChange={(next) => onSelect({ labels: next })}
/>
) : null}
@@ -368,7 +373,7 @@ export function SectionDetail({
: 'text-muted-foreground hover:bg-muted/50'
)}
>
{translate('auto.components.github.PRFilterSections.94b42b0edf', 'Review requested')}
{t('auto.components.github.PRFilterSections.94b42b0edf', 'Review requested')}
</button>
<button
type="button"
@@ -380,7 +385,7 @@ export function SectionDetail({
: 'text-muted-foreground hover:bg-muted/50'
)}
>
{translate('auto.components.github.PRFilterSections.0103e1cb18', 'Reviewed by')}
{t('auto.components.github.PRFilterSections.0103e1cb18', 'Reviewed by')}
</button>
</div>
<SingleSelectList
@@ -388,11 +393,11 @@ export function SectionDetail({
activeValue={reviewerMode === 'requested' ? parsed.reviewRequested : parsed.reviewedBy}
loading={usersLoading}
error={usersError}
searchPlaceholder={translate(
searchPlaceholder={t(
'auto.components.github.PRFilterSections.filterOrTypeLogin',
'Filter or type a login...'
)}
emptyText={translate('auto.components.github.PRFilterSections.a00830d3f7', 'No users')}
emptyText={t('auto.components.github.PRFilterSections.a00830d3f7', 'No users')}
allowCustomValue
renderOption={(opt) => <UserOptionRow option={opt} />}
onSelect={(login) =>

Some files were not shown because too many files have changed in this diff Show More