refactor(i18n): read render-path translations from useTranslate in pull request page, agent, onboarding, stats, new workspace and feature tips (#39421)

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 20:59:26 -08:00
committed by GitHub
co-authored by OrcaWin
parent c99e71b315
commit 2f45e26ee7
142 changed files with 1724 additions and 1827 deletions
-109
View File
@@ -64,26 +64,8 @@
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
8 src/renderer/src/components/agent/AgentCombobox.tsx
3 src/renderer/src/components/agent/AgentLaunchPicker.tsx
2 src/renderer/src/components/agent/AgentLaunchPickerMenu.tsx
2 src/renderer/src/components/agent/AgentLaunchPromptComposer.tsx
1 src/renderer/src/components/agent/AgentLaunchSetupActions.tsx
3 src/renderer/src/components/agent/AgentLaunchSetupButton.tsx
2 src/renderer/src/components/agent/AgentLaunchSetupFooter.tsx
9 src/renderer/src/components/agent/AgentLaunchSetupPanel.tsx
1 src/renderer/src/components/agent/AgentLaunchTextModelDefault.tsx
2 src/renderer/src/components/agent/AgentSettingsDialog.tsx
1 src/renderer/src/components/agent/AgentSettingsMenuItem.tsx
2 src/renderer/src/components/agent/agent-combobox-items.tsx
1 src/renderer/src/components/agent/agent-combobox-surface-items.tsx
6 src/renderer/src/components/agent/launch-settings/AgentChatChoiceControls.tsx
5 src/renderer/src/components/agent/launch-settings/AgentChoiceControls.tsx
1 src/renderer/src/components/agent/launch-settings/AgentDefaultSurfacePreviews.tsx
1 src/renderer/src/components/agent/launch-settings/AgentLaunchChoiceEditor.tsx
11 src/renderer/src/components/agent/launch-settings/AgentLaunchDefaultsEditor.tsx
19 src/renderer/src/components/agent/launch-settings/AgentPermissionChoiceControls.tsx
4 src/renderer/src/components/agent/launch-settings/codex-session-source-home-control.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
@@ -138,23 +120,6 @@
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
3 src/renderer/src/components/feature-tips/ChatUiHereCornerCard.tsx
2 src/renderer/src/components/feature-tips/ChatUiHereFeatureTipVisual.tsx
3 src/renderer/src/components/feature-tips/ChatUiHereTipDialog.tsx
5 src/renderer/src/components/feature-tips/ChatUiSwitchedCornerNote.tsx
2 src/renderer/src/components/feature-tips/CliFeatureTipVisual.tsx
6 src/renderer/src/components/feature-tips/CliSetupTipDialog.tsx
2 src/renderer/src/components/feature-tips/CliSkillSetupPanel.tsx
7 src/renderer/src/components/feature-tips/CliSkillSetupTerminal.tsx
1 src/renderer/src/components/feature-tips/CmdJPaletteFeatureTipVisual.tsx
1 src/renderer/src/components/feature-tips/CmdJPaletteTipDialog.tsx
1 src/renderer/src/components/feature-tips/FeatureTipActions.tsx
5 src/renderer/src/components/feature-tips/FeatureTipsModal.tsx
2 src/renderer/src/components/feature-tips/SessionSearchTipDialog.tsx
4 src/renderer/src/components/feature-tips/VoiceDictationFeatureTipVisual.tsx
3 src/renderer/src/components/feature-tips/VoiceDictationTipDialog.tsx
1 src/renderer/src/components/feature-tips/feature-tip-session-panel-demo.tsx
1 src/renderer/src/components/feature-tips/use-session-search-tip-setup.ts
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
@@ -244,44 +209,7 @@
18 src/renderer/src/components/mobile/slides/HomeSlide.tsx
35 src/renderer/src/components/mobile/slides/TerminalSlide.tsx
7 src/renderer/src/components/mobile/slides/WorktreeListSlide.tsx
1 src/renderer/src/components/new-workspace/ComposerBaseRefPicker.tsx
9 src/renderer/src/components/new-workspace/ComposerBranchChoice.tsx
6 src/renderer/src/components/new-workspace/ComposerParentWorktreePicker.tsx
4 src/renderer/src/components/new-workspace/DirectWorkItemSetupDialog.tsx
8 src/renderer/src/components/new-workspace/NewWorkspaceComposerAdvancedSection.tsx
6 src/renderer/src/components/new-workspace/NewWorkspaceComposerAgentSection.tsx
1 src/renderer/src/components/new-workspace/NewWorkspaceComposerFooter.tsx
4 src/renderer/src/components/new-workspace/NewWorkspaceComposerNameSection.tsx
8 src/renderer/src/components/new-workspace/NewWorkspaceComposerProjectSection.tsx
6 src/renderer/src/components/new-workspace/ProjectCombobox.tsx
2 src/renderer/src/components/new-workspace/RunTargetCombobox.tsx
3 src/renderer/src/components/new-workspace/RunTargetField.tsx
5 src/renderer/src/components/new-workspace/RunTargetRowActions.tsx
6 src/renderer/src/components/new-workspace/RunTargetSubmenus.tsx
6 src/renderer/src/components/new-workspace/SetProjectLocationDialog.tsx
13 src/renderer/src/components/new-workspace/SetProjectLocationForms.tsx
4 src/renderer/src/components/new-workspace/SmartWorkspaceSourceResultList.tsx
2 src/renderer/src/components/new-workspace/smart-workspace-source-row-content.tsx
9 src/renderer/src/components/notifications/mac-notification-permission-card.tsx
11 src/renderer/src/components/onboarding/AgentStep.tsx
12 src/renderer/src/components/onboarding/AgentStepSurfaceSections.tsx
10 src/renderer/src/components/onboarding/AgentSurfaceStep.tsx
3 src/renderer/src/components/onboarding/FeatureSetupInlineTerminal.tsx
7 src/renderer/src/components/onboarding/GhosttyDiscoveryRow.tsx
25 src/renderer/src/components/onboarding/IntegrationsStep.tsx
9 src/renderer/src/components/onboarding/NotificationStep.tsx
7 src/renderer/src/components/onboarding/OnboardingFlow.tsx
2 src/renderer/src/components/onboarding/OnboardingFooter.tsx
1 src/renderer/src/components/onboarding/OnboardingInlineCommandTerminal.tsx
4 src/renderer/src/components/onboarding/OnboardingLanguageLoading.tsx
18 src/renderer/src/components/onboarding/OnboardingLanguageSwitcher.tsx
4 src/renderer/src/components/onboarding/TerminalLaunchDemo.tsx
4 src/renderer/src/components/onboarding/ThemeLookCarousel.tsx
8 src/renderer/src/components/onboarding/ThemeLookGallery.tsx
6 src/renderer/src/components/onboarding/ThemeModeTiles.tsx
13 src/renderer/src/components/onboarding/ThemeStep.tsx
27 src/renderer/src/components/onboarding/WindowsTerminalStep.tsx
8 src/renderer/src/components/onboarding/YoloPermissionsControl.tsx
4 src/renderer/src/components/orca-profiles/OrcaProfileSignOutConfirmDialog.tsx
6 src/renderer/src/components/orchestration-crew/CrewRowDetails.tsx
5 src/renderer/src/components/orchestration-crew/CrewRowMenu.tsx
@@ -298,27 +226,7 @@
2 src/renderer/src/components/provider-accounts/AntigravityProviderSignInArea.tsx
2 src/renderer/src/components/provider-accounts/ProviderHostScopeControl.tsx
1 src/renderer/src/components/provider-accounts/UnsealedCredentialNotice.tsx
5 src/renderer/src/components/pull-request-page/actions/panel.tsx
14 src/renderer/src/components/pull-request-page/checks/details.tsx
3 src/renderer/src/components/pull-request-page/checks/fix-dialog.tsx
1 src/renderer/src/components/pull-request-page/checks/toolbar.tsx
3 src/renderer/src/components/pull-request-page/comments/composer.tsx
3 src/renderer/src/components/pull-request-page/comments/reply-form.tsx
8 src/renderer/src/components/pull-request-page/conversation/comment-card.tsx
2 src/renderer/src/components/pull-request-page/conversation/comment-group.tsx
2 src/renderer/src/components/pull-request-page/conversation/comments-list.tsx
7 src/renderer/src/components/pull-request-page/conversation/description.tsx
1 src/renderer/src/components/pull-request-page/conversation/tab.tsx
6 src/renderer/src/components/pull-request-page/edit/section.tsx
3 src/renderer/src/components/pull-request-page/files/combined-diff-viewer.tsx
7 src/renderer/src/components/pull-request-page/files/toolbar.tsx
16 src/renderer/src/components/pull-request-page/page/header.tsx
1 src/renderer/src/components/pull-request-page/page/surface.tsx
6 src/renderer/src/components/pull-request-page/page/tabs-shell.tsx
1 src/renderer/src/components/pull-request-page/reviewers/panel.tsx
3 src/renderer/src/components/pull-request-page/reviewers/picker-row.tsx
9 src/renderer/src/components/pull-request-page/reviewers/picker.tsx
4 src/renderer/src/components/pull-request-page/reviewers/requested-list.tsx
5 src/renderer/src/components/quick-open-install-rg-guidance.tsx
9 src/renderer/src/components/repo/CreateFromPicker.tsx
5 src/renderer/src/components/repo/NestedRepoChecklist.tsx
@@ -342,24 +250,7 @@
15 src/renderer/src/components/ssh-authentication/SshPassphraseDialog.tsx
10 src/renderer/src/components/star-nag/StarNagToastHost.tsx
12 src/renderer/src/components/stats/ClaudeUsageDailyChart.tsx
15 src/renderer/src/components/stats/ClaudeUsageDetails.tsx
1 src/renderer/src/components/stats/ClaudeUsageLoadingState.tsx
22 src/renderer/src/components/stats/ClaudeUsagePane.tsx
12 src/renderer/src/components/stats/CodexUsageDailyChart.tsx
14 src/renderer/src/components/stats/CodexUsageDetails.tsx
20 src/renderer/src/components/stats/CodexUsagePane.tsx
9 src/renderer/src/components/stats/GrokUsagePane.tsx
14 src/renderer/src/components/stats/MuseUsageDetails.tsx
20 src/renderer/src/components/stats/MuseUsagePane.tsx
14 src/renderer/src/components/stats/OpenCodeUsageDetails.tsx
20 src/renderer/src/components/stats/OpenCodeUsagePane.tsx
6 src/renderer/src/components/stats/ShareUsageButton.tsx
9 src/renderer/src/components/stats/ShareUsageCard.tsx
8 src/renderer/src/components/stats/StatsPane.tsx
2 src/renderer/src/components/stats/UsageBreakdownSection.tsx
20 src/renderer/src/components/stats/UsageOverviewPane.tsx
3 src/renderer/src/components/stats/share-card-utils.tsx
23 src/renderer/src/components/stats/usage-overview-sections.tsx
1 src/renderer/src/components/tab-group/TabGroupDropOverlay.tsx
3 src/renderer/src/components/tab-group/TabGroupPaneActionsMenu.tsx
1 src/renderer/src/components/tab-group/TabGroupPanel.tsx
@@ -18,7 +18,7 @@ import {
AgentIconLabel,
renderAgentComboboxItem
} from './agent-combobox-items'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { rowLabel } from '@/components/tab-bar/tab-agent-row-presentation'
import {
SurfaceAgentItems,
@@ -76,8 +76,9 @@ export default function AgentCombobox({
surfaceRows,
...selection
}: AgentComboboxProps): React.JSX.Element {
const t = useTranslate()
const blankText =
blankLabel ?? translate('auto.components.agent.AgentCombobox.986f946354', 'Blank Terminal')
blankLabel ?? t('auto.components.agent.AgentCombobox.986f946354', 'Blank Terminal')
const allowBlankTerminal = selection.allowBlankTerminal === true
const [open, setOpen] = useState(false)
const [query, setQuery] = useState('')
@@ -102,7 +103,7 @@ export default function AgentCombobox({
[allowBlankTerminal, query]
)
const pickerValues = { agents, query, value, blankValue: BLANK_VALUE, blankMatchesQuery }
const surface = surfaceRows ? surfaceCommandValues(surfaceRows.model, pickerValues) : null
const surface = surfaceRows ? surfaceCommandValues(surfaceRows.model, pickerValues, t) : null
const surfaceList = surface?.rows ?? []
const openCommandValue = selectionCleared
? ''
@@ -279,17 +280,14 @@ export default function AgentCombobox({
<span className={cn('inline-flex min-w-0 flex-1', canClear && 'pr-8')}>
{selectionCleared ? (
<span className="truncate text-muted-foreground">
{translate(
'components.agentSessionContinuation.selectAgent',
'Select an Agent'
)}
{t('components.agentSessionContinuation.selectAgent', 'Select an Agent')}
</span>
) : selectedAgent ? (
<AgentIconLabel
icon={<AgentIcon agent={selectedAgent.id} size={14} />}
label={
picksCli
? rowLabel({ kind: 'cli', agent: selectedAgent.id })
? rowLabel({ kind: 'cli', agent: selectedAgent.id }, t)
: selectedAgent.label
}
/>
@@ -299,10 +297,7 @@ export default function AgentCombobox({
label={
allowBlankTerminal
? blankText
: translate(
'components.agentSessionContinuation.selectAgent',
'Select an Agent'
)
: t('components.agentSessionContinuation.selectAgent', 'Select an Agent')
}
/>
)}
@@ -331,16 +326,13 @@ export default function AgentCombobox({
>
<CommandInput
ref={setInputNode}
placeholder={translate(
'auto.components.agent.AgentCombobox.48c6a5a9b4',
'Search agents...'
)}
placeholder={t('auto.components.agent.AgentCombobox.48c6a5a9b4', 'Search agents...')}
value={query}
onValueChange={setQuery}
/>
<CommandList>
<CommandEmpty>
{translate(
{t(
'auto.components.agent.AgentCombobox.579c768bde',
'No agents match your search.'
)}
@@ -390,7 +382,7 @@ export default function AgentCombobox({
onMouseEnter={() => setCommandValue('')}
className="h-9 w-full justify-start rounded-none px-3 text-xs font-normal text-muted-foreground"
>
{translate('auto.components.agent.AgentCombobox.19522e25ee', 'Manage agents')}
{t('auto.components.agent.AgentCombobox.19522e25ee', 'Manage agents')}
<ArrowRight className="ml-auto size-3" />
</Button>
</div>
@@ -407,10 +399,7 @@ export default function AgentCombobox({
size="icon-xs"
disabled={disabled}
className="absolute right-8 top-1/2 -translate-y-1/2"
aria-label={translate(
'components.agentCombobox.clearSelection',
'Clear agent selection'
)}
aria-label={t('components.agentCombobox.clearSelection', 'Clear agent selection')}
onClick={() => {
clearSelection.onClear()
closePicker()
@@ -421,7 +410,7 @@ export default function AgentCombobox({
</Button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}>
{translate('components.agentCombobox.clearSelection', 'Clear agent selection')}
{t('components.agentCombobox.clearSelection', 'Clear agent selection')}
</TooltipContent>
</Tooltip>
)}
@@ -22,7 +22,7 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { AgentIcon, getAgentLabel } from '@/lib/agent-catalog'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { rowLabel } from '../tab-bar/tab-agent-row-presentation'
import type { NewTabAgentMenuModel } from '../tab-bar/use-new-tab-agent-menu-rows'
import { useAgentPickerDraftScope } from './AgentLaunchSetupButton'
@@ -92,6 +92,7 @@ export const AgentLaunchPicker = memo(
surface?: AgentPickerDraft['surface']
) => void
}) {
const t = useTranslate()
useUiLanguageRevision()
const ownDraftScope = useAgentPickerDraftScope()
const [settingsOpen, setSettingsOpen] = useState(false)
@@ -175,9 +176,9 @@ export const AgentLaunchPicker = memo(
const summary = [modelLabel, choices?.options?.effort].filter(Boolean).join(' · ')
const agentLabel = agent
? cli
? rowLabel({ kind: 'cli', agent })
? rowLabel({ kind: 'cli', agent }, t)
: getAgentLabel(agent)
: (emptyLabel ?? translate('agentPicker.select', 'Select agent'))
: (emptyLabel ?? t('agentPicker.select', 'Select agent'))
return (
<div className="space-y-2">
<DropdownMenu open={open} onOpenChange={setOpen}>
@@ -186,7 +187,7 @@ export const AgentLaunchPicker = memo(
ref={trigger}
type="button"
data-agent-launch-picker-trigger="true"
aria-label={translate('agentPicker.select', 'Select agent')}
aria-label={t('agentPicker.select', 'Select agent')}
variant="outline"
className="w-full min-w-0"
disabled={disabled || model.state !== 'settled'}
@@ -260,7 +261,7 @@ export const AgentLaunchPicker = memo(
/>
) : (
<DropdownMenuItem disabled>
{translate('agentPicker.select', 'Select agent')}
{t('agentPicker.select', 'Select agent')}
</DropdownMenuItem>
)}
<AgentSettingsMenuItem onSelect={openAgentSettings} />
@@ -8,7 +8,7 @@ import { parseExecutionHostId } from '../../../../shared/execution-host'
import { Input } from '@/components/ui/input'
import { AgentPickerMenuRow, type AgentPickerMenuLaunch } from './AgentLaunchPickerMenuRow'
import { DropdownMenuItem } from '@/components/ui/dropdown-menu'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { retireAgentPickerDrafts, type AgentPickerSetupIdentity } from './AgentLaunchSetupButton'
import type { NewTabAgentMenuModel } from '../tab-bar/use-new-tab-agent-menu-rows'
import {
@@ -58,6 +58,7 @@ export function AgentLaunchPickerMenu({
defaultAgent?: AgentPickerEntry['agent'] | 'blank' | null
onSetDefault?: (agent: AgentPickerEntry['agent']) => void
}) {
const t = useTranslate()
useEffect(() => {
if (setupDisabled) {
return
@@ -154,7 +155,7 @@ export function AgentLaunchPickerMenu({
const end = event.currentTarget.value.length
event.currentTarget.setSelectionRange(end, end)
}}
placeholder={translate('agentPicker.search', 'Search agents and setups…')}
placeholder={t('agentPicker.search', 'Search agents and setups…')}
onChange={(event) => setQuery(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Escape') {
@@ -228,10 +229,7 @@ export function AgentLaunchPickerMenu({
})}
{!visible.length && !setups.length ? (
<DropdownMenuItem disabled>
{translate(
'auto.components.agent.AgentCombobox.579c768bde',
'No agents match your search.'
)}
{t('auto.components.agent.AgentCombobox.579c768bde', 'No agents match your search.')}
</DropdownMenuItem>
) : null}
</div>
@@ -19,6 +19,7 @@ import {
type AgentPickerEntry,
type AgentPickerSurface
} from '../../../../shared/agent-launch/picker/agent-launch-picker'
import { useTranslate } from '@/i18n/use-translate'
function pickerLaunchRow(entry: AgentPickerEntry, surface: AgentPickerSurface): NewTabAgentMenuRow {
return {
@@ -89,6 +90,7 @@ export const AgentPickerMenuRow = memo(function AgentPickerMenuRow({
/** Base agent rows only; setup rows are presets, not default-agent choices. */
onSetDefault?: (agent: AgentPickerEntry['agent']) => void
}) {
const t = useTranslate()
useUiLanguageRevision()
const setupId = useId()
const preset = initialModel ? { model: initialModel, effort: initialEffort } : undefined
@@ -115,7 +117,7 @@ export const AgentPickerMenuRow = memo(function AgentPickerMenuRow({
<DropdownMenuItem
disabled={disabled || launchBlocked || !entry.surfaces.length}
data-search-default={searchDefault || undefined}
aria-description={launchBlocked ? terminalPendingLabel() : undefined}
aria-description={launchBlocked ? terminalPendingLabel(t) : undefined}
onSelect={(event) => {
onSelectAgent?.(entry.agent)
if (
@@ -1,5 +1,5 @@
import { Button } from '@/components/ui/button'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import type {
AgentPickerEntry,
AgentPickerSurface
@@ -18,11 +18,12 @@ export function AgentLaunchSetupActions({
surface: AgentPickerSurface
onUse: (surface: AgentPickerSurface) => void
}): React.JSX.Element {
const t = useTranslate()
return (
<div className="flex items-center justify-end gap-2 border-t border-border px-2.5 py-2">
{entry.textGeneration ? (
<Button type="button" size="xs" disabled={disabled} onClick={() => onUse(surface)}>
{translate('agentPicker.use', 'Use')}
{t('agentPicker.use', 'Use')}
</Button>
) : (
<AgentLaunchSurfaceButtons
@@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import { AgentLaunchSetupSubmenu } from './AgentLaunchSetupSubmenu'
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { AgentLaunchSetupPanel } from './AgentLaunchSetupPanel'
import type {
AgentPickerDraft,
@@ -86,6 +86,7 @@ export function AgentLaunchSetupButton({
onOpenChange?: (open: boolean) => void
onUse: (draft: AgentPickerDraft) => boolean | void | Promise<boolean>
}) {
const t = useTranslate()
const [localOpen, setOpen] = useState(false)
const open = controlledOpen ?? localOpen
const setup = useAgentPickerSetup({
@@ -185,7 +186,7 @@ export function AgentLaunchSetupButton({
) : null}
</div>
)
const label = translate('agentPicker.configure', 'Configure {{agent}}', { agent: entry.label })
const label = t('agentPicker.configure', 'Configure {{agent}}', { agent: entry.label })
if (submenu && renderAnchor) {
return (
<AgentLaunchSetupSubmenu
@@ -209,7 +210,7 @@ export function AgentLaunchSetupButton({
variant="ghost"
size="icon-xs"
disabled={disabled}
aria-label={translate('agentPicker.configure', 'Configure {{agent}}', {
aria-label={t('agentPicker.configure', 'Configure {{agent}}', {
agent: entry.label
})}
onClick={(event) => event.stopPropagation()}
@@ -235,7 +236,7 @@ export function AgentLaunchSetupButton({
sticky="always"
align="start"
sideOffset={6}
aria-label={translate('agentPicker.configure', 'Configure {{agent}}', {
aria-label={t('agentPicker.configure', 'Configure {{agent}}', {
agent: entry.label
})}
// Why: base popovers sit below menus (z-60), so this menu-launched panel would render under the menu's shadow; sharing the menu layer stacks it by open order, like a submenu.
@@ -4,6 +4,7 @@ import { CornerDownLeft } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { getShortcutPlatform } from '@/lib/shortcut-platform'
import type { NativeChatComposerHandle } from '../native-chat/composer/native-chat-composer-types'
import { AgentLaunchPromptComposer } from './AgentLaunchPromptComposer'
@@ -18,20 +19,21 @@ import { formatKeybinding } from '../../../../shared/keybindings'
export function agentLaunchSetupArgumentsNote(
label: string,
options: ReturnType<typeof agentPickerEntryOptions>,
noModelFlag: boolean
noModelFlag: boolean,
t: Translate = translate
) {
const composed = options.some((option) => option.apply.composedIntoModel)
const noOptionFlag = options.some(
(option) => !option.apply.cli?.length && !option.apply.composedIntoModel
)
return noModelFlag || noOptionFlag
? translate(
? t(
'agentPicker.chatChoicesNote',
'{{agent}} model or effort choices without a CLI flag apply to chats.',
{ agent: label }
)
: composed
? translate(
? t(
'agentPicker.composedChoicesNote',
'{{agent}} effort and speed choices are included in the model argument.',
{ agent: label }
@@ -55,6 +57,7 @@ export function AgentLaunchSurfaceButtons({
primary: AgentPickerSurface
onSelect: (surface: AgentPickerSurface) => void
}): React.JSX.Element {
const t = useTranslate()
const platform = getShortcutPlatform()
return (
<div className="grid max-w-full auto-cols-fr grid-flow-col gap-2">
@@ -62,8 +65,8 @@ export function AgentLaunchSurfaceButtons({
const isPrimary = next === primary
const label =
next === 'chat'
? translate('agentPicker.launchChat', 'Launch chat')
: translate('agentPicker.launchTerminal', 'Launch terminal')
? t('agentPicker.launchChat', 'Launch chat')
: t('agentPicker.launchTerminal', 'Launch terminal')
return (
<Button
key={next}
@@ -76,9 +79,9 @@ export function AgentLaunchSurfaceButtons({
}
disabled={disabled || (next === 'terminal' && terminalBlocked)}
aria-description={
next === 'terminal' && terminalBlocked ? terminalPendingLabel() : undefined
next === 'terminal' && terminalBlocked ? terminalPendingLabel(t) : undefined
}
title={next === 'terminal' && terminalBlocked ? terminalPendingLabel() : undefined}
title={next === 'terminal' && terminalBlocked ? terminalPendingLabel(t) : undefined}
variant={isPrimary ? 'default' : 'outline'}
onClick={() => onSelect(next)}
>
@@ -1,5 +1,5 @@
import { CommandItem } from '@/components/ui/command'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import type { AgentPickerEntry } from '../../../../shared/agent-launch/picker/agent-launch-picker'
import { TEXT_ACTION_DEFAULT_MODEL } from '../../../../shared/agent-launch/picker/agent-launch-picker-text-action'
import type { AgentPickerEdit } from '../../../../shared/agent-launch/picker/agent-launch-picker-edits'
@@ -15,6 +15,7 @@ export function AgentLaunchTextModelDefault({
modelInArguments?: boolean
onSelect: (edit: AgentPickerEdit) => void
}) {
const t = useTranslate()
if (!entry.textGeneration) {
return null
}
@@ -31,7 +32,7 @@ export function AgentLaunchTextModelDefault({
) : null}
{modelInArguments ? (
<div className="px-2 py-1.5 text-xs text-muted-foreground">
{translate('agentPicker.setByArguments', 'Set by your Arguments')}
{t('agentPicker.setByArguments', 'Set by your Arguments')}
</div>
) : null}
</>
@@ -10,7 +10,7 @@ import {
DialogTitle
} from '@/components/ui/dialog'
import { AgentsPane } from '@/components/settings/agents/AgentsPane'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import {
isWindowsTerminalCapabilityHost,
useLocalWindowsTerminalCapabilities
@@ -30,6 +30,7 @@ export default function AgentSettingsDialog({
onOpenChange,
onCloseAutoFocus
}: AgentSettingsDialogProps): React.JSX.Element | null {
const t = useTranslate()
const viewActive = useAppStateViewActive()
const settings = useAppStateValue((s) => s.settings)
const updateSettings = useCallback(
@@ -77,10 +78,10 @@ export default function AgentSettingsDialog({
<DialogContent className="sm:max-w-2xl" onCloseAutoFocus={onCloseAutoFocus}>
<DialogHeader>
<DialogTitle className="text-sm">
{translate('auto.components.agent.AgentSettingsDialog.fc0268e4ed', 'Agents')}
{t('auto.components.agent.AgentSettingsDialog.fc0268e4ed', 'Agents')}
</DialogTitle>
<DialogDescription className="text-xs">
{translate(
{t(
'auto.components.agent.AgentSettingsDialog.50cdb57c03',
'Manage AI agents, set a default, and customize commands.'
)}
@@ -1,14 +1,15 @@
import type { JSX } from 'react'
import { Settings as SettingsIcon } from 'lucide-react'
import { DropdownMenuItem } from '@/components/ui/dropdown-menu'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
/** The new-tab menu's footer. Form pickers use the same row. */
export function AgentSettingsMenuItem({ onSelect }: { onSelect: () => void }): JSX.Element {
const t = useTranslate()
return (
<DropdownMenuItem onSelect={onSelect} variant="launch">
<SettingsIcon className="size-4" />
{translate('auto.components.tab.bar.QuickLaunchButton.348a04c1ad', 'Agent settings…')}
{t('auto.components.tab.bar.QuickLaunchButton.348a04c1ad', 'Agent settings…')}
</DropdownMenuItem>
)
}
@@ -8,7 +8,7 @@ import {
ContextMenuTrigger
} from '@/components/ui/context-menu'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
type ItemRenderArgs = {
key: string
@@ -53,6 +53,7 @@ export function AgentDefaultContextMenu({
isDefault,
onSetDefault
}: AgentDefaultContextMenuProps): React.ReactNode {
const t = useTranslate()
const [open, setOpen] = React.useState(false)
if (!onSetDefault) {
// Unmounting an open menu skips onOpenChange; reset so a remount is not marked open.
@@ -75,8 +76,8 @@ export function AgentDefaultContextMenu({
<ContextMenuItem onSelect={onSetDefault} disabled={isDefault}>
<Star className="size-3.5" />
{isDefault
? translate('auto.components.agent.AgentCombobox.1b0d6965fa', 'Current default')
: translate('auto.components.agent.AgentCombobox.9c6b59fe58', 'Set as default')}
? t('auto.components.agent.AgentCombobox.1b0d6965fa', 'Current default')
: t('auto.components.agent.AgentCombobox.9c6b59fe58', 'Set as default')}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
@@ -14,6 +14,7 @@ import {
} from '@/components/tab-bar/tab-agent-row-presentation'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import type { TuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent'
import type { resolveAgentMenuSelection } from '@/lib/agent-menu-selection'
import { renderAgentComboboxItem } from './agent-combobox-items'
@@ -36,8 +37,8 @@ export function surfaceAnswerPending(rows: AgentComboboxSurfaceRows | undefined)
}
// Nothing launches until the form is submitted, so the picker says "start", not "launch".
function terminalPickTitle(label: string): string {
return translate('components.agentCombobox.startInTerminal', 'Start {{value0}} in a terminal', {
function terminalPickTitle(label: string, t: Translate = translate): string {
return t('components.agentCombobox.startInTerminal', 'Start {{value0}} in a terminal', {
value0: label
})
}
@@ -66,7 +67,8 @@ export function surfaceCommandValues(
value: TuiAgent | null
blankValue: string
blankMatchesQuery: boolean
}
},
t?: Translate
): { rows: NewTabAgentMenuRow[]; selected: string | null; highlighted: string } {
// Disabled rows are listed with their reason, so every agent is searched, not only pickable ones.
const rows = visibleSurfaceRows(model, getAgentCatalog(), picker.query)
@@ -74,7 +76,7 @@ export function surfaceCommandValues(
surfaceRowValue(visibleSurfaceRows(model, getAgentCatalog(), ''), picker.value) ?? null
const pending = model.state === 'settled' && model.terminalPending
const first = rows.find(
(row) => row.kind !== 'chat-unavailable' && !terminalPendingReason(pending, row)
(row) => row.kind !== 'chat-unavailable' && !terminalPendingReason(pending, row, t)
)
const highlighted = !picker.query.trim()
? (selected ?? picker.blankValue)
@@ -161,6 +163,7 @@ export function SurfaceAgentItems({
defaultAgent?: TuiAgent | 'blank' | null
onSetDefault?: (agent: TuiAgent) => void
}): React.ReactNode {
const t = useTranslate()
const { model, onPick } = surfaceRows
if (model.state === 'loading') {
// Why: rows fill in once every answer is known, instead of reshuffling under the pointer.
@@ -169,10 +172,7 @@ export function SurfaceAgentItems({
key={width}
value={`loading:${width}`}
disabled
aria-label={translate(
'auto.components.tab.bar.QuickLaunchButton.agentsLoading',
'Loading agents'
)}
aria-label={t('auto.components.tab.bar.QuickLaunchButton.agentsLoading', 'Loading agents')}
>
<span aria-hidden="true" className="flex h-[1lh] flex-1 items-center">
<span className={cn('h-2 rounded-full bg-muted', width)} />
@@ -184,12 +184,12 @@ export function SurfaceAgentItems({
const terminalPicked = surfaceRows.selection.cli
return rows.map((row, index) => {
const id = rowId(row)
const label = rowLabel(row)
const label = rowLabel(row, t)
const startsCliGroup = row.kind === 'cli' && index > 0 && rows[index - 1]?.kind !== 'cli'
const pendingReason = terminalPendingReason(model.terminalPending, row)
const pendingReason = terminalPendingReason(model.terminalPending, row, t)
const unavailable = row.kind === 'chat-unavailable' || !!pendingReason
const pressed = row.kind === 'chat' && id === pickedValue && terminalPicked
const agentLabel = rowLabel({ ...row, kind: 'chat' })
const agentLabel = rowLabel({ ...row, kind: 'chat' }, t)
return (
<React.Fragment key={id}>
{startsCliGroup ? <CommandSeparator /> : null}
@@ -206,17 +206,19 @@ export function SurfaceAgentItems({
title: unavailable
? undefined
: row.kind === 'chat'
? rowTitle(row, agentLabel)
: terminalPickTitle(agentLabel),
? rowTitle(row, agentLabel, t)
: terminalPickTitle(agentLabel, t),
trailing: unavailable ? (
<span className="shrink-0 text-muted-foreground">
{row.kind === 'chat-unavailable' ? chatUnavailableLabel(row.reason) : pendingReason}
{row.kind === 'chat-unavailable'
? chatUnavailableLabel(row.reason, t)
: pendingReason}
</span>
) : rowOffersTerminal(model, row) ? (
<AgentSurfaceLaunchButton
surface="terminal"
reason={model.terminalPending ? terminalPendingLabel() : undefined}
title={terminalPickTitle(agentLabel)}
reason={model.terminalPending ? terminalPendingLabel(t) : undefined}
title={terminalPickTitle(agentLabel, t)}
aria-pressed={pressed}
className={cn(
'hover:bg-background [[data-selected=true]_&]:opacity-100',
@@ -3,6 +3,7 @@ import {
textActionArgumentsOwnEffort
} from '../../../../shared/agent-launch/picker/agent-launch-picker-text-action'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import type { SessionOptionDescriptor } from '../../../../shared/native-chat/input/native-chat-session-options'
import type {
agentPickerEntryOptions,
@@ -29,7 +30,8 @@ type AgentLaunchSetupOptionRowsInput = {
function argumentsOwnedRow(
id: string,
label: string,
category: SessionOptionDescriptor['category']
category: SessionOptionDescriptor['category'],
t: Translate = translate
): SessionOptionDescriptor {
return {
id,
@@ -40,7 +42,7 @@ function argumentsOwnedRow(
choices: [
{
value: 'arguments',
label: translate('agentPicker.setByArguments', 'Set by your Arguments')
label: t('agentPicker.setByArguments', 'Set by your Arguments')
}
],
currentValue: 'arguments'
@@ -52,32 +54,40 @@ function argumentsOwnedRow(
}
/** Clearing Effort keeps the selected model, so its default is that model's inherited effort. */
function textEffortDefaultLabel(kind: {
choices: readonly { value: string; label: string }[]
defaultValue: string
}): string {
function textEffortDefaultLabel(
kind: {
choices: readonly { value: string; label: string }[]
defaultValue: string
},
t: Translate = translate
): string {
const effort = kind.choices.find((choice) => choice.value === kind.defaultValue)?.label
return effort
? translate('agentPicker.sameAsTextDefault', 'Same as default · {{value0}}', {
? t('agentPicker.sameAsTextDefault', 'Same as default · {{value0}}', {
value0: effort
})
: translate('settings.sourceControl.actionModel.effortDefault', 'Default')
: t('settings.sourceControl.actionModel.effortDefault', 'Default')
}
export function agentLaunchSetupOptionRows({
options,
models,
entry,
draft,
currentModel,
argumentsView,
defaultsPending,
disabled,
argumentsKnown
}: AgentLaunchSetupOptionRowsInput): SessionOptionDescriptor[] {
export function agentLaunchSetupOptionRows(
{
options,
models,
entry,
draft,
currentModel,
argumentsView,
defaultsPending,
disabled,
argumentsKnown
}: AgentLaunchSetupOptionRowsInput,
t: Translate = translate
): SessionOptionDescriptor[] {
// Shown even when the menu's model has no levels, so Effort explains where it is set.
if (textActionArgumentsOwnEffort(entry, draft)) {
return [argumentsOwnedRow('effort', options[0]?.label ?? 'Reasoning effort', 'thought_level')]
return [
argumentsOwnedRow('effort', options[0]?.label ?? 'Reasoning effort', 'thought_level', t)
]
}
return options.map((option) => {
const effortInArguments =
@@ -96,7 +106,7 @@ export function agentLaunchSetupOptionRows({
: undefined
: undefined)
if (argumentsView.context.analysis.opaqueKeys.has(option.id)) {
return argumentsOwnedRow(option.id, option.label, option.category)
return argumentsOwnedRow(option.id, option.label, option.category, t)
}
const valueKnown =
current !== undefined ||
@@ -118,7 +128,7 @@ export function agentLaunchSetupOptionRows({
? [
{
value: 'arguments',
label: translate('agentPicker.setByArguments', 'Set by your Arguments')
label: t('agentPicker.setByArguments', 'Set by your Arguments')
}
]
: []),
@@ -126,7 +136,7 @@ export function agentLaunchSetupOptionRows({
? [
{
value: TEXT_ACTION_DEFAULT_MODEL,
label: textEffortDefaultLabel(option.kind)
label: textEffortDefaultLabel(option.kind, t)
}
]
: []),
@@ -156,7 +166,8 @@ export function agentLaunchSetupOptionRows({
export function agentLaunchSetupCatalogState(
entry: AgentPickerEntry,
models: readonly { id: string; hidden?: true }[],
currentModel: string | undefined
currentModel: string | undefined,
t: Translate = translate
) {
const hasModels = models.some((model) => !model.hidden)
const hasUsableModels =
@@ -167,45 +178,45 @@ export function agentLaunchSetupCatalogState(
? null
: entry.catalogRead === 'unsupported' ||
(!hasModels && entry.catalog.origin === 'unknown' && entry.catalogRead === undefined)
? translate(
? t(
'agentPicker.catalogUnsupported',
'{{agent}} models are unavailable on this host. You can keep your current choices or use Arguments.',
{ agent: entry.label }
)
: entry.catalogRead === 'failed'
? translate(
? t(
'agentPicker.catalogFailed',
'Could not load {{agent}} models. Close and reopen the agent picker to try again.',
{ agent: entry.label }
)
: entry.catalog.unavailable?.reason === 'notSignedIn'
? translate(
? t(
'agentPicker.catalogSignIn',
'Sign in to {{agent}} to load its models, then close and reopen the agent picker.',
{ agent: entry.label }
)
: entry.catalog.unavailable?.reason === 'cliMissing'
? translate(
? t(
'agentPicker.catalogInstall',
'Install {{agent}} on this host to load its models, then close and reopen the agent picker.',
{ agent: entry.label }
)
: entry.catalog.unavailable
? translate(
? t(
'agentPicker.catalogUnresolved',
'{{agent}} models have not loaded. Close and reopen the agent picker to try again.',
{ agent: entry.label }
)
: entry.catalog.origin === 'unknown' || entry.catalog.listingInProgress
? !hasModels
? translate(
? t(
'agentPicker.catalogUnresolved',
'{{agent}} models have not loaded. Close and reopen the agent picker to try again.',
{ agent: entry.label }
)
: null
: !hasModels
? translate('agentPicker.catalogEmpty', '{{agent}} has no available models.', {
? t('agentPicker.catalogEmpty', '{{agent}} has no available models.', {
agent: entry.label
})
: null
@@ -1,6 +1,6 @@
import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target'
import type { TuiAgent } from '../../../../../shared/agent-launch/catalog/tui-agent'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { antigravityChatSignInRunning } from '@/runtime/provider-accounts/sign-in/antigravity-chat-sign-in-status'
import { useManagedRuntimeInstall } from '@/runtime/hosts/installation/use-managed-runtime-install'
import { ManagedRuntimeInstallRow } from '../../managed-runtime/ManagedRuntimeInstallRow'
@@ -28,22 +28,23 @@ export function AgentOpensAsControl({
label: string
opensAs: AgentOpensAs
}): React.JSX.Element {
const t = useTranslate()
return (
<SettingsSegmentedControl<'chat' | 'cli'>
value={opensAs.value}
onChange={opensAs.onChange}
ariaLabel={translate('settings.agents.defaultOpensAs', 'Open {{value0}} as', {
ariaLabel={t('settings.agents.defaultOpensAs', 'Open {{value0}} as', {
value0: label
})}
size="sm"
options={[
{ value: 'chat', label: translate('settings.agents.defaultOpensAsChat', 'Chat') },
{ value: 'chat', label: t('settings.agents.defaultOpensAsChat', 'Chat') },
{
value: 'cli',
label: translate('settings.agents.defaultOpensAsCli', 'CLI'),
label: t('settings.agents.defaultOpensAsCli', 'CLI'),
disabled: opensAs.cliMissing,
tooltip: opensAs.cliMissing
? translate(
? t(
'settings.agents.installCliForTerminal',
'Install the {{value0}} CLI to open it in a terminal.',
{ value0: label }
@@ -62,9 +63,10 @@ export function chatUnavailableOnThisDesktop(agent: string): boolean {
/** Why an agent opens in a terminal on a Windows host; the Chat | CLI choice still routes remote hosts. */
export function ChatUnavailableOnWindowsNote({ label }: { label: string }): React.JSX.Element {
const t = useTranslate()
return (
<p className="text-xs text-muted-foreground">
{translate(
{t(
'settings.agents.chatUnavailableOnWindows',
"{{value0}} Chat isn't available on Windows yet, so {{value0}} opens in a terminal on this host. Chats on a macOS or Linux host still work.",
{ value0: label }
@@ -81,6 +83,7 @@ export function AgentChatRuntimeArea({
agent: TuiAgent
label: string
}): React.JSX.Element | null {
const t = useTranslate()
const runtime = useManagedRuntimeInstall(CHAT_HOST, agent, true)
// Install and Remove wait for a running sign-in, which runs the installed server.
const signIn = useAntigravityChatSignIn(
@@ -103,7 +106,7 @@ export function AgentChatRuntimeArea({
<div className="flex items-center gap-3">
<div className="min-w-0 flex-1">
<p className="text-xs font-medium leading-none">
{translate('settings.agents.chatRuntime', 'Chat runtime')}
{t('settings.agents.chatRuntime', 'Chat runtime')}
</p>
<div className="mt-1">{line}</div>
</div>
@@ -114,7 +117,7 @@ export function AgentChatRuntimeArea({
{agent === 'antigravity' && antigravityProviderSignInShown(runtime) ? (
<AntigravityProviderSignInArea
owner={CHAT_HOST}
label={getRuntimeSentenceLabel({ runtime: 'host', label: getHostRuntimeLabel() })}
label={getRuntimeSentenceLabel({ runtime: 'host', label: getHostRuntimeLabel(t) })}
install={runtime}
/>
) : null}
@@ -17,7 +17,7 @@ import { encodeCatalogOptionValue } from '../../../../../shared/agent-session/op
import type { TuiAgent } from '../../../../../shared/agent-launch/catalog/tui-agent'
import { AgentChoiceMenu } from './AgentChoiceMenu'
import { sourceControlActionRouteHint } from './AgentChoiceOptionField'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
const AGENT_DEFAULT_VALUE = '__agent_default__'
@@ -67,6 +67,7 @@ export function AgentChoiceFields({
}: Omit<AgentChoiceControlsProps, 'onChange'> & {
onSelect: (patch: AgentChoicePatch) => void
}): React.JSX.Element | null {
const t = useTranslate()
const picksModel = sourceControlActionAgentPicksModel(agent)
if (!agent || !picksModel) {
return null
@@ -81,12 +82,12 @@ export function AgentChoiceFields({
(Boolean(model) || option.category !== 'mode') &&
(!optionIds || optionIds.includes(option.id))
)
const defaultLabel = translate('settings.sourceControl.actionModel.effortDefault', 'Default')
const defaultLabel = t('settings.sourceControl.actionModel.effortDefault', 'Default')
return (
<div className={layout === 'contents' ? 'contents' : 'grid min-w-0 grid-cols-1 gap-2'}>
{catalog ? (
<AgentChoiceMenu
label={translate('settings.sourceControl.actionModel.model', 'Model')}
label={t('settings.sourceControl.actionModel.model', 'Model')}
row={layout === 'rows'}
value={selected?.model ?? AGENT_DEFAULT_VALUE}
disabled={disabled}
@@ -96,7 +97,7 @@ export function AgentChoiceFields({
value: AGENT_DEFAULT_VALUE,
label:
defaultLabels?.model ??
translate('settings.sourceControl.actionModel.agentDefault', 'Agent default')
t('settings.sourceControl.actionModel.agentDefault', 'Agent default')
},
...catalog.models.map(({ id, label }) => ({ value: id, label })),
...(unlistedModel ? [{ value: unlistedModel, label: unlistedModel }] : [])
@@ -111,11 +112,11 @@ export function AgentChoiceFields({
: [
{
value: 'true',
label: translate('settings.sourceControl.actionModel.on', 'On')
label: t('settings.sourceControl.actionModel.on', 'On')
},
{
value: 'false',
label: translate('settings.sourceControl.actionModel.off', 'Off')
label: t('settings.sourceControl.actionModel.off', 'Off')
}
]
const current = selected?.options?.[option.id]
@@ -124,7 +125,7 @@ export function AgentChoiceFields({
key={option.id}
label={option.label}
row={layout === 'rows'}
hint={sourceControlActionRouteHint(route)}
hint={sourceControlActionRouteHint(route, t)}
value={current ?? AGENT_DEFAULT_VALUE}
disabled={disabled}
onOpenChange={onOpenChange}
@@ -3,6 +3,7 @@ import { Label } from '../../ui/label'
import { SelectTrigger, SelectValue } from '../../ui/select'
import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/tooltip'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import type { SourceControlActionChoiceRoute } from '../../../../../shared/source-control/ai/recipes/source-control-action-options'
export function SourceControlActionOptionField({
@@ -24,11 +25,14 @@ export function SourceControlActionOptionField({
}
/** Says which launches a saved choice reaches when it is not both. */
export function sourceControlActionRouteHint(route: SourceControlActionChoiceRoute): string | null {
export function sourceControlActionRouteHint(
route: SourceControlActionChoiceRoute,
t: Translate = translate
): string | null {
return route === 'chats'
? translate('settings.sourceControl.actionModel.chatsOnly', 'Applies to chats only')
? t('settings.sourceControl.actionModel.chatsOnly', 'Applies to chats only')
: route === 'terminals'
? translate('settings.sourceControl.actionModel.terminalsOnly', 'Applies to terminals only')
? t('settings.sourceControl.actionModel.terminalsOnly', 'Applies to terminals only')
: null
}
@@ -15,7 +15,7 @@ import {
} from '@/components/tab-bar/drop-indicator'
import { composeActiveTerminalTheme } from '@/components/terminal-pane/appearance/terminal-appearance'
import { useSystemPrefersDark } from '@/hooks/theme/use-system-prefers-dark'
import { translate } from '@/i18n/i18n'
import { useIntlLocale, useTranslate } from '@/i18n/use-translate'
import { AgentIcon } from '@/lib/agent-catalog'
import { buildFontFamily } from '@/lib/monospace-font-family'
import { resolveEffectiveTerminalAppearance } from '@/lib/terminal-theme'
@@ -65,7 +65,9 @@ function MiniWindow({
}
export function ChatSurfacePreview(): JSX.Element {
const copy = getChatDemoCopy()
const t = useTranslate()
const intlLocale = useIntlLocale()
const copy = getChatDemoCopy(t, intlLocale)
return (
<MiniWindow title={copy.tabTitle} className="bg-chat-canvas">
<div
@@ -80,7 +82,7 @@ export function ChatSurfacePreview(): JSX.Element {
</div>
<div className="flex items-center gap-[5px] whitespace-nowrap text-chat-foreground-faint">
<Eye className="size-[11px] shrink-0" />
{nativeChatToolRowVerb('read')}
{nativeChatToolRowVerb('read', t)}
<span className="text-chat-foreground-strong">{CHAT_DEMO_FILE}</span>
</div>
<p className="line-clamp-4">
@@ -103,7 +105,7 @@ export function ChatSurfacePreview(): JSX.Element {
<div className="shrink-0 px-3 pt-2 pb-3">
<div className="rounded-[10px] border border-chat-composer-border bg-chat-composer-surface px-1.5 pt-1.5 pb-[5px] shadow-xs">
<div className="truncate px-1 pb-1.5 text-[9.5px] leading-[13px] text-chat-foreground-faint">
{translate(
{t(
'components.native-chat.composer.placeholder',
'Ask anything, @ to mention files, / for commands'
)}
@@ -112,7 +114,7 @@ export function ChatSurfacePreview(): JSX.Element {
<Plus className="size-[11px] shrink-0" />
<span className="flex min-w-0 items-center gap-[3px] px-[3px]">
<Hand className="size-[9px] shrink-0" />
<span className="truncate">{nativeChatPermissionModeLabel('ask')}</span>
<span className="truncate">{nativeChatPermissionModeLabel('ask', t)}</span>
<ChevronDown className="size-[9px] shrink-0" />
</span>
<span className="flex-1" />
@@ -154,7 +156,9 @@ function Mascot(): JSX.Element {
/** Claude Code's own interface, in the user's terminal theme and font. */
export function TerminalSurfacePreview({ settings }: { settings: GlobalSettings }): JSX.Element {
const copy = getChatDemoCopy()
const t = useTranslate()
const intlLocale = useIntlLocale()
const copy = getChatDemoCopy(t, intlLocale)
const systemPrefersDark = useSystemPrefersDark()
const customThemes = JSON.stringify(settings.terminalCustomThemes)
const pluginPalettes = JSON.stringify(settings.terminalPluginPalettes)
@@ -12,7 +12,7 @@ import { reconcileAgentChoiceArgs } from '../../../../../shared/agent-launch/cho
import type { TuiAgent } from '../../../../../shared/agent-launch/catalog/tui-agent'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { AgentChoiceFields } from './AgentChoiceControls'
export function AgentLaunchChoiceEditor({
@@ -34,6 +34,7 @@ export function AgentLaunchChoiceEditor({
permissionControlRef?: Ref<HTMLDivElement>
save: (patch: AgentChoicePatch | undefined, userArgs: string) => Promise<void>
}): React.JSX.Element {
const t = useTranslate()
const menu = useRef({ open: false, committed: false })
const editorRef = useRef<HTMLDivElement>(null)
const [draft, setDraft] = useState<string | null>(null)
@@ -135,7 +136,7 @@ export function AgentLaunchChoiceEditor({
choiceChips={chips}
note={
permission?.argsSetPermission
? translate(
? t(
'settings.agents.permissionArgsTerminalOnly',
'These arguments may control {{agent}}’s terminal permissions, so Orca adds no flag. Chats use the Permissions choice above.',
{ agent: label }
@@ -2,7 +2,7 @@ import { useId, useLayoutEffect, useRef, useState } from 'react'
import { Button } from '../../ui/button'
import { Input } from '../../ui/input'
import { Textarea } from '../../ui/textarea'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import {
parseAgentDefaultEnvDraft,
stringifyAgentDefaultEnvDraft,
@@ -20,6 +20,7 @@ export function AgentCommandOverrideInput({
cmdOverride: string | undefined
onSaveOverride: (value: string) => void | Promise<void>
}): React.JSX.Element {
const t = useTranslate()
const draftSeed = cmdOverride ?? defaultCmd
const [cmdDraft, setCmdDraft] = useState(draftSeed)
const { pending, write } = useAgentLaunchFieldWrite()
@@ -39,7 +40,7 @@ export function AgentCommandOverrideInput({
return (
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">
{translate('auto.components.settings.AgentsPane.2e45ca29b6', 'Command')}
{t('auto.components.settings.AgentsPane.2e45ca29b6', 'Command')}
</span>
<div className="flex items-center gap-2">
<Input
@@ -75,7 +76,7 @@ export function AgentCommandOverrideInput({
}}
className="h-7 shrink-0 text-xs text-muted-foreground hover:text-foreground"
>
{translate('auto.components.settings.AgentsPane.5200dac9da', 'Reset')}
{t('auto.components.settings.AgentsPane.5200dac9da', 'Reset')}
</Button>
)}
</div>
@@ -111,6 +112,7 @@ export function AgentDefaultArgsInput({
/** The caller already labels the field, e.g. with a disclosure row. */
hideLabel?: boolean
}): React.JSX.Element {
const t = useTranslate()
const [draft, setDraft] = useState<string | null>(null)
const inputRef = useRef<HTMLTextAreaElement>(null)
const chipRef = useRef<HTMLSpanElement>(null)
@@ -157,7 +159,7 @@ export function AgentDefaultArgsInput({
<div className="flex min-w-0 flex-col gap-1">
{hideLabel ? null : (
<span className="text-xs font-medium leading-none text-muted-foreground">
{translate('auto.components.settings.AgentsPane.cfb3f35775', 'Arguments')}
{t('auto.components.settings.AgentsPane.cfb3f35775', 'Arguments')}
</span>
)}
<div className="flex min-w-0 items-center gap-2">
@@ -203,7 +205,7 @@ export function AgentDefaultArgsInput({
value={argsDraft}
disabled={disabled}
readOnly={pending}
aria-label={translate('auto.components.settings.AgentsPane.cfb3f35775', 'Arguments')}
aria-label={t('auto.components.settings.AgentsPane.cfb3f35775', 'Arguments')}
onChange={(event) =>
(onDraftChange ?? setDraft)(event.target.value.replace(/[\r\n]/g, ''))
}
@@ -239,10 +241,7 @@ export function AgentDefaultArgsInput({
? undefined
: placeholder ||
defaultArgs ||
translate(
'auto.components.settings.AgentsPane.6f99bf5dd0',
'No default arguments'
)
t('auto.components.settings.AgentsPane.6f99bf5dd0', 'No default arguments')
}
spellCheck={false}
className="block min-h-5 max-w-full [field-sizing:content] [overflow-wrap:anywhere]"
@@ -268,7 +267,7 @@ export function AgentDefaultArgsInput({
}}
className="h-7 shrink-0 text-xs text-muted-foreground hover:text-foreground"
>
{translate('auto.components.settings.AgentsPane.5200dac9da', 'Reset')}
{t('auto.components.settings.AgentsPane.5200dac9da', 'Reset')}
</Button>
)}
</div>
@@ -286,6 +285,7 @@ export function AgentDefaultEnvInput({
envOverride: Record<string, string>
onSaveEnv: (value: Record<string, string>) => void | Promise<void>
}): React.JSX.Element {
const t = useTranslate()
const defaultEnvText = stringifyAgentDefaultEnvDraft(defaultEnv)
const draftSeed = stringifyAgentDefaultEnvDraft(envOverride)
const [draft, setDraft] = useState<string | null>(null)
@@ -316,7 +316,7 @@ export function AgentDefaultEnvInput({
return (
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">
{translate('auto.components.settings.AgentsPane.8fbe1f37c1', 'Environment')}
{t('auto.components.settings.AgentsPane.8fbe1f37c1', 'Environment')}
</span>
<div className="flex items-center gap-2">
<Input
@@ -341,7 +341,7 @@ export function AgentDefaultEnvInput({
}}
placeholder={
defaultEnvText ||
translate('auto.components.settings.AgentsPane.2d133152fa', 'No default environment')
t('auto.components.settings.AgentsPane.2d133152fa', 'No default environment')
}
spellCheck={false}
aria-invalid={envDraftError ? true : undefined}
@@ -366,18 +366,18 @@ export function AgentDefaultEnvInput({
}}
className="h-7 shrink-0 text-xs text-muted-foreground hover:text-foreground"
>
{translate('auto.components.settings.AgentsPane.5200dac9da', 'Reset')}
{t('auto.components.settings.AgentsPane.5200dac9da', 'Reset')}
</Button>
)}
</div>
{envDraftError && (
<p id={envDraftErrorId} className="mt-1 text-[11px] text-destructive">
{envDraftError === 'too-large'
? translate(
? t(
'auto.components.settings.AgentsPane.3f1bdf3cb4',
'Environment text is too large to parse safely.'
)
: translate(
: t(
'settings.agents.environmentUnclosedQuote',
'Close the quote to save the environment.'
)}
@@ -4,6 +4,7 @@ import type { Ref } from 'react'
import type { AgentChatPermissionMode } from '../../../../../shared/agent-launch/permissions/agent-chat-permission-mode'
import type { AgentPermissionChoice } from '../../../../../shared/agent-launch/permissions/tui-agent-permission-choice'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import type { Translate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
import { Button } from '../../ui/button'
@@ -48,9 +49,10 @@ export function AllAgentsPermissionSetting({
onChange: (choice: AgentPermissionChoice) => void
onChangeWithoutAuto: (choice: AgentPermissionChoice) => void
}): React.JSX.Element {
const title = translate('settings.agents.permissionsTitle', 'Permissions')
const ownLabel = translate('settings.agents.permissionEachOwn', "Each agent's own settings")
const withoutAutoTitle = translate(
const t = useTranslate()
const title = t('settings.agents.permissionsTitle', 'Permissions')
const ownLabel = t('settings.agents.permissionEachOwn', "Each agent's own settings")
const withoutAutoTitle = t(
'settings.agents.otherAgentsPermissionsLabel',
'Permissions for other agents'
)
@@ -88,7 +90,7 @@ export function AllAgentsPermissionSetting({
</DropdownMenu>
</div>
<p className="text-xs text-muted-foreground">
{translate(
{t(
'settings.agents.permissionsChoiceExplanation',
'What every agent may do without asking. You can set one agent differently below.'
)}
@@ -97,7 +99,7 @@ export function AllAgentsPermissionSetting({
{choice === 'auto' ? (
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">
{translate('settings.agents.permissionWithoutAuto', 'Agents without Approve for me')}
{t('settings.agents.permissionWithoutAuto', 'Agents without Approve for me')}
</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -110,7 +112,7 @@ export function AllAgentsPermissionSetting({
>
{withoutAutoChoice === null ? (
<span className="truncate">
{translate('settings.agents.permissionMixed', 'Mixed')}
{t('settings.agents.permissionMixed', 'Mixed')}
</span>
) : (
<AgentPermissionChoiceName
@@ -183,9 +185,9 @@ export function AgentPermissionMenu({
showInherited?: boolean
rawValue?: string
}): React.JSX.Element {
const followAll =
inheritLabel ?? translate('settings.agents.permissionSameAsAll', 'Same as all agents')
const ownLabel = agentPermissionChoiceLabel('own', label)
const t = useTranslate()
const followAll = inheritLabel ?? t('settings.agents.permissionSameAsAll', 'Same as all agents')
const ownLabel = agentPermissionChoiceLabel('own', label, t)
return (
<div
className={cn(
@@ -199,15 +201,13 @@ export function AgentPermissionMenu({
>
{layout === 'row' ? (
<span className="text-xs text-muted-foreground">
{translate('settings.agents.rowPermissions', 'Permissions')}
{t('settings.agents.rowPermissions', 'Permissions')}
</span>
) : layout === 'stacked' ? (
<Label variant="settings">
{translate('settings.agents.rowPermissions', 'Permissions')}
</Label>
<Label variant="settings">{t('settings.agents.rowPermissions', 'Permissions')}</Label>
) : (
<span className="text-xs font-medium">
{translate('settings.agents.rowPermissions', 'Permissions')}
{t('settings.agents.rowPermissions', 'Permissions')}
</span>
)}
<DropdownMenu onOpenChange={onOpenChange}>
@@ -216,7 +216,7 @@ export function AgentPermissionMenu({
type="button"
variant="outline"
size={size}
aria-label={translate('settings.agents.rowPermissionsLabel', '{{agent}} permissions', {
aria-label={t('settings.agents.rowPermissionsLabel', '{{agent}} permissions', {
agent: label
})}
className={cn('min-w-0 max-w-full', layout === 'stacked' && 'w-full justify-between')}
@@ -246,10 +246,10 @@ export function AgentPermissionMenu({
</DropdownMenu>
{permission.terminalChoice === 'own' && permission.chatChoice ? (
<span className="text-xs text-muted-foreground">
{translate(
{t(
'settings.agents.permissionSurfaceDifference',
'Terminals use the agent’s own settings; new chats use {{mode}}.',
{ mode: nativeChatPermissionModeLabel(permission.chatChoice) }
{ mode: nativeChatPermissionModeLabel(permission.chatChoice, t) }
)}
</span>
) : null}
@@ -276,6 +276,7 @@ function AgentArgsChoiceChip({
choiceKey: string
source?: 'arguments'
}): React.JSX.Element {
const t = useTranslate()
return (
<Tooltip>
<TooltipTrigger asChild>
@@ -305,8 +306,8 @@ function AgentArgsChoiceChip({
}}
aria-label={
source === 'arguments' || choiceKey !== 'permissionMode'
? translate('settings.agents.choiceChipRemove', 'Remove {{flag}}', { flag })
: translate(
? t('settings.agents.choiceChipRemove', 'Remove {{flag}}', { flag })
: t(
'settings.agents.permissionChipRemove',
"Remove {{flag}} and use {{agent}}'s own settings",
{ flag, agent: label }
@@ -320,19 +321,19 @@ function AgentArgsChoiceChip({
</TooltipTrigger>
<TooltipContent>
{source === 'arguments'
? translate('agentPicker.argumentsHint', 'Applies only to CLI launches.')
? t('agentPicker.argumentsHint', 'Applies only to CLI launches.')
: choiceKey !== 'permissionMode'
? translate(
? t(
'settings.agents.choiceChipTooltip',
'Added by {{choice}}. Remove it to use the default.',
{
choice:
choiceKey === 'model'
? translate('settings.sourceControl.actionModel.model', 'Model')
: translate('settings.sourceControl.actionModel.effort', 'Effort')
? t('settings.sourceControl.actionModel.model', 'Model')
: t('settings.sourceControl.actionModel.effort', 'Effort')
}
)
: translate(
: t(
'settings.agents.permissionChipTooltip',
'Added by Permissions. Remove it to use {{agent}}’s own settings.',
{ agent: label }
@@ -1,20 +1,21 @@
import { Terminal } from 'lucide-react'
import type { AgentPermissionChoice } from '../../../../../shared/agent-launch/permissions/tui-agent-permission-choice'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { DropdownMenuRadioItem, DropdownMenuSeparator } from '../../ui/dropdown-menu'
import {
NativeChatPermissionModeItems,
NativeChatPermissionModeName
} from '../../native-chat/session/options/controls/NativeChatPermissionModePicker'
function ownDescription(chatOwnSupported: boolean): string {
function ownDescription(chatOwnSupported: boolean, t: Translate = translate): string {
if (!chatOwnSupported) {
return translate(
return t(
'settings.agents.permissionOwnLegacyDescription',
'Orca adds no permission flag in terminals; chats use Settings → Chat.'
)
}
return translate(
return t(
'settings.agents.permissionOwnDescription',
'Terminals and chats follow the agent’s own permission settings.'
)
@@ -57,6 +58,7 @@ export function AgentPermissionChoiceItems({
disabled?: boolean
onSelect: (choice: AgentPermissionChoice) => void
}): React.JSX.Element {
const t = useTranslate()
const modes = choices.filter((choice) => choice !== 'own')
return (
<>
@@ -73,7 +75,7 @@ export function AgentPermissionChoiceItems({
<div className="min-w-0 py-0.5">
<AgentPermissionChoiceName choice="own" ownLabel={ownLabel} />
<div className="text-xs font-normal text-muted-foreground">
{ownDescription(chatOwnSupported)}
{ownDescription(chatOwnSupported, t)}
</div>
</div>
</DropdownMenuRadioItem>
@@ -5,7 +5,7 @@ import { normalizeGlobalWindowsRuntimeDefault } from '../../../../../shared/proj
import { Button } from '../../ui/button'
import { Input } from '../../ui/input'
import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/tooltip'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
export type AgentSessionSourceHomeControl = {
runtimeLabel: string
@@ -85,6 +85,7 @@ export function AgentSessionSourceHomeInput({
value,
onSave
}: AgentSessionSourceHomeControl): React.JSX.Element {
const t = useTranslate()
const [draft, setDraft] = useState(value)
const commit = (): void => {
@@ -94,15 +95,12 @@ export function AgentSessionSourceHomeInput({
return (
<div className="flex flex-col gap-1">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
{translate(
'auto.components.settings.AgentsPane.codexSessionSource',
'Codex home to import from'
)}
{t('auto.components.settings.AgentsPane.codexSessionSource', 'Codex home to import from')}
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={translate(
aria-label={t(
'auto.components.settings.AgentsPane.codexSessionSourceInfo',
'About importing Codex history'
)}
@@ -112,7 +110,7 @@ export function AgentSessionSourceHomeInput({
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={6} className="max-w-xs">
{translate(
{t(
'auto.components.settings.AgentsPane.codexSessionSourceTooltip',
'Orca runs Codex in an isolated home. Point this at your existing Codex home to import that session history. Empty uses ~/.codex.'
)}
@@ -149,7 +147,7 @@ export function AgentSessionSourceHomeInput({
}}
className="h-7 shrink-0 text-xs text-muted-foreground hover:text-foreground"
>
{translate('auto.components.settings.AgentsPane.5200dac9da', 'Reset')}
{t('auto.components.settings.AgentsPane.5200dac9da', 'Reset')}
</Button>
)}
</div>
@@ -4,7 +4,7 @@ import { FEATURE_TIPS } from '../../../../shared/feature-tips'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { useMountedRef } from '@/hooks/useMountedRef'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { useAppStore } from '@/store'
import { ChatSurfacePreview } from '../agent/launch-settings/AgentDefaultSurfacePreviews'
import { getChatUiHereVariantCopy } from './chat-ui-here-copy'
@@ -23,6 +23,7 @@ type CornerCardState =
/** The nonblocking launch card for terminal-default profiles, sharing the tip's copy and a compact chat preview. */
export function ChatUiHereCornerCard({ onClose }: { onClose: () => void }): JSX.Element | null {
const t = useTranslate()
const mountedRef = useMountedRef()
const settings = useAppStore((s) => s.settings)
const [state, setState] = useState<CornerCardState>({ stage: 'prompt' })
@@ -51,7 +52,7 @@ export function ChatUiHereCornerCard({ onClose }: { onClose: () => void }): JSX.
if (state.stage === 'switched') {
return <ChatUiSwitchedCornerNote onClose={onClose} focusSettings={state.focusSettings} />
}
const { paragraphs, secondaryLabel, primaryLabel } = getChatUiHereVariantCopy('chat-ui-off')
const { paragraphs, secondaryLabel, primaryLabel } = getChatUiHereVariantCopy('chat-ui-off', t)
const [intro = ''] = paragraphs
const handleUseChatUi = async (event: MouseEvent<HTMLButtonElement>): Promise<void> => {
@@ -95,13 +96,13 @@ export function ChatUiHereCornerCard({ onClose }: { onClose: () => void }): JSX.
<CardHeader>
<div className="flex items-center justify-between gap-2">
<CardTitle id="chat-ui-here-corner-heading">
{translate('featureTips.chatUiHere.title', 'Chat UI is here')}
{t('featureTips.chatUiHere.title', 'Chat UI is here')}
</CardTitle>
<Button
variant="ghost"
size="icon-xs"
onClick={handleDismiss}
aria-label={translate('featureTips.chatUiHere.dismiss', 'Dismiss')}
aria-label={t('featureTips.chatUiHere.dismiss', 'Dismiss')}
>
<X className="size-3.5" />
</Button>
@@ -121,7 +122,7 @@ export function ChatUiHereCornerCard({ onClose }: { onClose: () => void }): JSX.
<span className="block space-y-2">
<span className="block">{intro}</span>
<FeatureTipSettingsLine
text={translate(
text={t(
'featureTips.chatUiHere.changeInSettings',
'Change anytime in <link>Settings → Chat UI</link>.'
)}
@@ -4,7 +4,7 @@ import { LoadingSpinner } from '@/components/LoadingSpinner'
import { usePrefersReducedMotion } from '@/components/feature-wall/feature-wall-modal-helpers'
import { nativeChatPermissionModeLabel } from '@/components/native-chat/session/options/controls/native-chat-permission-mode-labels'
import { nativeChatToolRowVerb } from '@/components/native-chat/tools/lines/native-chat-tool-line-label'
import { translate } from '@/i18n/i18n'
import { useIntlLocale, useTranslate } from '@/i18n/use-translate'
import { AgentIcon } from '@/lib/agent-catalog'
import { cn } from '@/lib/utils'
import {
@@ -81,6 +81,7 @@ export function FlowDiagram({
}
function Transcript({ copy, frame }: { copy: ChatDemoCopy; frame: ChatDemoFrame }): JSX.Element {
const t = useTranslate()
return (
<div
className="flex min-h-full shrink-0 flex-col transition-opacity duration-700 ease-in-out"
@@ -104,7 +105,7 @@ function Transcript({ copy, frame }: { copy: ChatDemoCopy; frame: ChatDemoFrame
<span className="flex size-4 items-center justify-center text-chat-foreground-faint">
<Eye className="size-3.5" />
</span>
<span className="text-chat-foreground-faint">{nativeChatToolRowVerb('read')}</span>
<span className="text-chat-foreground-faint">{nativeChatToolRowVerb('read', t)}</span>
<span>{CHAT_DEMO_FILE}</span>
<ChevronRight className="ml-auto size-3.5 text-chat-foreground-faint" />
</div>
@@ -113,7 +114,7 @@ function Transcript({ copy, frame }: { copy: ChatDemoCopy; frame: ChatDemoFrame
<div className="mt-3 flex min-h-6 items-center gap-1.5 text-chat-foreground-faint">
<LoadingSpinner className="size-4 shrink-0" />
<span className="text-chat-foreground">
{translate('components.native-chat.status.working', 'Working…')}
{t('components.native-chat.status.working', 'Working…')}
</span>
</div>
</Row>
@@ -153,6 +154,7 @@ function Transcript({ copy, frame }: { copy: ChatDemoCopy; frame: ChatDemoFrame
}
function Composer({ prompt, frame }: { prompt: string; frame: ChatDemoFrame }): JSX.Element {
const t = useTranslate()
const typing = frame.typed > 0
return (
<div className="shrink-0 px-3 pt-1.5 pb-3">
@@ -165,7 +167,7 @@ function Composer({ prompt, frame }: { prompt: string; frame: ChatDemoFrame }):
</span>
) : (
<span className="text-chat-foreground-faint">
{translate(
{t(
'components.native-chat.composer.placeholder',
'Ask anything, @ to mention files, / for commands'
)}
@@ -179,7 +181,7 @@ function Composer({ prompt, frame }: { prompt: string; frame: ChatDemoFrame }):
</span>
<span className="flex h-5.5 min-w-0 items-center gap-1 px-1.5 text-xs font-medium whitespace-nowrap">
<Hand className="size-3 shrink-0" />
<span className="truncate">{nativeChatPermissionModeLabel('ask')}</span>
<span className="truncate">{nativeChatPermissionModeLabel('ask', t)}</span>
<ChevronDown className="size-3 shrink-0" />
</span>
</div>
@@ -224,8 +226,10 @@ export function ChatUiDemoCard({
loop?: boolean
className?: string
}): JSX.Element {
const t = useTranslate()
const intlLocale = useIntlLocale()
const reducedMotion = usePrefersReducedMotion()
const copy = getChatDemoCopy()
const copy = getChatDemoCopy(t, intlLocale)
const promptLength = Array.from(copy.prompt).length
const timeline = useMemo(
() => createChatDemoTimeline(promptLength, copy.reply.length),
@@ -1,7 +1,7 @@
import { useRef, type JSX } from 'react'
import type { FeatureTip } from '../../../../shared/feature-tips'
import { DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { ChatUiHereFeatureTipVisual } from './ChatUiHereFeatureTipVisual'
import { getChatUiHereVariantCopy, type ShownChatUiLaunchTipVariant } from './chat-ui-here-copy'
import { FeatureTipActions } from './FeatureTipActions'
@@ -34,8 +34,9 @@ export function ChatUiHereTipDialog({
onSkip: () => void
onSettingsClick: () => void
}): JSX.Element {
const t = useTranslate()
const primaryButtonRef = useRef<HTMLButtonElement>(null)
const { paragraphs, secondaryLabel, primaryLabel } = getChatUiHereVariantCopy(variant)
const { paragraphs, secondaryLabel, primaryLabel } = getChatUiHereVariantCopy(variant, t)
return (
<FeatureTipDialogFrame
@@ -51,9 +52,9 @@ export function ChatUiHereTipDialog({
>
<DialogHeader className="text-left">
<div>
<FeatureTipEyebrow label={translate('featureTips.eyebrowNew', 'New')} />
<FeatureTipEyebrow label={t('featureTips.eyebrowNew', 'New')} />
<DialogTitle size="display">
{translate('featureTips.chatUiHere.title', 'Chat UI is here')}
{t('featureTips.chatUiHere.title', 'Chat UI is here')}
</DialogTitle>
<DialogDescription className="mt-3 max-w-2xl">
<span className="block space-y-3 leading-relaxed">
@@ -63,7 +64,7 @@ export function ChatUiHereTipDialog({
</span>
))}
<FeatureTipSettingsLine
text={translate(
text={t(
'featureTips.chatUiHere.changeInSettings',
'Change anytime in <link>Settings → Chat UI</link>.'
)}
@@ -3,7 +3,8 @@ import { Trans } from 'react-i18next'
import { Check, X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { i18n, translate } from '@/i18n/i18n'
import { i18n } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { openChatUiFeatureTipSettings } from './chat-ui-here-actions'
const AUTO_DISMISS_MS = 12_000
@@ -15,6 +16,7 @@ export function ChatUiSwitchedCornerNote({
onClose: () => void
focusSettings?: boolean
}): JSX.Element {
const t = useTranslate()
const settingsButtonRef = useRef<HTMLButtonElement>(null)
useLayoutEffect(() => {
@@ -63,17 +65,14 @@ export function ChatUiSwitchedCornerNote({
<Check className="size-3" />
</span>
<CardTitle id="chat-ui-switched-note-heading">
{translate(
'featureTips.chatUiHere.switchedNoteTitle',
'New agents will open in Chat UI'
)}
{t('featureTips.chatUiHere.switchedNoteTitle', 'New agents will open in Chat UI')}
</CardTitle>
</div>
<Button
variant="ghost"
size="icon-xs"
onClick={onClose}
aria-label={translate('featureTips.chatUiHere.dismiss', 'Dismiss')}
aria-label={t('featureTips.chatUiHere.dismiss', 'Dismiss')}
>
<X className="size-3.5" />
</Button>
@@ -84,7 +83,7 @@ export function ChatUiSwitchedCornerNote({
<CardDescription>
<Trans
i18n={i18n}
defaults={translate(
defaults={t(
'featureTips.chatUiHere.switchedNoteBody',
'Prefer the terminal? Switch back anytime: search <term>Chat UI</term> in Settings.'
)}
@@ -93,7 +92,7 @@ export function ChatUiSwitchedCornerNote({
</CardDescription>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" onClick={onClose}>
{translate('featureTips.gotIt', 'Got it')}
{t('featureTips.gotIt', 'Got it')}
</Button>
<Button
ref={settingsButtonRef}
@@ -102,7 +101,7 @@ export function ChatUiSwitchedCornerNote({
className="flex-1"
onClick={handleOpenSettings}
>
{translate('featureTips.chatUiHere.openSettings', 'Open Settings')}
{t('featureTips.chatUiHere.openSettings', 'Open Settings')}
</Button>
</div>
</div>
@@ -5,7 +5,7 @@ import {
ORCHESTRATION_CLI_COMMAND_TIMINGS_MS
} from '@/components/feature-wall/agents-orchestration/orchestration-types'
import { usePrefersReducedMotion } from '@/components/feature-wall/feature-wall-modal-helpers'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
const CLI_AGENT_COMMANDS = [
'orca worktree create --name auth-pr-1',
@@ -15,6 +15,7 @@ const CLI_AGENT_COMMANDS = [
]
export function CliFeatureTipVisual(): JSX.Element {
const t = useTranslate()
const reducedMotion = usePrefersReducedMotion()
const [animatedVisibleCommandCount, setAnimatedVisibleCommandCount] = useState(0)
// Show the reduced-motion end state directly; only animation needs timer-backed state.
@@ -63,7 +64,7 @@ export function CliFeatureTipVisual(): JSX.Element {
<div className="space-y-1.5 px-3 py-3 font-mono text-[10.5px] leading-[1.35] text-foreground">
<div className="truncate text-muted-foreground">
<span className="mr-1.5 text-foreground">●</span>
{translate(
{t(
'auto.components.feature.tips.CliFeatureTipVisual.22e62f3bab',
'Claude Code session started'
)}
@@ -77,7 +78,7 @@ export function CliFeatureTipVisual(): JSX.Element {
className={`truncate ${isVisible ? 'animate-cli-tip-command-line' : 'invisible'}`}
>
<span className="text-foreground">
{translate('auto.components.feature.tips.CliFeatureTipVisual.badb4fc342', '>')}
{t('auto.components.feature.tips.CliFeatureTipVisual.badb4fc342', '>')}
</span>
<span>{command}</span>
{isCurrentLine ? (
@@ -9,7 +9,8 @@ import {
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import { i18n, translate } from '@/i18n/i18n'
import { i18n } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { Trans } from 'react-i18next'
import { CliFeatureTipVisual } from './CliFeatureTipVisual'
import { CliSkillSetupPanel } from './CliSkillSetupPanel'
@@ -41,6 +42,7 @@ export function CliSetupTipDialog({
onPrimaryAction: () => void
onSkip: () => void
}): JSX.Element {
const t = useTranslate()
const copy = getLocalizedFeatureTipCopy(tip)
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@@ -75,15 +77,12 @@ export function CliSetupTipDialog({
}`}
>
<p className="font-medium text-foreground">
{translate(
'auto.components.feature.tips.FeatureTipsModal.4795ac2d4a',
'Try asking:'
)}
{t('auto.components.feature.tips.FeatureTipsModal.4795ac2d4a', 'Try asking:')}
</p>
<p>
<Trans
i18n={i18n}
defaults={translate(
defaults={t(
'auto.components.feature.tips.FeatureTipsModal.splitPrPrompt',
'“Split this PR into two <term>worktrees</term> and create PRs for each.”'
)}
@@ -91,17 +90,14 @@ export function CliSetupTipDialog({
/>
</p>
<p>
{translate(
{t(
'auto.components.feature.tips.FeatureTipsModal.864e2db28f',
'“When the agent in'
)}
<WorktreePromptTerm>
{translate(
'auto.components.feature.tips.FeatureTipsModal.298301b7a0',
'worktree'
)}
{t('auto.components.feature.tips.FeatureTipsModal.298301b7a0', 'worktree')}
</WorktreePromptTerm>{' '}
{translate(
{t(
'auto.components.feature.tips.FeatureTipsModal.3c6c478462',
'X finishes, send it the review task.”'
)}
@@ -114,7 +110,7 @@ export function CliSetupTipDialog({
<DialogFooter className="mt-8 flex sm:justify-stretch">
{skillTerminalOpen ? (
<Button className="w-full" onClick={onSkip}>
{translate('auto.components.feature.tips.FeatureTipsModal.c169298e4d', 'Done')}
{t('auto.components.feature.tips.FeatureTipsModal.c169298e4d', 'Done')}
</Button>
) : (
<FeatureTipActions
@@ -7,11 +7,12 @@ import {
hasInstalledAgentSkill,
useInstalledAgentSkillNames
} from '@/hooks/useInstalledAgentSkills'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
const CLI_TIP_SKILL_NAMES = [ORCA_CLI_SKILL_NAME, ORCHESTRATION_SKILL_NAME] as const
function BundledCliSkillSetupPanel(): React.JSX.Element {
const t = useTranslate()
const activeSkillRuntime = useActiveProjectSkillRuntime()
// Why: a repair-required runtime has no distro to scan; Install stays disabled for it.
const agentRuntime = activeSkillRuntime.installDisabledReason
@@ -30,8 +31,8 @@ function BundledCliSkillSetupPanel(): React.JSX.Element {
variant="inline"
hideHeader
className="min-w-0"
title={translate('auto.components.feature.tips.CliSkillSetupPanel.title', 'Skill setup')}
description={translate(
title={t('auto.components.feature.tips.CliSkillSetupPanel.title', 'Skill setup')}
description={t(
'auto.components.feature.tips.CliSkillSetupPanel.description',
'Install the Orca CLI and orchestration skills for your agents.'
)}
@@ -10,9 +10,10 @@ import {
} from '@/lib/skill-runtime-command'
import { ORCA_CLI_ORCHESTRATION_SKILL_INSTALL_COMMAND } from '@/lib/agent-feature-install-commands'
import { useActiveProjectSkillRuntime } from '@/hooks/useActiveProjectSkillRuntime'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
export function CliSkillSetupTerminal(): React.JSX.Element {
const t = useTranslate()
const activeSkillRuntime = useActiveProjectSkillRuntime()
// Missing WSL distros fall back to the Windows host; this ungated auto-paste still needs npx preflight.
const skillCommand = buildSkillCommandForRuntime(
@@ -28,7 +29,7 @@ export function CliSkillSetupTerminal(): React.JSX.Element {
try {
await window.api.ui.writeClipboardText(skillCommand)
toast.success(
translate(
t(
'auto.components.feature.tips.CliSkillSetupTerminal.b8ad063571',
'Copied the skill install command.'
)
@@ -37,7 +38,7 @@ export function CliSkillSetupTerminal(): React.JSX.Element {
toast.error(
error instanceof Error
? error.message
: translate(
: t(
'auto.components.feature.tips.CliSkillSetupTerminal.6ff813fc1d',
'Failed to copy skill command.'
)
@@ -58,7 +59,7 @@ export function CliSkillSetupTerminal(): React.JSX.Element {
size="icon-sm"
className="shrink-0"
onClick={() => void handleCopySkillCommand()}
aria-label={translate(
aria-label={t(
'auto.components.feature.tips.CliSkillSetupTerminal.5eca672aac',
'Copy skill install command'
)}
@@ -67,25 +68,19 @@ export function CliSkillSetupTerminal(): React.JSX.Element {
</Button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}>
{translate(
'auto.components.feature.tips.CliSkillSetupTerminal.5c3aee22c0',
'Copy command'
)}
{t('auto.components.feature.tips.CliSkillSetupTerminal.5c3aee22c0', 'Copy command')}
</TooltipContent>
</Tooltip>
</div>
<OnboardingInlineCommandTerminal
command={skillCommand}
prepareCommandForShell={prepareCommandForShell}
title={translate(
'auto.components.feature.tips.CliSkillSetupTerminal.84e9576dac',
'Skill setup'
)}
ariaLabel={translate(
title={t('auto.components.feature.tips.CliSkillSetupTerminal.84e9576dac', 'Skill setup')}
ariaLabel={t(
'auto.components.feature.tips.CliSkillSetupTerminal.43b60ec5c3',
'Orca CLI and orchestration skill install terminal'
)}
description={translate(
description={t(
'auto.components.feature.tips.CliSkillSetupTerminal.1953e90447',
'Press Enter to install the Orca CLI orchestration skill for your agents.'
)}
@@ -2,7 +2,7 @@ import { Fragment, useEffect, useState, type JSX } from 'react'
import { Plus, Search } from 'lucide-react'
import { usePrefersReducedMotion } from '@/components/feature-wall/feature-wall-modal-helpers'
import { formatShortcutKeyComboDetails, useShortcutKeyDetails } from '@/hooks/useShortcutLabel'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
const TYPED_QUERY = 'auth'
// Mirror the real palette’s recent worktrees before typing narrows them.
@@ -40,6 +40,7 @@ const HOLD_BEFORE_TYPING_MS = 700
const TYPE_INTERVAL_MS = 120
export function CmdJPaletteFeatureTipVisual(): JSX.Element {
const t = useTranslate()
const reducedMotion = usePrefersReducedMotion()
// Read the live binding to reflect rebindings and platform defaults.
const shortcut = useShortcutKeyDetails('worktree.palette')
@@ -200,7 +201,7 @@ export function CmdJPaletteFeatureTipVisual(): JSX.Element {
<Plus size={13} aria-hidden="true" />
</div>
<div className="min-w-0 flex-1 truncate text-[12.5px] font-semibold tracking-[-0.01em] text-foreground">
{translate(
{t(
'auto.components.feature.tips.CmdJPaletteFeatureTipVisual.ab94e16d44',
'Create worktree "{{value0}}"',
{ value0: renderQuery.trim() }
@@ -10,7 +10,7 @@ import {
FeatureTipEyebrow,
FeatureTipSettingsLine
} from './FeatureTipDialogFrame'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
export function CmdJPaletteTipDialog({
open,
@@ -29,6 +29,7 @@ export function CmdJPaletteTipDialog({
onSkip: () => void
onRebindClick: () => void
}): JSX.Element {
const t = useTranslate()
// Use the live binding for rebindings and platform-specific shortcuts.
const worktreePaletteShortcutLabel = useShortcutLabel('worktree.palette')
const displayShortcutLabel =
@@ -69,7 +70,7 @@ export function CmdJPaletteTipDialog({
<DialogDescription className="mt-3 max-w-2xl space-y-3 text-sm leading-relaxed">
<span className="block">{copy.description}</span>
<FeatureTipSettingsLine
text={translate(
text={t(
'auto.components.feature.tips.CmdJPaletteTipDialog.rebindInSettings',
'Rebind the shortcut anytime in <link>Settings → Shortcuts</link>.'
)}
@@ -3,16 +3,17 @@ import { Loader2 } from 'lucide-react'
import type { FeatureTip, FeatureTipAction } from '../../../../shared/feature-tips'
import { Button } from '@/components/ui/button'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { getLocalizedFeatureTipCopy } from './feature-tip-localized-copy'
function getPrimaryBusyLabel(action: FeatureTipAction): string {
function getPrimaryBusyLabel(action: FeatureTipAction, t: Translate = translate): string {
if (action === 'setup-cli') {
return translate('auto.components.feature.tips.FeatureTipActions.installing', 'Installing...')
return t('auto.components.feature.tips.FeatureTipActions.installing', 'Installing...')
}
if (action === 'enable-session-search') {
return translate('auto.components.feature.tips.FeatureTipActions.turningOn', 'Turning on...')
return t('auto.components.feature.tips.FeatureTipActions.turningOn', 'Turning on...')
}
return translate('auto.components.feature.tips.FeatureTipActions.working', 'Working...')
return t('auto.components.feature.tips.FeatureTipActions.working', 'Working...')
}
export function FeatureTipActions({
@@ -41,6 +42,7 @@ export function FeatureTipActions({
/** Overrides the secondary button's default "Maybe Later". */
secondaryLabel?: string
}): JSX.Element {
const t = useTranslate()
const skipButton = showSkip ? (
<Button
ref={swapButtons ? primaryButtonRef : undefined}
@@ -50,7 +52,7 @@ export function FeatureTipActions({
disabled={primaryBusy}
>
{secondaryLabel ??
translate('auto.components.feature.tips.FeatureTipActions.eb04abece8', 'Maybe Later')}
t('auto.components.feature.tips.FeatureTipActions.eb04abece8', 'Maybe Later')}
</Button>
) : null
@@ -67,7 +69,7 @@ export function FeatureTipActions({
{primaryBusy ? (
<>
<Loader2 className="size-4 animate-spin" />
{getPrimaryBusyLabel(currentTip.action)}
{getPrimaryBusyLabel(currentTip.action, t)}
</>
) : (
(label ?? getLocalizedFeatureTipCopy(currentTip).ctaLabel)
@@ -25,12 +25,13 @@ import {
} from './feature-tip-telemetry'
import { useMountedRef } from '@/hooks/useMountedRef'
import { isWebClientLocation } from '@/lib/web-client-location'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { SessionSearchTipDialog } from './SessionSearchTipDialog'
import { useSessionSearchTipSetup } from './use-session-search-tip-setup'
import { VoiceDictationTipDialog } from './VoiceDictationTipDialog'
export default function FeatureTipsModal(): JSX.Element | null {
const t = useTranslate()
const activeModal = useAppStore((s) => s.activeModal)
const closeModal = useAppStore((s) => s.closeModal)
const openSettingsPage = useAppStore((s) => s.openSettingsPage)
@@ -213,7 +214,7 @@ export default function FeatureTipsModal(): JSX.Element | null {
}
enableOrchestrationSkillSetup()
toast.success(
translate(
t(
'auto.components.feature.tips.FeatureTipsModal.ce13a742d0',
'Registered `orca` in PATH.'
)
@@ -227,14 +228,14 @@ export default function FeatureTipsModal(): JSX.Element | null {
return
}
toast.warning(
translate(
t(
'auto.components.feature.tips.FeatureTipsModal.1da82af45b',
'Orca CLI needs attention'
),
{
description:
result.status.detail ??
translate(
t(
'auto.components.feature.tips.FeatureTipsModal.d1a86c7eb5',
'Open Settings to finish CLI setup.'
)
@@ -246,7 +247,7 @@ export default function FeatureTipsModal(): JSX.Element | null {
const message =
error instanceof Error
? error.message
: translate(
: t(
'auto.components.feature.tips.FeatureTipsModal.cliInstallFailed',
'Failed to install Orca CLI.'
)
@@ -260,7 +261,7 @@ export default function FeatureTipsModal(): JSX.Element | null {
}
enableOrchestrationSkillSetup()
toast.info(
translate(
t(
'auto.components.feature.tips.FeatureTipsModal.53905bd076',
'Development preview: opening skills setup terminal.'
)
@@ -5,6 +5,7 @@ import {
getSessionSearchDemoRecentRows,
getSessionSearchDemoScenes
} from './session-search-feature-tip-demo'
import { useTranslate } from '@/i18n/use-translate'
type DemoPhase = 'idle' | 'typing' | 'searching' | 'results' | 'focused' | 'erasing'
type DemoState = { scene: number; phase: DemoPhase; chars: number }
@@ -40,9 +41,10 @@ function nextDemoState(state: DemoState, queryLength: number, sceneCount: number
}
export function SessionSearchFeatureTipVisual(): JSX.Element {
const t = useTranslate()
const reducedMotion = usePrefersReducedMotion()
const scenes = getSessionSearchDemoScenes()
const recentRows = getSessionSearchDemoRecentRows()
const scenes = getSessionSearchDemoScenes(t)
const recentRows = getSessionSearchDemoRecentRows(t)
const [state, setState] = useState<DemoState>({ scene: 0, phase: 'idle', chars: 0 })
const shown: DemoState = reducedMotion
? { scene: 0, phase: 'results', chars: scenes[0].query.length }
@@ -7,6 +7,7 @@ import {
} from '@/components/settings/session-history/session-history-status-copy'
import { DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { translate } from '@/i18n/i18n'
import { useIntlLocale, useTranslate, type Translate } from '@/i18n/use-translate'
import { FeatureTipActions } from './FeatureTipActions'
import { getLocalizedFeatureTipCopy } from './feature-tip-localized-copy'
import {
@@ -19,7 +20,8 @@ import type { SessionSearchTipStage } from './use-session-search-tip-setup'
function stageCopy(
tip: FeatureTip,
stage: SessionSearchTipStage
stage: SessionSearchTipStage,
t: Translate = translate
): { title: string; description: string; cta: string } {
switch (stage) {
case 'offer': {
@@ -28,31 +30,35 @@ function stageCopy(
}
case 'indexing':
return {
title: translate('featureTips.sessionSearch.indexingTitle', 'Indexing your agent sessions'),
description: translate(
title: t('featureTips.sessionSearch.indexingTitle', 'Indexing your agent sessions'),
description: t(
'featureTips.sessionSearch.indexingDescription',
'Orca is reading your past agent transcripts so you can search them. This can take a few minutes.'
),
cta: translate('featureTips.sessionSearch.continueInBackground', 'Continue in background')
cta: t('featureTips.sessionSearch.continueInBackground', 'Continue in background')
}
case 'ready':
return {
title: translate('featureTips.sessionSearch.readyTitle', 'Session search is ready'),
description: translate(
title: t('featureTips.sessionSearch.readyTitle', 'Session search is ready'),
description: t(
'featureTips.sessionSearch.readyDescription',
'Every agent session on this computer can now be found by what was said in it.'
),
cta: translate('featureTips.sessionSearch.startSearching', 'Start searching')
cta: t('featureTips.sessionSearch.startSearching', 'Start searching')
}
}
}
function progressMessage(status: AiVaultSearchStatus | null): string {
function progressMessage(
status: AiVaultSearchStatus | null,
t: Translate = translate,
intlLocale?: string
): string {
// Why: right after turning on, the indexer may not have started; the shared copy would call that unavailable.
if (!status?.enabled || status.phase === 'idle' || status.phase === 'closed') {
return translate('featureTips.sessionSearch.progressStarting', 'Finding your sessions…')
return t('featureTips.sessionSearch.progressStarting', 'Finding your sessions…')
}
return sessionSearchStatusMessage(status)
return sessionSearchStatusMessage(status, t, intlLocale)
}
function IndexProgress({
@@ -62,6 +68,8 @@ function IndexProgress({
indexing: boolean
status: AiVaultSearchStatus | null
}): JSX.Element {
const t = useTranslate()
const intlLocale = useIntlLocale()
return (
<span className="block space-y-2" role="status" aria-live="polite">
{indexing ? (
@@ -71,9 +79,9 @@ function IndexProgress({
</span>
) : null}
<span className="block text-xs font-medium tabular-nums text-foreground">
{progressMessage(status)}
{progressMessage(status, t, intlLocale)}
</span>
{sessionSearchStatusDetails(status).map((line) => (
{sessionSearchStatusDetails(status, t).map((line) => (
<span key={line} className="block text-xs text-muted-foreground">
{line}
</span>
@@ -101,8 +109,9 @@ export function SessionSearchTipDialog({
stage: SessionSearchTipStage
status: AiVaultSearchStatus | null
}): JSX.Element {
const t = useTranslate()
const primaryButtonRef = useRef<HTMLButtonElement>(null)
const copy = stageCopy(tip, stage)
const copy = stageCopy(tip, stage, t)
return (
<FeatureTipDialogFrame
@@ -124,7 +133,7 @@ export function SessionSearchTipDialog({
<span className="block">{copy.description}</span>
{stage === 'offer' ? (
<span className="block">
{translate(
{t(
'featureTips.sessionSearch.agentsInstruction',
'Your agents can search it too. Ask one to “find the session where we fixed the login timeout.”'
)}
@@ -133,7 +142,7 @@ export function SessionSearchTipDialog({
<IndexProgress indexing={stage === 'indexing'} status={status} />
)}
<FeatureTipSettingsLine
text={translate(
text={t(
'featureTips.sessionSearch.changeInSettings',
'Change or turn it off anytime in <link>Settings → Agent Session Search</link>.'
)}
@@ -3,16 +3,17 @@ import { Mic, Square } from 'lucide-react'
import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo'
import { usePrefersReducedMotion } from '@/components/feature-wall/feature-wall-modal-helpers'
import { useShortcutKeyDetails } from '@/hooks/useShortcutLabel'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
const TYPE_START_DELAY_MS = 650
const TYPE_INTERVAL_MS = 36
const WAVEFORM_BAR_HEIGHTS = [38, 70, 100, 58, 82]
export function VoiceDictationFeatureTipVisual(): JSX.Element {
const t = useTranslate()
const reducedMotion = usePrefersReducedMotion()
const shortcut = useShortcutKeyDetails('voice.dictation')
const dictatedPrompt = translate(
const dictatedPrompt = t(
'featureTips.voice.demoPrompt',
'Review this diff for edge cases and add tests for anything you find.'
)
@@ -61,7 +62,7 @@ export function VoiceDictationFeatureTipVisual(): JSX.Element {
keyCapClassName="h-7 min-w-7 bg-foreground/[0.08] px-2 font-semibold shadow-xs"
/>
<span className="text-[10px] font-semibold uppercase tracking-[0.05em] text-muted-foreground">
{translate('featureTips.voice.startDictation', 'Start dictation')}
{t('featureTips.voice.startDictation', 'Start dictation')}
</span>
</div>
) : null}
@@ -69,7 +70,7 @@ export function VoiceDictationFeatureTipVisual(): JSX.Element {
<div className="relative mt-4 h-56 w-full max-w-[21rem] overflow-hidden rounded-xl border border-border/80 bg-card text-left shadow-xs">
<div className="flex h-10 items-center gap-2 border-b border-border px-3 text-[11px] text-muted-foreground">
<span className="size-2 rounded-full bg-foreground/35" />
<span>{translate('featureTips.voice.agentPromptTitle', 'Agent prompt')}</span>
<span>{t('featureTips.voice.agentPromptTitle', 'Agent prompt')}</span>
</div>
<div className="absolute inset-x-0 bottom-0 top-10 bg-[var(--editor-surface)] px-4 py-5 font-mono text-xs leading-relaxed">
@@ -99,7 +100,7 @@ export function VoiceDictationFeatureTipVisual(): JSX.Element {
))}
</span>
<span className="min-w-0 flex-1 truncate">
{translate('featureTips.voice.listening', 'Listening...')}
{t('featureTips.voice.listening', 'Listening...')}
</span>
<Square className="size-2.5 shrink-0 fill-current opacity-55" />
</div>
@@ -3,7 +3,8 @@ import type { FeatureTip } from '../../../../shared/feature-tips'
import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo'
import { DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { useShortcutKeyDetails } from '@/hooks/useShortcutLabel'
import { i18n, translate } from '@/i18n/i18n'
import { i18n } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { Trans } from 'react-i18next'
import { FeatureTipActions } from './FeatureTipActions'
import { getLocalizedFeatureTipCopy } from './feature-tip-localized-copy'
@@ -31,6 +32,7 @@ export function VoiceDictationTipDialog({
onSkip: () => void
onVoiceSettingsClick: () => void
}): JSX.Element {
const t = useTranslate()
const shortcut = useShortcutKeyDetails('voice.dictation')
const primaryButtonRef = useRef<HTMLButtonElement>(null)
const copy = getLocalizedFeatureTipCopy(tip)
@@ -56,7 +58,7 @@ export function VoiceDictationTipDialog({
<span className="block">
<Trans
i18n={i18n}
defaults={translate(
defaults={t(
'featureTips.voice.shortcutInstruction',
'Focus a terminal, editor, or agent prompt, then press <shortcut/> to start voice dictation. Press <shortcut/> again to stop.'
)}
@@ -74,14 +76,14 @@ export function VoiceDictationTipDialog({
</span>
) : (
<span className="block">
{translate(
{t(
'featureTips.voice.unassignedInstruction',
'Assign a dictation shortcut before starting voice dictation in a focused pane.'
)}
</span>
)}
<FeatureTipSettingsLine
text={translate(
text={t(
'featureTips.voice.changeInSettings',
'Change the model, dictation mode, or shortcut anytime in <link>Settings → Voice</link>.'
)}
@@ -1,9 +1,13 @@
import type { ChatUiLaunchTipVariant } from '../../../../shared/native-chat/chat-ui-launch-tip-audience'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
export type ShownChatUiLaunchTipVariant = Exclude<ChatUiLaunchTipVariant, 'none'>
export function getChatUiHereVariantCopy(variant: ShownChatUiLaunchTipVariant): {
export function getChatUiHereVariantCopy(
variant: ShownChatUiLaunchTipVariant,
t: Translate = translate
): {
paragraphs: string[]
secondaryLabel: string | null
primaryLabel: string
@@ -11,31 +15,31 @@ export function getChatUiHereVariantCopy(variant: ShownChatUiLaunchTipVariant):
if (variant === 'chat-ui-off') {
return {
paragraphs: [
translate(
t(
'featureTips.chatUiHere.offIntro',
'Agents can now open as a chat, with rich messages, visuals and images.'
),
translate(
t(
'featureTips.chatUiHere.offTerminal',
'Prefer the terminal? It stays fully supported. Your agents keep opening there until you choose Chat.'
)
],
secondaryLabel: translate('featureTips.chatUiHere.stayInTerminal', 'Stay in terminal'),
primaryLabel: translate('featureTips.chatUiHere.cta', 'Use Chat UI')
secondaryLabel: t('featureTips.chatUiHere.stayInTerminal', 'Stay in terminal'),
primaryLabel: t('featureTips.chatUiHere.cta', 'Use Chat UI')
}
}
return {
paragraphs: [
translate(
t(
'featureTips.chatUiHere.onIntro',
'Chat UI is out of Experimental and now part of Orca. Your agents keep opening as chats, with rich messages, visuals and images.'
),
translate(
t(
'featureTips.chatUiHere.onTerminal',
'The terminal stays fully supported too. Switch any agent back anytime.'
)
],
secondaryLabel: translate('featureTips.chatUiHere.switchToTerminal', 'Switch to terminal'),
primaryLabel: translate('featureTips.gotIt', 'Got it')
secondaryLabel: t('featureTips.chatUiHere.switchToTerminal', 'Switch to terminal'),
primaryLabel: t('featureTips.gotIt', 'Got it')
}
}
@@ -1,4 +1,5 @@
import { getIntlLocale, translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
export const CHAT_DEMO_FILE = 'auth.ts'
export const CHAT_DEMO_MODEL = 'Opus'
@@ -15,11 +16,12 @@ export type ChatDemoCopy = {
flow: { name: string; arrow: string | null }[]
}
export function chatDemoRevealTokens(text: string): ChatDemoToken[] {
export function chatDemoRevealTokens(
text: string,
intlLocale: string = getIntlLocale()
): ChatDemoToken[] {
const tokens: ChatDemoToken[] = []
for (const { segment } of new Intl.Segmenter(getIntlLocale(), { granularity: 'word' }).segment(
text
)) {
for (const { segment } of new Intl.Segmenter(intlLocale, { granularity: 'word' }).segment(text)) {
const last = tokens.at(-1)
if (last && !last.link && segment.trim() === '') {
last.text += segment
@@ -30,32 +32,35 @@ export function chatDemoRevealTokens(text: string): ChatDemoToken[] {
return tokens
}
export function getChatDemoCopy(): ChatDemoCopy {
const [before, ...after] = translate(
export function getChatDemoCopy(t: Translate = translate, intlLocale?: string): ChatDemoCopy {
const [before, ...after] = t(
'featureTips.chatUiHere.demoReply',
'Login starts in {{file}}: the browser posts to /login, the server checks the password and creates a session, then redirects you to the app. Here’s the flow:',
{ file: FILE_SLOT }
).split(FILE_SLOT)
return {
tabTitle: translate('featureTips.chatUiHere.demoTabTitle', 'Explain login'),
prompt: translate('featureTips.chatUiHere.demoPrompt', 'Map out how login works'),
tabTitle: t('featureTips.chatUiHere.demoTabTitle', 'Explain login'),
prompt: t('featureTips.chatUiHere.demoPrompt', 'Map out how login works'),
reply: [
...chatDemoRevealTokens(before),
...chatDemoRevealTokens(before, intlLocale),
...(after.length > 0
? [{ text: CHAT_DEMO_FILE, link: true as const }, ...chatDemoRevealTokens(after.join(''))]
? [
{ text: CHAT_DEMO_FILE, link: true as const },
...chatDemoRevealTokens(after.join(''), intlLocale)
]
: [])
],
flow: [
{
name: translate('featureTips.chatUiHere.demoBrowser', 'Browser'),
arrow: translate('featureTips.chatUiHere.demoPassword', 'password')
name: t('featureTips.chatUiHere.demoBrowser', 'Browser'),
arrow: t('featureTips.chatUiHere.demoPassword', 'password')
},
{ name: '/login', arrow: '' },
{
name: translate('featureTips.chatUiHere.demoSession', 'Session'),
arrow: translate('featureTips.chatUiHere.demoCookie', 'cookie')
name: t('featureTips.chatUiHere.demoSession', 'Session'),
arrow: t('featureTips.chatUiHere.demoCookie', 'cookie')
},
{ name: translate('featureTips.chatUiHere.demoApp', 'App'), arrow: null }
{ name: t('featureTips.chatUiHere.demoApp', 'App'), arrow: null }
]
}
}
@@ -17,7 +17,7 @@ import {
DEFAULT_AI_VAULT_HIDE_EMPTY_SESSIONS
} from '@/components/right-sidebar/ai-vault/scope/ai-vault-view-defaults'
import { AgentIcon } from '@/lib/agent-catalog'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import type { SessionSearchDemoRow } from './session-search-feature-tip-demo'
const ROW_STAGGER_MS = 90
@@ -136,6 +136,7 @@ export function DemoSessionRow({
index: number
children?: ReactNode
}): JSX.Element {
const t = useTranslate()
return (
<div
data-focused={focused}
@@ -153,11 +154,9 @@ export function DemoSessionRow({
<AgentIcon agent={row.agent} size={14} />
<span className="truncate">{AI_VAULT_AGENT_LABELS[row.agent]}</span>
<span className="shrink-0 tabular-nums">
{translate(
'auto.components.right.sidebar.AiVaultSessionRow.messageCount',
'{{value0}} msgs',
{ value0: row.messages }
)}
{t('auto.components.right.sidebar.AiVaultSessionRow.messageCount', '{{value0}} msgs', {
value0: row.messages
})}
</span>
<span className="shrink-0 text-muted-foreground/55">·</span>
<span className="shrink-0">{row.age}</span>
@@ -1,4 +1,5 @@
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
export type SessionSearchDemoRow = {
agent: 'claude' | 'codex'
@@ -12,131 +13,128 @@ export type SessionSearchDemoRow = {
export type SessionSearchDemoScene = { query: string; hits: SessionSearchDemoRow[] }
function assistantRole(): string {
return translate('featureTips.sessionSearch.demoRoleAssistant', 'Assistant')
function assistantRole(t: Translate = translate): string {
return t('featureTips.sessionSearch.demoRoleAssistant', 'Assistant')
}
function userRole(): string {
return translate('featureTips.sessionSearch.demoRoleUser', 'User')
function userRole(t: Translate = translate): string {
return t('featureTips.sessionSearch.demoRoleUser', 'User')
}
/** What the panel lists before anything is typed. */
export function getSessionSearchDemoRecentRows(): SessionSearchDemoRow[] {
export function getSessionSearchDemoRecentRows(t: Translate = translate): SessionSearchDemoRow[] {
return [
{
agent: 'claude',
title: translate('featureTips.sessionSearch.recent1Title', 'Tidy the settings sidebar'),
role: assistantRole(),
text: translate(
title: t('featureTips.sessionSearch.recent1Title', 'Tidy the settings sidebar'),
role: assistantRole(t),
text: t(
'featureTips.sessionSearch.recent1Text',
'Moved the search entries under Workflows and updated the tests.'
),
messages: 42,
age: translate('featureTips.sessionSearch.ageMinutes', '12m')
age: t('featureTips.sessionSearch.ageMinutes', '12m')
},
{
agent: 'codex',
title: translate('featureTips.sessionSearch.recent2Title', 'Bump the lockfile'),
role: userRole(),
text: translate(
title: t('featureTips.sessionSearch.recent2Title', 'Bump the lockfile'),
role: userRole(t),
text: t(
'featureTips.sessionSearch.recent2Text',
'Update the dependencies and make sure the build still passes.'
),
messages: 18,
age: translate('featureTips.sessionSearch.ageHours', '2h')
age: t('featureTips.sessionSearch.ageHours', '2h')
},
{
agent: 'claude',
title: translate('featureTips.sessionSearch.recent3Title', 'Release notes draft'),
role: assistantRole(),
text: translate(
title: t('featureTips.sessionSearch.recent3Title', 'Release notes draft'),
role: assistantRole(t),
text: t(
'featureTips.sessionSearch.recent3Text',
'Here is a first pass at the notes, grouped by area.'
),
messages: 9,
age: translate('featureTips.sessionSearch.ageYesterday', '1d')
age: t('featureTips.sessionSearch.ageYesterday', '1d')
}
]
}
export function getSessionSearchDemoScenes(): SessionSearchDemoScene[] {
export function getSessionSearchDemoScenes(t: Translate = translate): SessionSearchDemoScene[] {
return [
{
query: translate('featureTips.sessionSearch.demoQuery', 'login timeout'),
query: t('featureTips.sessionSearch.demoQuery', 'login timeout'),
hits: [
{
agent: 'claude',
title: translate('featureTips.sessionSearch.demoHit1Title', 'Fix flaky auth redirect'),
role: assistantRole(),
text: translate(
title: t('featureTips.sessionSearch.demoHit1Title', 'Fix flaky auth redirect'),
role: assistantRole(t),
text: t(
'featureTips.sessionSearch.demoHit1Snippet',
'Raised the [[login timeout]] to 30s and added a retry on the token refresh.'
),
messages: 64,
age: translate('featureTips.sessionSearch.demoHit1Age', '3d')
age: t('featureTips.sessionSearch.demoHit1Age', '3d')
},
{
agent: 'codex',
title: translate(
'featureTips.sessionSearch.demoHit2Title',
'Session refresh after sleep'
),
role: userRole(),
text: translate(
title: t('featureTips.sessionSearch.demoHit2Title', 'Session refresh after sleep'),
role: userRole(t),
text: t(
'featureTips.sessionSearch.demoHit2Snippet',
'Why does the [[login]] page hang after a [[timeout]] on wake?'
),
messages: 27,
age: translate('featureTips.sessionSearch.demoHit2Age', '1w')
age: t('featureTips.sessionSearch.demoHit2Age', '1w')
},
{
agent: 'claude',
title: translate('featureTips.sessionSearch.demoHit3Title', 'Auth e2e hardening'),
role: assistantRole(),
text: translate(
title: t('featureTips.sessionSearch.demoHit3Title', 'Auth e2e hardening'),
role: assistantRole(t),
text: t(
'featureTips.sessionSearch.demoHit3Snippet',
'Added a test that covers the [[login timeout]] path end to end.'
),
messages: 31,
age: translate('featureTips.sessionSearch.demoHit3Age', '2w')
age: t('featureTips.sessionSearch.demoHit3Age', '2w')
}
]
},
{
query: translate('featureTips.sessionSearch.demoQuery2', 'migration rollback'),
query: t('featureTips.sessionSearch.demoQuery2', 'migration rollback'),
hits: [
{
agent: 'codex',
title: translate('featureTips.sessionSearch.demoHit4Title', 'Orders table migration'),
role: assistantRole(),
text: translate(
title: t('featureTips.sessionSearch.demoHit4Title', 'Orders table migration'),
role: assistantRole(t),
text: t(
'featureTips.sessionSearch.demoHit4Snippet',
'The [[rollback]] drops the new index first, then restores the old column.'
),
messages: 53,
age: translate('featureTips.sessionSearch.demoHit4Age', '5d')
age: t('featureTips.sessionSearch.demoHit4Age', '5d')
},
{
agent: 'claude',
title: translate('featureTips.sessionSearch.demoHit5Title', 'Staging deploy failed'),
role: userRole(),
text: translate(
title: t('featureTips.sessionSearch.demoHit5Title', 'Staging deploy failed'),
role: userRole(t),
text: t(
'featureTips.sessionSearch.demoHit5Snippet',
'Can you write a [[migration]] [[rollback]] plan before we retry?'
),
messages: 22,
age: translate('featureTips.sessionSearch.demoHit5Age', '3w')
age: t('featureTips.sessionSearch.demoHit5Age', '3w')
},
{
agent: 'claude',
title: translate('featureTips.sessionSearch.demoHit6Title', 'Schema review notes'),
role: assistantRole(),
text: translate(
title: t('featureTips.sessionSearch.demoHit6Title', 'Schema review notes'),
role: assistantRole(t),
text: t(
'featureTips.sessionSearch.demoHit6Snippet',
'Every [[migration]] here is reversible except the enum rename.'
),
messages: 14,
age: translate('featureTips.sessionSearch.demoHit6Age', '1mo')
age: t('featureTips.sessionSearch.demoHit6Age', '1mo')
}
]
}
@@ -9,6 +9,7 @@ import {
} from '@/components/settings/session-history/session-history-status-copy'
import { useSessionSearchStatus } from '@/components/settings/session-history/use-session-search-status'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { useAppStore } from '@/store'
export type SessionSearchTipStage = 'offer' | 'indexing' | 'ready'
@@ -26,6 +27,7 @@ export function useSessionSearchTipSetup({
}: {
dialogOpen: boolean
}): SessionSearchTipSetup {
const t = useTranslate()
const showAiVaultSearch = useAppStore((s) => s.showAiVaultSearch)
const searchEnabled = useAppStore((s) => resolveAiVaultSearchSettings(s.settings).enabled)
const [wasOpen, setWasOpen] = useState(dialogOpen)
@@ -79,7 +81,7 @@ export function useSessionSearchTipSetup({
})
} catch {
toast.error(
translate(
t(
'featureTips.sessionSearch.enableFailed',
'Could not turn on session search. Try again.'
)
@@ -5,7 +5,7 @@ import { useRepoMap } from '@/store/repos/use-repository-catalog'
import { useWorktreesForRepo } from '@/store/slices/worktrees/listing/use-worktree-catalog'
import { getRepoExecutionHostId } from '../../../../shared/execution-host'
import { toast } from 'sonner'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
type ComposerBaseRefPickerProps = {
repoId: string
@@ -23,6 +23,7 @@ export function ComposerBaseRefPicker({
resetHint,
readOnly = false
}: ComposerBaseRefPickerProps): React.JSX.Element {
const t = useTranslate()
const repoMap = useRepoMap()
const repo = repoMap.get(repoId)
const repoWorktrees = useWorktreesForRepo(repoId)
@@ -50,7 +51,7 @@ export function ComposerBaseRefPicker({
)
if (!updated) {
toast.error(
translate(
t(
'auto.components.NewWorkspaceComposerCard.defaultBranchUpdateFailed',
'Could not update the project default branch. Try again.'
)
@@ -3,7 +3,7 @@ import { ArrowRight } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
type Props = {
@@ -25,16 +25,17 @@ export function ComposerBranchChoice({
onOpenSelectedBranchWorkspace,
disabled
}: Props) {
const t = useTranslate()
const id = useId()
const branchNameInputRef = useRef<HTMLInputElement>(null)
const showOpenWorkspace = reuseSelectedBranch && Boolean(onOpenSelectedBranchWorkspace)
return (
<fieldset className="min-w-0 space-y-2" disabled={disabled}>
<legend className="text-xs font-medium text-muted-foreground">
{translate('workspace.branchChoice.title', 'Branch for this worktree')}
{t('workspace.branchChoice.title', 'Branch for this worktree')}
</legend>
<RadioGroup
aria-label={translate('workspace.branchChoice.title', 'Branch for this worktree')}
aria-label={t('workspace.branchChoice.title', 'Branch for this worktree')}
value={reuseSelectedBranch ? 'existing' : 'new'}
onValueChange={(value) => onReuseSelectedBranchChange(value === 'existing')}
disabled={disabled}
@@ -59,7 +60,7 @@ export function ComposerBranchChoice({
<span className="min-w-0 space-y-1">
<span className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-1">
<span id={`${id}-existing`} className="shrink-0 text-xs font-medium">
{translate('workspace.branchChoice.existing', 'Existing branch')}
{t('workspace.branchChoice.existing', 'Existing branch')}
</span>
{existingBranchName ? (
<span
@@ -74,7 +75,7 @@ export function ComposerBranchChoice({
id={`${id}-existing-description`}
className="block text-xs text-muted-foreground"
>
{translate('workspace.branchChoice.existingHint', 'New commits go on this branch.')}
{t('workspace.branchChoice.existingHint', 'New commits go on this branch.')}
</span>
</span>
</label>
@@ -86,7 +87,7 @@ export function ComposerBranchChoice({
size="xs"
onClick={onOpenSelectedBranchWorkspace}
>
{translate(
{t(
'auto.components.NewWorkspaceComposerCard.openExistingWorkspace',
'Open existing workspace'
)}
@@ -111,17 +112,17 @@ export function ComposerBranchChoice({
/>
<span className="min-w-0 space-y-1">
<span id={`${id}-new`} className="block text-xs font-medium">
{translate('workspace.branchChoice.new', 'New branch')}
{t('workspace.branchChoice.new', 'New branch')}
</span>
<span id={`${id}-new-description`} className="block text-xs text-muted-foreground">
{translate('workspace.branchChoice.newHint', 'Create a new local branch')}
{t('workspace.branchChoice.newHint', 'Create a new local branch')}
</span>
</span>
</label>
{!reuseSelectedBranch ? (
<div className="space-y-1 px-3 pb-3 pl-9">
<label htmlFor={`${id}-name`} className="text-xs font-medium">
{translate('workspace.branchChoice.newName', 'New branch name')}
{t('workspace.branchChoice.newName', 'New branch name')}
</label>
<Input
id={`${id}-name`}
@@ -131,7 +132,7 @@ export function ComposerBranchChoice({
autoFocus
value={branchNameOverride ?? ''}
onChange={(event) => onBranchNameOverrideChange(event.target.value)}
placeholder={translate('workspace.branchChoice.automaticName', 'Automatic name')}
placeholder={t('workspace.branchChoice.automaticName', 'Automatic name')}
autoCapitalize="none"
spellCheck={false}
/>
@@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button'
import { Command, CommandInput, CommandList } from '@/components/ui/command'
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 {
getIndexedAllWorktrees,
@@ -87,6 +87,7 @@ function ComposerParentWorktreePickerImpl({
disabled = false,
activeFolderWorkspaceId = null
}: ComposerParentWorktreePickerProps): React.JSX.Element {
const t = useTranslate()
// Subscribe so translate() refreshes on language changes.
useTranslation()
const [open, setOpen] = useState(false)
@@ -122,7 +123,7 @@ function ComposerParentWorktreePickerImpl({
return (
<div className="space-y-1.5">
<span id={labelId} className="block text-xs font-medium text-muted-foreground">
{translate('auto.components.ComposerParentWorktreePicker.label', 'Parent worktree')}
{t('auto.components.ComposerParentWorktreePicker.label', 'Parent worktree')}
</span>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
@@ -137,7 +138,7 @@ function ComposerParentWorktreePickerImpl({
>
<span className="truncate">
{parentName ??
translate('auto.components.ComposerParentWorktreePicker.noParent', 'No parent')}
t('auto.components.ComposerParentWorktreePicker.noParent', 'No parent')}
</span>
<ChevronsUpDown className="size-3.5 shrink-0 opacity-50" />
</Button>
@@ -168,7 +169,7 @@ function ComposerParentWorktreePickerImpl({
</PopoverContent>
</Popover>
<p className="text-[11px] text-muted-foreground">
{translate(
{t(
'auto.components.ComposerParentWorktreePicker.description',
'Nests this workspace under another in the sidebar. Does not change the base branch.'
)}
@@ -217,6 +218,7 @@ function ParentWorktreeCandidateList({
activeFolderWorkspaceId,
onSelect
}: ParentWorktreeCandidateListProps): React.JSX.Element {
const t = useTranslate()
const worktreesByRepo = useAppStore((s) => selectWorktreesByRepo(s))
const repoMap = useRepoMap()
const worktreeLineageById = useAppStore((s) => s.worktreeLineageById)
@@ -357,7 +359,7 @@ function ParentWorktreeCandidateList({
onValueChange={handleSearchChange}
onKeyDown={handleKeyDown}
wrapperClassName="shrink-0"
placeholder={translate(
placeholder={t(
'auto.components.ComposerParentWorktreePicker.searchPlaceholder',
'Search workspaces...'
)}
@@ -413,10 +415,7 @@ function ParentWorktreeCandidateList({
</div>
) : (
<span className="truncate">
{translate(
'auto.components.ComposerParentWorktreePicker.noParent',
'No parent'
)}
{t('auto.components.ComposerParentWorktreePicker.noParent', 'No parent')}
</span>
)}
</div>
@@ -425,7 +424,7 @@ function ParentWorktreeCandidateList({
</div>
{filtered.length === 0 && search.trim().length > 0 ? (
<div className="py-6 text-center text-sm text-muted-foreground">
{translate('auto.components.ComposerParentWorktreePicker.noMatches', 'No matches.')}
{t('auto.components.ComposerParentWorktreePicker.noMatches', 'No matches.')}
</div>
) : null}
</CommandList>
@@ -8,7 +8,7 @@ import {
DialogFooter
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import type { DirectWorkItemSetupRequest } from '@/lib/workspace/creation/work-item/direct-work-item-setup'
import { WorkspaceSetupSection } from './WorkspaceSetupSection'
import { selectWorkspaceSetupCopy } from './workspace-setup-section-model'
@@ -19,6 +19,7 @@ export function DirectWorkItemSetupDialog({
}: {
request: DirectWorkItemSetupRequest
}): React.JSX.Element {
const t = useTranslate()
const [inspection, setInspection] = useState(request.inspection)
const [checking, setChecking] = useState(false)
const retry = async (): Promise<void> => {
@@ -49,14 +50,14 @@ export function DirectWorkItemSetupDialog({
<DialogContent surface="security" scrollable className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>
{translate('components.newWorkspace.directSetup.run', 'Run setup')} · {request.repoName}
{t('components.newWorkspace.directSetup.run', 'Run setup')} · {request.repoName}
</DialogTitle>
</DialogHeader>
<WorkspaceSetupSection
setupConfig={inspection.kind === 'needs-modal' ? inspection.setupConfig : null}
copy={selectWorkspaceSetupCopy(
inspection.kind === 'needs-modal' ? inspection.setupConfig : null,
getDirectWorkspaceSetupLabels()
getDirectWorkspaceSetupLabels(t)
)}
requiresExplicitSetupChoice
resolvedSetupDecision={null}
@@ -66,11 +67,11 @@ export function DirectWorkItemSetupDialog({
unavailable={
inspection.kind === 'unavailable'
? {
message: translate(
message: t(
'components.newWorkspace.setupCheckFailed',
'Could not check setup configuration. Try again.'
),
retryLabel: translate('components.newWorkspace.directSetup.retry', 'Retry'),
retryLabel: t('components.newWorkspace.directSetup.retry', 'Retry'),
checking,
retry: () => void retry()
}
@@ -79,7 +80,7 @@ export function DirectWorkItemSetupDialog({
/>
<DialogFooter>
<Button variant="ghost" onClick={request.cancel}>
{translate('components.newWorkspace.directSetup.cancel', 'Cancel')}
{t('components.newWorkspace.directSetup.cancel', 'Cancel')}
</Button>
</DialogFooter>
</DialogContent>
@@ -15,6 +15,7 @@ import {
shouldHandleTextControlPaste
} from '@/lib/text-control-paste'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { ComposerParentWorktreePicker } from './ComposerParentWorktreePicker'
import type { NewWorkspaceComposerCardProps } from './new-workspace-composer-card-props'
@@ -95,6 +96,7 @@ export function NewWorkspaceComposerAdvancedSection({
onSparseSelectPreset,
canUseSparseCheckout
}: NewWorkspaceComposerAdvancedSectionProps): React.JSX.Element {
const t = useTranslate()
const handleNotePaste = React.useCallback((event: React.ClipboardEvent<HTMLTextAreaElement>) => {
const text = event.clipboardData.getData('text/plain')
const byteLengthMeasurement = measureTextControlPasteByteLength(text, {
@@ -150,13 +152,13 @@ export function NewWorkspaceComposerAdvancedSection({
{smartNameSelection ? (
<div className="space-y-1">
<label className="text-xs font-medium text-muted-foreground">
{translate('auto.components.NewWorkspaceComposerCard.2688050e4b', 'Name')}
{t('auto.components.NewWorkspaceComposerCard.2688050e4b', 'Name')}
</label>
<input
type="text"
value={name}
onChange={(event) => onNameValueChange(event.target.value)}
placeholder={translate(
placeholder={t(
'auto.components.NewWorkspaceComposerCard.0ee17638fe',
'Workspace name'
)}
@@ -174,14 +176,14 @@ export function NewWorkspaceComposerAdvancedSection({
htmlFor={branchNameInputId}
className="text-xs font-medium text-muted-foreground"
>
{translate('auto.components.NewWorkspaceComposerCard.branchName', 'Branch name')}
{t('auto.components.NewWorkspaceComposerCard.branchName', 'Branch name')}
</label>
<input
id={branchNameInputId}
type="text"
value={branchNameOverride ?? ''}
onChange={(event) => onBranchNameOverrideChange(event.target.value)}
placeholder={translate(
placeholder={t(
'auto.components.NewWorkspaceComposerCard.branchNamePlaceholder',
'feature/my-branch'
)}
@@ -204,16 +206,13 @@ export function NewWorkspaceComposerAdvancedSection({
<div className="space-y-1">
<label className="text-xs font-medium text-muted-foreground">
{translate('auto.components.NewWorkspaceComposerCard.f8728aa4f9', 'Note')}
{t('auto.components.NewWorkspaceComposerCard.f8728aa4f9', 'Note')}
</label>
<textarea
value={note}
onChange={(event) => onNoteChange(event.target.value)}
onPaste={handleNotePaste}
placeholder={translate(
'auto.components.NewWorkspaceComposerCard.090cfedeb4',
'Write a note'
)}
placeholder={t('auto.components.NewWorkspaceComposerCard.090cfedeb4', 'Write a note')}
rows={1}
className="w-full min-w-0 resize-none overflow-y-auto scrollbar-sleek rounded-md border border-input bg-transparent px-3 py-1.5 text-sm shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [field-sizing:content] max-h-40"
/>
@@ -222,7 +221,7 @@ export function NewWorkspaceComposerAdvancedSection({
{setupControlsEnabled && setupConfig ? (
<WorkspaceSetupSection
setupConfig={setupConfig}
copy={selectWorkspaceSetupCopy(setupConfig, getComposerWorkspaceSetupLabels())}
copy={selectWorkspaceSetupCopy(setupConfig, getComposerWorkspaceSetupLabels(t))}
requiresExplicitSetupChoice={requiresExplicitSetupChoice}
resolvedSetupDecision={resolvedSetupDecision}
setupDecision={setupDecision}
@@ -233,7 +232,7 @@ export function NewWorkspaceComposerAdvancedSection({
? {
value: setupAgentStartupPolicy,
onChange: onSetupAgentStartupPolicyChange,
label: getComposerSetupStartupLabel()
label: getComposerSetupStartupLabel(t)
}
: undefined
}
@@ -243,10 +242,7 @@ export function NewWorkspaceComposerAdvancedSection({
{sparseControlsEnabled ? (
<div className="space-y-1.5">
<label className="text-xs font-medium text-muted-foreground">
{translate(
'auto.components.NewWorkspaceComposerCard.d861de981b',
'Sparse checkout'
)}
{t('auto.components.NewWorkspaceComposerCard.d861de981b', 'Sparse checkout')}
</label>
<SparseCheckoutPresetSelect
key={repoId}
@@ -259,7 +255,7 @@ export function NewWorkspaceComposerAdvancedSection({
/>
{!canUseSparseCheckout ? (
<p className="text-[11px] text-muted-foreground">
{translate(
{t(
'auto.components.NewWorkspaceComposerCard.cbb47ee0dc',
'Only available for local Git projects.'
)}
@@ -7,7 +7,7 @@ import { DropdownMenuItem, DropdownMenuSeparator } from '@/components/ui/dropdow
import { Button } from '@/components/ui/button'
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 type { NewWorkspaceComposerCardProps } from './new-workspace-composer-card-props'
type NewWorkspaceComposerAgentSectionProps = Pick<
@@ -53,12 +53,13 @@ export function NewWorkspaceComposerAgentSection({
defaultTuiAgent,
handleSetDefaultAgent
}: NewWorkspaceComposerAgentSectionProps): React.JSX.Element {
const t = useTranslate()
return (
<>
<div className="min-w-0 space-y-1" data-contextual-tour-target="workspace-creation-agent">
<div className="flex items-center justify-between gap-2">
<label className="text-xs font-medium text-muted-foreground">
{translate('components.newWorkspace.agentOptional', 'Agent [Optional]')}
{t('components.newWorkspace.agentOptional', 'Agent [Optional]')}
</label>
<Tooltip>
<TooltipTrigger asChild>
@@ -69,7 +70,7 @@ export function NewWorkspaceComposerAgentSection({
onClick={(event) => onOpenAgentSettings(event.currentTarget)}
tabIndex={-1}
className="size-5 shrink-0 rounded-sm text-muted-foreground hover:text-foreground"
aria-label={translate(
aria-label={t(
'auto.components.NewWorkspaceComposerCard.ab63f25397',
'Open agent settings'
)}
@@ -78,7 +79,7 @@ export function NewWorkspaceComposerAgentSection({
</Button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={6}>
{translate('auto.components.NewWorkspaceComposerCard.ba64270bdb', 'Configure agents')}
{t('auto.components.NewWorkspaceComposerCard.ba64270bdb', 'Configure agents')}
</TooltipContent>
</Tooltip>
</div>
@@ -95,7 +96,7 @@ export function NewWorkspaceComposerAgentSection({
}
onOpenAgentSettings={onOpenAgentSettings}
onTriggerEnter={createDisabled ? undefined : onCreate}
emptyLabel={translate('auto.components.agent.AgentCombobox.986f946354', 'Blank Terminal')}
emptyLabel={t('auto.components.agent.AgentCombobox.986f946354', 'Blank Terminal')}
defaultAgent={defaultTuiAgent}
onSetDefault={handleSetDefaultAgent}
onPick={(agent, draft, entry) => onQuickAgentChange(agent, draft?.surface, draft, entry)}
@@ -106,7 +107,7 @@ export function NewWorkspaceComposerAgentSection({
>
<DropdownMenuItem onSelect={() => onQuickAgentChange(null)}>
<Terminal className="size-3.5" />
{translate('auto.components.agent.AgentCombobox.986f946354', 'Blank Terminal')}
{t('auto.components.agent.AgentCombobox.986f946354', 'Blank Terminal')}
</DropdownMenuItem>
</AgentDefaultContextMenu>
<DropdownMenuSeparator />
@@ -123,7 +124,7 @@ export function NewWorkspaceComposerAgentSection({
aria-expanded={advancedOpen}
className="-ml-2 text-xs focus-visible:ring-inset"
>
{translate('auto.components.NewWorkspaceComposerCard.f0470c7383', 'Advanced')}
{t('auto.components.NewWorkspaceComposerCard.f0470c7383', 'Advanced')}
<ChevronDown
className={cn('size-4 transition-transform', advancedOpen && 'rotate-180')}
/>
@@ -3,7 +3,7 @@ import { LoaderCircle } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { SwitchIndicator } from '@/components/ui/switch'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { ButtonKeyHint } from '@/components/ButtonKeyHint'
import type { NewWorkspaceComposerCardProps } from './new-workspace-composer-card-props'
@@ -32,6 +32,7 @@ export function NewWorkspaceComposerFooter({
primaryActionLabel,
submitShortcutModifierLabel
}: NewWorkspaceComposerFooterProps): React.JSX.Element {
const t = useTranslate()
return (
<>
{createError ? (
@@ -67,7 +68,7 @@ export function NewWorkspaceComposerFooter({
>
<SwitchIndicator checked={createMultiple} />
<span className="text-muted-foreground transition-colors group-hover:text-foreground">
{translate('auto.components.NewWorkspaceComposerCard.createMultiple', 'Create more')}
{t('auto.components.NewWorkspaceComposerCard.createMultiple', 'Create more')}
</span>
</button>
) : null}
@@ -2,7 +2,7 @@ import React from 'react'
import { AlertTriangle } from 'lucide-react'
import SmartWorkspaceNameField from '@/components/new-workspace/SmartWorkspaceNameField'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { shouldShowComposerBaseRefPicker } from './base-ref-picker-visibility'
import { ComposerBaseRefPicker } from './ComposerBaseRefPicker'
import { ComposerBranchChoice } from './ComposerBranchChoice'
@@ -91,6 +91,7 @@ export function NewWorkspaceComposerNameSection({
onBaseBranchChange,
startFromResetHint
}: NewWorkspaceComposerNameSectionProps): React.JSX.Element {
const t = useTranslate()
const showBaseRefPicker =
Boolean(onBaseBranchChange) &&
shouldShowComposerBaseRefPicker({
@@ -104,13 +105,10 @@ export function NewWorkspaceComposerNameSection({
<div className="flex items-center justify-between gap-2">
<label className="min-w-0 truncate text-xs font-medium text-muted-foreground">
{selectedRepoIsGit
? translate('auto.components.NewWorkspaceComposerCard.ac3748dcda', 'Create From')
: translate(
'auto.components.NewWorkspaceComposerCard.0ee17638fe',
'Workspace name'
)}{' '}
? t('auto.components.NewWorkspaceComposerCard.ac3748dcda', 'Create From')
: t('auto.components.NewWorkspaceComposerCard.0ee17638fe', 'Workspace name')}{' '}
<span className="text-muted-foreground/70">
{translate('auto.components.NewWorkspaceComposerCard.0c5d6a479c', '[Optional]')}
{t('auto.components.NewWorkspaceComposerCard.0c5d6a479c', '[Optional]')}
</span>
</label>
{showBaseRefPicker && onBaseBranchChange ? (
@@ -142,7 +140,7 @@ export function NewWorkspaceComposerNameSection({
githubSourceContext={smartNameGitHubSourceContext}
jiraSourceContext={smartNameJiraSourceContext}
disabled={selectedRepoRequiresConnection}
disabledPlaceholder={translate(
disabledPlaceholder={t(
'auto.components.NewWorkspaceComposerCard.connectProjectFirst',
'Connect this project first'
)}
@@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import ProjectCombobox from '@/components/new-workspace/ProjectCombobox'
import RunTargetCombobox from '@/components/new-workspace/RunTargetCombobox'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
import { getSelectedNewWorkspaceProjectOptionId } from '@/lib/new-workspace-project-options'
import type {
@@ -85,14 +85,14 @@ export function NewWorkspaceComposerProjectSection({
connectButtonLabel,
selectedProjectName
}: NewWorkspaceComposerProjectSectionProps): React.JSX.Element {
const t = useTranslate()
return (
<fieldset disabled={disabled} className="space-y-1">
<div className={cn('grid gap-3', shouldShowRunTargetPicker && 'grid-cols-2')}>
<div className="min-w-0 space-y-1">
<div className="flex h-5 items-center justify-between gap-2">
<label className="text-xs font-medium text-muted-foreground">
{projectLabel ??
translate('auto.components.NewWorkspaceComposerCard.969a8bff66', 'Project')}
{projectLabel ?? t('auto.components.NewWorkspaceComposerCard.969a8bff66', 'Project')}
</label>
{showAddProjectButton ? (
<Tooltip>
@@ -103,7 +103,7 @@ export function NewWorkspaceComposerProjectSection({
size="icon-xs"
onClick={onAddProject}
className="size-5 shrink-0 rounded-sm text-muted-foreground hover:text-foreground"
aria-label={translate(
aria-label={t(
'auto.components.NewWorkspaceComposerCard.d6b0a96f32',
'Add project'
)}
@@ -112,7 +112,7 @@ export function NewWorkspaceComposerProjectSection({
</Button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={6}>
{translate('auto.components.NewWorkspaceComposerCard.d6b0a96f32', 'Add project')}
{t('auto.components.NewWorkspaceComposerCard.d6b0a96f32', 'Add project')}
</TooltipContent>
</Tooltip>
) : null}
@@ -131,7 +131,7 @@ export function NewWorkspaceComposerProjectSection({
onAddProject={onAddProject}
placeholder={
projectPlaceholder ??
translate('auto.components.NewWorkspaceComposerCard.dccd26d4e4', 'Choose project')
t('auto.components.NewWorkspaceComposerCard.dccd26d4e4', 'Choose project')
}
triggerClassName="h-9 w-full border-input text-sm focus:border-ring focus:ring-[3px] focus:ring-ring/50"
invalid={Boolean(projectError)}
@@ -144,7 +144,7 @@ export function NewWorkspaceComposerProjectSection({
) : projectOptions.length === 0 ? (
<p id={projectDescriptionId} className="text-[11px] text-muted-foreground">
{emptyProjectMessage ??
translate(
t(
'auto.components.NewWorkspaceComposerCard.addProjectBeforeWorkspace',
'Add a project before creating a workspace.'
)}
@@ -155,7 +155,7 @@ export function NewWorkspaceComposerProjectSection({
{shouldShowRunTargetPicker ? (
<div className="min-w-0 space-y-1">
<label className="flex h-5 min-w-0 items-center truncate text-xs font-medium text-muted-foreground">
{translate('auto.components.NewWorkspaceComposerCard.runOn', 'Run on')}
{t('auto.components.NewWorkspaceComposerCard.runOn', 'Run on')}
</label>
<RunTargetCombobox
hostOptions={projectHostSetupOptions ?? []}
@@ -189,11 +189,9 @@ export function NewWorkspaceComposerProjectSection({
>
<div className="min-w-0">
<div className="truncate text-xs font-medium text-foreground">
{translate(
'auto.components.NewWorkspaceComposerCard.connectProject',
'Connect {{name}}',
{ name: selectedProjectName }
)}
{t('auto.components.NewWorkspaceComposerCard.connectProject', 'Connect {{name}}', {
name: selectedProjectName
})}
</div>
<div className="mt-0.5 text-[11px] text-muted-foreground">{sshStatusLabel}</div>
</div>
@@ -211,7 +209,7 @@ export function NewWorkspaceComposerProjectSection({
<PlugZap className="size-3.5" />
)}
{selectedRepoConnectInProgress
? translate('auto.components.NewWorkspaceComposerCard.f660aa1454', 'Connecting')
? t('auto.components.NewWorkspaceComposerCard.f660aa1454', 'Connecting')
: connectButtonLabel}
</Button>
</div>
@@ -5,7 +5,7 @@ import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover'
import { cn } from '@/lib/utils'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import type { NewWorkspaceProjectOption } from '@/lib/new-workspace-project-options'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import {
getAmbiguousProjectOptionIds,
rankProjectOptions,
@@ -43,6 +43,7 @@ export default function ProjectCombobox({
invalid = false,
describedBy
}: ProjectComboboxProps): React.JSX.Element {
const t = useTranslate()
const [tooltipOpen, setTooltipOpen] = useState(false)
const recentIds = useRecentProjectIds()
// Derive keys from the displayed sections: folder grouping changes raw rank order, so keyboard selection must follow what users see.
@@ -155,10 +156,7 @@ export default function ProjectCombobox({
type="text"
role="combobox"
data-project-combobox-root="true"
aria-label={translate(
'auto.components.new.workspace.ProjectCombobox.label',
'Project'
)}
aria-label={t('auto.components.new.workspace.ProjectCombobox.label', 'Project')}
aria-expanded={open}
aria-controls={listId}
aria-autocomplete="list"
@@ -190,7 +188,7 @@ export default function ProjectCombobox({
<button
type="button"
tabIndex={-1}
aria-label={translate(
aria-label={t(
'auto.components.new.workspace.ProjectCombobox.browse',
'Browse projects'
)}
@@ -245,10 +243,7 @@ export default function ProjectCombobox({
<div
id={listId}
role="listbox"
aria-label={translate(
'auto.components.new.workspace.ProjectCombobox.listLabel',
'Projects'
)}
aria-label={t('auto.components.new.workspace.ProjectCombobox.listLabel', 'Projects')}
className="flex min-h-0 flex-col"
>
{/* Presentation preserves listbox ownership through this scrolling wrapper. */}
@@ -261,11 +256,11 @@ export default function ProjectCombobox({
// Match row height so empty results stay visually neutral.
<p className="flex h-8 items-center justify-center px-2 text-sm text-muted-foreground">
{options.length === 0
? translate(
? t(
'auto.components.new.workspace.ProjectCombobox.noProjects',
'No projects yet.'
)
: translate(
: t(
'auto.components.new.workspace.ProjectCombobox.empty',
'No projects match your search.'
)}
@@ -317,10 +312,7 @@ export default function ProjectCombobox({
<FolderPlus className="size-3.5 text-muted-foreground" />
</span>
<span className="truncate">
{translate(
'auto.components.new.workspace.ProjectCombobox.addProject',
'Add project'
)}
{t('auto.components.new.workspace.ProjectCombobox.addProject', 'Add project')}
</span>
</div>
) : null}
@@ -1,7 +1,7 @@
import React, { useCallback, useMemo, useState } from 'react'
import { Popover, PopoverContent } from '@/components/ui/popover'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import {
getProjectHostSetupSelectionKey,
type NeedsSetupProjectHostOption,
@@ -53,6 +53,7 @@ export default function RunTargetCombobox({
onConnectHost,
onSetLocation
}: RunTargetComboboxProps): React.JSX.Element {
const t = useTranslate()
const [submenu, setSubmenu] = useState<'recipes' | 'add-host' | null>(null)
// Track in-flight connects per host so one stalling connect never blocks the others.
const [connectingHostIds, setConnectingHostIds] = useState<ReadonlySet<string>>(() => new Set())
@@ -202,9 +203,11 @@ export default function RunTargetCombobox({
)
const fieldLabel = selectedRecipe
? `${getEphemeralVmLabel()} / ${selectedRecipe.name}`
? `${getEphemeralVmLabel(t)} / ${selectedRecipe.name}`
: (selectedHost?.label ?? '')
const fieldDetail = selectedRecipe ? getRecipeDetail(selectedRecipe) : (selectedHost?.path ?? '')
const fieldDetail = selectedRecipe
? getRecipeDetail(selectedRecipe, t)
: (selectedHost?.path ?? '')
return (
<Popover open={open} onOpenChange={handleOpenChange}>
@@ -252,10 +255,7 @@ export default function RunTargetCombobox({
<div
id={listId}
role="listbox"
aria-label={translate(
'auto.components.new.workspace.RunTargetCombobox.listLabel',
'Run targets'
)}
aria-label={t('auto.components.new.workspace.RunTargetCombobox.listLabel', 'Run targets')}
className="flex min-h-0 flex-col"
>
<div
@@ -265,7 +265,7 @@ export default function RunTargetCombobox({
>
{rows.filter((row) => row.kind !== 'add-host').length === 0 ? (
<p className="flex h-8 items-center justify-center px-2 text-sm text-muted-foreground">
{translate(
{t(
'auto.components.NewWorkspaceComposerCard.noRunTargets',
'No run targets are ready for this project.'
)}
@@ -4,7 +4,7 @@ import { ChevronDown, Cloud } from 'lucide-react'
import { PopoverAnchor } from '@/components/ui/popover'
import { cn } from '@/lib/utils'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import type { ExecutionHostId } from '../../../../shared/execution-host'
import { HostRowIcon } from './RunTargetComboboxRow'
import { COMBOBOX_FIELD_SHELL } from './type-ahead-combobox-styles'
@@ -41,6 +41,7 @@ export default function RunTargetField({
inputRef: React.RefObject<HTMLInputElement | null>
onKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void
}): React.JSX.Element {
const t = useTranslate()
const [tooltipOpen, setTooltipOpen] = useState(false)
return (
<Tooltip open={tooltipOpen && committed && !open} onOpenChange={setTooltipOpen}>
@@ -69,10 +70,7 @@ export default function RunTargetField({
type="text"
role="combobox"
data-run-target-combobox-root="true"
aria-label={translate(
'auto.components.new.workspace.RunTargetCombobox.label',
'Run on'
)}
aria-label={t('auto.components.new.workspace.RunTargetCombobox.label', 'Run on')}
aria-expanded={open}
aria-controls={listId}
aria-autocomplete="list"
@@ -81,10 +79,7 @@ export default function RunTargetField({
placeholder={
committed
? ''
: translate(
'auto.components.NewWorkspaceComposerCard.chooseRunTarget',
'Choose target'
)
: t('auto.components.NewWorkspaceComposerCard.chooseRunTarget', 'Choose target')
}
onChange={(event) => onQueryChange(event.target.value)}
onFocus={onOpenRequest}
@@ -106,7 +101,7 @@ export default function RunTargetField({
<button
type="button"
tabIndex={-1}
aria-label={translate(
aria-label={t(
'auto.components.new.workspace.RunTargetCombobox.browse',
'Browse run targets'
)}
@@ -3,7 +3,7 @@ import { AlertTriangle, LoaderCircle } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import type { ExecutionHostId } from '../../../../shared/execution-host'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { HostRowIcon } from '../host-row-icon'
/** Glyph for a host that can't run yet: connecting, broken, or merely dormant. */
@@ -32,6 +32,7 @@ export function ConnectHostButton({
connecting: boolean
onConnect: () => void
}): React.JSX.Element {
const t = useTranslate()
return (
<Button
type="button"
@@ -53,10 +54,10 @@ export function ConnectHostButton({
{connecting ? (
<>
<LoaderCircle className="size-3 animate-spin" />
{translate('auto.components.NewWorkspaceComposerCard.connectingHost', 'Connecting…')}
{t('auto.components.NewWorkspaceComposerCard.connectingHost', 'Connecting…')}
</>
) : (
translate('auto.components.NewWorkspaceComposerCard.connectHost', 'Connect')
t('auto.components.NewWorkspaceComposerCard.connectHost', 'Connect')
)}
</Button>
)
@@ -69,10 +70,8 @@ export function SetLocationButton({
hostLabel: string
onSetLocation: () => void
}): React.JSX.Element {
const label = translate(
'auto.components.NewWorkspaceComposerCard.setLocation',
'Set project location'
)
const t = useTranslate()
const label = t('auto.components.NewWorkspaceComposerCard.setLocation', 'Set project location')
return (
<Tooltip>
<TooltipTrigger asChild>
@@ -81,7 +80,7 @@ export function SetLocationButton({
variant="outline"
size="xs"
className="ml-auto shrink-0 self-center"
aria-label={translate(
aria-label={t(
'auto.components.NewWorkspaceComposerCard.setLocationOnHost',
'Set project location on {{host}}',
{ host: hostLabel }
@@ -100,7 +99,7 @@ export function SetLocationButton({
</Button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={6}>
{translate(
{t(
'auto.components.NewWorkspaceComposerCard.setLocationTooltip',
'Choose a folder or clone this project onto {{host}}.',
{ host: hostLabel }
@@ -2,7 +2,7 @@ import React from 'react'
import { ChevronDown, Cloud, Plus, Server } from 'lucide-react'
import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { RunTargetRow } from './RunTargetComboboxRow'
import {
getEphemeralVmLabel,
@@ -34,6 +34,7 @@ export function RecipesSubmenuRow({
onArm: () => void
onSelectRecipe: (recipeId: string) => void
}): React.JSX.Element {
const t = useTranslate()
const [hoveredKey, setHoveredKey] = React.useState<string | null>(null)
return (
<Popover open={open} onOpenChange={onOpenChange}>
@@ -41,8 +42,8 @@ export function RecipesSubmenuRow({
<div>
<RunTargetRow
icon={<Cloud className="size-3.5 shrink-0 text-muted-foreground" />}
label={getEphemeralVmLabel()}
detail={translate(
label={getEphemeralVmLabel(t)}
detail={t(
'auto.components.NewWorkspaceComposerCard.perWorkspaceEnvHint',
'Provision an on-demand environment from a recipe'
)}
@@ -65,7 +66,7 @@ export function RecipesSubmenuRow({
{/* Nested rows own hover so pointer movement updates their highlight too. */}
<div
role="listbox"
aria-label={getEphemeralVmLabel()}
aria-label={getEphemeralVmLabel(t)}
onMouseLeave={() => setHoveredKey(null)}
>
{recipes.map((recipe) => (
@@ -73,7 +74,7 @@ export function RecipesSubmenuRow({
key={recipe.id}
icon={<Cloud className="size-3.5 shrink-0 text-muted-foreground" />}
label={recipe.name}
detail={getRecipeDetail(recipe)}
detail={getRecipeDetail(recipe, t)}
armed={hoveredKey === recipe.id}
current={recipe.id === selectedRecipeId}
optionId={undefined}
@@ -105,8 +106,9 @@ export function AddHostSubmenuRow({
onAddSshHost?: () => void
onAddRemoteServer?: () => void
}): React.JSX.Element {
const t = useTranslate()
const [hoveredKey, setHoveredKey] = React.useState<string | null>(null)
const addHostLabel = translate('auto.components.NewWorkspaceComposerCard.addHost', 'Add host')
const addHostLabel = t('auto.components.NewWorkspaceComposerCard.addHost', 'Add host')
return (
<Popover open={open} onOpenChange={onOpenChange}>
<PopoverAnchor asChild>
@@ -146,11 +148,8 @@ export function AddHostSubmenuRow({
{onAddSshHost ? (
<RunTargetRow
icon={<Server className="size-3.5 shrink-0 text-muted-foreground" />}
label={translate(
'auto.components.NewWorkspaceComposerCard.addSshHost',
'Add SSH host'
)}
detail={translate(
label={t('auto.components.NewWorkspaceComposerCard.addSshHost', 'Add SSH host')}
detail={t(
'auto.components.NewWorkspaceComposerCard.addSshHostHint',
'Use an existing machine over SSH'
)}
@@ -165,11 +164,11 @@ export function AddHostSubmenuRow({
{onAddRemoteServer ? (
<RunTargetRow
icon={<Cloud className="size-3.5 shrink-0 text-muted-foreground" />}
label={translate(
label={t(
'auto.components.NewWorkspaceComposerCard.addRemoteOrcaServer',
'Add Remote Orca Server'
)}
detail={translate(
detail={t(
'auto.components.NewWorkspaceComposerCard.addRemoteOrcaServerHint',
'Pair another Orca runtime'
)}
@@ -7,7 +7,7 @@ import {
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { useAppStore } from '@/store'
import { CreateProjectParentBrowser } from '@/components/sidebar/project-admission/create/CreateProjectLocationField'
import {
@@ -99,6 +99,7 @@ function SetProjectLocationDialogBody({
exitHostBrowser: RefObject<(() => boolean) | null>
onReady: (setupId: string) => void
}): React.JSX.Element {
const t = useTranslate()
const setupProjectExistingFolder = useAppStore((state) => state.setupProjectExistingFolder)
const setupProjectClone = useAppStore((state) => state.setupProjectClone)
const [view, setView] = useState<DialogView>('choose')
@@ -206,13 +207,13 @@ function SetProjectLocationDialogBody({
<>
<DialogHeader>
<DialogTitle>
{translate(
{t(
'auto.components.new.workspace.SetProjectLocationDialog.title',
'Set project location'
)}
</DialogTitle>
<DialogDescription>
{translate(
{t(
'auto.components.new.workspace.SetProjectLocationDialog.description',
'Choose where {{project}} lives on {{host}}.',
{ project: projectName, host: option.label }
@@ -223,11 +224,11 @@ function SetProjectLocationDialogBody({
<div className="space-y-2">
<LocationActionButton
icon={FolderOpen}
title={translate(
title={t(
'auto.components.new.workspace.SetProjectLocationDialog.browseFolder',
'Browse folder'
)}
description={translate(
description={t(
'auto.components.new.workspace.SetProjectLocationDialog.browseFolderHelp',
'Use an existing checkout or folder on this host.'
)}
@@ -242,11 +243,11 @@ function SetProjectLocationDialogBody({
{canClone ? (
<LocationActionButton
icon={Download}
title={translate(
title={t(
'auto.components.new.workspace.SetProjectLocationDialog.cloneFromUrl',
'Clone from URL'
)}
description={translate(
description={t(
'auto.components.new.workspace.SetProjectLocationDialog.cloneFromUrlHelp',
'Clone this repository onto {{host}}.',
{ host: option.label }
@@ -8,7 +8,7 @@ import {
SelectTrigger,
SelectValue
} from '@/components/ui/select'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
import type { RepoKind } from '../../../../shared/repo-types'
@@ -31,20 +31,18 @@ export function ExistingFolderForm({
onBrowse: () => void
onSubmit: () => void
}): React.JSX.Element {
const t = useTranslate()
return (
<div className="space-y-3">
<StepBackButton
onBack={onBack}
label={translate(
'auto.components.settings.RepositoryPane.existingFolder',
'Existing folder'
)}
label={t('auto.components.settings.RepositoryPane.existingFolder', 'Existing folder')}
/>
<div className="flex gap-2">
<Input
value={setupPath}
onChange={(event) => onPathChange(event.target.value)}
placeholder={translate(
placeholder={t(
'auto.components.settings.RepositoryPane.setupExistingFolderPathPlaceholder',
'/path/to/project/on/host'
)}
@@ -57,7 +55,7 @@ export function ExistingFolderForm({
size="icon"
className="size-9 shrink-0"
onClick={onBrowse}
aria-label={translate(
aria-label={t(
'auto.components.sidebar.CreateProjectLocationField.f520f83a97',
'Browse host filesystem'
)}
@@ -71,10 +69,10 @@ export function ExistingFolderForm({
</SelectTrigger>
<SelectContent>
<SelectItem value="git">
{translate('auto.components.settings.RepositoryPane.setupKindGit', 'Git repo')}
{t('auto.components.settings.RepositoryPane.setupKindGit', 'Git repo')}
</SelectItem>
<SelectItem value="folder">
{translate('auto.components.settings.RepositoryPane.setupKindFolder', 'Folder')}
{t('auto.components.settings.RepositoryPane.setupKindFolder', 'Folder')}
</SelectItem>
</SelectContent>
</Select>
@@ -87,8 +85,8 @@ export function ExistingFolderForm({
>
{isSubmitting ? <LoaderCircle className="size-3.5 animate-spin" /> : null}
{isSubmitting
? translate('auto.components.settings.RepositoryPane.settingUpHost', 'Importing...')
: translate(
? t('auto.components.settings.RepositoryPane.settingUpHost', 'Importing...')
: t(
'auto.components.new.workspace.SetProjectLocationDialog.saveLocation',
'Set location'
)}
@@ -117,16 +115,17 @@ export function CloneForm({
onBrowse: () => void
onSubmit: () => void
}): React.JSX.Element {
const t = useTranslate()
return (
<div className="space-y-3">
<StepBackButton
onBack={onBack}
label={translate('auto.components.settings.RepositoryPane.cloneFromUrl', 'Clone from URL')}
label={t('auto.components.settings.RepositoryPane.cloneFromUrl', 'Clone from URL')}
/>
<Input
value={cloneUrl}
onChange={(event) => onCloneUrlChange(event.target.value)}
placeholder={translate(
placeholder={t(
'auto.components.settings.RepositoryPane.cloneUrlPlaceholder',
'Repository URL'
)}
@@ -137,7 +136,7 @@ export function CloneForm({
<Input
value={cloneDestination}
onChange={(event) => onCloneDestinationChange(event.target.value)}
placeholder={translate(
placeholder={t(
'auto.components.settings.RepositoryPane.cloneDestinationPlaceholder',
'/destination/on/host'
)}
@@ -150,7 +149,7 @@ export function CloneForm({
size="icon"
className="size-9 shrink-0"
onClick={onBrowse}
aria-label={translate(
aria-label={t(
'auto.components.sidebar.CreateProjectLocationField.f520f83a97',
'Browse host filesystem'
)}
@@ -167,8 +166,8 @@ export function CloneForm({
>
{isSubmitting ? <LoaderCircle className="size-3.5 animate-spin" /> : null}
{isSubmitting
? translate('auto.components.settings.RepositoryPane.cloningHost', 'Cloning...')
: translate('auto.components.settings.RepositoryPane.cloneHost', 'Clone')}
? t('auto.components.settings.RepositoryPane.cloningHost', 'Cloning...')
: t('auto.components.settings.RepositoryPane.cloneHost', 'Clone')}
</Button>
</div>
</div>
@@ -2,10 +2,12 @@ import React from 'react'
import { renderSmartWorkspaceNameField } from './smart-workspace-name-field-surface'
import type { SmartWorkspaceNameFieldProps } from './smart-workspace-name-field-model'
import { useSmartWorkspaceNameFieldController } from './use-smart-workspace-name-field-controller'
import { useTranslate } from '../../i18n/use-translate'
export default function SmartWorkspaceNameField(
props: SmartWorkspaceNameFieldProps
): React.JSX.Element {
const t = useTranslate()
const controller = useSmartWorkspaceNameFieldController(props)
return renderSmartWorkspaceNameField(controller)
return renderSmartWorkspaceNameField(controller, t)
}
@@ -2,7 +2,7 @@ import React from 'react'
import { isIntegrationCredentialDecryptionError } from '../../../../shared/integration-credential-errors'
import { Button } from '@/components/ui/button'
import { CommandGroup, CommandItem, CommandList } from '@/components/ui/command'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
import { getSmartWorkspaceEmptyHint } from '../../../../shared/new-workspace/smart-workspace-source-results'
import { RowIcon, RowLabel } from './smart-workspace-source-row-content'
@@ -43,6 +43,7 @@ export function SmartWorkspaceSourceResultList({
isRowDisabled?: (row: RowEntry) => boolean
focusBehavior?: 'page'
}): React.JSX.Element {
const t = useTranslate()
const {
mode,
mrStateFilters,
@@ -114,13 +115,13 @@ export function SmartWorkspaceSourceResultList({
{credentialError ? (
<>
<p className="font-medium text-foreground">
{translate(
{t(
'auto.components.new.workspace.SmartWorkspaceNameField.linearLoginUnreadableTitle',
'Reconnect Linear'
)}
</p>
<p className="mt-0.5 text-muted-foreground">
{translate(
{t(
'auto.components.new.workspace.SmartWorkspaceNameField.linearLoginUnreadableBody',
'Unable to read saved Linear login. Reconnect it in Settings.'
)}
@@ -157,7 +158,7 @@ export function SmartWorkspaceSourceResultList({
{jiraSource.intent
? null
: mode === 'linear' && linearStatusChecked && !linearStatus.connected
? translate(
? t(
'auto.components.new.workspace.SmartWorkspaceNameField.3e8bb1176a',
'Connect Linear in Settings to search issues.'
)
@@ -188,7 +189,7 @@ export function SmartWorkspaceSourceResultList({
/>
{isLinked?.(row) ? (
<span className="ml-auto shrink-0 text-xs text-muted-foreground">
{translate('workspace.links.linkedResult', 'Linked')}
{t('workspace.links.linkedResult', 'Linked')}
</span>
) : null}
</CommandItem>
@@ -1,24 +1,25 @@
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import type { WorkspaceSetupLabels } from './workspace-setup-section-model'
export function getDirectWorkspaceSetupLabels(): WorkspaceSetupLabels {
const run = translate('components.newWorkspace.directSetup.run', 'Run setup')
const skip = translate('components.newWorkspace.directSetup.skip', 'Skip setup')
export function getDirectWorkspaceSetupLabels(t: Translate = translate): WorkspaceSetupLabels {
const run = t('components.newWorkspace.directSetup.run', 'Run setup')
const skip = t('components.newWorkspace.directSetup.skip', 'Skip setup')
return {
kinds: {
setup: {
configLabel: translate('components.newWorkspace.directSetup.setupScript', 'Setup script'),
askLabel: translate('components.newWorkspace.directSetup.askRunSetup', 'Run setup now?'),
configLabel: t('components.newWorkspace.directSetup.setupScript', 'Setup script'),
askLabel: t('components.newWorkspace.directSetup.askRunSetup', 'Run setup now?'),
runLabel: run,
runButtonLabel: run,
skipButtonLabel: skip
},
'default-tabs': {
configLabel: translate(
configLabel: t(
'components.newWorkspace.directSetup.defaultTabCommands',
'Default tab commands'
),
askLabel: translate(
askLabel: t(
'components.newWorkspace.directSetup.askRunDefaultTabs',
'Run default tab commands now?'
),
@@ -27,11 +28,11 @@ export function getDirectWorkspaceSetupLabels(): WorkspaceSetupLabels {
skipButtonLabel: skip
},
'setup-and-default-tabs': {
configLabel: translate(
configLabel: t(
'components.newWorkspace.directSetup.setupAndDefaultTabs',
'Setup and default tab commands'
),
askLabel: translate(
askLabel: t(
'components.newWorkspace.directSetup.askRunSetupAndDefaultTabs',
'Run setup and default tab commands now?'
),
@@ -41,15 +42,15 @@ export function getDirectWorkspaceSetupLabels(): WorkspaceSetupLabels {
}
},
sources: {
yaml: translate('components.newWorkspace.directSetup.yamlSource', 'orca.yaml'),
both: translate('components.newWorkspace.directSetup.bothSources', 'orca.yaml + local'),
local: translate('components.newWorkspace.directSetup.localSource', 'local settings')
yaml: t('components.newWorkspace.directSetup.yamlSource', 'orca.yaml'),
both: t('components.newWorkspace.directSetup.bothSources', 'orca.yaml + local'),
local: t('components.newWorkspace.directSetup.localSource', 'local settings')
},
help: translate(
help: t(
'components.newWorkspace.directSetup.description',
'Choose whether to run setup before creating this workspace.'
),
checking: translate(
checking: t(
'components.newWorkspace.directSetup.description',
'Choose whether to run setup before creating this workspace.'
)
@@ -1,6 +1,7 @@
import type { NewWorkspaceProjectOption } from '@/lib/new-workspace-project-options'
import { isNewWorkspaceProjectOptionQueryTooLarge } from '@/lib/new-workspace-project-options'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
export type ScoredProjectOption = {
option: NewWorkspaceProjectOption
@@ -94,7 +95,8 @@ const RECENT_LIMIT = 4
export function sectionProjectOptions(
matches: readonly ScoredProjectOption[],
query: string,
recentIds: readonly string[]
recentIds: readonly string[],
t: Translate = translate
): ProjectOptionSection[] {
if (query.trim() !== '' || matches.length < SECTION_THRESHOLD) {
return [{ key: 'results', heading: null, items: [...matches] }]
@@ -108,20 +110,17 @@ export function sectionProjectOptions(
const sections: ProjectOptionSection[] = [
{
key: 'recent',
heading: translate('auto.components.new.workspace.projectCombobox.recentHeading', 'Recent'),
heading: t('auto.components.new.workspace.projectCombobox.recentHeading', 'Recent'),
items: recent
},
{
key: 'projects',
heading: translate(
'auto.components.new.workspace.projectCombobox.projectsHeading',
'Projects'
),
heading: t('auto.components.new.workspace.projectCombobox.projectsHeading', 'Projects'),
items: matches.filter((m) => m.option.kind === 'project' && !recentSet.has(m.option.id))
},
{
key: 'folders',
heading: translate('auto.components.new.workspace.projectCombobox.foldersHeading', 'Folders'),
heading: t('auto.components.new.workspace.projectCombobox.foldersHeading', 'Folders'),
// Exclude recent rows from folders too; duplicate options would desynchronize arming/rendering.
items: matches.filter((m) => m.option.kind === 'project-group' && !recentSet.has(m.option.id))
}
@@ -6,6 +6,7 @@ import {
type ReadyProjectHostSetupOption
} from '@/lib/project-host-setup-options'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { boundHostList } from '@/components/host-picker/bounded-host-list'
export type EphemeralVmRecipeOption = NonNullable<OrcaHooks['environmentRecipes']>[number]
@@ -20,11 +21,8 @@ export type RunTargetRowModel =
| { key: typeof RUN_TARGET_RECIPES_KEY; kind: 'recipes' }
| { key: typeof RUN_TARGET_ADD_HOST_KEY; kind: 'add-host' }
export function getEphemeralVmLabel(): string {
return translate(
'auto.components.NewWorkspaceComposerCard.ephemeralVm',
'Per-Workspace Environment'
)
export function getEphemeralVmLabel(t: Translate = translate): string {
return t('auto.components.NewWorkspaceComposerCard.ephemeralVm', 'Per-Workspace Environment')
}
export function getRecipeCommandDisplay(command: string): string {
@@ -33,21 +31,21 @@ export function getRecipeCommandDisplay(command: string): string {
return quoted?.[1] ?? trimmed.split(/\s+/)[0] ?? trimmed
}
export function getRecipeDestroyLabel(recipe: EphemeralVmRecipeOption): string {
export function getRecipeDestroyLabel(
recipe: EphemeralVmRecipeOption,
t: Translate = translate
): string {
if (recipe.destroyDisabled) {
return translate('auto.components.NewWorkspaceComposerCard.destroyDisabled', 'destroy disabled')
return t('auto.components.NewWorkspaceComposerCard.destroyDisabled', 'destroy disabled')
}
if (recipe.destroy) {
return translate(
'auto.components.NewWorkspaceComposerCard.destroyConfigured',
'destroy configured'
)
return t('auto.components.NewWorkspaceComposerCard.destroyConfigured', 'destroy configured')
}
return translate('auto.components.NewWorkspaceComposerCard.noDestroyConfigured', 'no destroy')
return t('auto.components.NewWorkspaceComposerCard.noDestroyConfigured', 'no destroy')
}
export function getRecipeDetail(recipe: EphemeralVmRecipeOption): string {
return `${getRecipeCommandDisplay(recipe.create)} · ${getRecipeDestroyLabel(recipe)}`
export function getRecipeDetail(recipe: EphemeralVmRecipeOption, t?: Translate): string {
return `${getRecipeCommandDisplay(recipe.create)} · ${getRecipeDestroyLabel(recipe, t)}`
}
function matches(haystack: string, query: string): boolean {
@@ -101,13 +99,14 @@ function buildHostRows(
function matchRecipes(
recipes: readonly EphemeralVmRecipeOption[],
query: string
query: string,
t?: Translate
): EphemeralVmRecipeOption[] {
return recipes.filter(
(recipe) =>
query === '' ||
matches(recipe.name, query) ||
matches(getEphemeralVmLabel(), query) ||
matches(getEphemeralVmLabel(t), query) ||
matches(recipe.description ?? '', query)
)
}
@@ -124,24 +123,27 @@ function buildSubmenuRows(hasRecipes: boolean, hasAddHost: boolean): RunTargetRo
}
/** Filter host/recipe rows but always keep Add host reachable; other submenus require matching contents. */
export function buildRunTargetRows({
hostOptions,
recipes,
query,
hasAddHost
}: {
hostOptions: readonly ProjectHostSetupOption[]
recipes: readonly EphemeralVmRecipeOption[]
query: string
hasAddHost: boolean
}): {
export function buildRunTargetRows(
{
hostOptions,
recipes,
query,
hasAddHost
}: {
hostOptions: readonly ProjectHostSetupOption[]
recipes: readonly EphemeralVmRecipeOption[]
query: string
hasAddHost: boolean
},
t?: Translate
): {
rows: RunTargetRowModel[]
matchedRecipes: EphemeralVmRecipeOption[]
/** Hosts the row limit left out; typing narrows them in. */
hiddenHostCount: number
} {
const trimmed = query.trim().toLowerCase()
const matchedRecipes = matchRecipes(recipes, trimmed)
const matchedRecipes = matchRecipes(recipes, trimmed, t)
const hostRows = buildHostRows(hostOptions, trimmed)
const rows: RunTargetRowModel[] = [
...hostRows.rows,
@@ -9,10 +9,12 @@ import {
DialogTitle
} from '@/components/ui/dialog'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import type { SmartWorkspaceNameFieldController } from './use-smart-workspace-name-field-controller'
export function renderSmartWorkspaceCrossRepoDialog(
controller: SmartWorkspaceNameFieldController
controller: SmartWorkspaceNameFieldController,
t: Translate = translate
): React.JSX.Element {
const {
crossRepoPrompt,
@@ -43,10 +45,7 @@ export function renderSmartWorkspaceCrossRepoDialog(
</DialogHeader>
<DialogFooter className="min-w-0 sm:flex-wrap">
<Button variant="outline" onClick={dismissCrossRepoPrompt}>
{translate(
'auto.components.new.workspace.SmartWorkspaceNameField.6859e2896c',
'Cancel'
)}
{t('auto.components.new.workspace.SmartWorkspaceNameField.6859e2896c', 'Cancel')}
</Button>
<Button
variant="outline"
@@ -54,7 +53,7 @@ export function renderSmartWorkspaceCrossRepoDialog(
onClick={() => void handleUseCurrentRepo()}
>
<span className="min-w-0 truncate">
{translate(
{t(
'auto.components.new.workspace.SmartWorkspaceNameField.keepRepo',
'Keep {{name}}',
{ name: selectedRepo?.displayName ?? crossRepoSwitchFallbackLabel }
@@ -67,7 +66,7 @@ export function renderSmartWorkspaceCrossRepoDialog(
onClick={() => void acceptGitHubLink(crossRepoPrompt.matchingRepo!)}
>
<span className="min-w-0 truncate">
{translate(
{t(
'auto.components.new.workspace.SmartWorkspaceNameField.switchToRepo',
'Switch to {{name}}',
{ name: crossRepoPrompt.matchingRepo.displayName }
@@ -76,7 +75,7 @@ export function renderSmartWorkspaceCrossRepoDialog(
</Button>
) : allowCrossRepoProjectAdd ? (
<Button onClick={() => void handleAddMatchingRepo()}>
{translate(
{t(
'auto.components.new.workspace.SmartWorkspaceNameField.e57c53727c',
'Add project...'
)}
@@ -3,6 +3,7 @@ import { CaseSensitive, GitBranch, Github, Gitlab, Sparkles } from 'lucide-react
import { JiraIcon } from '@/components/icons/JiraIcon'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import type { SmartNameMode } from '../../../../shared/new-workspace/smart-workspace-source-results'
export type MrStateFilter = 'opened' | 'merged' | 'closed' | 'all'
@@ -21,74 +22,66 @@ function LinearModeIcon({ className }: { className?: string }): React.JSX.Elemen
)
}
export function getMrStateFilters(): { id: MrStateFilter; label: string }[] {
export function getMrStateFilters(
t: Translate = translate
): { id: MrStateFilter; label: string }[] {
return [
{
id: 'opened',
label: translate('auto.components.new.workspace.SmartWorkspaceNameField.622864b52a', 'Open')
label: t('auto.components.new.workspace.SmartWorkspaceNameField.622864b52a', 'Open')
},
{
id: 'merged',
label: translate('auto.components.new.workspace.SmartWorkspaceNameField.2319d87718', 'Merged')
label: t('auto.components.new.workspace.SmartWorkspaceNameField.2319d87718', 'Merged')
},
{
id: 'closed',
label: translate('auto.components.new.workspace.SmartWorkspaceNameField.6fad211c66', 'Closed')
label: t('auto.components.new.workspace.SmartWorkspaceNameField.6fad211c66', 'Closed')
},
{
id: 'all',
label: translate('auto.components.new.workspace.SmartWorkspaceNameField.26824f60dd', 'All')
label: t('auto.components.new.workspace.SmartWorkspaceNameField.26824f60dd', 'All')
}
]
}
export function getSmartWorkspaceNameModes(): SmartWorkspaceNameModeOption[] {
export function getSmartWorkspaceNameModes(
t: Translate = translate
): SmartWorkspaceNameModeOption[] {
return [
{
id: 'smart',
label: translate('auto.components.new.workspace.SmartWorkspaceNameField.b3c60c2b7c', 'Smart'),
label: t('auto.components.new.workspace.SmartWorkspaceNameField.b3c60c2b7c', 'Smart'),
Icon: Sparkles
},
{
id: 'github',
label: translate(
'auto.components.new.workspace.SmartWorkspaceNameField.0a180280bd',
'GitHub'
),
label: t('auto.components.new.workspace.SmartWorkspaceNameField.0a180280bd', 'GitHub'),
Icon: Github
},
{
id: 'linear',
label: translate(
'auto.components.new.workspace.SmartWorkspaceNameField.7a47af0565',
'Linear'
),
label: t('auto.components.new.workspace.SmartWorkspaceNameField.7a47af0565', 'Linear'),
Icon: LinearModeIcon
},
{
id: 'jira',
label: translate('auto.components.new.workspace.SmartWorkspaceNameField.jiraMode', 'Jira'),
label: t('auto.components.new.workspace.SmartWorkspaceNameField.jiraMode', 'Jira'),
Icon: JiraIcon
},
{
id: 'gitlab',
label: translate(
'auto.components.new.workspace.SmartWorkspaceNameField.2cfc6be192',
'GitLab'
),
label: t('auto.components.new.workspace.SmartWorkspaceNameField.2cfc6be192', 'GitLab'),
Icon: Gitlab
},
{
id: 'branches',
label: translate(
'auto.components.new.workspace.SmartWorkspaceNameField.2e4c7c95fe',
'Branch'
),
label: t('auto.components.new.workspace.SmartWorkspaceNameField.2e4c7c95fe', 'Branch'),
Icon: GitBranch
},
{
id: 'text',
label: translate('auto.components.new.workspace.SmartWorkspaceNameField.6f07a18604', 'Name'),
label: t('auto.components.new.workspace.SmartWorkspaceNameField.6f07a18604', 'Name'),
Icon: CaseSensitive
}
]
@@ -1,114 +1,112 @@
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import type { SmartNameMode } from '../../../../shared/new-workspace/smart-workspace-source-results'
export function getSmartWorkspaceNameFieldCopy({
repoBackedSourcesDisabled,
linearAvailable,
branchesEnabled,
crossRepoSwitchTarget,
disabled,
disabledPlaceholder,
mode
}: {
repoBackedSourcesDisabled: boolean
linearAvailable: boolean
branchesEnabled: boolean
crossRepoSwitchTarget: 'project' | 'task-source'
disabled: boolean
disabledPlaceholder?: string
mode: SmartNameMode
}) {
export function getSmartWorkspaceNameFieldCopy(
{
repoBackedSourcesDisabled,
linearAvailable,
branchesEnabled,
crossRepoSwitchTarget,
disabled,
disabledPlaceholder,
mode
}: {
repoBackedSourcesDisabled: boolean
linearAvailable: boolean
branchesEnabled: boolean
crossRepoSwitchTarget: 'project' | 'task-source'
disabled: boolean
disabledPlaceholder?: string
mode: SmartNameMode
},
t: Translate = translate
) {
const smartPlaceholder = repoBackedSourcesDisabled
? linearAvailable
? translate(
? t(
'auto.components.new.workspace.SmartWorkspaceNameField.placeholderNameOrLinearUrl',
'Type a name, Linear URL, or Jira URL'
)
: translate(
: t(
'auto.components.new.workspace.SmartWorkspaceNameField.placeholderWorkspaceName',
'Type a workspace name'
)
: linearAvailable
? branchesEnabled
? translate(
? t(
'auto.components.new.workspace.SmartWorkspaceNameField.placeholderSmartWithBranchGitLabLinear',
'Type a name, #1234, branch, GitHub/GitLab, Linear, or Jira URL'
)
: translate(
: t(
'auto.components.new.workspace.SmartWorkspaceNameField.placeholderSmartGitLabLinear',
'Type a name, #1234, GitHub/GitLab, Linear, or Jira URL'
)
: branchesEnabled
? translate(
? t(
'auto.components.new.workspace.SmartWorkspaceNameField.placeholderSmartWithBranchGitLab',
'Type a name, #1234, branch, GitHub, GitLab, or Jira URL'
)
: translate(
: t(
'auto.components.new.workspace.SmartWorkspaceNameField.placeholderSmartGitLab',
'Type a name, #1234, GitHub, GitLab, or Jira URL'
)
const crossRepoSwitchIsTaskSource = crossRepoSwitchTarget === 'task-source'
const crossRepoSwitchTitle = crossRepoSwitchIsTaskSource
? translate(
? t(
'auto.components.new.workspace.SmartWorkspaceNameField.switchTaskSourceTitle',
'Switch task source?'
)
: translate(
'auto.components.new.workspace.SmartWorkspaceNameField.4bd98f1091',
'Switch project?'
)
: t('auto.components.new.workspace.SmartWorkspaceNameField.4bd98f1091', 'Switch project?')
const formatCrossRepoSwitchDescription = (repoSlug: string): string =>
crossRepoSwitchIsTaskSource
? translate(
? t(
'auto.components.new.workspace.SmartWorkspaceNameField.crossRepoDifferentTaskSource',
'The GitHub URL points to {{repo}}, which is different from the selected task source.',
{ repo: repoSlug }
)
: translate(
: t(
'auto.components.new.workspace.SmartWorkspaceNameField.crossRepoDifferentProject',
'The GitHub URL points to {{repo}}, which is different from the selected project.',
{ repo: repoSlug }
)
const crossRepoSwitchFallbackLabel = crossRepoSwitchIsTaskSource
? translate(
? t(
'auto.components.new.workspace.SmartWorkspaceNameField.currentTaskSource',
'current task source'
)
: translate(
'auto.components.new.workspace.SmartWorkspaceNameField.fda67f0b61',
'current project'
)
: t('auto.components.new.workspace.SmartWorkspaceNameField.fda67f0b61', 'current project')
const placeholder = disabled
? (disabledPlaceholder ??
translate('auto.components.new.workspace.SmartWorkspaceNameField.unavailable', 'Unavailable'))
t('auto.components.new.workspace.SmartWorkspaceNameField.unavailable', 'Unavailable'))
: mode === 'smart'
? smartPlaceholder
: mode === 'github'
? translate(
? t(
'auto.components.new.workspace.SmartWorkspaceNameField.searchGitHub',
'Search GitHub PRs and issues'
)
: mode === 'gitlab'
? translate(
? t(
'auto.components.new.workspace.SmartWorkspaceNameField.searchGitLab',
'Search GitLab MRs and issues'
)
: mode === 'branches'
? translate(
? t(
'auto.components.new.workspace.SmartWorkspaceNameField.searchBranches',
'Search branches'
)
: mode === 'linear'
? translate(
? t(
'auto.components.new.workspace.SmartWorkspaceNameField.searchLinear',
'Search Linear issues'
)
: mode === 'jira'
? translate(
? t(
'auto.components.new.workspace.SmartWorkspaceNameField.searchJira',
'Search Jira issues or paste an issue URL'
)
: translate(
: t(
'auto.components.new.workspace.SmartWorkspaceNameField.workspaceName',
'Workspace name'
)
@@ -5,6 +5,7 @@ import { Popover, PopoverAnchor } from '@/components/ui/popover'
import { SmartWorkspaceSourceTabs } from './SmartWorkspaceSourceTabs'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { WorkspaceEmojiSuggestionPopover } from '@/components/workspace-emoji/WorkspaceEmojiSuggestionPopover'
import { renderSmartWorkspaceCrossRepoDialog } from './smart-workspace-cross-repo-dialog'
import { renderSmartWorkspaceNameInput } from './smart-workspace-name-input-surface'
@@ -15,7 +16,8 @@ import { JiraProjectPicker } from '../jira-project-picker'
import { LinearWorkspacePicker } from '../linear-workspace-picker'
export function renderSmartWorkspaceNameField(
controller: SmartWorkspaceNameFieldController
controller: SmartWorkspaceNameFieldController,
t: Translate = translate
): React.JSX.Element {
const {
mode,
@@ -55,8 +57,8 @@ export function renderSmartWorkspaceNameField(
projects={controller.jiraProjectInventory.projects}
value={controller.jiraProject}
onChange={controller.setJiraProject}
label={translate('components.jiraProjectPicker.label', 'Jira project')}
emptyLabel={translate('components.jiraProjectPicker.all', 'All Jira projects')}
label={t('components.jiraProjectPicker.label', 'Jira project')}
emptyLabel={t('components.jiraProjectPicker.all', 'All Jira projects')}
disabled={disabled || controller.jiraProjectInventory.loading}
/>
) : null}
@@ -85,7 +87,7 @@ export function renderSmartWorkspaceNameField(
className="overflow-visible bg-transparent"
>
<PopoverAnchor asChild>
<div className="relative min-w-0">{renderSmartWorkspaceNameInput(controller)}</div>
<div className="relative min-w-0">{renderSmartWorkspaceNameInput(controller, t)}</div>
</PopoverAnchor>
{renderSmartWorkspaceSourceResults(controller)}
</Command>
@@ -103,27 +105,21 @@ export function renderSmartWorkspaceNameField(
'sr-only'
)}
>
<span>{getJiraSourceStatusMessage(jiraSource)}</span>
<span>{getJiraSourceStatusMessage(jiraSource, t)}</span>
{jiraSource.errorKind === 'disconnected' && onOpenJiraSettings ? (
<Button type="button" variant="link" size="xs" onClick={onOpenJiraSettings}>
{translate(
'auto.components.new.workspace.SmartWorkspaceNameField.openSettings',
'Settings'
)}
{t('auto.components.new.workspace.SmartWorkspaceNameField.openSettings', 'Settings')}
</Button>
) : jiraSource.errorKind === 'read-failed' ? (
<Button type="button" variant="link" size="xs" onClick={jiraSource.retry}>
{translate(
'auto.components.new.workspace.SmartWorkspaceNameField.retryJira',
'Retry'
)}
{t('auto.components.new.workspace.SmartWorkspaceNameField.retryJira', 'Retry')}
</Button>
) : null}
</div>
) : null}
{unresolvedLinearUrlIntent ? (
<div id={linearStatusId} role="status" aria-live="polite" className="sr-only">
{translate(
{t(
'auto.components.new.workspace.SmartWorkspaceNameField.loadingLinearIssue',
'Loading Linear issue…'
)}
@@ -133,7 +129,7 @@ export function renderSmartWorkspaceNameField(
anchorRef={localInputRef}
open={emojiMenuOpen}
commandValue={resolvedEmojiCommandValue}
heading={translate('auto.components.new.workspace.SmartWorkspaceNameField.emoji', 'Emoji')}
heading={t('auto.components.new.workspace.SmartWorkspaceNameField.emoji', 'Emoji')}
suggestions={emojiSuggestions}
onCommandValueChange={setEmojiCommandValue}
onSelect={handleEmojiSelect}
@@ -143,7 +139,7 @@ export function renderSmartWorkspaceNameField(
}
}}
/>
{renderSmartWorkspaceCrossRepoDialog(controller)}
{renderSmartWorkspaceCrossRepoDialog(controller, t)}
</div>
)
}
@@ -11,11 +11,13 @@ import { isComposerFieldToFieldFocus } from './smart-workspace-source-popover-fo
import { replaceCompletedWorkspaceEmojiShortcode } from '@/lib/workspace-emoji-shortcodes'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { SelectionIcon } from './smart-workspace-source-row-content'
import type { SmartWorkspaceNameFieldController } from './use-smart-workspace-name-field-controller'
export function renderSmartWorkspaceNameInput(
controller: SmartWorkspaceNameFieldController
controller: SmartWorkspaceNameFieldController,
t: Translate = translate
): React.JSX.Element {
const {
selectedSource,
@@ -128,7 +130,7 @@ export function renderSmartWorkspaceNameInput(
tabIndex={-1}
onClick={openSelectedSource}
className="size-6 shrink-0 rounded-sm text-muted-foreground hover:text-foreground"
aria-label={translate(
aria-label={t(
'auto.components.new.workspace.SmartWorkspaceNameField.2c69728c2a',
'Open link in browser'
)}
@@ -137,7 +139,7 @@ export function renderSmartWorkspaceNameInput(
</Button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={6}>
{translate(
{t(
'auto.components.new.workspace.SmartWorkspaceNameField.370a1faf67',
'Open in browser'
)}
@@ -153,7 +155,7 @@ export function renderSmartWorkspaceNameInput(
tabIndex={-1}
onClick={onClearSelectedSource}
className="size-6 shrink-0 rounded-sm text-muted-foreground hover:text-foreground"
aria-label={translate(
aria-label={t(
'auto.components.new.workspace.SmartWorkspaceNameField.7199ff19c7',
'Clear selected source'
)}
@@ -162,7 +164,7 @@ export function renderSmartWorkspaceNameInput(
</Button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={6}>
{translate('auto.components.new.workspace.SmartWorkspaceNameField.0c9e668e3a', 'Clear')}
{t('auto.components.new.workspace.SmartWorkspaceNameField.0c9e668e3a', 'Clear')}
</TooltipContent>
</Tooltip>
</div>
@@ -10,7 +10,8 @@ import {
import { LinearIcon } from '@/components/icons/LinearIcon'
import { JiraIcon } from '@/components/icons/JiraIcon'
import { Trans } from 'react-i18next'
import { i18n, translate } from '@/i18n/i18n'
import { i18n } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import type { JiraSite } from '../../../../shared/jira/jira-types'
import type { RowEntry, SmartWorkspaceNameSelection } from './smart-workspace-name-field-model'
@@ -76,12 +77,13 @@ export function RowLabel({
jiraSite?: JiraSite | null
showJiraSiteContext?: boolean
}): React.JSX.Element {
const t = useTranslate()
if (row.kind === 'use-name') {
return (
<span className="min-w-0 truncate">
<Trans
i18n={i18n}
defaults={translate(
defaults={t(
'auto.components.new.workspace.SmartWorkspaceNameField.useAsWorkspaceName',
'Use <name>"{{name}}"</name> as workspace name'
)}
@@ -96,7 +98,7 @@ export function RowLabel({
<span className="min-w-0 truncate">
<Trans
i18n={i18n}
defaults={translate(
defaults={t(
'auto.components.new.workspace.SmartWorkspaceNameField.createNewBranchNamed',
'Create new branch <name>{{name}}</name>'
)}
@@ -8,6 +8,7 @@ import {
} from './smart-workspace-name-field-model'
import { canUseGitLabSmartSource } from './smart-workspace-provider-availability'
import type { useWorkItemSourceState } from './use-work-item-source-state'
import { useTranslate } from '../../i18n/use-translate'
type FieldState = ReturnType<typeof useWorkItemSourceState>
@@ -43,6 +44,7 @@ export function useSmartWorkspaceFieldAvailability({
checkLinearConnection: () => Promise<void>
jiraSourceConnected: boolean
}) {
const t = useTranslate()
const { disabled, textOnly, repoBackedSourcesDisabled, branchesEnabled, value } = props
const { mode: storedMode, setMode, setDebouncedQuery } = state
const preflightStatusCurrent = preflightStatusContextKey === expectedPreflightContextKey
@@ -63,7 +65,7 @@ export function useSmartWorkspaceFieldAvailability({
[gitlabSourceAvailable, linearStatus.connected]
)
const linearAvailable = availableTaskProviders.includes('linear')
const availableModes = getSmartWorkspaceNameModes().filter((item) => {
const availableModes = getSmartWorkspaceNameModes(t).filter((item) => {
if (textOnly) {
return item.id === 'text'
}
@@ -95,7 +97,7 @@ export function useSmartWorkspaceFieldAvailability({
setMode(mode)
}
}, [storedMode, setMode, mode])
const mrStateFilters = getMrStateFilters()
const mrStateFilters = getMrStateFilters(t)
useEffect(() => {
if (disabled || textOnly) {
@@ -10,8 +10,10 @@ import { useSmartWorkspaceFieldFocusControls } from './use-smart-workspace-field
import { useSmartWorkspaceNameFieldActions } from './use-smart-workspace-name-field-actions'
import { getSmartWorkspaceNameFieldCopy } from './smart-workspace-name-field-copy'
import { useLatestHandler } from '@/lib/use-latest-handler'
import { useTranslate } from '@/i18n/use-translate'
export function useSmartWorkspaceNameFieldController(props: SmartWorkspaceNameFieldProps) {
const t = useTranslate()
const search = useWorkItemSourceSearch({
...props,
defaultLinearWorkspaceId: 'all',
@@ -80,10 +82,13 @@ export function useSmartWorkspaceNameFieldController(props: SmartWorkspaceNameFi
allowCrossRepoProjectAdd: props.allowCrossRepoProjectAdd ?? true
}
const actions = useSmartWorkspaceNameFieldActions(foundation, { ...search, activeEmojiShortcode })
const copy = getSmartWorkspaceNameFieldCopy({
...search,
disabledPlaceholder: props.disabledPlaceholder
})
const copy = getSmartWorkspaceNameFieldCopy(
{
...search,
disabledPlaceholder: props.disabledPlaceholder
},
t
)
return {
...foundation,
...actions,
@@ -1,94 +1,80 @@
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import type { WorkspaceSetupLabels } from './workspace-setup-section-model'
export function getComposerWorkspaceSetupLabels(): WorkspaceSetupLabels {
export function getComposerWorkspaceSetupLabels(t: Translate = translate): WorkspaceSetupLabels {
return {
kinds: {
setup: {
configLabel: translate(
configLabel: t(
'auto.components.NewWorkspaceComposerCard.setupConfigSetupScript',
'Setup script'
),
runLabel: translate(
runLabel: t(
'auto.components.NewWorkspaceComposerCard.runSetupCommand',
'Run setup command'
),
askLabel: translate(
'auto.components.NewWorkspaceComposerCard.askRunSetup',
'Run setup now?'
),
runButtonLabel: translate(
'auto.components.NewWorkspaceComposerCard.runSetupNow',
'Run setup now'
),
skipButtonLabel: translate(
'auto.components.NewWorkspaceComposerCard.skipForNow',
'Skip for now'
)
askLabel: t('auto.components.NewWorkspaceComposerCard.askRunSetup', 'Run setup now?'),
runButtonLabel: t('auto.components.NewWorkspaceComposerCard.runSetupNow', 'Run setup now'),
skipButtonLabel: t('auto.components.NewWorkspaceComposerCard.skipForNow', 'Skip for now')
},
'default-tabs': {
configLabel: translate(
configLabel: t(
'auto.components.NewWorkspaceComposerCard.setupConfigDefaultTabs',
'Default tab commands'
),
runLabel: translate(
runLabel: t(
'auto.components.NewWorkspaceComposerCard.runDefaultTabCommands',
'Run default tab commands'
),
askLabel: translate(
askLabel: t(
'auto.components.NewWorkspaceComposerCard.askRunDefaultTabCommands',
'Run default tab commands now?'
),
runButtonLabel: translate(
runButtonLabel: t(
'auto.components.NewWorkspaceComposerCard.runCommandsNow',
'Run commands now'
),
skipButtonLabel: translate(
'auto.components.NewWorkspaceComposerCard.skipCommands',
'Skip commands'
)
skipButtonLabel: t('auto.components.NewWorkspaceComposerCard.skipCommands', 'Skip commands')
},
'setup-and-default-tabs': {
configLabel: translate(
configLabel: t(
'auto.components.NewWorkspaceComposerCard.setupConfigSetupAndDefaultTabs',
'Setup and default tab commands'
),
runLabel: translate(
runLabel: t(
'auto.components.NewWorkspaceComposerCard.runSetupAndDefaultTabCommands',
'Run setup and default tab commands'
),
askLabel: translate(
askLabel: t(
'auto.components.NewWorkspaceComposerCard.askRunSetupAndDefaultTabCommands',
'Run setup and default tab commands now?'
),
runButtonLabel: translate(
runButtonLabel: t(
'auto.components.NewWorkspaceComposerCard.runCommandsNow',
'Run commands now'
),
skipButtonLabel: translate(
'auto.components.NewWorkspaceComposerCard.skipCommands',
'Skip commands'
)
skipButtonLabel: t('auto.components.NewWorkspaceComposerCard.skipCommands', 'Skip commands')
}
},
sources: {
yaml: translate('auto.components.NewWorkspaceComposerCard.23bb365554', 'orca.yaml'),
both: translate('auto.components.NewWorkspaceComposerCard.326a578923', 'orca.yaml + local'),
local: translate('auto.components.NewWorkspaceComposerCard.92e34f0311', 'local settings')
yaml: t('auto.components.NewWorkspaceComposerCard.23bb365554', 'orca.yaml'),
both: t('auto.components.NewWorkspaceComposerCard.326a578923', 'orca.yaml + local'),
local: t('auto.components.NewWorkspaceComposerCard.92e34f0311', 'local settings')
},
help: translate(
help: t(
'auto.components.NewWorkspaceComposerCard.9a70e4859e',
'Choose whether to run setup before creating this workspace.'
),
checking: translate(
checking: t(
'auto.components.NewWorkspaceComposerCard.803b7fe72f',
'Checking setup configuration...'
)
}
}
export function getComposerSetupStartupLabel(): string {
return translate(
export function getComposerSetupStartupLabel(t: Translate = translate): string {
return t(
'auto.components.NewWorkspaceComposerCard.waitForSetupBeforeAgent',
'Wait for setup to complete before starting agent'
)
@@ -7,7 +7,8 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/component
import type { TuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import { nativeChatPreferenceFor } from '../../../../shared/structured-native-chat-launch-route'
import { i18n, translate } from '@/i18n/i18n'
import { i18n } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { openAppHttpLink } from '@/lib/browser/navigation/app-http-link-open'
import { useScrollEdgeFades } from '@/lib/use-scroll-edge-fades'
import { cn } from '@/lib/utils'
@@ -57,6 +58,7 @@ export function AgentStep({
yoloPermissions = true,
onYoloPermissionsChange
}: AgentStepProps) {
const t = useTranslate()
const agentCatalog = getAgentCatalog()
const gridFades = useScrollEdgeFades<HTMLElement>()
const chatMode = agentSurface === 'chat'
@@ -117,19 +119,20 @@ export function AgentStep({
}
}
const fallbackRestLabel = openState
? translate('auto.components.onboarding.AgentStep.hideAgents', 'Hide agents')
: translate(
'auto.components.onboarding.AgentStep.showMoreAgents',
'Show {{count}} more agents →',
{ count: fallbackRest.length, defaultValue_one: 'Show {{count}} more agent →' }
)
? t('auto.components.onboarding.AgentStep.hideAgents', 'Hide agents')
: t('auto.components.onboarding.AgentStep.showMoreAgents', 'Show {{count}} more agents →', {
count: fallbackRest.length,
defaultValue_one: 'Show {{count}} more agent →'
})
const useTerminal = (): void => onAgentSurfaceChange?.('terminal')
const renderTile = (agent: AgentCatalogEntry, fromCollapsedSection: boolean) => (
<AgentPickerButton
key={agent.id}
agent={agent}
selected={selectedAgent === agent.id}
detail={agentSurface === 'terminal' ? chatAvailableDetail(opensAsChat(agent.id)) : undefined}
detail={
agentSurface === 'terminal' ? chatAvailableDetail(opensAsChat(agent.id), t) : undefined
}
onClick={() => onSelect(agent.id, fromCollapsedSection)}
/>
)
@@ -144,7 +147,7 @@ export function AgentStep({
)}
{!hasDetected && !isDetecting && !chatMode && (
<div className="shrink-0 rounded-lg border border-border px-4 py-3 text-xs text-muted-foreground">
{translate(
{t(
'auto.components.onboarding.AgentStep.1eee1c7bd8',
'No agents detected on your PATH. Pick one to install later, or continue with a blank terminal.'
)}
@@ -167,7 +170,7 @@ export function AgentStep({
<span>
<Trans
i18n={i18n}
defaults={translate(
defaults={t(
'auto.components.onboarding.AgentStep.agentNotOnPath',
"<agent>{{agent}}</agent> isn't on your PATH yet. Orca will set it as your default and you can install it any time.",
{ agent: selectedEntry.label }
@@ -182,7 +185,7 @@ export function AgentStep({
className="shrink-0"
onClick={(event) => openAppHttpLink(selectedEntry.homepageUrl, { event })}
>
{translate('auto.components.onboarding.AgentStep.9c163bb0e0', 'Install instructions')}
{t('auto.components.onboarding.AgentStep.9c163bb0e0', 'Install instructions')}
<ExternalLink className="size-3" />
</Button>
</div>
@@ -213,14 +216,11 @@ export function AgentStep({
<AgentSectionHeader
label={
hasDetected
? translate(
? t(
'auto.components.onboarding.AgentStep.d7b3ef168b',
'Detected on your system'
)
: translate(
'auto.components.onboarding.AgentStep.e6a369bd04',
'Popular agents'
)
: t('auto.components.onboarding.AgentStep.e6a369bd04', 'Popular agents')
}
count={primary.length}
/>
@@ -234,8 +234,8 @@ export function AgentStep({
<AgentSectionHeader
label={
noChatAgents
? translate('components.onboarding.agentStep.worksInChat', 'Works in Chat UI')
: translate('components.onboarding.agentStep.chatAgents', 'Chat UI agents')
? t('components.onboarding.agentStep.worksInChat', 'Works in Chat UI')
: t('components.onboarding.agentStep.chatAgents', 'Chat UI agents')
}
count={chatSection.length}
/>
@@ -247,11 +247,11 @@ export function AgentStep({
{terminalSection.length > 0 && (
<div className="space-y-3">
<AgentSectionHeader
label={translate(
label={t(
'components.onboarding.agentStep.terminalAgents',
'Available as terminal agents'
)}
hint={translate(
hint={t(
'components.onboarding.agentStep.terminalAgentsHint',
'No Chat UI support, so these fall back to a terminal.'
)}
@@ -6,17 +6,21 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
import { cn } from '@/lib/utils'
import { isWebClientLocation } from '@/lib/web-client-location'
import { i18n, translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import type { AgentSurface } from './use-onboarding-flow-types'
import { TerminalLaunchDemo } from './TerminalLaunchDemo'
const emphasis = { em: <span className="font-medium text-foreground" /> }
/** Terminal mode marks which agents could also open in Chat UI. */
export function chatAvailableDetail(opensAsChat: boolean): React.ReactNode {
export function chatAvailableDetail(
opensAsChat: boolean,
t: Translate = translate
): React.ReactNode {
return opensAsChat ? (
<>
<MessagesSquare />
{translate('components.onboarding.agentStep.chatAvailable', 'Chat available')}
{t('components.onboarding.agentStep.chatAvailable', 'Chat available')}
</>
) : undefined
}
@@ -25,15 +29,16 @@ const surfacePillClass =
'inline-flex h-6 items-center gap-1.5 rounded-full border px-2.5 font-medium text-foreground'
function AgentSurfaceLabel({ surface }: { surface: AgentSurface }) {
const t = useTranslate()
return surface === 'chat' ? (
<>
<MessagesSquare className="size-3.5" />
{translate('components.onboarding.agentSurface.chat.label', 'Chat UI')}
{t('components.onboarding.agentSurface.chat.label', 'Chat UI')}
</>
) : (
<>
<SquareTerminal className="size-3.5" />
{translate('components.onboarding.agentStep.terminalUi', 'Terminal UI')}
{t('components.onboarding.agentStep.terminalUi', 'Terminal UI')}
</>
)
}
@@ -46,7 +51,8 @@ export function AgentSurfaceModeRow({
surface: AgentSurface
onChange?: (surface: AgentSurface) => void
}) {
const label = translate('components.onboarding.agentStep.agentsOpenIn', 'Agents open in')
const t = useTranslate()
const label = t('components.onboarding.agentStep.agentsOpenIn', 'Agents open in')
if (!onChange) {
return (
<div className="flex shrink-0 items-center gap-2.5 text-xs text-muted-foreground">
@@ -118,12 +124,13 @@ export function NoChatAgentsNotice({
chatAgentLabels: readonly string[]
onUseTerminal: () => void
}) {
const t = useTranslate()
if (!anyDetected) {
return (
<AgentStepNotice>
<Trans
i18n={i18n}
defaults={translate(
defaults={t(
'components.onboarding.agentStep.noAgentsForChat',
'<em>No agents found on your PATH.</em> Chat UI works with {{agents}}. Install one, or continue and add an agent later.',
{
@@ -138,10 +145,10 @@ export function NoChatAgentsNotice({
)
}
return (
<AgentStepNotice tone="warning" actionLabel={terminalInsteadLabel()} onAction={onUseTerminal}>
<AgentStepNotice tone="warning" actionLabel={terminalInsteadLabel(t)} onAction={onUseTerminal}>
<Trans
i18n={i18n}
defaults={translate(
defaults={t(
'components.onboarding.agentStep.noChatAgentsDetected',
'<em>None of your agents work in Chat UI.</em> They would all open in a terminal, so Terminal fits better.'
)}
@@ -158,11 +165,12 @@ export function TerminalFitNotice({
agentLabel: string
onUseTerminal: () => void
}) {
const t = useTranslate()
return (
<AgentStepNotice tone="warning" actionLabel={terminalInsteadLabel()} onAction={onUseTerminal}>
<AgentStepNotice tone="warning" actionLabel={terminalInsteadLabel(t)} onAction={onUseTerminal}>
<Trans
i18n={i18n}
defaults={translate(
defaults={t(
'components.onboarding.agentStep.opensInTerminalFit',
'<em>{{agent}} opens in a terminal</em>, so Terminal fits better.',
{ agent: agentLabel }
@@ -173,13 +181,14 @@ export function TerminalFitNotice({
)
}
function terminalInsteadLabel(): string {
return translate('components.onboarding.agentStep.useTerminalInstead', 'Use Terminal instead')
function terminalInsteadLabel(t: Translate = translate): string {
return t('components.onboarding.agentStep.useTerminalInstead', 'Use Terminal instead')
}
/** Chat UI users can still start any agent's CLI from the + menu; the hover card shows how. */
export function ChatModeTerminalTip() {
const howLabel = translate(
const t = useTranslate()
const howLabel = t(
'components.onboarding.agentStep.terminalTipHow',
'Show how to start an agent in a terminal'
)
@@ -191,11 +200,11 @@ export function ChatModeTerminalTip() {
defaults={
// The browser client has no Chat UI launch switch; its default lives in the checklist.
isWebClientLocation()
? translate(
? t(
'components.onboarding.agentStep.terminalTipWeb',
'Switch back any time in <em>Settings → Onboarding checklist</em>. You can also start any agent in a terminal from the <em>+</em> menu.'
)
: translate(
: t(
'components.onboarding.agentStep.terminalTip',
'Switch back any time in <em>Settings → Chat UI</em>. You can also start any agent in a terminal from the <em>+</em> menu.'
)
@@ -227,7 +236,7 @@ export function ChatModeTerminalTip() {
<p className="max-w-[300px] text-xs leading-relaxed text-muted-foreground">
<Trans
i18n={i18n}
defaults={translate(
defaults={t(
'components.onboarding.agentStep.terminalTipCaption',
"Click <em>+</em> in the tab bar, then pick an agent marked <em>(CLI)</em>. If a chat agent's CLI is installed, hover its row and click the terminal icon to use the CLI."
)}
@@ -242,13 +251,14 @@ export function ChatModeTerminalTip() {
}
export function TerminalModeChatTip({ onUseChat }: { onUseChat: () => void }) {
const t = useTranslate()
return (
<div className="flex shrink-0 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
<MessagesSquare className="size-3.5" />
<span>
<Trans
i18n={i18n}
defaults={translate(
defaults={t(
'components.onboarding.agentStep.chatTip',
'Want chat too? Turn it on any time in <em>Settings → Chat UI</em>.'
)}
@@ -260,7 +270,7 @@ export function TerminalModeChatTip({ onUseChat }: { onUseChat: () => void }) {
className="font-medium text-foreground underline underline-offset-2 outline-none focus-visible:ring-2 focus-visible:ring-ring"
onClick={onUseChat}
>
{translate('components.onboarding.agentStep.turnOnChat', 'Turn on now')}
{t('components.onboarding.agentStep.turnOnChat', 'Turn on now')}
</button>
</div>
)
@@ -10,7 +10,8 @@ import {
import { Trans } from 'react-i18next'
import { cn } from '@/lib/utils'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { i18n, translate } from '@/i18n/i18n'
import { i18n } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { nativeChatToolRowVerb } from '@/components/native-chat/tools/lines/native-chat-tool-line-label'
import { DiffLineCounts } from '@/components/right-sidebar/source-control/listing/diff-line-counts'
import type { AgentSurface } from './use-onboarding-flow-types'
@@ -27,6 +28,7 @@ export function AgentSurfaceStep({
onSelect,
noChatAgentDetected = false
}: AgentSurfaceStepProps) {
const t = useTranslate()
return (
<div className="space-y-6">
<RadioGroup
@@ -41,18 +43,18 @@ export function AgentSurfaceStep({
<SurfaceOption
value="chat"
icon={MessagesSquare}
label={translate('components.onboarding.agentSurface.chat.label', 'Chat UI')}
description={translate(
label={t('components.onboarding.agentSurface.chat.label', 'Chat UI')}
description={t(
'components.onboarding.agentSurface.chat.description',
'Agents reply in a chat, with rendered messages and diffs.'
)}
note={translate(
note={t(
'components.onboarding.agentSurface.chat.terminalFallback',
'Agents without chat support, and SSH or WSL projects, still open in a terminal.'
)}
warning={
noChatAgentDetected
? translate(
? t(
'components.onboarding.agentSurface.chat.noneDetected',
'None of your detected agents work in Chat UI.'
)
@@ -64,8 +66,8 @@ export function AgentSurfaceStep({
<SurfaceOption
value="terminal"
icon={SquareTerminal}
label={translate('components.onboarding.agentSurface.terminal.label', 'Terminal')}
description={translate(
label={t('components.onboarding.agentSurface.terminal.label', 'Terminal')}
description={t(
'components.onboarding.agentSurface.terminal.description',
"Each agent's own CLI, exactly as it ships. Works with every CLI agent."
)}
@@ -78,7 +80,7 @@ export function AgentSurfaceStep({
<span>
<Trans
i18n={i18n}
defaults={translate(
defaults={t(
'components.onboarding.agentSurface.switchNote',
'Not a permanent choice. Switch any time in <em>Settings → Chat UI</em>.'
)}
@@ -166,19 +168,17 @@ const CHAT_DIFF_LINES = [
// Mirrors the real native chat: bordered user bubble, edit row with a rendered diff, plain reply.
function ChatPreview() {
const t = useTranslate()
return (
<div className="native-chat-appearance flex size-full flex-col bg-chat-canvas text-[10.5px] leading-[15px] text-chat-foreground">
<div className="flex min-h-0 flex-1 flex-col gap-2 overflow-hidden px-3.5 pt-2.5">
<div className="ml-auto max-w-[80%] truncate rounded-[10px] border border-chat-user-border bg-chat-user-surface px-2.5 py-1 text-chat-foreground-strong">
{translate(
'components.onboarding.agentSurface.preview.userMessage',
'Add retry to fetchUser'
)}
{t('components.onboarding.agentSurface.preview.userMessage', 'Add retry to fetchUser')}
</div>
<div>
<div className="flex items-center gap-1.5 whitespace-nowrap">
<FilePen className="size-3 shrink-0 text-chat-foreground-faint" />
<span className="text-chat-foreground-faint">{nativeChatToolRowVerb('edited')}</span>
<span className="text-chat-foreground-faint">{nativeChatToolRowVerb('edited', t)}</span>
<span className="text-chat-foreground-strong">{PREVIEW_FILE}</span>
<DiffLineCounts added={1} removed={1} />
</div>
@@ -210,7 +210,7 @@ function ChatPreview() {
</div>
</div>
<p className="truncate text-chat-foreground-strong">
{translate(
{t(
'components.onboarding.agentSurface.preview.agentMessage',
'Failed requests now retry up to 3 times.'
)}
@@ -219,7 +219,7 @@ function ChatPreview() {
<div className="shrink-0 px-3 pt-1.5 pb-2.5">
<div className="flex items-center gap-2 rounded-[10px] border border-chat-composer-border bg-chat-composer-surface py-1 pr-1 pl-2.5 shadow-xs">
<span className="truncate text-chat-foreground-faint">
{translate(
{t(
'components.onboarding.agentSurface.preview.composerPlaceholder',
'Ask a follow-up…'
)}
@@ -16,7 +16,7 @@ import {
onboardingFeatureSetupTelemetrySelection,
type OnboardingFeatureSetupSelection
} from './onboarding-feature-setup'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
type FeatureSetupInlineTerminalProps = {
command: string
@@ -29,6 +29,7 @@ export function FeatureSetupInlineTerminal({
runtimeContext,
selection
}: FeatureSetupInlineTerminalProps): React.JSX.Element {
const t = useTranslate()
const terminalOpenedTrackedRef = useRef(false)
const terminalInteractedTrackedRef = useRef(false)
const activeSkillRuntime = useActiveProjectSkillRuntime()
@@ -80,15 +81,12 @@ export function FeatureSetupInlineTerminal({
prepareCommandForShell={prepareCommandForShell}
shellOverride={setupRuntime.terminalShellOverride}
forceHostRuntime={Boolean(setupRuntime.installDisabledReason)}
title={translate(
'auto.components.onboarding.FeatureSetupInlineTerminal.c767ab7061',
'Skill setup'
)}
ariaLabel={translate(
title={t('auto.components.onboarding.FeatureSetupInlineTerminal.c767ab7061', 'Skill setup')}
ariaLabel={t(
'auto.components.onboarding.FeatureSetupInlineTerminal.47fc6cc6dc',
'Skill setup command'
)}
description={translate(
description={t(
'auto.components.onboarding.FeatureSetupInlineTerminal.789b59936e',
'Press Enter to run the command and confirm npx if asked. You can also set this up later in Settings.'
)}
@@ -1,7 +1,7 @@
import { Check } from 'lucide-react'
import type { TerminalConfigImportPreview } from '../../../../shared/terminal/appearance/terminal-config-import-preview'
import ghosttyIcon from '../../../../../resources/ghostty.svg'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import type { DiscoveryState } from './ThemeStep'
export function GhosttyDiscoveryRow({
@@ -15,6 +15,7 @@ export function GhosttyDiscoveryRow({
disabled: boolean
onImport: (preview: TerminalConfigImportPreview) => void
}) {
const t = useTranslate()
// Non-Mac discovery remains idle; hide it instead of leaving a permanent looking placeholder.
if (discovery.status === 'absent' || discovery.status === 'idle') {
return null
@@ -24,10 +25,7 @@ export function GhosttyDiscoveryRow({
return (
<div className="flex items-center gap-2.5 rounded-lg border border-dashed border-border bg-transparent px-3.5 py-2.5 text-[12px] text-muted-foreground">
<span className="size-1.5 animate-pulse rounded-full bg-muted-foreground/60" />
{translate(
'auto.components.onboarding.ThemeStep.2c3aa538f8',
'Looking for a Ghostty config…'
)}
{t('auto.components.onboarding.ThemeStep.2c3aa538f8', 'Looking for a Ghostty config…')}
</div>
)
}
@@ -38,7 +36,7 @@ export function GhosttyDiscoveryRow({
<Check className="size-3.5" />
<span className="flex-1">
<span className="font-medium">
{translate('auto.components.onboarding.ThemeStep.78b6386140', 'Imported from Ghostty.')}
{t('auto.components.onboarding.ThemeStep.78b6386140', 'Imported from Ghostty.')}
</span>
{discovery.fields.length > 0 && (
<span className="text-muted-foreground"> {discovery.fields.join(' · ')}</span>
@@ -55,16 +53,13 @@ export function GhosttyDiscoveryRow({
<div className="min-w-0 flex-1">
<div className="text-[12px] text-foreground">
<span className="font-medium">
{translate(
'auto.components.onboarding.ThemeStep.7ee9234e54',
'Ghostty config detected.'
)}
{t('auto.components.onboarding.ThemeStep.7ee9234e54', 'Ghostty config detected.')}
</span>{' '}
<span className="text-muted-foreground">
{translate('auto.components.onboarding.ThemeStep.248c812283', 'Import')}{' '}
{t('auto.components.onboarding.ThemeStep.248c812283', 'Import')}{' '}
{fields.length > 0
? fields.map((f) => f.toLowerCase()).join(', ')
: translate('auto.components.onboarding.ThemeStep.906c4373fe', 'settings')}
: t('auto.components.onboarding.ThemeStep.906c4373fe', 'settings')}
?
</span>
</div>
@@ -83,8 +78,8 @@ export function GhosttyDiscoveryRow({
onClick={() => onImport(preview)}
>
{importing
? translate('auto.components.onboarding.ThemeStep.ad19e5c916', 'Importing…')
: translate('auto.components.onboarding.ThemeStep.248c812283', 'Import')}
? t('auto.components.onboarding.ThemeStep.ad19e5c916', 'Importing…')
: t('auto.components.onboarding.ThemeStep.248c812283', 'Import')}
</button>
</div>
)
@@ -7,7 +7,7 @@ import { useAppStore } from '@/store'
import { IntegrationStatusPill } from '@/components/integration-status-pill'
import { cn } from '@/lib/utils'
import { OnboardingInlineCommandTerminal } from './OnboardingInlineCommandTerminal'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { openAppHttpLink } from '@/lib/browser/navigation/app-http-link-open'
type GitHubSetupState = 'checking' | 'connected' | 'not-installed' | 'not-authenticated'
@@ -25,6 +25,7 @@ function getGitHubSetupState(
}
export function GitHubRow(props: { compact?: boolean } = {}): React.JSX.Element {
const t = useTranslate()
const { compact = false } = props
const preflightStatus = useAppStore((s) => s.preflightStatus)
const preflightStatusLoading = useAppStore((s) => s.preflightStatusLoading)
@@ -45,34 +46,28 @@ export function GitHubRow(props: { compact?: boolean } = {}): React.JSX.Element
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<h3 className="text-[15px] font-semibold leading-tight text-foreground">
{translate('auto.components.onboarding.IntegrationsStep.217beb0658', 'GitHub')}
{t('auto.components.onboarding.IntegrationsStep.217beb0658', 'GitHub')}
</h3>
{state === 'connected' ? (
<IntegrationStatusPill tone="connected">
{translate('auto.components.onboarding.IntegrationsStep.c91a5782f1', 'Connected')}
{t('auto.components.onboarding.IntegrationsStep.c91a5782f1', 'Connected')}
</IntegrationStatusPill>
) : state === 'not-installed' ? (
<IntegrationStatusPill tone="attention">
{translate(
'auto.components.onboarding.IntegrationsStep.5c115cb713',
'CLI not installed'
)}
{t('auto.components.onboarding.IntegrationsStep.5c115cb713', 'CLI not installed')}
</IntegrationStatusPill>
) : state === 'not-authenticated' ? (
<IntegrationStatusPill tone="attention">
{translate(
'auto.components.onboarding.IntegrationsStep.8405043962',
'Sign in needed'
)}
{t('auto.components.onboarding.IntegrationsStep.8405043962', 'Sign in needed')}
</IntegrationStatusPill>
) : (
<IntegrationStatusPill tone="neutral">
{translate('auto.components.onboarding.IntegrationsStep.c1547656f0', 'Checking…')}
{t('auto.components.onboarding.IntegrationsStep.c1547656f0', 'Checking…')}
</IntegrationStatusPill>
)}
</div>
<p className="mt-1 text-[13px] leading-relaxed text-muted-foreground">
{translate(
{t(
'auto.components.onboarding.IntegrationsStep.50db38cf4b',
'Pull requests, issues, and check status.'
)}
@@ -87,7 +82,7 @@ export function GitHubRow(props: { compact?: boolean } = {}): React.JSX.Element
onClick={(event) => openAppHttpLink('https://cli.github.com', { event })}
>
<ExternalLink className="size-3.5" />
{translate('auto.components.onboarding.IntegrationsStep.bd5d976fb2', 'Install gh')}
{t('auto.components.onboarding.IntegrationsStep.bd5d976fb2', 'Install gh')}
</Button>
) : null}
{state === 'not-authenticated' ? (
@@ -99,8 +94,8 @@ export function GitHubRow(props: { compact?: boolean } = {}): React.JSX.Element
>
<Terminal className="size-3.5" />
{githubTerminalOpen
? translate('auto.components.onboarding.IntegrationsStep.0b4a7d23ab', 'Signing in')
: translate('auto.components.onboarding.IntegrationsStep.d6e5dba05a', 'Sign in')}
? t('auto.components.onboarding.IntegrationsStep.0b4a7d23ab', 'Signing in')
: t('auto.components.onboarding.IntegrationsStep.d6e5dba05a', 'Sign in')}
</Button>
) : null}
{state !== 'connected' ? (
@@ -109,7 +104,7 @@ export function GitHubRow(props: { compact?: boolean } = {}): React.JSX.Element
size="sm"
onClick={() => void refreshPreflightStatus({ force: true, wakeWslDistro: true })}
>
{translate('auto.components.onboarding.IntegrationsStep.80e3ce0bc9', 'Re-check')}
{t('auto.components.onboarding.IntegrationsStep.80e3ce0bc9', 'Re-check')}
</Button>
) : null}
</div>
@@ -118,15 +113,12 @@ export function GitHubRow(props: { compact?: boolean } = {}): React.JSX.Element
<div className={cn(compact ? 'px-4 pb-4' : 'px-5 pb-5')}>
<OnboardingInlineCommandTerminal
command="gh auth login"
title={translate(
'auto.components.onboarding.IntegrationsStep.6d469169f2',
'GitHub setup'
)}
ariaLabel={translate(
title={t('auto.components.onboarding.IntegrationsStep.6d469169f2', 'GitHub setup')}
ariaLabel={t(
'auto.components.onboarding.IntegrationsStep.f9d2e12d17',
'GitHub sign in command'
)}
description={translate(
description={t(
'auto.components.onboarding.IntegrationsStep.af69f42372',
'Press Enter to run GitHub CLI auth. Re-check GitHub after the browser or device flow finishes.'
)}
@@ -138,6 +130,7 @@ export function GitHubRow(props: { compact?: boolean } = {}): React.JSX.Element
}
export function LinearRow(props: { compact?: boolean } = {}): React.JSX.Element {
const t = useTranslate()
const { compact = false } = props
const linearStatus = useAppStore((s) => s.linearStatus)
const checkLinearConnection = useAppStore((s) => s.checkLinearConnection)
@@ -157,31 +150,28 @@ export function LinearRow(props: { compact?: boolean } = {}): React.JSX.Element
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<h3 className="text-[15px] font-semibold leading-tight text-foreground">
{translate('auto.components.onboarding.IntegrationsStep.27743304b1', 'Linear')}
{t('auto.components.onboarding.IntegrationsStep.27743304b1', 'Linear')}
</h3>
{linearStatus.connected ? (
<IntegrationStatusPill tone="connected">
{translate(
'auto.components.onboarding.IntegrationsStep.c91a5782f1',
'Connected'
)}
{t('auto.components.onboarding.IntegrationsStep.c91a5782f1', 'Connected')}
</IntegrationStatusPill>
) : null}
</div>
<p className="mt-1 text-[13px] leading-relaxed text-muted-foreground">
{linearStatus.connected
? workspaceCount === 1
? translate(
? t(
'auto.components.onboarding.IntegrationsStep.linearWorkspacesLinkedSingular',
'{{count}} workspace linked. Add another workspace or replace a restricted key any time.',
{ count: workspaceCount }
)
: translate(
: t(
'auto.components.onboarding.IntegrationsStep.linearWorkspacesLinkedPlural',
'{{count}} workspaces linked. Add another workspace or replace a restricted key any time.',
{ count: workspaceCount }
)
: translate(
: t(
'auto.components.onboarding.IntegrationsStep.4983ae7433',
'Add Linear access with a Personal API key. Full-access keys can show every team the key owner can access.'
)}
@@ -191,22 +181,19 @@ export function LinearRow(props: { compact?: boolean } = {}): React.JSX.Element
<div className={cn('flex items-center gap-2', compact ? 'flex-wrap' : 'shrink-0')}>
{linearStatus.connected ? (
<Button variant="outline" size="sm" onClick={() => setDialogOpen(true)}>
{translate(
{t(
'auto.components.onboarding.IntegrationsStep.dd9c186a8b',
'Add workspace access'
)}
</Button>
) : (
<Button size="sm" onClick={() => setDialogOpen(true)}>
{translate(
'auto.components.onboarding.IntegrationsStep.04ef416712',
'Add Linear access'
)}
{t('auto.components.onboarding.IntegrationsStep.04ef416712', 'Add Linear access')}
</Button>
)}
{!linearStatus.connected ? (
<Button variant="ghost" size="sm" onClick={() => void checkLinearConnection(true)}>
{translate('auto.components.onboarding.IntegrationsStep.80e3ce0bc9', 'Re-check')}
{t('auto.components.onboarding.IntegrationsStep.80e3ce0bc9', 'Re-check')}
</Button>
) : null}
</div>
@@ -218,7 +205,7 @@ export function LinearRow(props: { compact?: boolean } = {}): React.JSX.Element
onOpenChange={setDialogOpen}
overlayClassName="z-[110]"
contentClassName="z-[120]"
connectLabel={translate(
connectLabel={t(
'auto.components.onboarding.IntegrationsStep.04ef416712',
'Add Linear access'
)}
@@ -248,6 +235,7 @@ const CAPABILITIES = [
] as const
export function IntegrationsStep(): React.JSX.Element {
const t = useTranslate()
const refreshPreflightStatus = useAppStore((s) => s.refreshPreflightStatus)
useEffect(() => {
@@ -260,7 +248,7 @@ export function IntegrationsStep(): React.JSX.Element {
{CAPABILITIES.map(({ key, fallback }) => (
<li key={key} className="flex gap-2.5">
<span className="mt-2 size-1 shrink-0 rounded-full bg-muted-foreground" aria-hidden />
<span>{translate(key, fallback)}</span>
<span>{t(key, fallback)}</span>
</li>
))}
</ul>
@@ -270,13 +258,10 @@ export function IntegrationsStep(): React.JSX.Element {
<div className="mt-4 rounded-xl border border-border bg-muted/10 px-5 py-4">
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
<span className="text-[14px] font-medium text-foreground/70">
{translate(
'auto.components.onboarding.IntegrationsStep.3a3e360289',
'More task sources'
)}
{t('auto.components.onboarding.IntegrationsStep.3a3e360289', 'More task sources')}
</span>
<span className="text-[13px] leading-relaxed text-muted-foreground">
{translate(
{t(
'auto.components.onboarding.IntegrationsStep.277f30eb34',
'Linear, GitLab, Bitbucket, Azure DevOps, Gitea, and Jira live in Settings > Integrations.'
)}
@@ -18,7 +18,7 @@ import {
useMacNotificationPermissionState
} from '@/components/notifications/mac-notification-permission-card'
import { useMountedRef } from '@/hooks/useMountedRef'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
type NotificationStepProps = {
settings: GlobalSettings | null
@@ -31,6 +31,7 @@ export function NotificationStep({
settings,
updateSettings
}: NotificationStepProps): React.JSX.Element {
const t = useTranslate()
const notificationSettings = settings?.notifications
const notificationSettingsRef = useRef(notificationSettings)
// Unloaded settings use enabled’s default so fresh-install permission checks can start immediately.
@@ -86,7 +87,7 @@ export function NotificationStep({
if (!result.played) {
if (mountedRef.current) {
toast.error(
translate(
t(
'auto.components.onboarding.NotificationStep.b6a994e36e',
'Notification sound could not be played'
)
@@ -128,7 +129,7 @@ export function NotificationStep({
const handleSendTestNotification = async (): Promise<void> => {
if (!notificationSettings) {
toast.error(
translate(
t(
'auto.components.onboarding.NotificationStep.3cd5374e22',
'Notification settings are still loading'
)
@@ -155,7 +156,7 @@ export function NotificationStep({
if (!notificationSettings) {
return (
<div className="rounded-xl border border-border bg-muted/20 px-5 py-4 text-sm text-muted-foreground">
{translate(
{t(
'auto.components.onboarding.NotificationStep.e52aacf380',
'Loading notification settings…'
)}
@@ -174,10 +175,10 @@ export function NotificationStep({
<section className="space-y-3">
<div className="space-y-1">
<h2 className="text-sm font-semibold text-foreground">
{translate('auto.components.onboarding.NotificationStep.0af746e41f', 'Choose a sound')}
{t('auto.components.onboarding.NotificationStep.0af746e41f', 'Choose a sound')}
</h2>
<p className="text-[13px] leading-relaxed text-muted-foreground">
{translate(
{t(
'auto.components.onboarding.NotificationStep.0fe570690c',
'Pick the alert Orca plays after a desktop notification is delivered.'
)}
@@ -192,7 +193,7 @@ export function NotificationStep({
>
<SelectTrigger className="w-56 max-w-full" size="sm">
<SelectValue
placeholder={translate(
placeholder={t(
'auto.components.onboarding.NotificationStep.dc897423e1',
'Choose notification sound'
)}
@@ -217,11 +218,11 @@ export function NotificationStep({
<Upload className="size-4" />
<span>
{customPath
? translate(
? t(
'auto.components.onboarding.NotificationStep.ac80d97e02',
'Change custom file'
)
: translate(
: t(
'auto.components.onboarding.NotificationStep.c0692baa52',
'Choose custom file'
)}
@@ -237,10 +238,7 @@ export function NotificationStep({
onClick={() => void handleSendTestNotification()}
>
<BellRing className="size-3.5" />
{translate(
'auto.components.onboarding.NotificationStep.3bede04483',
'Send test notification'
)}
{t('auto.components.onboarding.NotificationStep.3bede04483', 'Send test notification')}
</Button>
</div>
</section>
@@ -15,7 +15,7 @@ import { OnboardingSkipConfirmationDialog } from './OnboardingSkipConfirmationDi
import { OnboardingFooter } from './OnboardingFooter'
import { shouldRequestOnboardingSkipConfirmation } from './onboarding-dismiss-target'
import logo from '../../../../../resources/logo.svg'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router'
import { useScrollEdgeFades } from '@/lib/use-scroll-edge-fades'
import { AgentSurfaceStep } from './AgentSurfaceStep'
@@ -30,6 +30,7 @@ export default function OnboardingFlow({
onboarding,
onOnboardingChange
}: OnboardingFlowProps): React.JSX.Element {
const t = useTranslate()
const flow = useOnboardingFlow(onboarding, onOnboardingChange)
const stepFades = useScrollEdgeFades<HTMLDivElement>()
const continueShortcutModifierLabel = getScreenSubmitModifierLabel()
@@ -43,8 +44,8 @@ export default function OnboardingFlow({
const footerPrimaryLabel =
busyLabel ??
(currentStep.id === 'notifications' && !flow.hasSelectedWorkspace
? translate('components.onboarding.flow.actions.addFirstProject', 'Add your first project')
: translate('components.onboarding.flow.actions.continue', 'Continue'))
? t('components.onboarding.flow.actions.addFirstProject', 'Add your first project')
: t('components.onboarding.flow.actions.continue', 'Continue'))
const [languagePickerOpen, setLanguagePickerOpen] = useState(false)
const [skipConfirmOpen, setSkipConfirmOpen] = useState(false)
const skipConfirmAdvancedViaRef = useRef<'button' | 'keyboard'>('button')
@@ -120,10 +121,7 @@ export default function OnboardingFlow({
<section
ref={flow.setLifecycleRootRef}
role="dialog"
aria-label={translate(
'auto.components.onboarding.OnboardingFlow.277ba45540',
'Orca onboarding'
)}
aria-label={t('auto.components.onboarding.OnboardingFlow.277ba45540', 'Orca onboarding')}
aria-modal="true"
data-onboarding-modal
className={cn(
@@ -140,9 +138,7 @@ export default function OnboardingFlow({
aria-hidden="true"
className="h-7 w-auto shrink-0 invert dark:invert-0"
/>
<span>
{translate('auto.components.onboarding.OnboardingFlow.a249f81538', 'Orca')}
</span>
<span>{t('auto.components.onboarding.OnboardingFlow.a249f81538', 'Orca')}</span>
</div>
<OnboardingLanguageSwitcher
open={languagePickerOpen}
@@ -169,7 +165,7 @@ export default function OnboardingFlow({
? 'w-6 bg-muted-foreground/70 hover:bg-foreground/80'
: 'w-6 bg-muted-foreground/25 hover:bg-muted-foreground/45'
)}
aria-label={translate(
aria-label={t(
'auto.components.onboarding.OnboardingFlow.adaa0aa627',
'Go to onboarding step {{value0}}: {{value1}}',
{ value0: progressIdx + 1, value1: stepTooltipLabel }
@@ -185,7 +181,7 @@ export default function OnboardingFlow({
)
})}
<span className="ml-3 text-xs font-medium text-muted-foreground">
{translate(
{t(
'auto.components.onboarding.OnboardingFlow.stepOfTotal',
'{{current}} of {{total}}',
{ current: flow.progressStepIndex + 1, total: flow.progressSteps.length }
@@ -196,10 +192,7 @@ export default function OnboardingFlow({
<div className="mt-8 shrink-0">
{stepIndex === 0 && (
<div className="mb-2 text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">
{translate(
'auto.components.onboarding.OnboardingFlow.1b5e182e9f',
'Welcome to Orca'
)}
{t('auto.components.onboarding.OnboardingFlow.1b5e182e9f', 'Welcome to Orca')}
</div>
)}
<h1 className="text-[34px] font-semibold leading-[1.15] tracking-tight text-foreground">
@@ -1,5 +1,5 @@
import { ChevronLeft, CornerDownLeft, Loader2 } from 'lucide-react'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
type OnboardingFooterProps = {
shouldShowSetUpLater: boolean
@@ -26,6 +26,7 @@ export function OnboardingFooter({
shortcutModifierLabel,
onPrimary
}: OnboardingFooterProps): React.JSX.Element {
const t = useTranslate()
return (
<footer className="mt-6 flex flex-none items-center justify-between border-t border-border pt-5">
{shouldShowSetUpLater ? (
@@ -34,7 +35,7 @@ export function OnboardingFooter({
disabled={Boolean(busyLabel)}
onClick={onSetUpLater}
>
{translate('auto.components.onboarding.OnboardingFooter.111d3f8d92', 'Set up later')}
{t('auto.components.onboarding.OnboardingFooter.111d3f8d92', 'Set up later')}
</button>
) : (
<span />
@@ -47,7 +48,7 @@ export function OnboardingFooter({
onClick={onBack}
>
<ChevronLeft className="size-4" />
{translate('auto.components.onboarding.OnboardingFooter.ba58547306', 'Back')}
{t('auto.components.onboarding.OnboardingFooter.ba58547306', 'Back')}
</button>
)}
{showPrimary && (
@@ -9,7 +9,7 @@ import {
type TerminalCommandFinishedEventDetail
} from '@/hooks/terminal-command-finished-event'
import { useAppStore } from '@/store'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { brandEphemeralSetupTerminalWorktreeId } from '../../../../shared/ephemeral-setup-terminal-worktree-id'
const ONBOARDING_INLINE_TERMINAL_WORKTREE_ID = 'onboarding-inline-terminal'
@@ -58,6 +58,7 @@ export function OnboardingInlineCommandTerminal({
onTerminalExit,
onCommandFinished
}: OnboardingInlineCommandTerminalProps): React.JSX.Element {
const t = useTranslate()
// Brand synthetic IDs so remote runtimes recognize the floating terminal scope.
const worktreeId = useMemo(
() => brandEphemeralSetupTerminalWorktreeId(worktreeIdProp),
@@ -329,7 +330,7 @@ export function OnboardingInlineCommandTerminal({
) : (
<div className="flex h-full items-center justify-center gap-2 text-xs text-muted-foreground">
<Loader2 className="size-4 animate-spin" />
{translate(
{t(
'auto.components.onboarding.OnboardingInlineCommandTerminal.4123609efd',
'Starting terminal...'
)}
@@ -1,10 +1,11 @@
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { useAppStore } from '@/store'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { LoadingSpinner } from '@/components/LoadingSpinner'
export function OnboardingLanguageLoading(): React.JSX.Element {
const t = useTranslate()
const settings = useAppStore((state) => state.settings)
const fetchSettings = useAppStore((state) => state.fetchSettings)
const [retrying, setRetrying] = useState(false)
@@ -16,7 +17,7 @@ export function OnboardingLanguageLoading(): React.JSX.Element {
{settings === null && !retrying ? (
<div className="space-y-3 text-center">
<p className="text-sm">
{translate('onboarding.language.settingsError', 'Could not load your settings.')}
{t('onboarding.language.settingsError', 'Could not load your settings.')}
</p>
<Button
disabled={retrying}
@@ -27,15 +28,15 @@ export function OnboardingLanguageLoading(): React.JSX.Element {
)
}}
>
{translate('onboarding.language.retry', 'Retry')}
{t('onboarding.language.retry', 'Retry')}
</Button>
</div>
) : (
<div role="status" className="flex items-center gap-2 text-sm text-muted-foreground">
<LoadingSpinner className="size-4" />
{settings === null
? translate('onboarding.language.loadingSettings', 'Loading settings…')
: translate('onboarding.language.loading', 'Loading language…')}
? t('onboarding.language.loadingSettings', 'Loading settings…')
: t('onboarding.language.loading', 'Loading language…')}
</div>
)}
</div>
@@ -5,7 +5,8 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { i18n, translate, loadRendererUiLanguage } from '@/i18n/i18n'
import { i18n, loadRendererUiLanguage } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { getUiLanguageNativeLabel } from '@/i18n/supported-languages'
import { useLanguageReadiness } from '@/i18n/language-readiness'
import { track } from '@/lib/telemetry'
@@ -25,6 +26,7 @@ export function OnboardingLanguageSwitcher({
open: boolean
onOpenChange: (open: boolean) => void
}): React.JSX.Element | null {
const t = useTranslate()
const cohort = useAppStore((state) => state.settings?.telemetry?.onboardingLanguageCohort)
const language = useAppStore((state) => state.settings?.uiLanguage ?? 'system')
const updateSettings = useAppStore((state) => state.updateSettingsOrThrow)
@@ -144,7 +146,7 @@ export function OnboardingLanguageSwitcher({
<PopoverContent
ref={contentRef}
align="end"
aria-label={translate('onboarding.language.title', 'Display language')}
aria-label={t('onboarding.language.title', 'Display language')}
className="z-[110] w-80 max-w-[calc(100vw-2rem)]"
wheelScroll
onOpenAutoFocus={(event) => {
@@ -179,10 +181,10 @@ export function OnboardingLanguageSwitcher({
}}
>
<ArrowLeft />
{translate('onboarding.language.back', 'Languages')}
{t('onboarding.language.back', 'Languages')}
</Button>
<Label htmlFor="onboarding-language-request">
{translate(
{t(
'onboarding.language.requestPrompt',
'Which language would you like Orca to support?'
)}
@@ -200,7 +202,7 @@ export function OnboardingLanguageSwitcher({
/>
{requestState === 'success' ? (
<p role="status" className="text-xs text-muted-foreground">
{translate(
{t(
'onboarding.language.requestSuccess',
'Language request recorded. You can continue onboarding.'
)}
@@ -209,7 +211,7 @@ export function OnboardingLanguageSwitcher({
<>
{requestState === 'error' && (
<p role="alert" className="text-xs text-destructive">
{translate(
{t(
'onboarding.language.requestError',
'Could not record your request. Try again.'
)}
@@ -223,8 +225,8 @@ export function OnboardingLanguageSwitcher({
}
>
{requestState === 'error'
? translate('onboarding.language.retry', 'Retry')
: translate('onboarding.language.requestAction', 'Request language')}
? t('onboarding.language.retry', 'Retry')
: t('onboarding.language.requestAction', 'Request language')}
</Button>
</>
)}
@@ -233,20 +235,20 @@ export function OnboardingLanguageSwitcher({
<div className="space-y-3">
<div className="space-y-1">
<p className="text-sm font-medium">
{translate('onboarding.language.title', 'Display language')}
{t('onboarding.language.title', 'Display language')}
</p>
<p className="text-xs text-muted-foreground">
{detection.status === 'supported'
? translate('onboarding.language.device', 'System language: {{language}}', {
? t('onboarding.language.device', 'System language: {{language}}', {
language: getUiLanguageNativeLabel(detection.locale)
})
: detection.status === 'unsupported'
? translate(
? t(
'onboarding.language.unsupported',
'System language: {{language}}. This language is not supported yet.',
{ language: detection.detectedTag }
)
: translate(
: t(
'onboarding.language.unknown',
'Your system language could not be detected. Choose a language below.'
)}
@@ -280,7 +282,7 @@ export function OnboardingLanguageSwitcher({
void chooseLanguage(locale)
}
}}
aria-label={translate('onboarding.language.title', 'Display language')}
aria-label={t('onboarding.language.title', 'Display language')}
>
{SUPPORTED_UI_LOCALES.map((locale) => (
<div key={locale} className="rounded-md px-2 py-2 hover:bg-accent">
@@ -300,20 +302,20 @@ export function OnboardingLanguageSwitcher({
</RadioGroup>
<p role="status" className="h-4 text-xs text-muted-foreground">
{selectionPhase === 'save'
? translate('onboarding.language.saving', 'Saving language…')
? t('onboarding.language.saving', 'Saving language…')
: busy
? translate('onboarding.language.loading', 'Loading language…')
? t('onboarding.language.loading', 'Loading language…')
: null}
</p>
{(selectionFailure !== null || catalogError) && (
<div role="alert" className="space-y-2">
<p className="text-xs text-destructive">
{selectionFailure?.stage === 'save'
? translate(
? t(
'onboarding.language.saveError',
'Could not save your language preference. Try again.'
)
: translate(
: t(
'onboarding.language.loadError',
'Could not load this language. Try again.'
)}
@@ -331,7 +333,7 @@ export function OnboardingLanguageSwitcher({
}
}}
>
{translate('onboarding.language.retry', 'Retry')}
{t('onboarding.language.retry', 'Retry')}
</Button>
</div>
)}
@@ -344,7 +346,7 @@ export function OnboardingLanguageSwitcher({
disabled={busy}
onClick={() => setRequestView(true)}
>
{translate('onboarding.language.other', 'Other language…')}
{t('onboarding.language.other', 'Other language…')}
</Button>
</div>
)}
@@ -1,6 +1,6 @@
import { MessagesSquare, SquareTerminal } from 'lucide-react'
import { AgentIcon, getAgentLabel } from '@/lib/agent-catalog'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
// Shell text, not prose: what a terminal shows when the agent's CLI starts.
const DEMO_PROMPT = '~/uploader ❯'
@@ -8,7 +8,8 @@ const DEMO_COMMAND = 'gemini'
// Pixel positions here are the keyframe targets in main.css (.onboarding-launch-demo).
export function TerminalLaunchDemo(): React.JSX.Element {
const cli = translate('auto.components.tab.bar.QuickLaunchButton.cliTag', '(CLI)')
const t = useTranslate()
const cli = t('auto.components.tab.bar.QuickLaunchButton.cliTag', '(CLI)')
return (
<div
aria-hidden
@@ -35,10 +36,10 @@ export function TerminalLaunchDemo(): React.JSX.Element {
</div>
<div className="absolute inset-x-0 bottom-0 top-7 flex flex-col gap-1.5 p-2.5">
<span className="max-w-[75%] self-end rounded-lg bg-foreground px-2 py-1 text-background">
{translate('components.onboarding.launchDemo.userMessage', 'Fix the flaky upload test')}
{t('components.onboarding.launchDemo.userMessage', 'Fix the flaky upload test')}
</span>
<span className="max-w-[80%] rounded-lg border border-border px-2 py-1">
{translate(
{t(
'components.onboarding.launchDemo.agentMessage',
"Found it. The retry timer wasn't mocked."
)}
@@ -52,7 +53,7 @@ export function TerminalLaunchDemo(): React.JSX.Element {
<span className="text-status-success">{DEMO_PROMPT}</span> {DEMO_COMMAND}
</div>
<div className="text-muted-foreground">
{translate('components.onboarding.launchDemo.cliReady', '{{agent}} CLI ready.', {
{t('components.onboarding.launchDemo.cliReady', '{{agent}} CLI ready.', {
agent: getAgentLabel('gemini')
})}
</div>
@@ -2,7 +2,7 @@ import type React from 'react'
import { useEffect, useRef, useState } from 'react'
import { Check, ChevronLeft, ChevronRight, ExternalLink, Puzzle } from 'lucide-react'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { Button } from '@/components/ui/button'
import type { ThemeLook } from './onboarding-theme-looks'
import { ThemeLookCardPreview } from './ThemeLookPreview'
@@ -37,6 +37,7 @@ export function ThemeLookCarousel({
ariaLabel: string
header: React.ReactNode
}): React.JSX.Element {
const t = useTranslate()
const trackRef = useRef<HTMLDivElement>(null)
const [edges, setEdges] = useState<ScrollEdges>({ start: true, end: false })
const tabStopId = looks.some((look) => look.id === selectedId) ? selectedId : looks[0]?.id
@@ -90,7 +91,7 @@ export function ThemeLookCarousel({
variant="ghost"
size="icon-sm"
disabled={edges.start}
aria-label={translate('components.onboarding.theme.looks.previous', 'Previous looks')}
aria-label={t('components.onboarding.theme.looks.previous', 'Previous looks')}
onClick={() => page(-1)}
>
<ChevronLeft />
@@ -99,7 +100,7 @@ export function ThemeLookCarousel({
variant="ghost"
size="icon-sm"
disabled={edges.end}
aria-label={translate('components.onboarding.theme.looks.next', 'More looks')}
aria-label={t('components.onboarding.theme.looks.next', 'More looks')}
onClick={() => page(1)}
>
<ChevronRight />
@@ -162,10 +163,10 @@ export function ThemeLookCarousel({
>
<Puzzle className="size-4" />
<span className="text-xs font-medium">
{translate('components.onboarding.theme.looks.moreThemes', 'More themes')}
{t('components.onboarding.theme.looks.moreThemes', 'More themes')}
</span>
<span className="flex items-center gap-1 text-2xs">
{translate('components.onboarding.theme.looks.browsePlugins', 'Browse plugins')}
{t('components.onboarding.theme.looks.browsePlugins', 'Browse plugins')}
<ExternalLink className="size-2.5" />
</span>
</button>
@@ -1,6 +1,6 @@
import type React from 'react'
import { useMemo } from 'react'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { useUiLanguageRevision } from '@/i18n/use-ui-language-revision'
import { usePluginTerminalThemes, usePluginThemes } from '@/store/plugins/appearance/plugin-themes'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
@@ -19,6 +19,7 @@ export function useThemeLooks(
selected: Record<ThemeLookBase, string | null>
pluginTerminalThemes: ReturnType<typeof usePluginTerminalThemes>
} {
const t = useTranslate()
const pluginThemes = usePluginThemes()
const pluginTerminalThemes = usePluginTerminalThemes()
const languageRevision = useUiLanguageRevision()
@@ -27,18 +28,18 @@ export function useThemeLooks(
return { dark: [], light: [] }
}
const labels = {
orca: translate('components.onboarding.theme.looks.sourceBuiltIn', 'Built in'),
community: translate('components.onboarding.theme.looks.sourceCommunity', 'Terminal palette'),
orca: t('components.onboarding.theme.looks.sourceBuiltIn', 'Built in'),
community: t('components.onboarding.theme.looks.sourceCommunity', 'Terminal palette'),
builtIn: (base: ThemeLookBase) =>
base === 'dark'
? translate('components.onboarding.theme.looks.orcaDark', 'Orca Dark')
: translate('components.onboarding.theme.looks.orcaLight', 'Orca Light')
? t('components.onboarding.theme.looks.orcaDark', 'Orca Dark')
: t('components.onboarding.theme.looks.orcaLight', 'Orca Light')
}
const build = (base: ThemeLookBase): ThemeLook[] =>
buildThemeLooks({ base, pluginThemes, pluginTerminalThemes, baseline, labels })
buildThemeLooks({ base, pluginThemes, pluginTerminalThemes, baseline, labels }, t)
return { dark: build('dark'), light: build('light') }
// eslint-disable-next-line react-hooks/exhaustive-deps -- languageRevision re-reads translated labels
}, [baseline, pluginThemes, pluginTerminalThemes, languageRevision])
}, [baseline, pluginThemes, pluginTerminalThemes, languageRevision, t])
const selected = {
dark: settings ? selectedThemeLookId(settings, 'dark', looks.dark) : null,
light: settings ? selectedThemeLookId(settings, 'light', looks.light) : null
@@ -56,21 +57,22 @@ export function ThemeSlotControl({
osBase: ThemeLookBase
onChange: (base: ThemeLookBase) => void
}): React.JSX.Element {
const t = useTranslate()
const label = (base: ThemeLookBase): React.ReactNode => (
<span className="flex items-center gap-1.5 whitespace-nowrap">
{base === 'dark'
? translate('components.onboarding.theme.looks.darkLook', 'Dark look')
: translate('components.onboarding.theme.looks.lightLook', 'Light look')}
? t('components.onboarding.theme.looks.darkLook', 'Dark look')
: t('components.onboarding.theme.looks.lightLook', 'Light look')}
{base === osBase && (
<span className="text-2xs font-normal text-muted-foreground">
{translate('components.onboarding.theme.looks.activeNow', 'now')}
{t('components.onboarding.theme.looks.activeNow', 'now')}
</span>
)}
</span>
)
return (
<SettingsSegmentedControl
ariaLabel={translate('components.onboarding.theme.looks.slotLabel', 'Look to edit')}
ariaLabel={t('components.onboarding.theme.looks.slotLabel', 'Look to edit')}
value={value}
onChange={onChange}
equalWidth
@@ -1,7 +1,7 @@
import type React from 'react'
import { Check, Monitor, Moon, Sun } from 'lucide-react'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import { ChromePreview } from './theme-chrome-preview'
@@ -13,6 +13,7 @@ export function ThemeModeTiles({
value: GlobalSettings['theme']
onChange: (theme: GlobalSettings['theme']) => void
}): React.JSX.Element {
const t = useTranslate()
const themes: {
id: GlobalSettings['theme']
label: string
@@ -21,20 +22,20 @@ export function ThemeModeTiles({
}[] = [
{
id: 'system',
label: translate('auto.components.onboarding.ThemeStep.827ea7b4a2', 'System'),
hint: translate('components.onboarding.theme.hints.system', 'Match OS'),
label: t('auto.components.onboarding.ThemeStep.827ea7b4a2', 'System'),
hint: t('components.onboarding.theme.hints.system', 'Match OS'),
icon: Monitor
},
{
id: 'dark',
label: translate('auto.components.onboarding.ThemeStep.fa7b673ea9', 'Dark'),
hint: translate('components.onboarding.theme.hints.dark', 'Easy on the eyes'),
label: t('auto.components.onboarding.ThemeStep.fa7b673ea9', 'Dark'),
hint: t('components.onboarding.theme.hints.dark', 'Easy on the eyes'),
icon: Moon
},
{
id: 'light',
label: translate('auto.components.onboarding.ThemeStep.ad192706e6', 'Light'),
hint: translate('components.onboarding.theme.hints.light', 'Bright & crisp'),
label: t('auto.components.onboarding.ThemeStep.ad192706e6', 'Light'),
hint: t('components.onboarding.theme.hints.light', 'Bright & crisp'),
icon: Sun
}
]
@@ -8,6 +8,7 @@ import { GhosttyDiscoveryRow } from './GhosttyDiscoveryRow'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import type { DiscoveryStatusEmitted } from '../../../../shared/onboarding-state-types'
import { i18n, translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { ThemeSlotControl, useThemeLooks } from './ThemeLookGallery'
import { ThemeLookCarousel, openThemePluginDirectory } from './ThemeLookCarousel'
import { ThemeModeTiles } from './ThemeModeTiles'
@@ -77,6 +78,7 @@ export function ThemeStep({
lookBaseline,
onLookBaselineChange
}: ThemeStepProps) {
const t = useTranslate()
const [importing, setImporting] = useState(false)
const [discovery, setDiscovery] = useState<DiscoveryState>({ status: 'idle' })
const mountedRef = useMountedRef()
@@ -138,7 +140,7 @@ export function ThemeStep({
if (!resolved.found || Object.keys(resolved.diff).length === 0) {
if (mountedRef.current) {
toast.info(
translate(
t(
'auto.components.onboarding.ThemeStep.16a9f0446a',
'No Ghostty settings found to import'
)
@@ -172,10 +174,7 @@ export function ThemeStep({
} catch (err) {
if (mountedRef.current) {
toast.error(
translate(
'auto.components.onboarding.ThemeStep.699ddf83c2',
'Failed to import Ghostty settings'
),
t('auto.components.onboarding.ThemeStep.699ddf83c2', 'Failed to import Ghostty settings'),
{
description: err instanceof Error ? err.message : String(err)
}
@@ -208,8 +207,8 @@ export function ThemeStep({
<div className="space-y-5">
<section className="space-y-2.5">
<ThemeSectionHeading
title={translate('components.onboarding.theme.looks.colorMode', 'Color mode')}
caption={translate(
title={t('components.onboarding.theme.looks.colorMode', 'Color mode')}
caption={t(
'components.onboarding.theme.looks.colorModeCaption',
'Light, dark, or follow your OS.'
)}
@@ -229,25 +228,25 @@ export function ThemeStep({
onPick={pickLook}
ariaLabel={
base === 'dark'
? translate('components.onboarding.theme.looks.darkLooks', 'Dark looks')
: translate('components.onboarding.theme.looks.lightLooks', 'Light looks')
? t('components.onboarding.theme.looks.darkLooks', 'Dark looks')
: t('components.onboarding.theme.looks.lightLooks', 'Light looks')
}
header={
<div className="flex flex-wrap items-center gap-x-3 gap-y-1.5">
<ThemeSectionHeading
title={translate('components.onboarding.theme.looks.themes', 'Theme')}
title={t('components.onboarding.theme.looks.themes', 'Theme')}
caption={
theme === 'system' && base !== osBase
? base === 'dark'
? translate(
? t(
'components.onboarding.theme.looks.showsWhenDark',
'Shows when your system is dark'
)
: translate(
: t(
'components.onboarding.theme.looks.showsWhenLight',
'Shows when your system is light'
)
: translate(
: t(
'components.onboarding.theme.looks.themesCaption',
'Optional. Go further with app themes and terminal palettes.'
)
@@ -273,7 +272,7 @@ export function ThemeStep({
<div className="flex items-center gap-2">
<Puzzle className="size-3.5" />
<span>
{translate(
{t(
'components.onboarding.theme.looks.pluginsHint',
'More themes are available as plugins.'
)}
@@ -283,10 +282,7 @@ export function ThemeStep({
className="inline-flex items-center gap-1 font-medium text-foreground underline-offset-4 hover:underline"
onClick={openThemePluginDirectory}
>
{translate(
'components.onboarding.theme.looks.browseThemePlugins',
'Browse theme plugins'
)}
{t('components.onboarding.theme.looks.browseThemePlugins', 'Browse theme plugins')}
<ExternalLink className="size-3" />
</button>
</div>
@@ -295,7 +291,7 @@ export function ThemeStep({
<span>
<Trans
i18n={i18n}
defaults={translate(
defaults={t(
'auto.components.onboarding.ThemeStep.moreTerminalOptions',
'More terminal options, including font, cursor, and palette, in <settings>Settings → Terminal</settings>'
)}
@@ -14,7 +14,7 @@ import {
SelectTrigger,
SelectValue
} from '@/components/ui/select'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
type WindowsTerminalStepProps = {
settings: GlobalSettings | null
@@ -52,6 +52,7 @@ export function WindowsTerminalStep({
settings,
updateSettings
}: WindowsTerminalStepProps): React.JSX.Element {
const t = useTranslate()
const capabilities = useWindowsTerminalCapabilities(Boolean(settings), true)
const [selectPortalRoot, setSelectPortalRoot] = useState<HTMLElement | null>(null)
const windowsShell = normalizeWindowsShell(settings?.terminalWindowsShell)
@@ -72,24 +73,21 @@ export function WindowsTerminalStep({
const shellOptions: ShellOption[] = [
{
value: 'powershell.exe',
label: translate('auto.components.onboarding.WindowsTerminalStep.powerShell', 'PowerShell'),
label: t('auto.components.onboarding.WindowsTerminalStep.powerShell', 'PowerShell'),
description: capabilities.pwshAvailable
? translate(
? t(
'auto.components.onboarding.WindowsTerminalStep.powerShellPwsh',
'Uses PowerShell 7+ when available, with Windows PowerShell as fallback.'
)
: translate(
: t(
'auto.components.onboarding.WindowsTerminalStep.powerShellInbox',
'Uses the Windows PowerShell available on every supported Windows install.'
)
},
{
value: 'cmd.exe',
label: translate(
'auto.components.onboarding.WindowsTerminalStep.commandPrompt',
'Command Prompt'
),
description: translate(
label: t('auto.components.onboarding.WindowsTerminalStep.commandPrompt', 'Command Prompt'),
description: t(
'auto.components.onboarding.WindowsTerminalStep.commandPromptDescription',
'Opens new terminal panes with classic cmd.exe behavior.'
)
@@ -98,13 +96,13 @@ export function WindowsTerminalStep({
? [
{
value: WINDOWS_GIT_BASH_SHELL,
label: translate('auto.components.onboarding.WindowsTerminalStep.gitBash', 'Git Bash'),
label: t('auto.components.onboarding.WindowsTerminalStep.gitBash', 'Git Bash'),
description: capabilities.gitBashAvailable
? translate(
? t(
'auto.components.onboarding.WindowsTerminalStep.gitBashDescription',
'Uses Git for Windows bash.exe for Unix-style shell workflows.'
)
: translate(
: t(
'auto.components.onboarding.WindowsTerminalStep.gitBashUnavailable',
'Selected, but Git Bash was not detected on this machine.'
),
@@ -116,13 +114,13 @@ export function WindowsTerminalStep({
? [
{
value: 'wsl.exe',
label: translate('auto.components.onboarding.WindowsTerminalStep.wsl', 'WSL'),
label: t('auto.components.onboarding.WindowsTerminalStep.wsl', 'WSL'),
description: capabilities.wslAvailable
? translate(
? t(
'auto.components.onboarding.WindowsTerminalStep.wslDescription',
'Starts new terminal panes inside your Windows Subsystem for Linux default.'
)
: translate(
: t(
'auto.components.onboarding.WindowsTerminalStep.wslUnavailable',
'Selected, but WSL was not detected on this machine.'
),
@@ -135,22 +133,22 @@ export function WindowsTerminalStep({
const rightClickOptions: RightClickOption[] = [
{
value: 'paste',
label: translate(
label: t(
'auto.components.onboarding.WindowsTerminalStep.rightClickPaste',
'Paste on right-click'
),
description: translate(
description: t(
'auto.components.onboarding.WindowsTerminalStep.rightClickPasteDescription',
'Right-click pastes the clipboard. Ctrl+right-click opens the context menu.'
)
},
{
value: 'menu',
label: translate(
label: t(
'auto.components.onboarding.WindowsTerminalStep.rightClickMenu',
'Open context menu'
),
description: translate(
description: t(
'auto.components.onboarding.WindowsTerminalStep.rightClickMenuDescription',
'Right-click opens the terminal menu. Paste from the menu or keyboard.'
)
@@ -160,7 +158,7 @@ export function WindowsTerminalStep({
if (!settings) {
return (
<div className="rounded-xl border border-border bg-muted/20 px-5 py-4 text-sm text-muted-foreground">
{translate(
{t(
'auto.components.onboarding.WindowsTerminalStep.loading',
'Loading terminal settings...'
)}
@@ -178,13 +176,10 @@ export function WindowsTerminalStep({
<section className="space-y-3">
<div className="space-y-1">
<h2 className="text-sm font-semibold text-foreground">
{translate(
'auto.components.onboarding.WindowsTerminalStep.defaultShell',
'Default Shell'
)}
{t('auto.components.onboarding.WindowsTerminalStep.defaultShell', 'Default Shell')}
</h2>
<p className="text-[13px] leading-relaxed text-muted-foreground">
{translate(
{t(
'auto.components.onboarding.WindowsTerminalStep.defaultShellDescription',
'Choose the shell Orca opens for new Windows terminal panes.'
)}
@@ -210,13 +205,13 @@ export function WindowsTerminalStep({
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0 space-y-1">
<div className="text-sm font-medium text-foreground">
{translate(
{t(
'auto.components.onboarding.WindowsTerminalStep.wslDistribution',
'WSL Distribution'
)}
</div>
<p className="text-[13px] leading-relaxed text-muted-foreground">
{translate(
{t(
'auto.components.onboarding.WindowsTerminalStep.wslDistributionDescription',
'Use the Windows default distribution or choose a specific installed distro.'
)}
@@ -233,7 +228,7 @@ export function WindowsTerminalStep({
>
<SelectTrigger
size="sm"
aria-label={translate(
aria-label={t(
'auto.components.onboarding.WindowsTerminalStep.wslDistribution',
'WSL Distribution'
)}
@@ -242,11 +237,11 @@ export function WindowsTerminalStep({
<SelectValue
placeholder={
capabilities.isLoading
? translate(
? t(
'auto.components.onboarding.WindowsTerminalStep.loadingDistros',
'Loading distributions'
)
: translate(
: t(
'auto.components.onboarding.WindowsTerminalStep.windowsDefault',
'Windows default'
)
@@ -259,7 +254,7 @@ export function WindowsTerminalStep({
className="z-[120] w-[--radix-select-trigger-width]"
>
<SelectItem value={DEFAULT_WSL_DISTRO_VALUE}>
{translate(
{t(
'auto.components.onboarding.WindowsTerminalStep.windowsDefault',
'Windows default'
)}
@@ -279,13 +274,13 @@ export function WindowsTerminalStep({
<section className="space-y-3">
<div className="space-y-1">
<h2 className="text-sm font-semibold text-foreground">
{translate(
{t(
'auto.components.onboarding.WindowsTerminalStep.rightClickBehavior',
'Right-click behavior'
)}
</h2>
<p className="text-[13px] leading-relaxed text-muted-foreground">
{translate(
{t(
'auto.components.onboarding.WindowsTerminalStep.rightClickBehaviorDescription',
'Pick the terminal mouse behavior that matches your Windows muscle memory.'
)}
@@ -299,7 +294,7 @@ export function WindowsTerminalStep({
void updateSettings({ terminalRightClickToPaste: value === 'paste' })
}
options={rightClickOptions}
ariaLabel={translate(
ariaLabel={t(
'auto.components.onboarding.WindowsTerminalStep.rightClickBehavior',
'Right-click behavior'
)}
@@ -1,7 +1,7 @@
import { useId, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Switch } from '@/components/ui/switch'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { getAgentLabel } from '@/lib/agent-catalog'
import { useAppStore } from '@/store'
import {
@@ -17,6 +17,7 @@ export function YoloPermissionsControl({
yoloPermissions: boolean
onYoloPermissionsChange?: (enabled: boolean) => void
}): React.JSX.Element {
const t = useTranslate()
const switchId = useId()
const overrides = useAppStore((s) => s.settings?.agentPermissionOverrides)
const [saving, setSaving] = useState(false)
@@ -45,10 +46,10 @@ export function YoloPermissionsControl({
<div className="flex items-center justify-between gap-6">
<label htmlFor={switchId} className="min-w-0 cursor-pointer space-y-0.5">
<span className="block text-sm font-medium text-foreground">
{translate('auto.components.onboarding.AgentStep.yoloModeLabel', 'Yolo mode')}
{t('auto.components.onboarding.AgentStep.yoloModeLabel', 'Yolo mode')}
</span>
<span className="block text-xs text-muted-foreground">
{translate(
{t(
'auto.components.onboarding.AgentStep.yoloPermissionsDescription',
'Agents run commands and edit files without asking, and some also bypass their sandbox. Use only in projects you trust.'
)}
@@ -67,7 +68,7 @@ export function YoloPermissionsControl({
className="scrollbar-sleek max-h-48 space-y-3 overflow-y-auto rounded-lg border border-status-warning-border bg-status-warning-background px-4 py-3 text-xs text-foreground"
>
<p>
{translate(
{t(
'components.onboarding.yoloPermissions.overrideDescription',
'These agents can still run commands and edit files without asking. Their individual settings override Yolo mode.'
)}
@@ -75,7 +76,7 @@ export function YoloPermissionsControl({
{fullAccessAgents.map((agent) => (
<div key={agent} className="flex flex-wrap items-center justify-between gap-2">
<span className="font-medium">
{translate(
{t(
'components.onboarding.yoloPermissions.fullAccessOverride',
'{{agent}} still has full access',
{ agent: getAgentLabel(agent) }
@@ -86,17 +87,14 @@ export function YoloPermissionsControl({
variant="outline"
size="xs"
disabled={saving}
aria-label={translate(
aria-label={t(
'components.onboarding.yoloPermissions.useAgentOwnSettings',
'Use {{agent}}’s own settings',
{ agent: getAgentLabel(agent) }
)}
onClick={() => void handleUseOwnSettings(agent)}
>
{translate(
'components.onboarding.yoloPermissions.useOwnSettings',
'Use own settings'
)}
{t('components.onboarding.yoloPermissions.useOwnSettings', 'Use own settings')}
</Button>
</div>
))}
@@ -104,14 +102,14 @@ export function YoloPermissionsControl({
)}
{error && (
<p role="alert" className="text-xs text-destructive">
{translate(
{t(
'components.onboarding.yoloPermissions.saveError',
'Could not change permissions. Try again.'
)}
</p>
)}
<p className="text-xs text-muted-foreground">
{translate(
{t(
'components.onboarding.yoloPermissions.ownSettingsDescription',
'Off uses each agent’s own settings. Individual agent permissions can override this switch.'
)}
@@ -11,6 +11,7 @@ import {
} from '../plugins/presentation/plugin-name-disambiguation'
import { resolveTerminalPalette } from '../../../../shared/terminal-themes/terminal-theme-selection'
import { TERMINAL_THEME_CATALOG } from '../../../../shared/terminal-themes/terminal-theme-catalog'
import type { Translate } from '../../i18n/use-translate'
export type ThemeLookBase = 'light' | 'dark'
@@ -213,13 +214,16 @@ function terminalLook(name: string, base: ThemeLookBase, source: string): ThemeL
/** Every look the theme step offers for one mode: Orca's own first, then installed plugin
* themes, then popular terminal palettes painted through Match Terminal. */
export function buildThemeLooks(options: {
base: ThemeLookBase
pluginThemes: readonly PluginThemeRegistration[]
pluginTerminalThemes: readonly PluginTerminalThemeRegistration[]
baseline: GlobalSettings
labels: { orca: string; builtIn: (base: ThemeLookBase) => string; community: string }
}): ThemeLook[] {
export function buildThemeLooks(
options: {
base: ThemeLookBase
pluginThemes: readonly PluginThemeRegistration[]
pluginTerminalThemes: readonly PluginTerminalThemeRegistration[]
baseline: GlobalSettings
labels: { orca: string; builtIn: (base: ThemeLookBase) => string; community: string }
},
t?: Translate
): ThemeLook[] {
const { base, pluginThemes, pluginTerminalThemes, baseline, labels } = options
const builtIn: ThemeLook = {
kind: 'built-in',
@@ -230,8 +234,8 @@ export function buildThemeLooks(options: {
palette: { ...BUILT_IN_CHROME[base], terminal: currentTerminal(baseline, base) }
}
const modeThemes = pluginThemes.filter((theme) => theme.base === base)
const themeLabels = disambiguatedPluginContentLabels(modeThemes)
const pluginNames = disambiguatedContentPluginNames(modeThemes)
const themeLabels = disambiguatedPluginContentLabels(modeThemes, undefined, t)
const pluginNames = disambiguatedContentPluginNames(modeThemes, t)
const plugins = modeThemes.map((theme) =>
pluginLook(theme, pluginTerminalThemes, baseline, {
label: themeLabels.get(theme.id) ?? theme.label,
@@ -25,7 +25,7 @@ import { getGitHubRepoRoutingTarget } from '@/lib/github-source-runtime-context'
import { presentGitHubPRMergeState } from '@/components/github-pr-merge-state'
import { resolveGitHubPRMergeMethods } from '../../../../../shared/github/pull-request-merge-methods'
import { resolvePullRequestRepo } from '@/components/github/github-work-item-identity'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { getPullRequestMergeMethodLabel } from '@/components/github/pull-request-confirmation-copy'
import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types'
import type { TaskSourceContext } from '../../../../../shared/task-source-context'
@@ -53,13 +53,14 @@ export function PRActionsPanel({
onStateChange: (state: GitHubWorkItem['state']) => void
onMutated: () => void
}): React.JSX.Element {
const t = useTranslate()
const [statePending, setStatePending] = useState(false)
const [mergePending, setMergePending] = useState(false)
const patchWorkItem = useAppStore((s) => s.patchWorkItem)
const patchProjectRowContent = useAppStore((s) => s.patchProjectRowContent)
const confirm = useConfirmationDialog()
const actionItem = { ...item, state: localState }
const mergePresentation = presentGitHubPRMergeState(actionItem)
const mergePresentation = presentGitHubPRMergeState(actionItem, t)
const mergeMethods = resolveGitHubPRMergeMethods(actionItem.mergeMethodSettings)
const mergeTarget = useAppStore(
useShallow((s) => getGitHubRepoRoutingTarget(s, item.repoId ?? repoId ?? null, sourceContext))
@@ -98,7 +99,7 @@ export function PRActionsPanel({
<div className="flex items-center gap-2">
<GitPullRequest className="size-3.5 text-muted-foreground" />
<span className="text-[13px] font-medium text-foreground">
{translate('auto.components.PullRequestPage.1939d0f663', 'Pull request')}
{t('auto.components.PullRequestPage.1939d0f663', 'Pull request')}
</span>
</div>
<WorkItemStateBadge item={actionItem} />
@@ -124,7 +125,7 @@ export function PRActionsPanel({
)}
{mergePresentation.autoMergeAction?.label ??
(mergePresentation.directMergeAvailable
? getPullRequestMergeMethodLabel(mergeMethods.defaultMethod)
? getPullRequestMergeMethodLabel(mergeMethods.defaultMethod, t)
: mergePresentation.label)}
<ChevronDown className="size-3 opacity-60" />
</Button>
@@ -132,7 +133,7 @@ export function PRActionsPanel({
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{!canMergeWithRepoContext
? translate(
? t(
'auto.components.PullRequestPage.eca289e593',
'Merge requires a registered local repo'
)
@@ -186,12 +187,12 @@ export function PRActionsPanel({
}
>
<GitMerge className="size-4" />
{getPullRequestMergeMethodLabel(method)}
{getPullRequestMergeMethodLabel(method, t)}
</DropdownMenuItem>
))}
<DropdownMenuItem onSelect={() => openAppHttpLink(item.url)}>
<ExternalLink className="size-4" />
{translate('auto.components.PullRequestPage.7df8d5fc60', 'Open GitHub merge box')}
{t('auto.components.PullRequestPage.7df8d5fc60', 'Open GitHub merge box')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -233,8 +234,8 @@ export function PRActionsPanel({
<CircleDot className="size-3.5" />
)}
{nextState === 'closed'
? translate('auto.components.PullRequestPage.96d013ed28', 'Close pull request')
: translate('auto.components.PullRequestPage.9d5425918e', 'Reopen PR')}
? t('auto.components.PullRequestPage.96d013ed28', 'Close pull request')
: t('auto.components.PullRequestPage.9d5425918e', 'Reopen PR')}
</Button>
</div>
</aside>
@@ -10,7 +10,7 @@ import {
getCheckStatusLabel
} from '@/components/github/pr-check-presentation'
import type { CheckDetailsLoadState } from '@/components/github-checks-tab-state'
import { translate } from '@/i18n/i18n'
import { useIntlLocale, useTranslate } from '@/i18n/use-translate'
import type { PRCheckDetail } from '../../../../../shared/github/check-types'
import { assignUniqueListKeys } from './details-list-keys'
import { openAppHttpLink } from '@/lib/browser/navigation/app-http-link-open'
@@ -24,10 +24,12 @@ export function CheckDetailsPanel({
state: CheckDetailsLoadState | undefined
onRetry: (check: PRCheckDetail, key: string) => void
}): React.JSX.Element {
const t = useTranslate()
const intlLocale = useIntlLocale()
const details = state?.details
const openUrl = details?.detailsUrl ?? details?.url ?? check.url
const startedAt = formatCheckTimestamp(details?.startedAt)
const completedAt = formatCheckTimestamp(details?.completedAt)
const startedAt = formatCheckTimestamp(details?.startedAt, intlLocale)
const completedAt = formatCheckTimestamp(details?.completedAt, intlLocale)
const detailsStatusCheck: PRCheckDetail = {
...check,
status: (details?.status as PRCheckDetail['status'] | undefined) ?? check.status,
@@ -55,28 +57,28 @@ export function CheckDetailsPanel({
{state?.loading && !state.error ? (
<div className="flex items-center gap-2 py-2 text-[12px] text-muted-foreground">
<LoaderCircle className="size-3.5 animate-spin" />
{translate('auto.components.PullRequestPage.d8e82b7f15', 'Loading check details…')}
{t('auto.components.PullRequestPage.d8e82b7f15', 'Loading check details…')}
</div>
) : (
<div className="flex min-w-0 flex-col gap-2">
<div className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-muted-foreground">
<span>
{translate('auto.components.PullRequestPage.662bc2998d', 'Status:')}{' '}
{details ? getCheckStatusLabel(detailsStatusCheck) : getCheckStatusLabel(check)}
{t('auto.components.PullRequestPage.662bc2998d', 'Status:')}{' '}
{details ? getCheckStatusLabel(detailsStatusCheck, t) : getCheckStatusLabel(check, t)}
</span>
{startedAt && (
<span>
{translate('auto.components.PullRequestPage.76551b1161', 'Started')} {startedAt}
{t('auto.components.PullRequestPage.76551b1161', 'Started')} {startedAt}
</span>
)}
{completedAt && (
<span>
{translate('auto.components.PullRequestPage.000f90afcf', 'Completed')} {completedAt}
{t('auto.components.PullRequestPage.000f90afcf', 'Completed')} {completedAt}
</span>
)}
{check.checkRunId && (
<span className="font-mono">
{translate('auto.components.PullRequestPage.f01bf79a79', 'check #')}
{t('auto.components.PullRequestPage.f01bf79a79', 'check #')}
{check.checkRunId}
</span>
)}
@@ -96,8 +98,8 @@ export function CheckDetailsPanel({
>
<RefreshCw className={cn('size-3', state.loading && 'animate-spin')} />
{state.loading
? translate('githubChecks.retrying', 'Retrying…')
: translate('auto.components.PullRequestPage.5df7c41d2a', 'Retry')}
? t('githubChecks.retrying', 'Retrying…')
: t('auto.components.PullRequestPage.5df7c41d2a', 'Retry')}
</Button>
</div>
)}
@@ -127,7 +129,7 @@ export function CheckDetailsPanel({
{hasAnnotations && (
<div className="min-w-0 rounded-md border border-border/40 bg-background/70">
<div className="border-b border-border/40 px-2.5 py-1.5 text-[11px] font-medium text-foreground">
{translate('auto.components.PullRequestPage.8432d17901', 'Annotations')}
{t('auto.components.PullRequestPage.8432d17901', 'Annotations')}
</div>
<div className="flex flex-col">
{annotationRows.map(({ item: annotation, key }, index) => (
@@ -141,7 +143,7 @@ export function CheckDetailsPanel({
<div className="flex min-w-0 items-center gap-2">
<span className="min-w-0 truncate font-mono text-[11px] text-muted-foreground">
{annotation.path ??
translate('auto.components.PullRequestPage.35a0573f41', 'Annotation')}
t('auto.components.PullRequestPage.35a0573f41', 'Annotation')}
{annotation.startLine ? `:${annotation.startLine}` : ''}
</span>
{annotation.annotationLevel && (
@@ -172,7 +174,7 @@ export function CheckDetailsPanel({
{hasJobs && (
<div className="min-w-0 rounded-md border border-border/40 bg-background/70">
<div className="border-b border-border/40 px-2.5 py-1.5 text-[11px] font-medium text-foreground">
{translate('auto.components.PullRequestPage.7720c9c3f5', 'Jobs')}
{t('auto.components.PullRequestPage.7720c9c3f5', 'Jobs')}
</div>
<div className="flex flex-col">
{jobRows.map(({ item: job, key }, index) => (
@@ -187,7 +189,7 @@ export function CheckDetailsPanel({
<span className="shrink-0 text-[11px] text-muted-foreground">
{job.conclusion ??
job.status ??
translate('auto.components.PullRequestPage.77d9388fb0', 'unknown')}
t('auto.components.PullRequestPage.77d9388fb0', 'unknown')}
</span>
</div>
{job.steps.length > 0 && (
@@ -212,11 +214,11 @@ export function CheckDetailsPanel({
{!state?.error && !hasOutput && !hasAnnotations && !hasJobs && (
<div className="text-[12px] text-muted-foreground">
{getCheckConclusion(check) === 'action_required'
? translate(
? t(
'auto.components.PullRequestPage.checkActionRequiredHint',
'Needs a manual action on GitHub (e.g. approving the run) to unblock merging.'
)
: translate(
: t(
'auto.components.PullRequestPage.1550675e5f',
'No inline output is available for this check.'
)}
@@ -232,7 +234,7 @@ export function CheckDetailsPanel({
className="h-7 gap-1 px-2 text-[11px]"
onClick={(event) => openAppHttpLink(openUrl, { event })}
>
{translate('auto.components.PullRequestPage.1b14d0a69c', 'Open in GitHub')}
{t('auto.components.PullRequestPage.1b14d0a69c', 'Open in GitHub')}
<ExternalLink className="size-3" />
</Button>
</div>
@@ -4,7 +4,7 @@ import type { useAppStore } from '@/store'
import { SourceControlAgentActionDialog } from '@/components/right-sidebar/source-control/ai/SourceControlAgentActionDialog'
import { readSourceControlLaunchRecipeAgentId } from '../../../../../shared/source-control/ai/source-control-launch-agent-selection'
import type { SourceControlActionRecipe } from '../../../../../shared/source-control/ai/recipes/source-control-ai-actions'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types'
import { saveFixChecksActionDefault, startFixChecksFromDialog } from './fix-launch'
@@ -30,6 +30,7 @@ export function ChecksFixDialog({
updateSettings: ReturnType<typeof useAppStore.getState>['updateSettings']
updateRepo: ReturnType<typeof useAppStore.getState>['updateRepo']
}): React.JSX.Element {
const t = useTranslate()
return (
<SourceControlAgentActionDialog
open={fixChecksComposerPrompt !== null}
@@ -39,8 +40,8 @@ export function ChecksFixDialog({
}
}}
actionId="fixChecks"
title={translate('auto.components.PullRequestPage.a053bdd082', 'Fix Broken Checks With AI')}
description={translate(
title={t('auto.components.PullRequestPage.a053bdd082', 'Fix Broken Checks With AI')}
description={t(
'auto.components.PullRequestPage.ddfd42f460',
'Review the prompt before starting an agent.'
)}
@@ -58,7 +59,7 @@ export function ChecksFixDialog({
}
onLaunched={() => {
toast.success(
translate(
t(
'auto.components.PullRequestPage.85e62c5266',
'Started an AI agent for the broken checks.'
)
@@ -7,6 +7,7 @@ import { CHECK_COLOR } from '@/components/right-sidebar/checks-panel/presentatio
import { getCheckCountChips, type getCheckCounts } from '@/components/pr-check-counts'
import { getCheckDetailsKey } from '@/components/github/pr-check-presentation'
import type { PRCheckDetail } from '../../../../../shared/github/check-types'
import { useTranslate } from '@/i18n/use-translate'
export function ChecksTabLayouts({
owner,
@@ -39,6 +40,7 @@ export function ChecksTabLayouts({
renderCheckRow: (check: PRCheckDetail) => React.JSX.Element
fixChecksAgentDialog: React.JSX.Element
}): React.JSX.Element {
const t = useTranslate()
if (loading && listLength === 0) {
return (
<>
@@ -63,7 +65,7 @@ export function ChecksTabLayouts({
)
}
if (variant === 'page') {
const countChips = getCheckCountChips(counts)
const countChips = getCheckCountChips(counts, t)
return (
<>
<div className="flex flex-col gap-3 px-4 py-3">
@@ -10,6 +10,7 @@ import { getCheckDetailsKey, getCheckStatusLabel } from '@/components/github/pr-
import type { CheckDetailsLoadState } from '@/components/github-checks-tab-state'
import type { PRCheckDetail } from '../../../../../shared/github/check-types'
import { CheckDetailsPanel } from './details'
import { useTranslate } from '@/i18n/use-translate'
export function CheckRow({
check,
@@ -26,10 +27,11 @@ export function CheckRow({
onToggle: (check: PRCheckDetail) => void
onRetryDetails: (check: PRCheckDetail, key: string) => void
}): React.JSX.Element {
const t = useTranslate()
const conclusion = getCheckConclusion(check)
const Icon = CHECK_ICON[conclusion] ?? CircleDashed
const color = CHECK_COLOR[conclusion] ?? 'text-muted-foreground'
const statusLabel = getCheckStatusLabel(check)
const statusLabel = getCheckStatusLabel(check, t)
const key = getCheckDetailsKey(check)
return (
<div key={key} className="min-w-0">
@@ -43,6 +43,7 @@ import { ChecksCompactHeader, buildChecksToolbar } from './toolbar'
import { CheckRow } from './row'
import { ChecksFixDialog } from './fix-dialog'
import { ChecksTabLayouts } from './layouts'
import { useTranslate } from '@/i18n/use-translate'
export function ChecksTab({
item,
@@ -65,6 +66,7 @@ export function ChecksTab({
variant?: 'compact' | 'page'
onChecksUpdated: (checks: PRCheckDetail[]) => void
}): React.JSX.Element {
const t = useTranslate()
const targetRepoId = repoId ?? item.repoId
const settings = useAppStore((s) => s.settings)
const updateSettingsOrThrow = useAppStore((s) => s.updateSettingsOrThrow)
@@ -175,7 +177,8 @@ export function ChecksTab({
const summaryLabel = getChecksSummaryLabel(
list,
checkOwner,
resolvedChecksState.checkObservationOwner
resolvedChecksState.checkObservationOwner,
t
)
const { SummaryIcon, summaryColor } = getChecksTabSummaryPresentation(overallCounts)
const canFixBrokenChecks = Boolean((repoId ?? item.repoId) && failedChecks.length > 0)
@@ -288,33 +291,36 @@ export function ChecksTab({
[detailsByCheckKey, requestCheckDetails]
)
const { refreshAction, rerunAction, secondaryActions, actions } = buildChecksToolbar({
variant,
canUseChecksRepoContext,
refreshing,
rerunning,
fixingChecks,
canFixBrokenChecks,
failedChecksCount: failedChecks.length,
listLength: list.length,
onRefresh: () => {
void handleRefresh()
const { refreshAction, rerunAction, secondaryActions, actions } = buildChecksToolbar(
{
variant,
canUseChecksRepoContext,
refreshing,
rerunning,
fixingChecks,
canFixBrokenChecks,
failedChecksCount: failedChecks.length,
listLength: list.length,
onRefresh: () => {
void handleRefresh()
},
onFix: () => {
void fixBrokenPullRequestChecks({
targetRepoId,
fixingChecks,
failedChecks,
item,
list,
setFixingChecks,
setFixChecksComposerPrompt
})
},
onRerun: (failedOnly) => {
void handleRerun(failedOnly)
}
},
onFix: () => {
void fixBrokenPullRequestChecks({
targetRepoId,
fixingChecks,
failedChecks,
item,
list,
setFixingChecks,
setFixChecksComposerPrompt
})
},
onRerun: (failedOnly) => {
void handleRerun(failedOnly)
}
})
t
)
const compactHeader = (
<ChecksCompactHeader

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