refactor(i18n): read render-path translations from useTranslate in settings (appearance, accounts, agents, source control, mobile, shortcuts, editor, voice) (#39366)

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 16:42:09 -08:00
committed by GitHub
co-authored by OrcaWin
parent 63e019af72
commit 2daee44e9b
144 changed files with 1936 additions and 2190 deletions
-102
View File
@@ -790,23 +790,6 @@
7 src/renderer/src/components/right-sidebar/use-right-sidebar-activity-items.ts
16 src/renderer/src/components/runtime-access/RuntimeHostAccessForm.tsx
8 src/renderer/src/components/runtime-access/RuntimeRepairDialog.tsx
2 src/renderer/src/components/settings/accounts/AccountsPane.tsx
5 src/renderer/src/components/settings/accounts/claude/claude-running-session-switch-setting.tsx
2 src/renderer/src/components/settings/accounts/claude/claude-sign-in-link-menu.tsx
5 src/renderer/src/components/settings/accounts/codex/CodexLoginLinkNotice.tsx
12 src/renderer/src/components/settings/accounts/orca/OrcaAccountSettingsPane.tsx
25 src/renderer/src/components/settings/accounts/providers/AntigravityAccountsSection.tsx
18 src/renderer/src/components/settings/accounts/providers/CursorAccountsSection.tsx
17 src/renderer/src/components/settings/accounts/providers/GrokAccountsSection.tsx
17 src/renderer/src/components/settings/accounts/providers/ManagedDataAccountsSection.tsx
12 src/renderer/src/components/settings/accounts/sign-in/HostAccountSignInPrompt.tsx
19 src/renderer/src/components/settings/accounts/usage/MiniMaxAccountsSection.tsx
32 src/renderer/src/components/settings/accounts/usage/ZcodePlanAccountsSection.tsx
36 src/renderer/src/components/settings/accounts/usage/accounts-pane-minimax-credentials.tsx
13 src/renderer/src/components/settings/accounts/usage/accounts-pane-opencode-credentials.tsx
1 src/renderer/src/components/settings/accounts/usage/use-usage-host-settings.ts
2 src/renderer/src/components/settings/accounts/usage/use-zcode-plan-credentials.ts
1 src/renderer/src/components/settings/accounts/usage/zcode-plan-usage-windows.tsx
10 src/renderer/src/components/settings/advanced/AdvancedPane.tsx
27 src/renderer/src/components/settings/advanced/DevToolsPane.tsx
23 src/renderer/src/components/settings/advanced/ExperimentalPane.tsx
@@ -814,39 +797,6 @@
11 src/renderer/src/components/settings/advanced/network/AdvancedNetworkSettingsSection.tsx
7 src/renderer/src/components/settings/advanced/network/NodeDistMirrorDialog.tsx
3 src/renderer/src/components/settings/advanced/network/NodeDistMirrorSettingField.tsx
13 src/renderer/src/components/settings/agents/behavior/AgentCacheTimerSection.tsx
14 src/renderer/src/components/settings/agents/launch/AgentCatalogRow.tsx
3 src/renderer/src/components/settings/agents/launch/AgentDefaultSetting.tsx
15 src/renderer/src/components/settings/agents/launch/AgentDetectionCatalog.tsx
3 src/renderer/src/components/settings/agents/launch/AgentLaunchSettingsCatalog.tsx
1 src/renderer/src/components/settings/agents/launch/AgentLaunchSettingsLoading.tsx
2 src/renderer/src/components/settings/agents/launch/AgentLaunchSettingsSection.tsx
8 src/renderer/src/components/settings/agents/launch/HostAgentEnvironmentEditor.tsx
4 src/renderer/src/components/settings/agents/launch/HostAgentLaunchSettings.tsx
12 src/renderer/src/components/settings/agents/launch/ZcodeCliSetupSection.tsx
2 src/renderer/src/components/settings/agents/permissions/AgentPermissionModeMenu.tsx
9 src/renderer/src/components/settings/agents/permissions/AgentPermissionsSetting.tsx
6 src/renderer/src/components/settings/agents/permissions/AgentRowPermissions.tsx
5 src/renderer/src/components/settings/agents/permissions/agent-permission-override-notice.tsx
6 src/renderer/src/components/settings/agents/runtime/AgentRuntimeSetting.tsx
20 src/renderer/src/components/settings/agents/surfaces/AgentDefaultSurfaceSection.tsx
3 src/renderer/src/components/settings/appearance/AppIconSelector.tsx
1 src/renderer/src/components/settings/appearance/AppearanceAdvancedDisclosure.tsx
25 src/renderer/src/components/settings/appearance/AppearanceInterfaceSection.tsx
7 src/renderer/src/components/settings/appearance/AppearancePane.tsx
1 src/renderer/src/components/settings/appearance/AppearanceShortcutHintList.tsx
20 src/renderer/src/components/settings/appearance/AppearanceWindowSidebarSection.tsx
9 src/renderer/src/components/settings/appearance/LeftSidebarAppearanceSetting.tsx
5 src/renderer/src/components/settings/appearance/RecentTabOrderControl.tsx
1 src/renderer/src/components/settings/appearance/ReduceVisualEffectsSetting.tsx
1 src/renderer/src/components/settings/appearance/UIZoomControl.tsx
5 src/renderer/src/components/settings/appearance/WindowMaterialSetting.tsx
3 src/renderer/src/components/settings/appearance/chat/AppearanceChatContrastControls.tsx
3 src/renderer/src/components/settings/appearance/chat/AppearanceChatSection.tsx
3 src/renderer/src/components/settings/appearance/chat/ChatFontFamilySetting.tsx
6 src/renderer/src/components/settings/appearance/fonts/FontAutocomplete.tsx
12 src/renderer/src/components/settings/appearance/themes/AppearanceThemeControl.tsx
1 src/renderer/src/components/settings/appearance/use-appearance-picker-labels.ts
23 src/renderer/src/components/settings/artifacts/ArtifactsSettingsPane.tsx
12 src/renderer/src/components/settings/automations/AutomationsSettingsPane.tsx
3 src/renderer/src/components/settings/chat/ChatBehaviorSetting.tsx
@@ -864,14 +814,6 @@
7 src/renderer/src/components/settings/developer-permissions/DeveloperPermissionsPane.tsx
9 src/renderer/src/components/settings/developer-permissions/LocalNetworkConnectionTest.tsx
3 src/renderer/src/components/settings/developer-permissions/TerminalTccAttributionNotice.tsx
2 src/renderer/src/components/settings/editor/CollapseUnchangedRegionsSetting.tsx
7 src/renderer/src/components/settings/editor/DiffShowWhitespaceSetting.tsx
5 src/renderer/src/components/settings/editor/EditorFontFamilySetting.tsx
9 src/renderer/src/components/settings/editor/EditorTypographySettings.tsx
7 src/renderer/src/components/settings/editor/EditorWordWrapSetting.tsx
42 src/renderer/src/components/settings/editor/GeneralEditorSettingsSection.tsx
4 src/renderer/src/components/settings/editor/MarkdownDefaultViewSetting.tsx
4 src/renderer/src/components/settings/editor/RichMarkdownSpellcheckSetting.tsx
11 src/renderer/src/components/settings/floating-workspaces/FloatingWorkspacePane.tsx
1 src/renderer/src/components/settings/form-controls/SettingsFormControls.tsx
1 src/renderer/src/components/settings/form-controls/SettingsSearchableSelect.tsx
@@ -885,20 +827,6 @@
5 src/renderer/src/components/settings/host-scope/SettingsHostScopePicker.tsx
5 src/renderer/src/components/settings/input/InputPane.tsx
4 src/renderer/src/components/settings/integrations/IntegrationsPane.tsx
2 src/renderer/src/components/settings/mobile/MobileAutoRestoreFitSection.tsx
1 src/renderer/src/components/settings/mobile/MobilePane.tsx
10 src/renderer/src/components/settings/mobile/MobileSettingsPane.tsx
9 src/renderer/src/components/settings/mobile/emulator/EmulatorOpeningSettings.tsx
6 src/renderer/src/components/settings/mobile/emulator/MobileEmulatorAgentControlRow.tsx
12 src/renderer/src/components/settings/mobile/emulator/MobileEmulatorAvailabilityDetails.tsx
6 src/renderer/src/components/settings/mobile/emulator/MobileEmulatorExamples.tsx
8 src/renderer/src/components/settings/mobile/emulator/MobileEmulatorSettingsPane.tsx
8 src/renderer/src/components/settings/mobile/pairing/MobilePairedDevicesSection.tsx
10 src/renderer/src/components/settings/mobile/pairing/MobilePairingConnectionOptions.tsx
9 src/renderer/src/components/settings/mobile/pairing/MobilePairingQrSection.tsx
11 src/renderer/src/components/settings/mobile/pairing/MobilePairingSetupSection.tsx
1 src/renderer/src/components/settings/mobile/pairing/MobileRelayBetaNotice.tsx
3 src/renderer/src/components/settings/mobile/pairing/MobileRelaySignInPanel.tsx
10 src/renderer/src/components/settings/navigation/SettingsSidebar.tsx
3 src/renderer/src/components/settings/notifications/NotificationHostToggles.tsx
8 src/renderer/src/components/settings/notifications/NotificationSoundSection.tsx
@@ -918,33 +846,10 @@
7 src/renderer/src/components/settings/session-history/SessionHistorySettingsPane.tsx
8 src/renderer/src/components/settings/session-history/SessionSearchAdvancedSection.tsx
4 src/renderer/src/components/settings/session-history/SessionSearchComputerList.tsx
11 src/renderer/src/components/settings/shortcuts/KeybindingsFileActions.tsx
14 src/renderer/src/components/settings/shortcuts/ShortcutCommandBlock.tsx
5 src/renderer/src/components/settings/shortcuts/ShortcutFilterRail.tsx
8 src/renderer/src/components/settings/shortcuts/ShortcutRecorderButton.tsx
2 src/renderer/src/components/settings/shortcuts/ShortcutRemoveButton.tsx
1 src/renderer/src/components/settings/shortcuts/ShortcutRowsList.tsx
6 src/renderer/src/components/settings/shortcuts/ShortcutTerminalPolicyControl.tsx
8 src/renderer/src/components/settings/shortcuts/ShortcutsPane.tsx
11 src/renderer/src/components/settings/skills/linear/LinearAgentSkillGuide.tsx
2 src/renderer/src/components/settings/skills/linear/LinearAgentSkillNotes.tsx
8 src/renderer/src/components/settings/skills/linear/LinearAgentSkillPane.tsx
27 src/renderer/src/components/settings/skills/sharing/ShareSkillsSettingsPane.tsx
16 src/renderer/src/components/settings/source-control/AutoRenameBranchFromWorkSetting.tsx
3 src/renderer/src/components/settings/source-control/BranchPrefixFeedback.tsx
34 src/renderer/src/components/settings/source-control/CommitMessageAiPane.tsx
2 src/renderer/src/components/settings/source-control/CompareAgainstUpstreamSetting.tsx
5 src/renderer/src/components/settings/source-control/CustomAgentCommandField.tsx
21 src/renderer/src/components/settings/source-control/GitPane.tsx
17 src/renderer/src/components/settings/source-control/GitProviderApiBudgetPane.tsx
4 src/renderer/src/components/settings/source-control/HostedReviewCreationDefaults.tsx
5 src/renderer/src/components/settings/source-control/RepositorySourceControlAiActionSaveControls.tsx
2 src/renderer/src/components/settings/source-control/SourceControlActionAgentPicker.tsx
6 src/renderer/src/components/settings/source-control/SourceControlActionArguments.tsx
8 src/renderer/src/components/settings/source-control/SourceControlActionRecipeRow.tsx
6 src/renderer/src/components/settings/source-control/SourceControlActionRepoOverrideNote.tsx
2 src/renderer/src/components/settings/source-control/SourceControlAiActionRecipeDefaults.tsx
4 src/renderer/src/components/settings/source-control/text-actions/SourceControlTextActionAgentPicker.tsx
2 src/renderer/src/components/settings/tasks/JiraIssueKeyCasingSetting.tsx
13 src/renderer/src/components/settings/tasks/TaskSourceLinearSetup.tsx
2 src/renderer/src/components/settings/tasks/TaskSourceProviderCard.tsx
@@ -964,13 +869,6 @@
5 src/renderer/src/components/settings/virtual-machines/runtimes/EphemeralVmCleanupStopDialog.tsx
5 src/renderer/src/components/settings/virtual-machines/runtimes/EphemeralVmRuntimeRow.tsx
10 src/renderer/src/components/settings/virtual-machines/runtimes/EphemeralVmRuntimesSection.tsx
8 src/renderer/src/components/settings/voice/OpenAiTranscriptionKeyDialog.tsx
7 src/renderer/src/components/settings/voice/OpenAiTranscriptionSettingsRow.tsx
2 src/renderer/src/components/settings/voice/VoiceDictationLanguageSetting.tsx
7 src/renderer/src/components/settings/voice/VoiceDictationSettingsSection.tsx
7 src/renderer/src/components/settings/voice/VoiceMicrophoneSetting.tsx
9 src/renderer/src/components/settings/voice/VoicePane.tsx
11 src/renderer/src/components/settings/voice/VoiceSpeechModelSection.tsx
1 src/renderer/src/components/settings/wsl/hosts/WslHostsExperimentalSetting.tsx
13 src/renderer/src/components/settings/wsl/hosts/WslHostsSection.tsx
3 src/renderer/src/components/setup-guide/SetupGuideModal.tsx
@@ -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 type { Translate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
import { Button } from '../../ui/button'
import { Label } from '../../ui/label'
@@ -20,16 +21,17 @@ import { AgentPermissionChoiceItems, AgentPermissionChoiceName } from './AgentPe
/** Use chat-mode wording; own names the agent when available. */
export function agentPermissionChoiceLabel(
choice: AgentPermissionChoice,
agentLabel: string | null
agentLabel: string | null,
t: Translate = translate
): string {
if (choice !== 'own') {
return nativeChatPermissionModeLabel(choice)
return nativeChatPermissionModeLabel(choice, t)
}
return agentLabel
? translate('settings.agents.permissionOwnAgent', "Use {{agent}}'s own settings", {
? t('settings.agents.permissionOwnAgent', "Use {{agent}}'s own settings", {
agent: agentLabel
})
: translate('settings.agents.permissionOwnAll', "Agents' own settings")
: t('settings.agents.permissionOwnAll', "Agents' own settings")
}
export function AllAgentsPermissionSetting({
@@ -1,7 +1,8 @@
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
export function relayClockSkewMessage(clockOffsetMs: number): string {
return translate(
export function relayClockSkewMessage(clockOffsetMs: number, t: Translate = translate): string {
return t(
'auto.components.mobile.relayClockSkewMessage.message',
"Your computer's clock is off by about {{seconds}} seconds. Turn on automatic time, then retry.",
{ seconds: Math.round(Math.abs(clockOffsetMs) / 1000) }
@@ -18,6 +18,7 @@ import { pluginDisplayNameFromKey } from './plugin-display-name'
import { sanitizePluginDisplayText } from '../../../../../shared/plugins/manifest/plugin-display-text'
import type { PluginAutoUpdateState } from '../../../../../shared/plugins/catalog/plugin-update-policy'
import { withDisambiguatedPluginNames } from '../presentation/plugin-name-disambiguation'
import type { Translate } from '../../../i18n/use-translate'
/** One plugin as the list and detail page show it, whether installed, listed, or both. */
export type PluginListEntry = {
@@ -265,7 +266,8 @@ export function buildPluginListEntries(
sources?: readonly PluginMarketplaceHostSourceState[]
orcaVersion?: string | null
autoUpdates?: PluginAutoUpdateState | null
} = {}
} = {},
t?: Translate
): { installed: PluginListEntry[]; available: PluginListEntry[] } {
const listingsByKey = new Map<string, PluginMarketplaceHostListing[]>()
for (const listing of listings) {
@@ -275,54 +277,57 @@ export function buildPluginListEntries(
const catalogIdsOf = (pluginKey: string): string[] =>
(listingsByKey.get(pluginKey) ?? []).map((listing) => listing.marketplaceSourceId)
const installedKeys = new Set(installed.map((plugin) => plugin.pluginKey))
return withDisambiguatedPluginNames({
installed: installed.map((plugin) => {
const { listing, ownCatalog } = sourceListing(
plugin,
listingsByKey.get(plugin.pluginKey) ?? [],
catalogById
)
const entry = pluginListEntryFromInstalled(plugin, listing, {
catalogIds: catalogIdsOf(plugin.pluginKey),
orcaVersion,
autoUpdates
})
const own = ownCatalog && plugin.source?.marketplace ? listing : null
// Why bundled too: it ships the official catalog's release, never local bytes.
entry.installCountEligible =
!plugin.isDev &&
((plugin.source?.kind === 'marketplace' && isPublicCatalogListing(own, catalogById)) ||
(plugin.bundled && isPublicCatalogListing(listing, catalogById)))
entry.alsoListedBy = (listingsByKey.get(plugin.pluginKey) ?? [])
.filter((candidate) => candidate !== own)
.map((candidate) => candidate.marketplaceName)
if (!own && entry.verifiedSource.kind === 'catalog') {
// Why: another catalog's listing must never lend the install its name.
const from = plugin.source?.marketplace?.reference
entry.verifiedSource.catalog =
sources.find((source) => source.source.url === from)?.marketplace?.name ?? null
}
return ownCatalog
? entry
: {
...entry,
updateVersion: null,
reinstallVersion: null,
updatable: false,
autoUpdate: { ...entry.autoUpdate, needsReview: false }
}
}),
available: [...listingsByKey.entries()]
.filter(([pluginKey]) => !installedKeys.has(pluginKey))
.map(([pluginKey, candidates]) => ({
...pluginListEntryFromListing(
candidates[0],
catalogIdsOf(pluginKey),
catalogById.get(candidates[0].marketplaceSourceId)?.source.url ?? null
),
installCountEligible: isPublicCatalogListing(candidates[0], catalogById)
}))
})
return withDisambiguatedPluginNames(
{
installed: installed.map((plugin) => {
const { listing, ownCatalog } = sourceListing(
plugin,
listingsByKey.get(plugin.pluginKey) ?? [],
catalogById
)
const entry = pluginListEntryFromInstalled(plugin, listing, {
catalogIds: catalogIdsOf(plugin.pluginKey),
orcaVersion,
autoUpdates
})
const own = ownCatalog && plugin.source?.marketplace ? listing : null
// Why bundled too: it ships the official catalog's release, never local bytes.
entry.installCountEligible =
!plugin.isDev &&
((plugin.source?.kind === 'marketplace' && isPublicCatalogListing(own, catalogById)) ||
(plugin.bundled && isPublicCatalogListing(listing, catalogById)))
entry.alsoListedBy = (listingsByKey.get(plugin.pluginKey) ?? [])
.filter((candidate) => candidate !== own)
.map((candidate) => candidate.marketplaceName)
if (!own && entry.verifiedSource.kind === 'catalog') {
// Why: another catalog's listing must never lend the install its name.
const from = plugin.source?.marketplace?.reference
entry.verifiedSource.catalog =
sources.find((source) => source.source.url === from)?.marketplace?.name ?? null
}
return ownCatalog
? entry
: {
...entry,
updateVersion: null,
reinstallVersion: null,
updatable: false,
autoUpdate: { ...entry.autoUpdate, needsReview: false }
}
}),
available: [...listingsByKey.entries()]
.filter(([pluginKey]) => !installedKeys.has(pluginKey))
.map(([pluginKey, candidates]) => ({
...pluginListEntryFromListing(
candidates[0],
catalogIdsOf(pluginKey),
catalogById.get(candidates[0].marketplaceSourceId)?.source.url ?? null
),
installCountEligible: isPublicCatalogListing(candidates[0], catalogById)
}))
},
t
)
}
/** Search across what a user can see or would type: name, key, publisher, description, categories. */
@@ -97,12 +97,16 @@ type NamedPluginEntry = Omit<PluginNameCandidate, 'key'> & {
}
/** Installed and listed plugins sharing a name: the non-official ones show their publisher. */
export function withDisambiguatedPluginNames<Entry extends NamedPluginEntry>(lists: {
installed: Entry[]
available: Entry[]
}): { installed: Entry[]; available: Entry[] } {
export function withDisambiguatedPluginNames<Entry extends NamedPluginEntry>(
lists: {
installed: Entry[]
available: Entry[]
},
t?: Translate
): { installed: Entry[]; available: Entry[] } {
const names = disambiguatedPluginNames(
[...lists.installed, ...lists.available].map((entry) => ({ ...entry, key: entry.pluginKey }))
[...lists.installed, ...lists.available].map((entry) => ({ ...entry, key: entry.pluginKey })),
t
)
const rename = (entry: Entry): Entry => {
const name = names.get(entry.pluginKey) ?? entry.name
@@ -1,4 +1,5 @@
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { getLocalExecutionHostLabel } from '../../../../shared/execution-host'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import { defaultScopeHost } from '@/lib/default-creation-host'
@@ -10,8 +11,8 @@ export type ProviderHostScope = {
description: string
}
function remoteServerScopeLabel(name: string): string {
return translate(
function remoteServerScopeLabel(name: string, t: Translate = translate): string {
return t(
'auto.components.settings.providerAccountScope.remoteServer',
'Remote server: {{value0}}',
{ value0: name }
@@ -25,17 +26,17 @@ function providerHostScopeLabel(target: RuntimeClientTarget): string {
}
/** Accounts-pane scope while a remote server owns the roster (#8186). */
export function getRemoteAccountsPaneScope(serverName: string | null): ProviderHostScope {
export function getRemoteAccountsPaneScope(
serverName: string | null,
t: Translate = translate
): ProviderHostScope {
const name = serverName?.trim()
return {
// Unloaded server names need bare fallback labels; interpolation would repeat Remote server.
label: name
? remoteServerScopeLabel(name)
: translate(
'auto.components.settings.AccountsPane.accountScopeRemoteServerUnnamed',
'Remote server'
),
description: translate(
? remoteServerScopeLabel(name, t)
: t('auto.components.settings.AccountsPane.accountScopeRemoteServerUnnamed', 'Remote server'),
description: t(
'auto.components.settings.AccountsPane.remoteScopeLocalAccountsKeptHostPicker',
'Accounts managed on this desktop are unchanged. Choose this computer in the Host menu at the top of Settings to view them.'
)
@@ -7,6 +7,7 @@ import type { CodexConfigSyncStatus } from '../../../../../shared/codex-config-s
import { toast } from 'sonner'
import { useAppStore } from '../../../store/index'
import { translate } from '@/i18n/i18n'
import { useIntlLocale, useTranslate } from '@/i18n/use-translate'
import { isWebClientLocation } from '@/lib/web-client-location'
import {
emptyClaudeAccountsState,
@@ -86,6 +87,8 @@ export function AccountsPane({
accountOwnerPlatform = null,
wslHostDistro = null
}: AccountsPaneProps): React.JSX.Element {
const t = useTranslate()
const intlLocale = useIntlLocale()
const searchQuery = useAppStore((s) => s.settingsSearchQuery)
const codexRateLimits = useAppStore((s) => s.rateLimits.codex)
const codexRateLimitTarget = useAppStore((s) => s.rateLimits.codexTarget)
@@ -102,7 +105,8 @@ export function AccountsPane({
wslAvailable,
wslDistros,
wslCapabilitiesLoading,
wslHostDistro
wslHostDistro,
t
)
// Route all account actions to the Settings-selected owner, independent of focused host (#7973).
const accountOwner = hostScope.target
@@ -122,7 +126,7 @@ export function AccountsPane({
: null
const remoteServerLabel = isRemoteAccountScope
? (remoteServerName ??
translate('auto.components.settings.AccountsPane.remoteServerFallback', 'the remote server'))
t('auto.components.settings.AccountsPane.remoteServerFallback', 'the remote server'))
: null
const accountRuntime = isRemoteAccountScope
? { runtime: 'host' as const, label: remoteServerLabel ?? '' }
@@ -135,11 +139,8 @@ export function AccountsPane({
const remoteAccountScopeNotice =
isRemoteAccountScope && !isWebClientLocation() ? (
<ProviderHostScopeControl
labelPrefix={translate(
'auto.components.settings.AccountsPane.accountScopePrefix',
'Account scope'
)}
scope={getRemoteAccountsPaneScope(remoteServerName)}
labelPrefix={t('auto.components.settings.AccountsPane.accountScopePrefix', 'Account scope')}
scope={getRemoteAccountsPaneScope(remoteServerName, t)}
className="text-xs"
/>
) : null
@@ -335,16 +336,16 @@ export function AccountsPane({
!isRemoteAccountScope &&
!wslHostDistro &&
matchesSettingsSearch(searchQuery, getAccountsLocationSearchEntries())
? renderAccountsLocationSection(model)
? renderAccountsLocationSection(model, t)
: null,
matchesSettingsSearch(searchQuery, getAccountsClaudeSearchEntries())
? renderClaudeAccountsSection(model)
? renderClaudeAccountsSection(model, t, intlLocale)
: null,
matchesSettingsSearch(searchQuery, getAccountsCodexSearchEntries())
? renderCodexAccountsSection(model)
? renderCodexAccountsSection(model, t, intlLocale)
: null,
matchesSettingsSearch(searchQuery, getAccountsGeminiSearchEntries())
? renderGeminiAccountsSection(model)
? renderGeminiAccountsSection(model, t)
: null,
matchesSettingsSearch(searchQuery, getAccountsAntigravitySearchEntries()) ? (
<AntigravityAccountsSection
@@ -356,7 +357,7 @@ export function AccountsPane({
/>
) : null,
matchesSettingsSearch(searchQuery, getAccountsOpencodeSearchEntries())
? renderOpenCodeAccountsSection(model)
? renderOpenCodeAccountsSection(model, t)
: null,
matchesSettingsSearch(searchQuery, getAccountsMiniMaxSearchEntries()) ? (
<MiniMaxAccountsSection
@@ -384,7 +385,7 @@ export function AccountsPane({
return (
<div className="space-y-8">
{renderAccountsRemovalDialogs(model, removeCodexTarget, removeClaudeTarget)}
{renderAccountsRemovalDialogs(model, removeCodexTarget, removeClaudeTarget, t)}
<SettingsSectionStack sections={visibleSections} />
</div>
)
@@ -1,4 +1,5 @@
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import {
removeClaudeProviderAccount,
removeCodexProviderAccount
@@ -17,7 +18,8 @@ import type { AccountsPaneSectionModel, RemoveAccountTarget } from './accounts-p
export function renderAccountsRemovalDialogs(
model: AccountsPaneSectionModel,
removeCodexTarget: RemoveAccountTarget | null,
removeClaudeTarget: RemoveAccountTarget | null
removeClaudeTarget: RemoveAccountTarget | null,
t: Translate = translate
): React.JSX.Element {
const {
runClaudeAccountAction,
@@ -35,13 +37,10 @@ export function renderAccountsRemovalDialogs(
<DialogContent showCloseButton={false}>
<DialogHeader>
<DialogTitle>
{translate(
'auto.components.settings.AccountsPane.0d47394635',
'Remove Codex Account?'
)}
{t('auto.components.settings.AccountsPane.0d47394635', 'Remove Codex Account?')}
</DialogTitle>
<DialogDescription>
{translate(
{t(
'auto.components.settings.AccountsPane.380a7736cc',
'Removing this account permanently deletes its managed Codex home, including all Codex session history and MCP logins stored inside. This cannot be undone. If the account is currently active, Orca falls back to the system default Codex login.'
)}
@@ -49,7 +48,7 @@ export function renderAccountsRemovalDialogs(
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setRemoveCodexTarget(null)}>
{translate('auto.components.settings.AccountsPane.dbb9626ed1', 'Cancel')}
{t('auto.components.settings.AccountsPane.dbb9626ed1', 'Cancel')}
</Button>
<Button
variant="destructive"
@@ -66,7 +65,7 @@ export function renderAccountsRemovalDialogs(
)
}}
>
{translate('auto.components.settings.AccountsPane.c2d2751587', 'Remove Account')}
{t('auto.components.settings.AccountsPane.c2d2751587', 'Remove Account')}
</Button>
</DialogFooter>
</DialogContent>
@@ -78,13 +77,10 @@ export function renderAccountsRemovalDialogs(
<DialogContent showCloseButton={false}>
<DialogHeader>
<DialogTitle>
{translate(
'auto.components.settings.AccountsPane.63843e37e2',
'Remove Claude Account?'
)}
{t('auto.components.settings.AccountsPane.63843e37e2', 'Remove Claude Account?')}
</DialogTitle>
<DialogDescription>
{translate(
{t(
'settings.accounts.claudeRemoveFolder',
"Orca deletes this account's Claude folder and its sign-in from this computer. Chat history shared with System default stays. If it is selected, the next Claude you start uses System default."
)}
@@ -92,7 +88,7 @@ export function renderAccountsRemovalDialogs(
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setRemoveClaudeTarget(null)}>
{translate('auto.components.settings.AccountsPane.dbb9626ed1', 'Cancel')}
{t('auto.components.settings.AccountsPane.dbb9626ed1', 'Cancel')}
</Button>
<Button
variant="destructive"
@@ -109,7 +105,7 @@ export function renderAccountsRemovalDialogs(
)
}}
>
{translate('auto.components.settings.AccountsPane.c2d2751587', 'Remove Account')}
{t('auto.components.settings.AccountsPane.c2d2751587', 'Remove Account')}
</Button>
</DialogFooter>
</DialogContent>
@@ -1,5 +1,6 @@
import { Loader2, Plus, RefreshCw, Trash2, X } from 'lucide-react'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { selectClaudeProviderAccount } from '@/runtime/provider-accounts/observations/runtime-provider-accounts-client'
import { Badge } from '../../../ui/badge'
import { Button } from '../../../ui/button'
@@ -31,7 +32,11 @@ import {
} from '../sign-in/account-add-cancel-target'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
export function renderClaudeAccountsSection(model: AccountsPaneSectionModel): React.JSX.Element {
export function renderClaudeAccountsSection(
model: AccountsPaneSectionModel,
t: Translate = translate,
intlLocale?: string
): React.JSX.Element {
const {
accountRuntime,
accountRuntimeSentenceLabel,
@@ -84,17 +89,17 @@ export function renderClaudeAccountsSection(model: AccountsPaneSectionModel): Re
title={
<span className="flex items-center gap-2">
<ClaudeIcon size={16} />
{translate('auto.components.settings.AccountsPane.26ef4b55be', 'Claude')}
{t('auto.components.settings.AccountsPane.26ef4b55be', 'Claude')}
</span>
}
description={translate(
description={t(
'auto.components.settings.AccountsPane.72b36ea174',
'Optional. Orca can use your normal Claude login; add accounts only if you want quick switching without moving chat sessions.'
)}
>
<SearchableSetting
title={translate('auto.components.settings.AccountsPane.8bbfd74556', 'Claude Accounts')}
description={translate(
title={t('auto.components.settings.AccountsPane.8bbfd74556', 'Claude Accounts')}
description={t(
'accounts.claude.profileSwitching',
'Switching applies to the next Claude you start in any tab.'
)}
@@ -103,17 +108,15 @@ export function renderClaudeAccountsSection(model: AccountsPaneSectionModel): Re
>
<div className="flex items-center justify-between gap-3">
<div className="space-y-0.5">
<Label>
{translate('auto.components.settings.AccountsPane.94d351af4a', 'Accounts')}
</Label>
<Label>{t('auto.components.settings.AccountsPane.94d351af4a', 'Accounts')}</Label>
<p className="text-xs text-muted-foreground">
{isRemoteAccountScope
? translate(
? t(
'accounts.remoteScopeAccountsAddHere',
'Showing accounts managed by {{value0}}. New accounts sign in there; you finish in this device’s browser.',
{ value0: accountRuntimeSentenceLabel }
)
: translate(
: t(
'auto.components.settings.AccountsPane.c0a52abfc5',
'Showing accounts for {{value0}}. New accounts are added there.',
{ value0: accountRuntimeSentenceLabel }
@@ -134,7 +137,7 @@ export function renderClaudeAccountsSection(model: AccountsPaneSectionModel): Re
) : (
<Plus className="size-3" />
)}
{translate('auto.components.settings.AccountsPane.b0e948a4f9', 'Add Account')}
{t('auto.components.settings.AccountsPane.b0e948a4f9', 'Add Account')}
</Button>
{!isRemoteAccountScope && canOfferClaudeSignInLink(accountRuntime.runtime) ? (
<ClaudeSignInLinkMenu
@@ -158,7 +161,7 @@ export function renderClaudeAccountsSection(model: AccountsPaneSectionModel): Re
className="gap-1.5 text-muted-foreground hover:text-foreground"
>
<X className="size-3" />
{translate('auto.components.settings.AccountsPane.dbb9626ed1', 'Cancel')}
{t('auto.components.settings.AccountsPane.dbb9626ed1', 'Cancel')}
</Button>
) : null}
</div>
@@ -195,29 +198,22 @@ export function renderClaudeAccountsSection(model: AccountsPaneSectionModel): Re
<div className="flex min-w-0 items-center gap-2">
<span className="truncate text-sm font-medium">
{systemDefaultEmail
? translate(
'accounts.claude.systemDefaultNamed',
'System default: {{value0}}',
{
value0: systemDefaultEmail
}
)
: translate(
'auto.components.settings.AccountsPane.f2a265f8c7',
'System default'
)}
? t('accounts.claude.systemDefaultNamed', 'System default: {{value0}}', {
value0: systemDefaultEmail
})
: t('auto.components.settings.AccountsPane.f2a265f8c7', 'System default')}
</span>
{systemClaudeActive ? (
<Badge
variant="outline"
className="h-4 shrink-0 rounded px-1.5 text-[10px] font-medium leading-none text-foreground/80"
>
{translate('auto.components.settings.AccountsPane.e74831fb6b', 'Active')}
{t('auto.components.settings.AccountsPane.e74831fb6b', 'Active')}
</Badge>
) : null}
</div>
<span className="truncate text-[11px] text-muted-foreground">
{translate(
{t(
'auto.components.settings.AccountsPane.e05d0ff737',
'Use your current {{value0}} Claude login.',
{ value0: accountRuntimeSentenceLabel }
@@ -228,7 +224,7 @@ export function renderClaudeAccountsSection(model: AccountsPaneSectionModel): Re
{systemDefaultIsSaved ? (
<div className="flex items-start justify-between gap-2">
<p className="text-xs text-muted-foreground">
{translate(
{t(
'accounts.claude.systemDefaultIsSaved',
'System default is signed in as {{value0}}, which is also a saved account. An earlier Orca version may have copied that login there. If it is not your own login, select System default and run claude /login.',
{ value0: systemDefaultEmail }
@@ -239,19 +235,19 @@ export function renderClaudeAccountsSection(model: AccountsPaneSectionModel): Re
size="xs"
onClick={() => updateSettings({ claudeCopiedSystemDefaultNoticeDismissed: true })}
>
{translate('accounts.claude.systemDefaultIsSavedDismiss', 'Dismiss')}
{t('accounts.claude.systemDefaultIsSavedDismiss', 'Dismiss')}
</Button>
</div>
) : null}
{visibleClaudeAccounts.length === 0 ? (
<div className="rounded-md border border-dashed border-border/70 px-3 py-4 text-xs text-muted-foreground">
{isRemoteAccountScope
? translate(
? t(
'auto.components.settings.AccountsPane.remoteEmptyClaudeAccounts',
'No managed Claude accounts on {{value0}}. It uses its system default Claude login; add accounts on that server.',
{ value0: accountRuntimeSentenceLabel }
)
: translate(
: t(
'auto.components.settings.AccountsPane.3fe7862418',
"No managed Claude accounts for {{value0}}. Orca will use that environment's system default Claude login until you add one here.",
{ value0: accountRuntimeSentenceLabel }
@@ -321,22 +317,19 @@ export function renderClaudeAccountsSection(model: AccountsPaneSectionModel): Re
variant="outline"
className="h-4 shrink-0 rounded px-1.5 text-[10px] font-medium leading-none text-foreground/80"
>
{translate(
'auto.components.settings.AccountsPane.e74831fb6b',
'Active'
)}
{t('auto.components.settings.AccountsPane.e74831fb6b', 'Active')}
</Badge>
) : null}
</div>
<span className="truncate text-[11px] text-muted-foreground">
{account.needsSignIn
? translate(
? t(
'accounts.claude.signInRequired',
'Sign in again to use this account'
)
: account.organizationName
? `${account.organizationName} · ${formatAccountTimestamp(account.lastAuthenticatedAt)}`
: formatAccountTimestamp(account.lastAuthenticatedAt)}
? `${account.organizationName} · ${formatAccountTimestamp(account.lastAuthenticatedAt, intlLocale)}`
: formatAccountTimestamp(account.lastAuthenticatedAt, intlLocale)}
</span>
</button>
<div className="flex shrink-0 items-center justify-end gap-1 max-md:w-full max-md:flex-wrap">
@@ -356,7 +349,7 @@ export function renderClaudeAccountsSection(model: AccountsPaneSectionModel): Re
) : (
<RefreshCw className="size-3" />
)}
{translate('accounts.claude.signInAgain', 'Sign in again')}
{t('accounts.claude.signInAgain', 'Sign in again')}
</Button>
{canOfferClaudeSignInLink(accountRuntimeView.runtime) ? (
<ClaudeSignInLinkMenu
@@ -380,7 +373,7 @@ export function renderClaudeAccountsSection(model: AccountsPaneSectionModel): Re
className="h-6 px-2 text-muted-foreground hover:text-destructive"
>
<Trash2 className="size-3" />
{translate('auto.components.settings.AccountsPane.db209ee572', 'Remove')}
{t('auto.components.settings.AccountsPane.db209ee572', 'Remove')}
</Button>
</div>
</div>
@@ -1,6 +1,6 @@
import type { GlobalSettings } from '../../../../../../shared/global-settings-types'
import type { ClaudeAccountSwitchRunningSessions } from '../../../../../../shared/claude-account-switch-settings'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { Label } from '../../../ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../ui/select'
import { SearchableSetting } from '../../section-layout/SearchableSetting'
@@ -18,11 +18,12 @@ export function ClaudeRunningSessionSwitchSetting({
settings: GlobalSettings
updateSettings: (updates: Partial<GlobalSettings>) => void
}): React.JSX.Element {
const title = translate(
const t = useTranslate()
const title = t(
'accounts.claude.switchSessions.settingTitle',
'When you switch accounts while Claude is running in terminals'
)
const description = translate(
const description = t(
'accounts.claude.switchSessions.settingDescription',
'Restarting continues each terminal conversation on the new account.'
)
@@ -50,13 +51,13 @@ export function ClaudeRunningSessionSwitchSetting({
</SelectTrigger>
<SelectContent>
<SelectItem value="ask">
{translate('accounts.claude.switchSessions.settingAsk', 'Ask')}
{t('accounts.claude.switchSessions.settingAsk', 'Ask')}
</SelectItem>
<SelectItem value="restart">
{translate('accounts.claude.switchSessions.settingRestart', 'Always restart')}
{t('accounts.claude.switchSessions.settingRestart', 'Always restart')}
</SelectItem>
<SelectItem value="keep">
{translate('accounts.claude.switchSessions.settingKeep', 'Never restart')}
{t('accounts.claude.switchSessions.settingKeep', 'Never restart')}
</SelectItem>
</SelectContent>
</Select>
@@ -1,6 +1,7 @@
import { ChevronDown, Link } from 'lucide-react'
import { toast } from 'sonner'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import type { ClaudeRateLimitAccountsState } from '../../../../../../shared/managed-account-types'
import { canCopyClaudeSignInLink } from '../../../../../../shared/claude-sign-in-link'
import { Button } from '../../../ui/button'
@@ -53,6 +54,7 @@ export function ClaudeSignInLinkMenu({
disabled: boolean
onCopyLink: () => void
}): React.JSX.Element {
const t = useTranslate()
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
@@ -60,7 +62,7 @@ export function ClaudeSignInLinkMenu({
variant={variant}
size="icon-xs"
disabled={disabled}
aria-label={translate('accounts.claude.moreSignInOptions', 'More sign-in options')}
aria-label={t('accounts.claude.moreSignInOptions', 'More sign-in options')}
>
<ChevronDown />
</Button>
@@ -68,7 +70,7 @@ export function ClaudeSignInLinkMenu({
<DropdownMenuContent align="end">
<DropdownMenuItem onSelect={onCopyLink}>
<Link />
{translate('accounts.claude.copySignInLink', 'Copy sign-in link instead')}
{t('accounts.claude.copySignInLink', 'Copy sign-in link instead')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -1,11 +1,12 @@
import { Check, Copy, ExternalLink } from 'lucide-react'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { useClipboardTextCopyFeedback } from '@/hooks/use-clipboard-text-copy-feedback'
import { Button } from '../../../ui/button'
import { useCodexPendingLoginUrl } from './use-codex-pending-login-url'
/** Expose in-flight login links so users can finish in a different browser. */
export function CodexLoginLinkNotice(): React.JSX.Element | null {
const t = useTranslate()
const url = useCodexPendingLoginUrl()
const { copyText, status } = useClipboardTextCopyFeedback(url ?? '')
if (!url) {
@@ -14,7 +15,7 @@ export function CodexLoginLinkNotice(): React.JSX.Element | null {
return (
<div className="space-y-2 rounded-md border border-border/70 bg-muted/30 px-3 py-2">
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.AccountsPane.codexLoginLinkPending',
'Codex opened this sign-in link in your browser. Copy it to finish signing in somewhere else — a private window, or another profile.'
)}
@@ -26,17 +27,17 @@ export function CodexLoginLinkNotice(): React.JSX.Element | null {
<Button variant="outline" size="xs" onClick={() => void copyText()}>
{status === 'copied' ? <Check /> : <Copy />}
{status === 'copied'
? translate('auto.components.settings.AccountsPane.codexLoginLinkCopied', 'Copied')
: translate('auto.components.settings.AccountsPane.codexLoginLinkCopy', 'Copy link')}
? t('auto.components.settings.AccountsPane.codexLoginLinkCopied', 'Copied')
: t('auto.components.settings.AccountsPane.codexLoginLinkCopy', 'Copy link')}
</Button>
<Button variant="ghost" size="xs" onClick={() => void window.api.shell.openUrl(url)}>
<ExternalLink />
{translate('auto.components.settings.AccountsPane.codexLoginLinkOpen', 'Open')}
{t('auto.components.settings.AccountsPane.codexLoginLinkOpen', 'Open')}
</Button>
</div>
{status === 'failed' ? (
<p className="text-xs text-destructive">
{translate(
{t(
'auto.components.settings.AccountsPane.codexLoginLinkCopyFailed',
'Could not copy the link.'
)}
@@ -1,6 +1,7 @@
import { Loader2, RefreshCw, Trash2 } from 'lucide-react'
import type { CodexRateLimitAccountsState } from '../../../../../../shared/managed-account-types'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { getCodexAccountDisplayDetail } from '@/lib/codex-account-display-label'
import { selectCodexProviderAccount } from '@/runtime/provider-accounts/observations/runtime-provider-accounts-client'
import { Badge } from '../../../ui/badge'
@@ -15,7 +16,9 @@ import type { AccountsPaneSectionModel } from '../accounts-pane-types'
export function renderCodexAccountRow(
account: CodexRateLimitAccountsState['accounts'][number],
model: AccountsPaneSectionModel
model: AccountsPaneSectionModel,
t: Translate = translate,
intlLocale?: string
): React.JSX.Element {
const {
accountRuntime,
@@ -95,7 +98,7 @@ export function renderCodexAccountRow(
variant="outline"
className="h-4 shrink-0 rounded px-1.5 text-[10px] font-medium leading-none text-foreground/80"
>
{translate('auto.components.settings.AccountsPane.e74831fb6b', 'Active')}
{t('auto.components.settings.AccountsPane.e74831fb6b', 'Active')}
</Badge>
) : null}
{needsReauthentication ? (
@@ -103,7 +106,7 @@ export function renderCodexAccountRow(
variant="destructive"
className="h-4 shrink-0 rounded px-1.5 text-[10px] font-medium leading-none"
>
{translate('auto.components.settings.AccountsPane.589eba1eee', 'Needs re-auth')}
{t('auto.components.settings.AccountsPane.589eba1eee', 'Needs re-auth')}
</Badge>
) : null}
</div>
@@ -120,14 +123,16 @@ export function renderCodexAccountRow(
) : null}
{needsReauthentication ? (
<span className="truncate">
{translate(
{t(
'auto.components.settings.AccountsPane.3d245ef7d9',
'Codex reported this sign-in is out of date'
)}
</span>
) : null}
{needsReauthentication ? <span className="shrink-0 opacity-50">•</span> : null}
<span className="shrink-0">{formatAccountTimestamp(account.lastAuthenticatedAt)}</span>
<span className="shrink-0">
{formatAccountTimestamp(account.lastAuthenticatedAt, intlLocale)}
</span>
</div>
</button>
@@ -155,7 +160,7 @@ export function renderCodexAccountRow(
) : (
<RefreshCw className="size-3" />
)}
{translate('auto.components.settings.AccountsPane.8a0f870153', 'Re-authenticate')}
{t('auto.components.settings.AccountsPane.8a0f870153', 'Re-authenticate')}
</Button>
<Button
variant="ghost"
@@ -175,7 +180,7 @@ export function renderCodexAccountRow(
) : (
<Trash2 className="size-3" />
)}
{translate('auto.components.settings.AccountsPane.db209ee572', 'Remove')}
{t('auto.components.settings.AccountsPane.db209ee572', 'Remove')}
</Button>
</div>
</div>
@@ -1,5 +1,6 @@
import { AlertTriangle, Loader2, Plus, X } from 'lucide-react'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { selectCodexProviderAccount } from '@/runtime/provider-accounts/observations/runtime-provider-accounts-client'
import { Badge } from '../../../ui/badge'
import { Button } from '../../../ui/button'
@@ -20,7 +21,11 @@ import {
} from '../sign-in/account-add-cancel-target'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
export function renderCodexAccountsSection(model: AccountsPaneSectionModel): React.JSX.Element {
export function renderCodexAccountsSection(
model: AccountsPaneSectionModel,
t: Translate = translate,
intlLocale?: string
): React.JSX.Element {
const {
accountRuntime,
accountRuntimeSentenceLabel,
@@ -47,25 +52,25 @@ export function renderCodexAccountsSection(model: AccountsPaneSectionModel): Rea
title={
<span className="flex items-center gap-2">
<OpenAIIcon size={16} />
{translate('auto.components.settings.AccountsPane.ef91cfa06b', 'Codex')}
{t('auto.components.settings.AccountsPane.ef91cfa06b', 'Codex')}
</span>
}
description={
<>
<span className="block">
{translate(
{t(
'auto.components.settings.AccountsPane.cedfab35ab',
'Optional. Orca can use your normal Codex login; add accounts only if you want quick switching in Orca.'
)}
</span>
<span className="block">
{isRemoteAccountScope
? translate(
? t(
'auto.components.settings.AccountsPane.remoteScopeAuthContext',
'Each account keeps its own sign-in context on {{value0}}.',
{ value0: accountRuntimeSentenceLabel }
)
: translate(
: t(
'auto.components.settings.AccountsPane.340d6f7a85',
'Each account keeps its own local sign-in context in Orca. Account auth stays on this device.'
)}
@@ -74,8 +79,8 @@ export function renderCodexAccountsSection(model: AccountsPaneSectionModel): Rea
}
>
<SearchableSetting
title={translate('auto.components.settings.AccountsPane.3180536c7a', 'Codex Accounts')}
description={translate(
title={t('auto.components.settings.AccountsPane.3180536c7a', 'Codex Accounts')}
description={t(
'auto.components.settings.AccountsPane.d0d53b7eb0',
'Manage which Codex account Orca uses for live rate limit fetching.'
)}
@@ -93,17 +98,17 @@ export function renderCodexAccountsSection(model: AccountsPaneSectionModel): Rea
<AlertTriangle className="mt-0.5 size-3.5 shrink-0" />
<span>
{systemCodexMissingSignIn
? translate(
? t(
'auto.components.settings.AccountsPane.codexSystemDefaultNeedsSignIn',
'No Codex sign-in was found for {{value0}}.',
{ value0: accountRuntimeSentenceLabel }
)
: activeCodexAccountId
? translate(
? t(
'auto.components.settings.AccountsPane.75ca9b718e',
'Codex reported that the active account needs a fresh sign-in. Re-authenticate it before starting new Codex sessions.'
)
: translate(
: t(
'auto.components.settings.AccountsPane.e4a28e8894',
'Codex reported that the {{value0}} login needs a fresh sign-in. Sign in again before starting new Codex sessions.',
{ value0: accountRuntimeSentenceLabel }
@@ -116,23 +121,23 @@ export function renderCodexAccountsSection(model: AccountsPaneSectionModel): Rea
<AlertTriangle className="mt-0.5 size-3.5 shrink-0" />
<span>
{codexConfigSyncWarning === 'managed-home-unavailable'
? translate(
? t(
'auto.components.settings.AccountsPane.codexConfigSyncManagedHomeUnavailable',
'Orca could not read this account’s Codex files just now, so settings may not be syncing. This usually clears on its own — antivirus or a backup tool briefly locks them.'
)
: codexConfigSyncWarning === 'missing-source'
? translate(
? t(
'auto.components.settings.AccountsPane.codexConfigSyncMissingSource',
'Codex is still using the settings it last synced because {{value0}} is missing. Restore that file to resume syncing.',
{ value0: codexConfigSync?.systemConfigPath ?? '' }
)
: codexConfigSyncWarning === 'blank-source'
? translate(
? t(
'auto.components.settings.AccountsPane.codexConfigSyncBlankSource',
'Codex is still using the settings it last synced because {{value0}} is empty. That is expected while a synced folder finishes downloading.',
{ value0: codexConfigSync?.systemConfigPath ?? '' }
)
: translate(
: t(
'auto.components.settings.AccountsPane.codexConfigSyncUnreadableSource',
"Codex is still using the settings it last synced because {{value0}} could not be read. Check that file's permissions.",
{ value0: codexConfigSync?.systemConfigPath ?? '' }
@@ -142,17 +147,15 @@ export function renderCodexAccountsSection(model: AccountsPaneSectionModel): Rea
) : null}
<div className="flex items-center justify-between gap-3">
<div className="space-y-0.5">
<Label>
{translate('auto.components.settings.AccountsPane.94d351af4a', 'Accounts')}
</Label>
<Label>{t('auto.components.settings.AccountsPane.94d351af4a', 'Accounts')}</Label>
<p className="text-xs text-muted-foreground">
{isRemoteAccountScope
? translate(
? t(
'accounts.remoteScopeAccountsAddHere',
'Showing accounts managed by {{value0}}. New accounts sign in there; you finish in this device’s browser.',
{ value0: accountRuntimeSentenceLabel }
)
: translate(
: t(
'auto.components.settings.AccountsPane.c0a52abfc5',
'Showing accounts for {{value0}}. New accounts are added there.',
{ value0: accountRuntimeSentenceLabel }
@@ -186,7 +189,7 @@ export function renderCodexAccountsSection(model: AccountsPaneSectionModel): Rea
) : (
<Plus className="size-3" />
)}
{translate('auto.components.settings.AccountsPane.b0e948a4f9', 'Add Account')}
{t('auto.components.settings.AccountsPane.b0e948a4f9', 'Add Account')}
</Button>
{/* Follow the running addition; hide Cancel only when its server prompt already offers it. */}
{codexAction === 'adding' && !accountAdditionShownInPrompt('codex', accountOwner) ? (
@@ -201,7 +204,7 @@ export function renderCodexAccountsSection(model: AccountsPaneSectionModel): Rea
className="gap-1.5"
>
<X />
{translate('auto.components.settings.AccountsPane.dbb9626ed1', 'Cancel')}
{t('auto.components.settings.AccountsPane.dbb9626ed1', 'Cancel')}
</Button>
) : null}
</div>
@@ -242,17 +245,14 @@ export function renderCodexAccountsSection(model: AccountsPaneSectionModel): Rea
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<div className="flex min-w-0 items-center gap-2">
<span className="truncate text-sm font-medium">
{translate(
'auto.components.settings.AccountsPane.f2a265f8c7',
'System default'
)}
{t('auto.components.settings.AccountsPane.f2a265f8c7', 'System default')}
</span>
{systemCodexActive ? (
<Badge
variant="outline"
className="h-4 shrink-0 rounded px-1.5 text-[10px] font-medium leading-none text-foreground/80"
>
{translate('auto.components.settings.AccountsPane.e74831fb6b', 'Active')}
{t('auto.components.settings.AccountsPane.e74831fb6b', 'Active')}
</Badge>
) : null}
{systemCodexNeedsSignIn ? (
@@ -260,10 +260,7 @@ export function renderCodexAccountsSection(model: AccountsPaneSectionModel): Rea
variant="destructive"
className="h-4 shrink-0 rounded px-1.5 text-[10px] font-medium leading-none"
>
{translate(
'auto.components.settings.AccountsPane.93c47b333a',
'Needs sign-in'
)}
{t('auto.components.settings.AccountsPane.93c47b333a', 'Needs sign-in')}
</Badge>
) : null}
</div>
@@ -274,19 +271,20 @@ export function renderCodexAccountsSection(model: AccountsPaneSectionModel): Rea
>
{systemCodexNeedsSignIn
? systemCodexMissingSignIn
? translate(
? t(
'auto.components.settings.AccountsPane.codexSystemDefaultNeedsSignIn',
'No Codex sign-in was found for {{value0}}.',
{ value0: accountRuntimeSentenceLabel }
)
: translate(
: t(
'auto.components.settings.AccountsPane.fd62f37c24',
'Codex reported this {{value0}} login is out of date.',
{ value0: accountRuntimeSentenceLabel }
)
: getCodexSystemDefaultSubtitle(
systemCodexIdentity,
accountRuntimeSentenceLabel
accountRuntimeSentenceLabel,
t
)}
</span>
</div>
@@ -294,19 +292,21 @@ export function renderCodexAccountsSection(model: AccountsPaneSectionModel): Rea
{visibleCodexAccounts.length === 0 ? (
<div className="rounded-md border border-dashed border-border/70 px-3 py-4 text-xs text-muted-foreground">
{isRemoteAccountScope
? translate(
? t(
'auto.components.settings.AccountsPane.remoteEmptyCodexAccounts',
'No managed Codex accounts on {{value0}}. It uses its system default Codex login; add accounts on that server.',
{ value0: accountRuntimeSentenceLabel }
)
: translate(
: t(
'auto.components.settings.AccountsPane.b4c9450319',
"No managed Codex accounts for {{value0}}. Orca will use that environment's system default Codex login until you add one here.",
{ value0: accountRuntimeSentenceLabel }
)}
</div>
) : (
visibleCodexAccounts.map((account) => renderCodexAccountRow(account, model))
visibleCodexAccounts.map((account) =>
renderCodexAccountRow(account, model, t, intlLocale)
)
)}
</div>
</SearchableSetting>
@@ -5,36 +5,38 @@ import { Button } from '@/components/ui/button'
import { useOrcaProfileAuthStatusRefresh } from '@/hooks/use-orca-profile-auth-status-refresh'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { useAppStore } from '@/store/index'
import { OrcaProfileSignOutConfirmDialog } from '../../../orca-profiles/OrcaProfileSignOutConfirmDialog'
import { OrcaSignInButton, OrcaSignInWaitNotice } from '../../../OrcaSignInProgress'
function accountStatusCopy(
state: 'local' | 'unconfigured' | 'connected' | 'reconnect-required' | undefined,
email: string | undefined
email: string | undefined,
t: Translate = translate
): string {
if (state === 'connected') {
return email ?? translate('auto.components.settings.orcaAccount.connected', 'Connected')
return email ?? t('auto.components.settings.orcaAccount.connected', 'Connected')
}
if (state === 'reconnect-required') {
return translate(
return t(
'auto.components.settings.orcaAccount.reconnectRequired',
'Your session expired. Sign in again to use cloud features.'
)
}
if (state === 'unconfigured') {
return translate(
return t(
'auto.components.settings.orcaAccount.unavailable',
'Orca sign-in is unavailable in this build.'
)
}
if (state === 'local') {
return translate(
return t(
'auto.components.settings.orcaAccount.signedOut',
'Sign in to extend Orca with cloud features, including Artifacts and Orca Relay.'
)
}
return translate('auto.components.settings.orcaAccount.checking', 'Checking account status…')
return t('auto.components.settings.orcaAccount.checking', 'Checking account status…')
}
function AccountBenefit({
@@ -60,6 +62,7 @@ function AccountBenefit({
}
export function OrcaAccountSettingsPane(): React.JSX.Element {
const t = useTranslate()
const authStatus = useAppStore((state) => state.orcaProfileAuthStatus)
const signOut = useAppStore((state) => state.signOutCurrentOrcaProfile)
const [signOutOpen, setSignOutOpen] = useState(false)
@@ -92,17 +95,17 @@ export function OrcaAccountSettingsPane(): React.JSX.Element {
<div className="flex flex-wrap items-center gap-2">
<p className="text-sm font-medium">
{authStatus?.cloud?.displayName?.trim() ||
translate('auto.components.settings.orcaAccount.account', 'Orca account')}
t('auto.components.settings.orcaAccount.account', 'Orca account')}
</p>
{connected ? (
<Badge variant="outline" className="text-[11px] text-muted-foreground">
<Check />
{translate('auto.components.settings.orcaAccount.connected', 'Connected')}
{t('auto.components.settings.orcaAccount.connected', 'Connected')}
</Badge>
) : null}
</div>
<p className="truncate text-xs text-muted-foreground">
{accountStatusCopy(authStatus?.state, authStatus?.cloud?.email)}
{accountStatusCopy(authStatus?.state, authStatus?.cloud?.email, t)}
</p>
</div>
{connected ? (
@@ -113,13 +116,13 @@ export function OrcaAccountSettingsPane(): React.JSX.Element {
disabled={signingOut}
onClick={() => setSignOutOpen(true)}
>
{translate('auto.components.settings.orcaAccount.signOut', 'Sign out')}
{t('auto.components.settings.orcaAccount.signOut', 'Sign out')}
</Button>
) : (
<OrcaSignInButton size="sm" disabled={!canConnect}>
{authStatus?.state === 'reconnect-required'
? translate('auto.components.settings.orcaAccount.signInAgain', 'Sign in again')
: translate('auto.components.settings.orcaAccount.signIn', 'Sign in to Orca')}
? t('auto.components.settings.orcaAccount.signInAgain', 'Sign in again')
: t('auto.components.settings.orcaAccount.signIn', 'Sign in to Orca')}
</OrcaSignInButton>
)}
{connected ? null : <OrcaSignInWaitNotice className="basis-full" />}
@@ -127,21 +130,15 @@ export function OrcaAccountSettingsPane(): React.JSX.Element {
<div className="space-y-4 border-t border-border/60 pt-5">
<p className="text-[11px] font-semibold uppercase tracking-[0.05em] text-muted-foreground">
{translate(
'auto.components.settings.orcaAccount.benefitsTitle',
'Included with your account'
)}
{t('auto.components.settings.orcaAccount.benefitsTitle', 'Included with your account')}
</p>
<div className="space-y-5">
<div className="grid gap-5 md:grid-cols-2 md:gap-0 md:divide-x md:divide-border/60">
<AccountBenefit
icon={Files}
className="md:pr-6"
title={translate(
'auto.components.settings.orcaAccount.artifactsTitle',
'Artifact sharing'
)}
description={translate(
title={t('auto.components.settings.orcaAccount.artifactsTitle', 'Artifact sharing')}
description={t(
'auto.components.settings.orcaAccount.artifactsDescription',
'Publish HTML and Markdown files, then manage every shared link from Orca.'
)}
@@ -149,8 +146,8 @@ export function OrcaAccountSettingsPane(): React.JSX.Element {
<AccountBenefit
icon={Smartphone}
className="md:pl-6"
title={translate('auto.components.settings.orcaAccount.relayTitle', 'Orca Relay')}
description={translate(
title={t('auto.components.settings.orcaAccount.relayTitle', 'Orca Relay')}
description={t(
'auto.components.settings.orcaAccount.relayDescription',
'Connect Orca Mobile to this desktop across cellular or any Wi-Fi.'
)}
@@ -160,8 +157,8 @@ export function OrcaAccountSettingsPane(): React.JSX.Element {
<AccountBenefit
icon={BookOpen}
className="border-t border-border/60 pt-5"
title={translate('auto.components.settings.orcaAccount.skillsTitle', 'Skill sharing')}
description={translate(
title={t('auto.components.settings.orcaAccount.skillsTitle', 'Skill sharing')}
description={t(
'auto.components.settings.orcaAccount.skillsDescription',
'Share one skill or a whole set behind an unlisted link, and install them on any machine you use.'
)}
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react'
import { Loader2 } from 'lucide-react'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { AgentIcon } from '@/lib/agent-catalog'
import { callAntigravityAccounts } from '@/runtime/provider-accounts/sign-in/runtime-antigravity-accounts-client'
import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call'
@@ -27,15 +28,11 @@ import { ChatUnavailableOnWindowsNote } from '../../../agent/launch-settings/Age
import { structuredChatUnavailableOnPlatform } from '../../../../../../shared/agent-session/structured-chat-platform-support'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
function formatUsageReading(usage: ProviderRateLimits): string {
return translate(
'accounts.antigravity.usageReading',
'Session: {{session}} · Weekly: {{weekly}}',
{
session: usage.session ? `${Math.round(usage.session.usedPercent)}%` : '—',
weekly: usage.weekly ? `${Math.round(usage.weekly.usedPercent)}%` : '—'
}
)
function formatUsageReading(usage: ProviderRateLimits, t: Translate = translate): string {
return t('accounts.antigravity.usageReading', 'Session: {{session}} · Weekly: {{weekly}}', {
session: usage.session ? `${Math.round(usage.session.usedPercent)}%` : '—',
weekly: usage.weekly ? `${Math.round(usage.weekly.usedPercent)}%` : '—'
})
}
function usageMatchesTarget(provenance: string, target: AntigravityAccountTarget): boolean {
@@ -63,6 +60,7 @@ export function AntigravityAccountsSection({
label: string
ownerPlatform?: string | null
}): React.JSX.Element {
const t = useTranslate()
const [state, setState] = useState<AntigravityAccountState | null>(null)
const [usage, setUsage] = useState<ProviderRateLimits | null>(null)
const [error, setError] = useState<string | null>(null)
@@ -188,13 +186,13 @@ export function AntigravityAccountsSection({
title={
<span className="flex items-center gap-2">
<AgentIcon agent="antigravity" size={16} />
{translate('accounts.antigravity.title', 'Antigravity')}
{t('accounts.antigravity.title', 'Antigravity')}
</span>
}
>
<div className="space-y-4 py-4">
<p className="text-xs text-muted-foreground">
{translate(
{t(
'accounts.antigravity.signIn',
'Start agy on this host and complete its browser sign-in, then save the current account. To add a different account, use /logout in agy and sign in again.'
)}{' '}
@@ -205,7 +203,7 @@ export function AntigravityAccountsSection({
onClick={openAppHttpLinkFromAnchor}
rel="noopener noreferrer"
>
{translate('accounts.antigravity.docs', 'Sign-in instructions')}
{t('accounts.antigravity.docs', 'Sign-in instructions')}
</a>
</p>
{error && (
@@ -215,7 +213,7 @@ export function AntigravityAccountsSection({
)}
{!state && error && (
<Button variant="outline" size="sm" disabled={busy} onClick={() => void run('List')}>
{translate('accounts.antigravity.retry', 'Retry')}
{t('accounts.antigravity.retry', 'Retry')}
</Button>
)}
{state && (
@@ -223,23 +221,17 @@ export function AntigravityAccountsSection({
<p className="text-xs">
{state.currentAccount
? (state.currentAccount.email ??
translate(
'accounts.antigravity.identityUnknown',
'Signed-in identity unavailable'
))
: translate(
'accounts.antigravity.signedOut',
'No native agy account is signed in.'
)}
t('accounts.antigravity.identityUnknown', 'Signed-in identity unavailable'))
: t('accounts.antigravity.signedOut', 'No native agy account is signed in.')}
</p>
{state.selectedAccountId && state.activeAccountId !== state.selectedAccountId && (
<p className="text-xs text-destructive">
{windowsHost
? translate(
? t(
'accounts.antigravity.windowsChanged',
'The host default account changed. Select a saved account to apply it again.'
)
: translate(
: t(
'accounts.antigravity.changed',
'The native account changed. Select a saved account again before launching agy.'
)}
@@ -251,7 +243,7 @@ export function AntigravityAccountsSection({
disabled={busy || !state.currentAccount}
onClick={() => void run('AddCurrent')}
>
{translate('accounts.antigravity.save', 'Save current account')}
{t('accounts.antigravity.save', 'Save current account')}
</Button>
<Button
variant="outline"
@@ -259,7 +251,7 @@ export function AntigravityAccountsSection({
disabled={busy}
onClick={() => void run('List')}
>
{translate('accounts.antigravity.refresh', 'Refresh accounts')}
{t('accounts.antigravity.refresh', 'Refresh accounts')}
</Button>
<Button
variant="ghost"
@@ -267,18 +259,18 @@ export function AntigravityAccountsSection({
disabled={busy || !state.currentAccount}
onClick={() => void run('Usage')}
>
{translate('accounts.antigravity.usage', 'Refresh usage')}
{t('accounts.antigravity.usage', 'Refresh usage')}
</Button>
{busy && (
<Loader2
className="size-4 animate-spin"
aria-label={translate('accounts.antigravity.working', 'Updating account')}
aria-label={t('accounts.antigravity.working', 'Updating account')}
/>
)}
</div>
{usage && (
<p className="text-xs text-muted-foreground">
{usage.error ?? formatUsageReading(usage)}
{usage.error ?? formatUsageReading(usage, t)}
</p>
)}
{state.accounts.map((account) => (
@@ -288,29 +280,21 @@ export function AntigravityAccountsSection({
>
<div className="space-y-1">
<p className="text-xs font-medium">
{account.email ??
translate('accounts.antigravity.saved', 'Saved Google account')}
{account.email ?? t('accounts.antigravity.saved', 'Saved Google account')}
</p>
{account.usage !== undefined && (
<p className="text-xs text-muted-foreground">
{account.usage
? translate(
'accounts.antigravity.lastReading',
'{{reading}} · Read {{time}}',
{
reading: formatUsageReading(account.usage),
time: formatTimeAgo(account.usage.updatedAt)
}
)
: translate(
'accounts.antigravity.noReading',
'Usage unknown: not read yet'
)}
? t('accounts.antigravity.lastReading', '{{reading}} · Read {{time}}', {
reading: formatUsageReading(account.usage, t),
time: formatTimeAgo(account.usage.updatedAt, t)
})
: t('accounts.antigravity.noReading', 'Usage unknown: not read yet')}
</p>
)}
{state.activeAccountId === account.id && (
<Badge variant="secondary">
{translate('accounts.antigravity.nativeActive', 'Native account')}
{t('accounts.antigravity.nativeActive', 'Native account')}
</Badge>
)}
</div>
@@ -323,8 +307,8 @@ export function AntigravityAccountsSection({
>
{state.selectedAccountId === account.id &&
state.activeAccountId === account.id
? translate('accounts.antigravity.selected', 'Selected')
: translate('accounts.antigravity.select', 'Select')}
? t('accounts.antigravity.selected', 'Selected')
: t('accounts.antigravity.select', 'Select')}
</Button>
<Button
size="xs"
@@ -336,7 +320,7 @@ export function AntigravityAccountsSection({
}
onClick={() => void run('Remove', account.id)}
>
{translate('accounts.antigravity.remove', 'Remove')}
{t('accounts.antigravity.remove', 'Remove')}
</Button>
</div>
</div>
@@ -345,11 +329,11 @@ export function AntigravityAccountsSection({
)}
<p className="text-xs text-muted-foreground">
{windowsHost
? translate(
? t(
'accounts.antigravity.windowsSessions',
'Select applies and verifies the host default agy credential. Command choices and later account changes are handled by agy. Existing sessions may keep their previous account.'
)
: translate(
: t(
'accounts.antigravity.sessions',
'Selection applies to new agy sessions on this host. Existing sessions may keep their previous account.'
)}
@@ -358,7 +342,7 @@ export function AntigravityAccountsSection({
<AntigravityChatSignInRow
owner={owner}
label={label}
description={translate(
description={t(
'accounts.antigravity.chatSeparate',
'Separate from your agy terminal account.'
)}
@@ -366,7 +350,7 @@ export function AntigravityAccountsSection({
runtime={
<ManagedRuntimeInstallRow
observation={chatRuntime}
agentLabel={translate('accounts.antigravity.title', 'Antigravity')}
agentLabel={t('accounts.antigravity.title', 'Antigravity')}
signInRunning={antigravityChatSignInRunning(signIn.flow)}
buttonSize="default"
>
@@ -374,7 +358,7 @@ export function AntigravityAccountsSection({
<div className="flex items-center justify-between gap-4">
<div className="min-w-0 flex-1">
<p className="text-sm font-medium">
{translate('settings.agents.chatRuntime', 'Chat runtime')}
{t('settings.agents.chatRuntime', 'Chat runtime')}
</p>
<div className="mt-0.5">{line}</div>
</div>
@@ -385,9 +369,7 @@ export function AntigravityAccountsSection({
}
/>
) : chats && structuredChatUnavailableOnPlatform('antigravity', ownerPlatform) ? (
<ChatUnavailableOnWindowsNote
label={translate('accounts.antigravity.title', 'Antigravity')}
/>
<ChatUnavailableOnWindowsNote label={t('accounts.antigravity.title', 'Antigravity')} />
) : null}
</div>
</SettingsGroup>
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import { ExternalLink, Loader2, RefreshCw, ShieldCheck } from 'lucide-react'
import { AgentIcon } from '@/lib/agent-catalog'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
import { useAppStore } from '../../../../store/index'
import { Badge } from '../../../ui/badge'
@@ -14,26 +15,27 @@ import { SettingsGroup } from '../../section-layout/SettingsGroup'
const CURSOR_USAGE_DASHBOARD_URL = 'https://cursor.com/dashboard/spending'
const SEARCH_KEYWORDS = ['cursor', 'usage', 'plan', 'spend', 'billing', 'rate limit']
function credentialSourceLabel(source: CursorAccountStatus['credentialSource']): string | null {
function credentialSourceLabel(
source: CursorAccountStatus['credentialSource'],
t: Translate = translate
): string | null {
if (source === 'keychain') {
return translate(
return t(
'auto.components.settings.CursorAccountsSection.source.keychain',
'macOS Keychain (cursor-agent)'
)
}
if (source === 'cli') {
return translate(
'auto.components.settings.CursorAccountsSection.source.cli',
'Cursor CLI auth file'
)
return t('auto.components.settings.CursorAccountsSection.source.cli', 'Cursor CLI auth file')
}
if (source === 'desktop') {
return translate('auto.components.settings.CursorAccountsSection.source.desktop', 'Cursor IDE')
return t('auto.components.settings.CursorAccountsSection.source.desktop', 'Cursor IDE')
}
return null
}
export function CursorAccountsSection(): React.JSX.Element {
const t = useTranslate()
const refreshRateLimits = useAppStore((s) => s.refreshRateLimits)
const cursorUsage = useAppStore((s) => s.rateLimits.cursor)
const [status, setStatus] = useState<CursorAccountStatus | null>(null)
@@ -86,7 +88,7 @@ export function CursorAccountsSection(): React.JSX.Element {
const signedIn = status?.signedIn === true
const tokenFresh = status?.tokenFresh === true
const sourceLabel = credentialSourceLabel(status?.credentialSource ?? null)
const sourceLabel = credentialSourceLabel(status?.credentialSource ?? null, t)
const pools = cursorUsage?.buckets ?? []
const monthly = cursorUsage?.monthly ?? null
// Explain unknown/failed usage rather than hiding the signed-in account or implying healthy allowance.
@@ -108,10 +110,10 @@ export function CursorAccountsSection(): React.JSX.Element {
title={
<span className="flex items-center gap-2">
<AgentIcon agent="cursor" size={16} />
{translate('auto.components.settings.CursorAccountsSection.title', 'Cursor')}
{t('auto.components.settings.CursorAccountsSection.title', 'Cursor')}
</span>
}
description={translate(
description={t(
'auto.components.settings.CursorAccountsSection.subtitle',
'Shows your Cursor plan usage from the sign-in already on this computer. Orca only reads it — it never changes your Cursor login.'
)}
@@ -123,10 +125,7 @@ export function CursorAccountsSection(): React.JSX.Element {
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
>
{translate(
'auto.components.settings.CursorAccountsSection.dashboardLink',
'Cursor dashboard'
)}
{t('auto.components.settings.CursorAccountsSection.dashboardLink', 'Cursor dashboard')}
<ExternalLink className="size-3" />
</a>
}
@@ -141,31 +140,28 @@ export function CursorAccountsSection(): React.JSX.Element {
<div className="min-w-0 flex-1 space-y-1">
{loading ? (
<p className="text-xs text-muted-foreground">
{translate('auto.components.settings.CursorAccountsSection.loading', 'Loading…')}
{t('auto.components.settings.CursorAccountsSection.loading', 'Loading…')}
</p>
) : signedIn ? (
<>
<p className="truncate text-xs font-medium">
{status?.email ??
status?.displayName ??
translate(
'auto.components.settings.CursorAccountsSection.signedIn',
'Signed in'
)}
t('auto.components.settings.CursorAccountsSection.signedIn', 'Signed in')}
</p>
<p className="text-xs text-muted-foreground">
{tokenFresh
? sourceLabel
? translate(
? t(
'auto.components.settings.CursorAccountsSection.signedInFrom',
'Signed in. Orca reads the session stored in {{source}}.',
{ source: sourceLabel }
)
: translate(
: t(
'auto.components.settings.CursorAccountsSection.signedInGeneric',
'Signed in. Orca reads the Cursor session stored on this computer.'
)
: translate(
: t(
'auto.components.settings.CursorAccountsSection.expired',
'Sign-in expired — run cursor-agent login on the computer running Orca, then click Refresh usage.'
)}
@@ -174,13 +170,13 @@ export function CursorAccountsSection(): React.JSX.Element {
) : (
<>
<p className="text-xs font-medium">
{translate(
{t(
'auto.components.settings.CursorAccountsSection.signedOut',
'No Cursor sign-in found on this computer'
)}
</p>
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.CursorAccountsSection.signedOutHelp',
'Sign in with Cursor IDE, or run cursor-agent login in a terminal, then click Refresh usage here.'
)}
@@ -201,20 +197,14 @@ export function CursorAccountsSection(): React.JSX.Element {
) : (
<RefreshCw className="size-3" />
)}
{translate(
'auto.components.settings.CursorAccountsSection.refreshUsage',
'Refresh usage'
)}
{t('auto.components.settings.CursorAccountsSection.refreshUsage', 'Refresh usage')}
</Button>
</div>
{pools.length > 0 || monthly ? (
<SearchableSetting
title={translate(
'auto.components.settings.CursorAccountsSection.usageTitle',
'Plan usage'
)}
description={translate(
title={t('auto.components.settings.CursorAccountsSection.usageTitle', 'Plan usage')}
description={t(
'auto.components.settings.CursorAccountsSection.usageDescription',
'Cursor bills two pools that reset with your billing cycle, plus on-demand spend once they run out.'
)}
@@ -236,22 +226,20 @@ export function CursorAccountsSection(): React.JSX.Element {
<span className="tabular-nums">{Math.round(monthly.usedPercent)}%</span>
</Badge>
<span className="text-muted-foreground">
{translate('auto.components.settings.CursorAccountsSection.planTotal', 'Plan')}
{t('auto.components.settings.CursorAccountsSection.planTotal', 'Plan')}
</span>
</div>
) : null}
{(monthly ?? pools[0])?.resetDescription ? (
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.CursorAccountsSection.resets',
'Resets {{when}}',
{ when: (monthly ?? pools[0])?.resetDescription ?? '' }
)}
{t('auto.components.settings.CursorAccountsSection.resets', 'Resets {{when}}', {
when: (monthly ?? pools[0])?.resetDescription ?? ''
})}
</p>
) : null}
{staleUsageError ? (
<p className="text-xs text-destructive">
{translate(
{t(
'auto.components.settings.CursorAccountsSection.staleUsage',
'Last known usage — the latest refresh failed: {{reason}}',
{ reason: staleUsageError }
@@ -262,8 +250,8 @@ export function CursorAccountsSection(): React.JSX.Element {
</SearchableSetting>
) : unavailableReason ? (
<SearchableSetting
title={translate('auto.components.settings.CursorAccountsSection.usageLabel', 'Usage')}
description={translate(
title={t('auto.components.settings.CursorAccountsSection.usageLabel', 'Usage')}
description={t(
'auto.components.settings.CursorAccountsSection.noAllowance',
'Cursor reported no usage allowance for this account.'
)}
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from 'react'
import { ExternalLink, Loader2, RefreshCw, ShieldCheck } from 'lucide-react'
import { AgentIcon } from '@/lib/agent-catalog'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
import { useAppStore } from '../../../../store/index'
import { Badge } from '../../../ui/badge'
@@ -13,6 +13,7 @@ import { SettingsGroup } from '../../section-layout/SettingsGroup'
const GROK_CLI_DOCS_URL = 'https://docs.x.ai/build/overview'
export function GrokAccountsSection(): React.JSX.Element {
const t = useTranslate()
const refreshGrokRateLimits = useAppStore((s) => s.refreshGrokRateLimits)
const grokUsage = useAppStore((s) => s.rateLimits.grok)
const [status, setStatus] = useState<GrokAccountStatus | null>(null)
@@ -69,10 +70,10 @@ export function GrokAccountsSection(): React.JSX.Element {
title={
<span className="flex items-center gap-2">
<AgentIcon agent="grok" size={16} />
{translate('auto.components.settings.GrokAccountsSection.a1b2c3d4e5', 'Grok (xAI)')}
{t('auto.components.settings.GrokAccountsSection.a1b2c3d4e5', 'Grok (xAI)')}
</span>
}
description={translate(
description={t(
'auto.components.settings.GrokAccountsSection.f6e5d4c3b2',
'Shows weekly credit usage from your Grok CLI sign-in (session file ~/.grok/auth.json).'
)}
@@ -84,7 +85,7 @@ export function GrokAccountsSection(): React.JSX.Element {
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
>
{translate('auto.components.settings.GrokAccountsSection.0d8e77bc40', 'Grok CLI docs')}
{t('auto.components.settings.GrokAccountsSection.0d8e77bc40', 'Grok CLI docs')}
<ExternalLink className="size-3" />
</a>
}
@@ -99,24 +100,21 @@ export function GrokAccountsSection(): React.JSX.Element {
<div className="min-w-0 flex-1 space-y-1">
{loading ? (
<p className="text-xs text-muted-foreground">
{translate('auto.components.settings.GrokAccountsSection.ad47a33f72', 'Loading…')}
{t('auto.components.settings.GrokAccountsSection.ad47a33f72', 'Loading…')}
</p>
) : signedIn ? (
<>
<p className="truncate text-xs font-medium">
{status?.email ??
translate(
'auto.components.settings.GrokAccountsSection.b2c3d4e5f6',
'Signed in'
)}
t('auto.components.settings.GrokAccountsSection.b2c3d4e5f6', 'Signed in')}
</p>
<p className="text-xs text-muted-foreground">
{tokenFresh
? translate(
? t(
'auto.components.settings.GrokAccountsSection.b36fa2c908',
'Signed in. Orca reads the Grok CLI session stored on disk.'
)
: translate(
: t(
'auto.components.settings.GrokAccountsSection.f08c41de73',
'Session expired — run grok on the computer running Orca and wait for it to start. If prompted, complete sign-in, then click Refresh usage. No chat message is needed.'
)}
@@ -125,13 +123,13 @@ export function GrokAccountsSection(): React.JSX.Element {
) : (
<>
<p className="text-xs font-medium">
{translate(
{t(
'auto.components.settings.GrokAccountsSection.e5f6a7b8c9',
'Not signed in to Grok CLI'
)}
</p>
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.GrokAccountsSection.f6a7b8c9d0',
'In a terminal, run grok login, then click Refresh usage here.'
)}
@@ -152,7 +150,7 @@ export function GrokAccountsSection(): React.JSX.Element {
) : (
<RefreshCw className="size-3" />
)}
{translate('auto.components.settings.GrokAccountsSection.3325d996cb', 'Refresh usage')}
{t('auto.components.settings.GrokAccountsSection.3325d996cb', 'Refresh usage')}
</Button>
</div>
@@ -160,22 +158,16 @@ export function GrokAccountsSection(): React.JSX.Element {
<SearchableSetting
title={
usageIsWeekly
? translate(
'auto.components.settings.GrokAccountsSection.a8f3e2c1b4',
'Weekly credits'
)
: translate(
'auto.components.settings.GrokAccountsSection.e6dadc1e2b',
'Monthly usage'
)
? t('auto.components.settings.GrokAccountsSection.a8f3e2c1b4', 'Weekly credits')
: t('auto.components.settings.GrokAccountsSection.e6dadc1e2b', 'Monthly usage')
}
description={
usageIsWeekly
? translate(
? t(
'auto.components.settings.GrokAccountsSection.b7e2d9f0a3',
'Same weekly credit % as the grok /usage screen in the terminal.'
)
: translate(
: t(
'auto.components.settings.GrokAccountsSection.75e396bf42',
'Included monthly usage for Grok unified-billing accounts.'
)
@@ -189,11 +181,9 @@ export function GrokAccountsSection(): React.JSX.Element {
</Badge>
{usageWindow.resetDescription ? (
<span className="text-muted-foreground">
{translate(
'auto.components.settings.GrokAccountsSection.c6d1a8f4e2',
'Resets {{when}}',
{ when: usageWindow.resetDescription }
)}
{t('auto.components.settings.GrokAccountsSection.c6d1a8f4e2', 'Resets {{when}}', {
when: usageWindow.resetDescription
})}
</span>
) : null}
{grokUsage?.usageMetadata?.authProvenance ? (
@@ -205,8 +195,8 @@ export function GrokAccountsSection(): React.JSX.Element {
</SearchableSetting>
) : unavailableReason ? (
<SearchableSetting
title={translate('auto.components.settings.GrokAccountsSection.0bb18642b7', 'Usage')}
description={translate(
title={t('auto.components.settings.GrokAccountsSection.0bb18642b7', 'Usage')}
description={t(
'auto.components.settings.GrokAccountsSection.a8f4139350',
'Grok reported no usage percentage for this account.'
)}
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call'
import type { RuntimeClientTarget } from '../../../../../../shared/runtime-client-target'
import type {
@@ -27,6 +27,7 @@ export function ManagedDataAccountsSection({
provider: ManagedDataAccountProvider
target: RuntimeClientTarget
}): React.JSX.Element {
const t = useTranslate()
const [state, setState] = useState<ManagedDataAccountsState | null>(null)
const [error, setError] = useState<string | null>(null)
const [busy, setBusy] = useState(false)
@@ -90,10 +91,10 @@ export function ManagedDataAccountsSection({
<SettingsGroup
title={
provider === 'opencode'
? translate('auto.lib.agent.catalog.e7a4ca5103', 'OpenCode')
: translate('auto.lib.agent.catalog.fc80296033', 'Devin')
? t('auto.lib.agent.catalog.e7a4ca5103', 'OpenCode')
: t('auto.lib.agent.catalog.fc80296033', 'Devin')
}
description={translate(
description={t(
'accounts.managedData.description',
'Add accounts by running this command in a terminal on the Orca host. Selection applies to new explicit agent launches on that host; direct SSH relay and Windows-hosted WSL launches use their own credentials.'
)}
@@ -112,8 +113,8 @@ export function ManagedDataAccountsSection({
}}
>
{copied
? translate('accounts.managedData.copied', 'Copied')
: translate('accounts.managedData.add', 'Copy add account command')}
? t('accounts.managedData.copied', 'Copied')
: t('accounts.managedData.add', 'Copy add account command')}
</Button>
<Button
variant="ghost"
@@ -123,12 +124,12 @@ export function ManagedDataAccountsSection({
void refresh()
}}
>
{translate('accounts.managedData.refresh', 'Refresh accounts')}
{t('accounts.managedData.refresh', 'Refresh accounts')}
</Button>
</div>
{!state && !error && (
<p className="text-xs text-muted-foreground">
{translate(
{t(
'accounts.managedData.upgrade',
'If accounts do not appear, update or restart the Orca host.'
)}
@@ -143,9 +144,7 @@ export function ManagedDataAccountsSection({
{state && (
<>
<div data-settings-row="" className="flex items-center justify-between gap-3 py-2">
<span className="text-xs">
{translate('accounts.managedData.system', 'System default')}
</span>
<span className="text-xs">{t('accounts.managedData.system', 'System default')}</span>
<Button
variant="ghost"
size="xs"
@@ -155,8 +154,8 @@ export function ManagedDataAccountsSection({
}}
>
{state.activeAccountId === null
? translate('accounts.managedData.active', 'Active')
: translate('accounts.managedData.select', 'Select')}
? t('accounts.managedData.active', 'Active')
: t('accounts.managedData.select', 'Select')}
</Button>
</div>
{state.accounts.map((account) => (
@@ -171,9 +170,7 @@ export function ManagedDataAccountsSection({
</div>
<div className="flex items-center gap-2">
{state.activeAccountId === account.id ? (
<Badge variant="secondary">
{translate('accounts.managedData.active', 'Active')}
</Badge>
<Badge variant="secondary">{t('accounts.managedData.active', 'Active')}</Badge>
) : (
<Button
variant="ghost"
@@ -183,7 +180,7 @@ export function ManagedDataAccountsSection({
void mutate('select', account.id)
}}
>
{translate('accounts.managedData.select', 'Select')}
{t('accounts.managedData.select', 'Select')}
</Button>
)}
<Button
@@ -192,7 +189,7 @@ export function ManagedDataAccountsSection({
disabled={busy}
onClick={() => setRemoveId(account.id)}
>
{translate('accounts.managedData.remove', 'Remove')}
{t('accounts.managedData.remove', 'Remove')}
</Button>
</div>
</div>
@@ -210,10 +207,10 @@ export function ManagedDataAccountsSection({
<DialogContent>
<DialogHeader>
<DialogTitle>
{translate('accounts.managedData.removeTitle', 'Remove managed account?')}
{t('accounts.managedData.removeTitle', 'Remove managed account?')}
</DialogTitle>
<DialogDescription>
{translate(
{t(
'accounts.managedData.removeDescription',
'Stop agents using this profile first. Removal deletes its saved credentials and conversation data. Your system login stays unchanged.'
)}
@@ -221,7 +218,7 @@ export function ManagedDataAccountsSection({
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setRemoveId(null)}>
{translate('accounts.managedData.cancel', 'Cancel')}
{t('accounts.managedData.cancel', 'Cancel')}
</Button>
<Button
variant="destructive"
@@ -233,7 +230,7 @@ export function ManagedDataAccountsSection({
}
}}
>
{translate('accounts.managedData.remove', 'Remove')}
{t('accounts.managedData.remove', 'Remove')}
</Button>
</DialogFooter>
</DialogContent>
@@ -1,4 +1,5 @@
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { Button } from '../../../ui/button'
import { Label } from '../../../ui/label'
import { Switch } from '../../../ui/switch'
@@ -9,7 +10,10 @@ import { OpenCodeGoCredentials } from '../usage/accounts-pane-opencode-credentia
import { DebouncedSettingsTextInput } from '../../form-controls/DebouncedSettingsTextInput'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
export function renderGeminiAccountsSection(model: AccountsPaneSectionModel): React.JSX.Element {
export function renderGeminiAccountsSection(
model: AccountsPaneSectionModel,
t: Translate = translate
): React.JSX.Element {
const { localAccountRuntimeSentenceLabel, recordFeatureInteraction, settings, updateSettings } =
model
return (
@@ -18,20 +22,20 @@ export function renderGeminiAccountsSection(model: AccountsPaneSectionModel): Re
title={
<span className="flex items-center gap-2">
<GeminiIcon size={16} />
{translate('auto.components.settings.AccountsPane.0c64dc2a64', 'Gemini CLI (legacy)')}
{t('auto.components.settings.AccountsPane.0c64dc2a64', 'Gemini CLI (legacy)')}
</span>
}
description={translate(
description={t(
'auto.components.settings.AccountsPane.973741a871',
'Keep existing Gemini CLI sessions available. Gemini credentials do not sign you into Antigravity.'
)}
>
<SearchableSetting
title={translate(
title={t(
'auto.components.settings.AccountsPane.0c7f915b01',
'Use Gemini CLI credentials'
)}
description={translate(
description={t(
'auto.components.settings.AccountsPane.d676c41fc6',
'Extracts OAuth credentials from your local Gemini CLI installation to authenticate with Google. This uses credentials issued to the Gemini CLI app, not Orca. May break if Google updates the CLI. Use at your own risk.'
)}
@@ -48,13 +52,13 @@ export function renderGeminiAccountsSection(model: AccountsPaneSectionModel): Re
>
<div className="space-y-0.5">
<Label>
{translate(
{t(
'auto.components.settings.AccountsPane.96f3649526',
'Use Gemini CLI credentials (experimental)'
)}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.AccountsPane.c2aee76420',
'Extracts OAuth credentials from your local Gemini CLI installation to authenticate with Google for {{value0}}. This uses credentials issued to the Gemini CLI app, not Orca. May break if Google updates the CLI. Use at your own risk.',
{ value0: localAccountRuntimeSentenceLabel }
@@ -62,7 +66,7 @@ export function renderGeminiAccountsSection(model: AccountsPaneSectionModel): Re
</p>
</div>
<Switch
aria-label={translate(
aria-label={t(
'auto.components.settings.AccountsPane.96f3649526',
'Use Gemini CLI credentials (experimental)'
)}
@@ -80,7 +84,10 @@ export function renderGeminiAccountsSection(model: AccountsPaneSectionModel): Re
)
}
export function renderOpenCodeAccountsSection(model: AccountsPaneSectionModel): React.JSX.Element {
export function renderOpenCodeAccountsSection(
model: AccountsPaneSectionModel,
t: Translate = translate
): React.JSX.Element {
const { recordFeatureInteraction, recordOpenCodeSettingEdit, settings, updateSettings } = model
return (
<div key="opencode-go" id="accounts-opencode-go" className="scroll-mt-6">
@@ -88,10 +95,10 @@ export function renderOpenCodeAccountsSection(model: AccountsPaneSectionModel):
title={
<span className="flex items-center gap-2">
<OpenCodeGoIcon size={16} />
{translate('auto.components.settings.AccountsPane.4ac10b4d08', 'OpenCode Go')}
{t('auto.components.settings.AccountsPane.4ac10b4d08', 'OpenCode Go')}
</span>
}
description={translate(
description={t(
'auto.components.settings.AccountsPane.ea631977b5',
'Configure OpenCode Go provider settings.'
)}
@@ -99,11 +106,11 @@ export function renderOpenCodeAccountsSection(model: AccountsPaneSectionModel):
<OpenCodeGoCredentials onSaved={() => recordOpenCodeSettingEdit('apiKey')} />
<SearchableSetting
title={translate(
title={t(
'auto.components.settings.AccountsPane.36223200ac',
'OpenCode Go Session Cookie'
)}
description={translate(
description={t(
'auto.components.settings.AccountsPane.0335bd31d5',
'Paste the full opencode.ai Cookie header, including __Host-console_session, for rate limit fetching.'
)}
@@ -111,10 +118,7 @@ export function renderOpenCodeAccountsSection(model: AccountsPaneSectionModel):
className="space-y-2 py-3"
>
<Label>
{translate(
'auto.components.settings.AccountsPane.67e3c33670',
'OpenCode Go session cookie'
)}
{t('auto.components.settings.AccountsPane.67e3c33670', 'OpenCode Go session cookie')}
</Label>
<div className="flex gap-2">
<DebouncedSettingsTextInput
@@ -122,7 +126,7 @@ export function renderOpenCodeAccountsSection(model: AccountsPaneSectionModel):
value={settings.opencodeSessionCookie}
onEdit={() => recordOpenCodeSettingEdit('cookie')}
commit={(opencodeSessionCookie) => updateSettings({ opencodeSessionCookie })}
placeholder={translate(
placeholder={t(
'auto.components.settings.AccountsPane.37b4b4a3f7',
'auth=…; __Host-console_session=…'
)}
@@ -139,22 +143,22 @@ export function renderOpenCodeAccountsSection(model: AccountsPaneSectionModel):
}}
className="h-7 shrink-0 text-xs text-muted-foreground hover:text-foreground"
>
{translate('auto.components.settings.AccountsPane.b398b834c9', 'Clear')}
{t('auto.components.settings.AccountsPane.b398b834c9', 'Clear')}
</Button>
)}
</div>
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.AccountsPane.62ab430f94',
"Paste the full Cookie header from your browser's DevTools → Network → any opencode.ai request, including __Host-console_session (e.g."
)}{' '}
<code className="text-xs">
{translate(
{t(
'auto.components.settings.AccountsPane.37b4b4a3f7',
'auth=…; __Host-console_session=…'
)}
</code>
{translate(
{t(
'auto.components.settings.AccountsPane.d5267cce63',
'). The auth cookie still covers workspace discovery; auth alone is not enough for usage. OpenCode Go auth is web-based and shared across Windows and WSL terminals.'
)}
@@ -162,11 +166,8 @@ export function renderOpenCodeAccountsSection(model: AccountsPaneSectionModel):
</SearchableSetting>
<SearchableSetting
title={translate(
'auto.components.settings.AccountsPane.02cb127710',
'OpenCode Go Workspace ID'
)}
description={translate(
title={t('auto.components.settings.AccountsPane.02cb127710', 'OpenCode Go Workspace ID')}
description={t(
'auto.components.settings.AccountsPane.d70a5287a4',
'Optional workspace ID override if the automatic lookup fails.'
)}
@@ -174,7 +175,7 @@ export function renderOpenCodeAccountsSection(model: AccountsPaneSectionModel):
className="space-y-2 py-3"
>
<Label>
{translate('auto.components.settings.AccountsPane.dbdb0b0bd8', 'Workspace ID override')}
{t('auto.components.settings.AccountsPane.dbdb0b0bd8', 'Workspace ID override')}
</Label>
<div className="flex gap-2">
<DebouncedSettingsTextInput
@@ -182,7 +183,7 @@ export function renderOpenCodeAccountsSection(model: AccountsPaneSectionModel):
value={settings.opencodeWorkspaceId}
onEdit={() => recordOpenCodeSettingEdit('workspaceId')}
commit={(opencodeWorkspaceId) => updateSettings({ opencodeWorkspaceId })}
placeholder={translate(
placeholder={t(
'auto.components.settings.AccountsPane.a122332371',
'wrk_… (leave blank for automatic lookup)'
)}
@@ -199,17 +200,17 @@ export function renderOpenCodeAccountsSection(model: AccountsPaneSectionModel):
}}
className="h-7 shrink-0 text-xs text-muted-foreground hover:text-foreground"
>
{translate('auto.components.settings.AccountsPane.b398b834c9', 'Clear')}
{t('auto.components.settings.AccountsPane.b398b834c9', 'Clear')}
</Button>
)}
</div>
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.AccountsPane.51c9104e13',
'Find this in the URL after logging into opencode.ai (e.g.'
)}{' '}
<code className="text-xs">
{translate(
{t(
'auto.components.settings.AccountsPane.ae3b21eb6c',
'opencode.ai/workspace/wrk_…/go'
)}
@@ -1,5 +1,6 @@
import { toast } from 'sonner'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { SearchableSetting } from '../../section-layout/SearchableSetting'
import { SettingsRow, SettingsSegmentedControl } from '../../form-controls/SettingsFormControls'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../ui/select'
@@ -10,7 +11,8 @@ import type { AccountsPaneSectionModel } from '../accounts-pane-types'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
export function renderAccountsLocationSection(
model: AccountsPaneSectionModel
model: AccountsPaneSectionModel,
t: Translate = translate
): React.JSX.Element | null {
const {
accountRuntime,
@@ -28,7 +30,7 @@ export function renderAccountsLocationSection(
onAccountLocationChange?.()
} catch {
toast.error(
translate(
t(
'auto.components.settings.AccountsPane.locationSaveFailed',
'Account location could not be saved. Try again.'
)
@@ -37,24 +39,24 @@ export function renderAccountsLocationSection(
}
const accountRuntimeControls = wslSupportedPlatform ? (
<SearchableSetting
title={translate('auto.components.settings.AccountsPane.f54b4fbd71', 'Account Location')}
description={translate(
title={t('auto.components.settings.AccountsPane.f54b4fbd71', 'Account Location')}
description={t(
'auto.components.settings.AccountsPane.2cd197025c',
'Choose whether provider accounts are inspected and added in {{value0}} or WSL.',
{ value0: getHostRuntimeLabel() }
{ value0: getHostRuntimeLabel(t) }
)}
keywords={['account', 'location', 'windows', 'wsl', 'linux', 'provider', 'auth']}
>
<SettingsRow
label={translate('auto.components.settings.AccountsPane.46cf7e7495', 'Account location')}
label={t('auto.components.settings.AccountsPane.46cf7e7495', 'Account location')}
alignTop
description={
accountRuntime.runtime === 'wsl' && !wslAvailable && !wslCapabilitiesLoading
? translate(
? t(
'auto.components.settings.AccountsPane.0c67a2a1aa',
'WSL is not available on this machine.'
)
: translate(
: t(
'auto.components.settings.AccountsPane.0b4591ff93',
'Choose which local environment to inspect and where new managed Claude and Codex accounts are added.'
)
@@ -62,20 +64,17 @@ export function renderAccountsLocationSection(
control={
<div className="flex w-44 flex-col items-stretch gap-2">
<SettingsSegmentedControl
ariaLabel={translate(
'auto.components.settings.AccountsPane.46cf7e7495',
'Account location'
)}
ariaLabel={t('auto.components.settings.AccountsPane.46cf7e7495', 'Account location')}
value={accountRuntime.runtime}
onChange={(value) => void updateAccountLocation({ localAccountRuntime: value })}
equalWidth
options={[
{ value: 'host', label: getHostRuntimeLabel() },
{ value: 'host', label: getHostRuntimeLabel(t) },
...(wslSupportedPlatform
? [
{
value: 'wsl',
label: translate('auto.components.settings.AccountsPane.8619f9afa9', 'WSL'),
label: t('auto.components.settings.AccountsPane.8619f9afa9', 'WSL'),
disabled: wslCapabilitiesLoading || !wslAvailable
} as const
]
@@ -97,20 +96,14 @@ export function renderAccountsLocationSection(
<SelectValue
placeholder={
wslCapabilitiesLoading
? translate(
'auto.components.settings.AccountsPane.ad47a33f72',
'Loading WSL'
)
: translate(
'auto.components.settings.AccountsPane.2358ac71d2',
'WSL default'
)
? t('auto.components.settings.AccountsPane.ad47a33f72', 'Loading WSL')
: t('auto.components.settings.AccountsPane.2358ac71d2', 'WSL default')
}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value={WSL_DEFAULT_DISTRO_KEY}>
{translate('auto.components.settings.AccountsPane.2358ac71d2', 'WSL default')}
{t('auto.components.settings.AccountsPane.2358ac71d2', 'WSL default')}
</SelectItem>
{wslDistros.map((distro) => (
<SelectItem key={distro} value={distro}>
@@ -7,31 +7,33 @@ import type {
import { resolveLocalAccountRuntimeTarget } from '../../../../../../shared/local-account-runtime'
import { getRendererAppPlatform } from '../../../../lib/renderer-app-platform'
import { getIntlLocale, translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import type { LocalAccountRuntime } from '../accounts-pane-types'
export const EMPTY_WSL_DISTROS: string[] = []
export function getHostRuntimeLabel(): string {
export function getHostRuntimeLabel(t: Translate = translate): string {
return navigator.userAgent.includes('Windows')
? 'Windows'
: translate('auto.components.settings.AccountsPane.9baf45d071', 'This device')
: t('auto.components.settings.AccountsPane.9baf45d071', 'This device')
}
// System default has no saved identity; show live host login or custom-provider detail, falling back when signed out.
export function getCodexSystemDefaultSubtitle(
identity: CodexSystemDefaultIdentity | undefined,
runtimeSentenceLabel: string
runtimeSentenceLabel: string,
t: Translate = translate
): string {
if (identity?.authKind === 'oauth' && identity.email) {
return identity.email
}
if (identity?.authKind === 'api-key') {
return translate(
return t(
'auto.components.settings.AccountsPane.codexSystemDefaultCustomProvider',
'Custom provider — no usage tracked.'
)
}
return translate(
return t(
'auto.components.settings.AccountsPane.fcc4093fc1',
'Use your current {{value0}} Codex login.',
{ value0: runtimeSentenceLabel }
@@ -65,7 +67,8 @@ export function getSelectedAccountRuntime(
wslDistros: string[],
wslCapabilitiesLoading: boolean,
/** A WSL host's distro: its accounts are this desktop's for that distro, with no location to pick. */
wslHostDistro: string | null = null
wslHostDistro: string | null = null,
t: Translate = translate
): LocalAccountRuntime {
// Display the concrete target behind persisted auto policy.
const resolvedRuntime = wslHostDistro
@@ -75,7 +78,7 @@ export function getSelectedAccountRuntime(
if (!wslHostDistro && !wslAvailable && !wslCapabilitiesLoading) {
return {
runtime: 'wsl',
label: translate('auto.components.settings.AccountsPane.8619f9afa9', 'WSL')
label: t('auto.components.settings.AccountsPane.8619f9afa9', 'WSL')
}
}
const configuredDistro = resolvedRuntime.wslDistro?.trim() || null
@@ -89,10 +92,10 @@ export function getSelectedAccountRuntime(
wslDistro: selectedDistro,
label: selectedDistro
? `WSL ${selectedDistro}`
: translate('auto.components.settings.AccountsPane.2358ac71d2', 'WSL default')
: t('auto.components.settings.AccountsPane.2358ac71d2', 'WSL default')
}
}
return { runtime: 'host', label: getHostRuntimeLabel() }
return { runtime: 'host', label: getHostRuntimeLabel(t) }
}
// Standalone runtime labels need sentence casing when interpolated into prose.
@@ -102,8 +105,11 @@ export function getRuntimeSentenceLabel(runtime: LocalAccountRuntime): string {
: runtime.label
}
export function formatAccountTimestamp(timestamp: number): string {
return new Date(timestamp).toLocaleString(getIntlLocale(), {
export function formatAccountTimestamp(
timestamp: number,
intlLocale: string = getIntlLocale()
): string {
return new Date(timestamp).toLocaleString(intlLocale, {
month: 'short',
day: 'numeric',
hour: 'numeric',
@@ -1,6 +1,6 @@
import { useId, useState, useSyncExternalStore } from 'react'
import { Check, Copy, ExternalLink, X } from 'lucide-react'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { useClipboardTextCopyFeedback } from '@/hooks/use-clipboard-text-copy-feedback'
import {
cancelHostAccountSignIn,
@@ -19,14 +19,15 @@ import { showHostCancelFailure } from './account-add-cancel-target'
import { Input } from '../../../ui/input'
function CopyButton(props: { text: string; label: string }): React.JSX.Element {
const t = useTranslate()
const { copyText, status } = useClipboardTextCopyFeedback(props.text)
return (
<Button variant="outline" size="xs" onClick={() => void copyText()}>
{status === 'copied' ? <Check /> : <Copy />}
{status === 'copied'
? translate('accounts.hostSignIn.copied', 'Copied')
? t('accounts.hostSignIn.copied', 'Copied')
: status === 'failed'
? translate('accounts.hostSignIn.copyFailed', 'Could not copy')
? t('accounts.hostSignIn.copyFailed', 'Could not copy')
: props.label}
</Button>
)
@@ -36,6 +37,7 @@ function PastedCodeForm(props: {
owner: RuntimeClientTarget
provider: HostAccountSignInProvider
}): React.JSX.Element {
const t = useTranslate()
const [code, setCode] = useState('')
const [pending, setPending] = useState(false)
const [refusal, setRefusal] = useState<string | null>(null)
@@ -58,7 +60,7 @@ function PastedCodeForm(props: {
}}
>
<p id={helpId} className="text-xs text-muted-foreground">
{translate(
{t(
'accounts.hostSignIn.pasteCodeHelp',
'After you sign in, the page shows a code. Paste it here.'
)}
@@ -78,7 +80,7 @@ function PastedCodeForm(props: {
}}
/>
<Button type="submit" size="sm" variant="outline" disabled={pending || !trimmed}>
{translate('accounts.hostSignIn.continue', 'Continue')}
{t('accounts.hostSignIn.continue', 'Continue')}
</Button>
</div>
{refusal !== null ? (
@@ -95,6 +97,7 @@ export function HostAccountSignInPrompt(props: {
provider: HostAccountSignInProvider
hostLabel: string
}): React.JSX.Element | null {
const t = useTranslate()
const read = () => readHostAccountSignIn(props.owner, props.provider)
const status = useSyncExternalStore(subscribeHostAccountSignIns, read, read)
const readRunning = () => hasHostAccountSignIn(props.owner, props.provider)
@@ -109,14 +112,14 @@ export function HostAccountSignInPrompt(props: {
<div className="flex items-start justify-between gap-2">
<p role="status" className="text-xs text-muted-foreground">
{!status || status.phase === 'starting'
? translate('accounts.hostSignIn.starting', 'Starting sign-in on {{host}}…', {
? t('accounts.hostSignIn.starting', 'Starting sign-in on {{host}}…', {
host: props.hostLabel
})
: status.phase === 'verifying'
? translate('accounts.hostSignIn.verifying', 'Finishing sign-in on {{host}}…', {
? t('accounts.hostSignIn.verifying', 'Finishing sign-in on {{host}}…', {
host: props.hostLabel
})
: translate(
: t(
'accounts.hostSignIn.waiting',
'Open this page on this device and sign in. The account is saved on {{host}}.',
{ host: props.hostLabel }
@@ -132,7 +135,7 @@ export function HostAccountSignInPrompt(props: {
className="gap-1.5"
>
<X />
{translate('accounts.hostSignIn.cancel', 'Cancel')}
{t('accounts.hostSignIn.cancel', 'Cancel')}
</Button>
</div>
{url ? (
@@ -140,24 +143,24 @@ export function HostAccountSignInPrompt(props: {
<span className="min-w-0 flex-1 truncate rounded bg-muted px-1.5 py-1 font-mono text-[11px] text-foreground/80">
{url}
</span>
<CopyButton text={url} label={translate('accounts.hostSignIn.copyLink', 'Copy link')} />
<CopyButton text={url} label={t('accounts.hostSignIn.copyLink', 'Copy link')} />
<Button variant="ghost" size="xs" onClick={() => void window.api.shell.openUrl(url)}>
<ExternalLink />
{translate('accounts.hostSignIn.open', 'Open')}
{t('accounts.hostSignIn.open', 'Open')}
</Button>
</div>
) : null}
{url && prompt?.userCode ? (
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">
{translate('accounts.hostSignIn.enterCode', 'Enter this code on that page:')}
{t('accounts.hostSignIn.enterCode', 'Enter this code on that page:')}
</span>
<span className="rounded bg-muted px-1.5 py-1 font-mono text-xs text-foreground">
{prompt.userCode}
</span>
<CopyButton
text={prompt.userCode}
label={translate('accounts.hostSignIn.copyCode', 'Copy code')}
label={t('accounts.hostSignIn.copyCode', 'Copy code')}
/>
</div>
) : null}
@@ -1,5 +1,5 @@
import { ExternalLink, ShieldCheck } from 'lucide-react'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
import type { FeatureInteractionId } from '../../../../../../shared/feature-interaction-catalog'
import type { ProviderRateLimits } from '../../../../../../shared/rate-limit-types'
@@ -30,6 +30,7 @@ export function MiniMaxAccountsSection({
miniMaxRateLimits: ProviderRateLimits | null
recordFeatureInteraction: (featureId: FeatureInteractionId) => void
}): React.JSX.Element {
const t = useTranslate()
const credentials = useMiniMaxCredentials(host.owner)
const { miniMaxConfigured, miniMaxApiKeyConfigured, miniMaxCredentialBusy } = credentials
const settings = usageSettings.settings
@@ -38,7 +39,7 @@ export function MiniMaxAccountsSection({
? 'https://platform.minimaxi.com/console/usage'
: 'https://platform.minimax.io/console/usage'
const configured = miniMaxConfigured || miniMaxApiKeyConfigured
const accessNotice = usageCredentialAccessNotice(host, credentials.access)
const accessNotice = usageCredentialAccessNotice(host, credentials.access, t)
const settingsUnavailable = settings === null
const handleMiniMaxEndpointChange = (value: string): void => {
if ((value !== 'overseas' && value !== 'cn') || value === settings?.minimaxEndpoint) {
@@ -53,10 +54,10 @@ export function MiniMaxAccountsSection({
title={
<span className="flex items-center gap-2">
<MiniMaxIcon size={16} />
{translate('auto.components.settings.AccountsPane.5d63bbfbec', 'MiniMax')}
{t('auto.components.settings.AccountsPane.5d63bbfbec', 'MiniMax')}
</span>
}
description={translate(
description={t(
'auto.components.settings.AccountsPane.usageTracking',
'Configure MiniMax usage tracking for your account.'
)}
@@ -67,7 +68,7 @@ export function MiniMaxAccountsSection({
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
>
{translate('auto.components.settings.AccountsPane.0d8e77bc40', 'Open console')}
{t('auto.components.settings.AccountsPane.0d8e77bc40', 'Open console')}
<ExternalLink className="size-3" />
</a>
}
@@ -85,20 +86,20 @@ export function MiniMaxAccountsSection({
) : (
<p className="text-xs font-medium">
{configured
? usageCredentialStoredLabel(host)
: translate(
? usageCredentialStoredLabel(host, t)
: t(
'auto.components.settings.AccountsPane.credentialsNotSet',
'Credentials not set'
)}
</p>
)}
<p className="text-xs text-muted-foreground">{miniMaxStorageHelp(host)}</p>
<p className="text-xs text-muted-foreground">{miniMaxStorageHelp(host, t)}</p>
</div>
</div>
<SearchableSetting
title={translate('auto.components.settings.AccountsPane.f8a4b9d210', 'MiniMax endpoint')}
description={translate(
title={t('auto.components.settings.AccountsPane.f8a4b9d210', 'MiniMax endpoint')}
description={t(
'auto.components.settings.AccountsPane.0b3a9f6c2e',
'Pick the host that matches your account. Both overseas (platform.minimax.io) and China (platform.minimaxi.com) accept either a session cookie or an API key.'
)}
@@ -106,7 +107,7 @@ export function MiniMaxAccountsSection({
className="space-y-2 py-3"
>
<Label htmlFor="minimax-endpoint">
{translate('auto.components.settings.AccountsPane.f8a4b9d210', 'MiniMax endpoint')}
{t('auto.components.settings.AccountsPane.f8a4b9d210', 'MiniMax endpoint')}
</Label>
<Select
value={settings?.minimaxEndpoint ?? ''}
@@ -120,14 +121,14 @@ export function MiniMaxAccountsSection({
{[
{
value: 'overseas',
label: translate(
label: t(
'auto.components.settings.AccountsPane.endpointOverseas',
'Overseas (platform.minimax.io)'
)
},
{
value: 'cn',
label: translate(
label: t(
'auto.components.settings.AccountsPane.endpointChina',
'China (platform.minimaxi.com)'
)
@@ -152,10 +153,10 @@ export function MiniMaxAccountsSection({
<div className="flex items-center justify-between gap-3 pt-4 pb-3">
<div className="space-y-1">
<h4 className="text-xs font-semibold text-muted-foreground">
{translate('auto.components.settings.AccountsPane.9dd50d3f75', 'Advanced')}
{t('auto.components.settings.AccountsPane.9dd50d3f75', 'Advanced')}
</h4>
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.AccountsPane.174fb408f9',
'Leave these defaults alone unless MiniMax usage refresh points at the wrong workspace or model.'
)}
@@ -164,11 +165,8 @@ export function MiniMaxAccountsSection({
</div>
<SearchableSetting
title={translate(
'auto.components.settings.AccountsPane.bf160bb6c0',
'Group ID override'
)}
description={translate(
title={t('auto.components.settings.AccountsPane.bf160bb6c0', 'Group ID override')}
description={t(
'auto.components.settings.AccountsPane.b1e2743313',
'Optional. Leave blank to use minimax_group_id_v2 from the cookie.'
)}
@@ -176,14 +174,14 @@ export function MiniMaxAccountsSection({
className="space-y-2 py-3"
>
<Label>
{translate('auto.components.settings.AccountsPane.bf160bb6c0', 'Group ID override')}
{t('auto.components.settings.AccountsPane.bf160bb6c0', 'Group ID override')}
</Label>
<DebouncedSettingsTextInput
type="text"
value={settings?.minimaxGroupId ?? ''}
commit={(minimaxGroupId) => void usageSettings.update({ minimaxGroupId })}
disabled={settingsUnavailable}
placeholder={translate(
placeholder={t(
'auto.components.settings.AccountsPane.0747d6391a',
'Use group ID from cookie'
)}
@@ -193,11 +191,8 @@ export function MiniMaxAccountsSection({
</SearchableSetting>
<SearchableSetting
title={translate(
'auto.components.settings.AccountsPane.4ff2af7524',
'Usage model names'
)}
description={translate(
title={t('auto.components.settings.AccountsPane.4ff2af7524', 'Usage model names')}
description={t(
'auto.components.settings.AccountsPane.5cf4b0f85f',
'Optional comma-separated model names. Leave as general unless MiniMax returns a model-specific error.'
)}
@@ -205,14 +200,14 @@ export function MiniMaxAccountsSection({
className="space-y-2 py-3"
>
<Label>
{translate('auto.components.settings.AccountsPane.4ff2af7524', 'Usage model names')}
{t('auto.components.settings.AccountsPane.4ff2af7524', 'Usage model names')}
</Label>
<DebouncedSettingsTextInput
type="text"
value={settings?.minimaxUsageModels ?? ''}
commit={(minimaxUsageModels) => void usageSettings.update({ minimaxUsageModels })}
disabled={settingsUnavailable}
placeholder={translate('auto.components.settings.AccountsPane.3c92b0d31c', 'general')}
placeholder={t('auto.components.settings.AccountsPane.3c92b0d31c', 'general')}
spellCheck={false}
className="text-xs"
/>
@@ -3,6 +3,7 @@ import { isWebClientLocation } from '@/lib/web-client-location'
import { ExternalLink, Loader2, Lock, LockOpen, RefreshCw, ShieldCheck } from 'lucide-react'
import { AgentIcon } from '@/lib/agent-catalog'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import {
ZCODE_PLAN_SITE_CONSOLE_URLS,
type ZcodePlanSite
@@ -42,14 +43,14 @@ const SEARCH_KEYWORDS = [
'zcode'
]
function siteLabel(site: ZcodePlanSite): string {
function siteLabel(site: ZcodePlanSite, t: Translate = translate): string {
if (site === 'bigmodel') {
return translate(
return t(
'auto.components.settings.ZcodePlanAccountsSection.site.bigmodel',
'Zhipu · BigModel (open.bigmodel.cn)'
)
}
return translate('auto.components.settings.ZcodePlanAccountsSection.site.zai', 'Z.AI (z.ai)')
return t('auto.components.settings.ZcodePlanAccountsSection.site.zai', 'Z.AI (z.ai)')
}
export function ZcodePlanAccountsSection({
@@ -59,6 +60,7 @@ export function ZcodePlanAccountsSection({
host: UsageCredentialHost
usageSettings: UsageHostSettingsModel
}): React.JSX.Element {
const t = useTranslate()
const refreshRateLimits = useAppStore((s) => s.refreshRateLimits)
const storeZcodeUsage = useAppStore((s) => s.rateLimits.zcode)
const hostLabel = host.label
@@ -71,11 +73,11 @@ export function ZcodePlanAccountsSection({
const webClient = isWebClientLocation()
const credentialEditable = !webClient && access === 'ready'
const accessNotice = usageCredentialAccessNotice(host, access)
const accessNotice = usageCredentialAccessNotice(host, access, t)
const site =
usageSettings.settings?.zcodePlanSite ?? (!webClient && hostLabel === null ? 'zai' : undefined)
const siteEditable = credentialEditable && usageSettings.settings !== null
const unreadableNotice = usageCredentialUnreadableNotice(host, status?.apiKeyUnreadable)
const unreadableNotice = usageCredentialUnreadableNotice(host, status?.apiKeyUnreadable, t)
const keyEditable =
credentialEditable && !usageCredentialSealedElsewhere(host, status?.apiKeyUnreadable)
const detailsUnavailable = webClient || status?.detailsUnavailable === true
@@ -109,13 +111,10 @@ export function ZcodePlanAccountsSection({
title={
<span className="flex items-center gap-2">
<AgentIcon agent="zcode" size={16} />
{translate(
'auto.components.settings.ZcodePlanAccountsSection.title',
'GLM Coding Plan'
)}
{t('auto.components.settings.ZcodePlanAccountsSection.title', 'GLM Coding Plan')}
</span>
}
description={translate(
description={t(
'auto.components.settings.ZcodePlanAccountsSection.subtitle',
'Track Z.AI or Zhipu (BigModel) GLM Coding Plan usage in the status bar. Save the plan API key here — no ZCode CLI setup needed.'
)}
@@ -127,10 +126,7 @@ export function ZcodePlanAccountsSection({
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
>
{translate(
'auto.components.settings.ZcodePlanAccountsSection.consoleLink',
'Get API key'
)}
{t('auto.components.settings.ZcodePlanAccountsSection.consoleLink', 'Get API key')}
<ExternalLink className="size-3" />
</a>
) : null
@@ -148,7 +144,7 @@ export function ZcodePlanAccountsSection({
<p className="text-xs font-medium">{accessNotice}</p>
) : detailsUnavailable ? (
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.ZcodePlanAccountsSection.detailsUnavailable',
'Plan credential details are only readable on the computer running Orca.'
)}
@@ -156,19 +152,19 @@ export function ZcodePlanAccountsSection({
) : apiKeyConfigured ? (
<>
<p className="text-xs font-medium">
{translate(
{t(
'auto.components.settings.ZcodePlanAccountsSection.keyStored',
'API key saved · {{value0}}',
{ value0: siteLabel(site ?? 'zai') }
{ value0: siteLabel(site ?? 'zai', t) }
)}
</p>
<p className="text-xs text-muted-foreground">
{hostLabel === null
? translate(
? t(
'auto.components.settings.ZcodePlanAccountsSection.keyStoredHelp',
'Stored locally and sent only to the selected site for usage refreshes. It takes priority over the ZCode CLI sign-in.'
)
: translate(
: t(
'auto.components.settings.ZcodePlanAccountsSection.keyStoredOnHostHelp',
'Stored on {{host}} and sent from there only to the selected site for usage refreshes. It takes priority over the ZCode CLI sign-in.',
{ host: hostLabel }
@@ -178,18 +174,18 @@ export function ZcodePlanAccountsSection({
) : status?.zcodeCliConfigured ? (
<>
<p className="text-xs font-medium">
{translate(
{t(
'auto.components.settings.ZcodePlanAccountsSection.usingCli',
'Using the ZCode CLI sign-in'
)}
</p>
<p className="text-xs text-muted-foreground">
{hostLabel === null
? translate(
? t(
'auto.components.settings.ZcodePlanAccountsSection.usingCliHelp',
'Orca reads the selected Coding Plan from the ZCode CLI credential store on this computer. Save an API key below to link the plan here instead.'
)
: translate(
: t(
'auto.components.settings.ZcodePlanAccountsSection.usingCliOnHostHelp',
'Orca reads the selected Coding Plan from the ZCode CLI credential store on {{host}}. Save an API key below to link the plan there instead.',
{ host: hostLabel }
@@ -199,18 +195,18 @@ export function ZcodePlanAccountsSection({
) : (
<>
<p className="text-xs font-medium">
{translate(
{t(
'auto.components.settings.ZcodePlanAccountsSection.notConfigured',
'No GLM Coding Plan linked'
)}
</p>
<p className="text-xs text-muted-foreground">
{hostLabel === null
? translate(
? t(
'auto.components.settings.ZcodePlanAccountsSection.notConfiguredHelp',
'Save the plan API key below, or sign in with the ZCode CLI on this computer.'
)
: translate(
: t(
'auto.components.settings.ZcodePlanAccountsSection.notConfiguredOnHostHelp',
'Save the plan API key below, or sign in with the ZCode CLI on {{host}}.',
{ host: hostLabel }
@@ -233,17 +229,14 @@ export function ZcodePlanAccountsSection({
) : (
<RefreshCw className="size-3" />
)}
{translate(
'auto.components.settings.ZcodePlanAccountsSection.refreshUsage',
'Refresh usage'
)}
{t('auto.components.settings.ZcodePlanAccountsSection.refreshUsage', 'Refresh usage')}
</Button>
) : null}
</div>
{webClient ? (
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.ZcodePlanAccountsSection.hostOnly',
'Change the plan site and API key in the desktop app on the computer running Orca.'
)}
@@ -251,11 +244,8 @@ export function ZcodePlanAccountsSection({
) : null}
<SearchableSetting
title={translate(
'auto.components.settings.ZcodePlanAccountsSection.siteTitle',
'Plan site'
)}
description={translate(
title={t('auto.components.settings.ZcodePlanAccountsSection.siteTitle', 'Plan site')}
description={t(
'auto.components.settings.ZcodePlanAccountsSection.siteDescription',
'Pick the console your Coding Plan belongs to: Z.AI for the international site, Zhipu BigModel for the mainland site.'
)}
@@ -263,7 +253,7 @@ export function ZcodePlanAccountsSection({
className="space-y-2 py-3"
>
<Label htmlFor="zcode-plan-site">
{translate('auto.components.settings.ZcodePlanAccountsSection.siteTitle', 'Plan site')}
{t('auto.components.settings.ZcodePlanAccountsSection.siteTitle', 'Plan site')}
</Label>
<Select
value={site ?? ''}
@@ -272,7 +262,7 @@ export function ZcodePlanAccountsSection({
>
<SelectTrigger id="zcode-plan-site" size="sm" className="w-full">
<SelectValue
placeholder={translate(
placeholder={t(
'auto.components.settings.ZcodePlanAccountsSection.siteUnavailable',
'Host plan site unavailable'
)}
@@ -281,7 +271,7 @@ export function ZcodePlanAccountsSection({
<SelectContent>
{(['zai', 'bigmodel'] as const).map((option) => (
<SelectItem key={option} value={option}>
{siteLabel(option)}
{siteLabel(option, t)}
</SelectItem>
))}
</SelectContent>
@@ -289,32 +279,29 @@ export function ZcodePlanAccountsSection({
</SearchableSetting>
<SearchableSetting
title={translate('auto.components.settings.ZcodePlanAccountsSection.keyTitle', 'API key')}
description={zcodePlanKeyDescription(host)}
title={t('auto.components.settings.ZcodePlanAccountsSection.keyTitle', 'API key')}
description={zcodePlanKeyDescription(host, t)}
keywords={SEARCH_KEYWORDS}
className="space-y-2 py-3"
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<Label htmlFor="zcode-plan-api-key">
{translate('auto.components.settings.ZcodePlanAccountsSection.keyTitle', 'API key')}
{t('auto.components.settings.ZcodePlanAccountsSection.keyTitle', 'API key')}
</Label>
{!detailsUnavailable ? (
<Badge variant={apiKeyConfigured ? 'secondary' : 'outline'}>
{apiKeyConfigured ? <Lock className="size-3" /> : <LockOpen className="size-3" />}
{apiKeyConfigured
? translate('auto.components.settings.ZcodePlanAccountsSection.saved', 'Saved')
: translate(
'auto.components.settings.ZcodePlanAccountsSection.notSaved',
'Not saved'
)}
? t('auto.components.settings.ZcodePlanAccountsSection.saved', 'Saved')
: t('auto.components.settings.ZcodePlanAccountsSection.notSaved', 'Not saved')}
</Badge>
) : null}
</div>
</div>
<UnsealedCredentialNotice
protection={status?.apiKeyProtection ?? null}
credentialName={translate(
credentialName={t(
'auto.components.settings.ZcodePlanAccountsSection.title',
'GLM Coding Plan'
)}
@@ -329,7 +316,7 @@ export function ZcodePlanAccountsSection({
disabled={credentialBusy || !keyEditable}
value={apiKeyDraft}
onChange={(e) => setApiKeyDraft(e.target.value)}
placeholder={translate(
placeholder={t(
'auto.components.settings.ZcodePlanAccountsSection.keyPlaceholder',
'Paste your GLM Coding Plan API key'
)}
@@ -344,8 +331,8 @@ export function ZcodePlanAccountsSection({
>
{credentialBusy ? <Loader2 className="size-3 animate-spin" /> : null}
{apiKeyConfigured
? translate('auto.components.settings.ZcodePlanAccountsSection.replace', 'Replace')
: translate('auto.components.settings.ZcodePlanAccountsSection.save', 'Save')}
? t('auto.components.settings.ZcodePlanAccountsSection.replace', 'Replace')
: t('auto.components.settings.ZcodePlanAccountsSection.save', 'Save')}
</Button>
{apiKeyConfigured ? (
<Button
@@ -355,15 +342,12 @@ export function ZcodePlanAccountsSection({
disabled={credentialBusy || !credentialEditable}
className="shrink-0"
>
{translate(
'auto.components.settings.ZcodePlanAccountsSection.forgetKey',
'Forget key'
)}
{t('auto.components.settings.ZcodePlanAccountsSection.forgetKey', 'Forget key')}
</Button>
) : null}
</div>
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.ZcodePlanAccountsSection.keyHelp',
'The same key your coding tools use for the plan (for example Claude Code with ANTHROPIC_BASE_URL pointed at the site). Switching the site above changes which host receives it.'
)}
@@ -374,11 +358,8 @@ export function ZcodePlanAccountsSection({
{usageWindows.length > 0 ? (
<SearchableSetting
title={translate(
'auto.components.settings.ZcodePlanAccountsSection.usageTitle',
'Plan usage'
)}
description={translate(
title={t('auto.components.settings.ZcodePlanAccountsSection.usageTitle', 'Plan usage')}
description={t(
'auto.components.settings.ZcodePlanAccountsSection.usageDescription',
'Live quota windows for the linked Coding Plan, refreshed with the status bar usage cycle.'
)}
@@ -391,7 +372,7 @@ export function ZcodePlanAccountsSection({
))}
{usage?.planType ? (
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.ZcodePlanAccountsSection.planLevel',
'Plan: {{value0}}',
{ value0: usage.planType }
@@ -4,6 +4,7 @@ import type { MiniMaxCredentialsStatus } from '../../../../../../shared/minimax-
import type { RuntimeClientTarget } from '../../../../../../shared/runtime-client-target'
import { toast } from 'sonner'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import {
callMiniMaxCredentials,
UsageCredentialsHostOutdatedError,
@@ -23,7 +24,10 @@ type MiniMaxCredentialActionContext = {
recordFeatureInteraction: (featureId: FeatureInteractionId) => void
}
export function createMiniMaxCredentialActions(context: MiniMaxCredentialActionContext): {
export function createMiniMaxCredentialActions(
context: MiniMaxCredentialActionContext,
t: Translate = translate
): {
saveMiniMaxApiKey: () => Promise<void>
clearMiniMaxApiKey: () => Promise<void>
saveMiniMaxCookie: () => Promise<void>
@@ -72,11 +76,11 @@ export function createMiniMaxCredentialActions(context: MiniMaxCredentialActionC
const clearCookieDraft = (): void => context.setMiniMaxCookieDraft('')
const clearApiKeyDraft = (): void => context.setMiniMaxApiKeyDraft('')
const cookieFailure = translate(
const cookieFailure = t(
'auto.components.settings.AccountsPane.b43e761fe5',
'MiniMax cookie update failed.'
)
const apiKeyFailure = translate(
const apiKeyFailure = t(
'auto.components.settings.AccountsPane.apiKeyUpdateFailed',
'MiniMax API key update failed.'
)
@@ -85,7 +89,7 @@ export function createMiniMaxCredentialActions(context: MiniMaxCredentialActionC
const cookie = context.miniMaxCookieDraft.trim()
if (!cookie) {
toast.error(
translate('auto.components.settings.AccountsPane.2f24f244a4', 'MiniMax cookie is required.')
t('auto.components.settings.AccountsPane.2f24f244a4', 'MiniMax cookie is required.')
)
return
}
@@ -95,11 +99,11 @@ export function createMiniMaxCredentialActions(context: MiniMaxCredentialActionC
failureMessage: cookieFailure,
saved: {
landed: (status) => status.cookieConfigured,
notSaved: translate(
notSaved: t(
'auto.components.settings.AccountsPane.8e6f0cb1d8',
'MiniMax cookie was not saved.'
),
ok: translate('auto.components.settings.AccountsPane.8d61637a77', 'MiniMax cookie saved.')
ok: t('auto.components.settings.AccountsPane.8d61637a77', 'MiniMax cookie saved.')
}
})
}
@@ -108,10 +112,7 @@ export function createMiniMaxCredentialActions(context: MiniMaxCredentialActionC
const apiKey = context.miniMaxApiKeyDraft.trim()
if (!apiKey) {
toast.error(
translate(
'auto.components.settings.AccountsPane.d6f1b9b6a2',
'MiniMax API key is required.'
)
t('auto.components.settings.AccountsPane.d6f1b9b6a2', 'MiniMax API key is required.')
)
return
}
@@ -121,11 +122,11 @@ export function createMiniMaxCredentialActions(context: MiniMaxCredentialActionC
failureMessage: apiKeyFailure,
saved: {
landed: (status) => status.apiKeyConfigured,
notSaved: translate(
notSaved: t(
'auto.components.settings.AccountsPane.7c5d8a4e1b',
'MiniMax API key was not saved.'
),
ok: translate('auto.components.settings.AccountsPane.4d2c7b9e83', 'MiniMax API key saved.')
ok: t('auto.components.settings.AccountsPane.4d2c7b9e83', 'MiniMax API key saved.')
}
})
}
@@ -1,6 +1,7 @@
import { HelpCircle, Loader2, Lock, LockOpen } from 'lucide-react'
import { useNow } from '../../../../hooks/use-now'
import { translate } from '@/i18n/i18n'
import { useIntlLocale, useTranslate, type Translate } from '@/i18n/use-translate'
import { formatUiRelativeTime } from '@/i18n/relative-time-format'
import { Badge } from '../../../ui/badge'
import { Button } from '../../../ui/button'
@@ -18,25 +19,30 @@ import {
} from './usage-credential-host'
/** Name the storage host because its poller sends the key. */
export function miniMaxStorageHelp(host: UsageCredentialHost): string {
export function miniMaxStorageHelp(host: UsageCredentialHost, t: Translate = translate): string {
return host.label === null
? translate(
? t(
'auto.components.settings.AccountsPane.selectedEndpointStorage',
'Stored locally and sent to the selected MiniMax endpoint for usage refreshes.'
)
: translate(
: t(
'auto.components.settings.usageCredentialHost.miniMaxStorage',
'Stored on {{host}} and sent from there to the selected MiniMax endpoint for usage refreshes.',
{ host: host.label }
)
}
function formatMiniMaxRelativeRefresh(updatedAt: number, now: number): string {
function formatMiniMaxRelativeRefresh(
updatedAt: number,
now: number,
t: Translate = translate,
intlLocale?: string
): string {
const diffMs = Math.max(0, now - updatedAt)
if (diffMs < 60_000) {
return translate('auto.components.settings.AccountsPane.3a30aaf526', 'just now')
return t('auto.components.settings.AccountsPane.3a30aaf526', 'just now')
}
return formatUiRelativeTime(-diffMs)
return formatUiRelativeTime(-diffMs, intlLocale)
}
function MiniMaxCookieHelpPopover({
@@ -46,41 +52,42 @@ function MiniMaxCookieHelpPopover({
consoleUrl: string
host: UsageCredentialHost
}): React.JSX.Element {
const t = useTranslate()
const steps = [
translate(
t(
'auto.components.settings.AccountsPane.openSelectedConsole',
'Open {{url}} in your browser and sign in.',
{ url: consoleUrl }
),
translate('auto.components.settings.AccountsPane.24560fe830', 'Open DevTools.'),
translate(
t('auto.components.settings.AccountsPane.24560fe830', 'Open DevTools.'),
t(
'auto.components.settings.AccountsPane.4cab0fa42d',
'Go to the Network tab and enable Preserve log.'
),
translate('auto.components.settings.AccountsPane.bee4e63e1c', 'Reload the page.'),
translate(
t('auto.components.settings.AccountsPane.bee4e63e1c', 'Reload the page.'),
t(
'auto.components.settings.AccountsPane.87f814af6f',
'Filter for remains and select the coding_plan/remains request.'
),
translate(
t(
'auto.components.settings.AccountsPane.435df0ee51',
'Under Request Headers, copy the Cookie value.'
),
translate('auto.components.settings.AccountsPane.7492fb3bba', 'Paste it here and click Save.')
t('auto.components.settings.AccountsPane.7492fb3bba', 'Paste it here and click Save.')
]
return (
<div className="space-y-3 p-3 text-xs">
<div className="space-y-1">
<p className="font-medium">
{translate('auto.components.settings.AccountsPane.9fec52de4b', 'How to copy the cookie')}
{t('auto.components.settings.AccountsPane.9fec52de4b', 'How to copy the cookie')}
</p>
<p className="text-muted-foreground">
{host.label === null
? translate(
? t(
'auto.components.settings.AccountsPane.cookieSelectedEndpoint',
'Stored locally and sent to the selected MiniMax endpoint for usage refreshes.'
)
: miniMaxStorageHelp(host)}
: miniMaxStorageHelp(host, t)}
</p>
</div>
<ol className="list-decimal space-y-1 pl-4 text-muted-foreground">
@@ -103,6 +110,8 @@ export function MiniMaxCredentials({
miniMaxRateLimits: ProviderRateLimits | null
consoleUrl: string
}): React.JSX.Element {
const t = useTranslate()
const intlLocale = useIntlLocale()
const {
editable,
sessionCookieSupported,
@@ -123,8 +132,8 @@ export function MiniMaxCredentials({
clearMiniMaxApiKey
} = credentials
const now = useNow(60_000)
const cookieNotice = usageCredentialUnreadableNotice(host, miniMaxCookieUnreadable)
const apiKeyNotice = usageCredentialUnreadableNotice(host, miniMaxApiKeyUnreadable)
const cookieNotice = usageCredentialUnreadableNotice(host, miniMaxCookieUnreadable, t)
const apiKeyNotice = usageCredentialUnreadableNotice(host, miniMaxApiKeyUnreadable, t)
const cookieEditable =
editable &&
sessionCookieSupported &&
@@ -136,11 +145,8 @@ export function MiniMaxCredentials({
return (
<>
<SearchableSetting
title={translate(
'auto.components.settings.AccountsPane.21d6eb141e',
'MiniMax Session Cookie'
)}
description={translate(
title={t('auto.components.settings.AccountsPane.21d6eb141e', 'MiniMax Session Cookie')}
description={t(
'auto.components.settings.AccountsPane.33bba5ad83',
'Paste your MiniMax session cookie for local rate-limit fetching.'
)}
@@ -150,10 +156,7 @@ export function MiniMaxCredentials({
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<Label htmlFor="minimax-cookie">
{translate(
'auto.components.settings.AccountsPane.21d6eb141e',
'MiniMax Session Cookie'
)}
{t('auto.components.settings.AccountsPane.21d6eb141e', 'MiniMax Session Cookie')}
</Label>
<Badge
variant={miniMaxConfigured ? 'secondary' : 'outline'}
@@ -161,8 +164,8 @@ export function MiniMaxCredentials({
>
{miniMaxConfigured ? <Lock className="size-3" /> : <LockOpen className="size-3" />}
{miniMaxConfigured
? translate('auto.components.settings.AccountsPane.73ea15f24b', 'Saved')
: translate('auto.components.settings.AccountsPane.23afe8f226', 'Not saved')}
? t('auto.components.settings.AccountsPane.73ea15f24b', 'Saved')
: t('auto.components.settings.AccountsPane.23afe8f226', 'Not saved')}
</Badge>
</div>
<Popover>
@@ -173,7 +176,7 @@ export function MiniMaxCredentials({
className="h-6 gap-1 px-2 text-xs text-muted-foreground hover:text-foreground"
>
<HelpCircle className="size-3" />
{translate('auto.components.settings.AccountsPane.43d7a45b97', 'How to copy')}
{t('auto.components.settings.AccountsPane.43d7a45b97', 'How to copy')}
</Button>
</PopoverTrigger>
<PopoverContent align="end" side="bottom" sideOffset={6} className="w-80 p-0">
@@ -183,7 +186,7 @@ export function MiniMaxCredentials({
</div>
<UnsealedCredentialNotice
protection={miniMaxCookieProtection}
credentialName={translate(
credentialName={t(
'auto.components.settings.AccountsPane.21d6eb141e',
'MiniMax Session Cookie'
)}
@@ -196,7 +199,7 @@ export function MiniMaxCredentials({
disabled={miniMaxCredentialBusy || !cookieEditable}
value={miniMaxCookieDraft}
onChange={(e) => setMiniMaxCookieDraft(e.target.value)}
placeholder={translate(
placeholder={t(
'auto.components.settings.AccountsPane.b8a4f21c3e',
'Paste the Cookie header from DevTools'
)}
@@ -211,8 +214,8 @@ export function MiniMaxCredentials({
>
{miniMaxCredentialBusy ? <Loader2 className="size-3 animate-spin" /> : null}
{miniMaxConfigured
? translate('auto.components.settings.AccountsPane.f38b9cc4bd', 'Replace')
: translate('auto.components.settings.AccountsPane.590a3130f9', 'Save')}
? t('auto.components.settings.AccountsPane.f38b9cc4bd', 'Replace')
: t('auto.components.settings.AccountsPane.590a3130f9', 'Save')}
</Button>
{miniMaxConfigured ? (
<Button
@@ -222,13 +225,13 @@ export function MiniMaxCredentials({
disabled={miniMaxCredentialBusy || !editable}
className="h-7 shrink-0 text-xs text-muted-foreground hover:text-foreground"
>
{translate('auto.components.settings.AccountsPane.316ca4e610', 'Forget cookie')}
{t('auto.components.settings.AccountsPane.316ca4e610', 'Forget cookie')}
</Button>
) : null}
</div>
{editable && !sessionCookieSupported ? (
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.usageCredentialHost.cookieNeedsDesktop',
'{{host}} has no Orca desktop app to send a session cookie from. Save an API key instead.',
{ host: host.label ?? '' }
@@ -236,7 +239,7 @@ export function MiniMaxCredentials({
</p>
) : null}
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.AccountsPane.copySelectedConsoleCookie',
'Open the selected console, sign in, then copy the Cookie request header from DevTools (Network → any remains request → Cookie).'
)}
@@ -245,17 +248,13 @@ export function MiniMaxCredentials({
miniMaxRateLimits?.status === 'ok' &&
miniMaxRateLimits.error === null ? (
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.AccountsPane.53f7b8c7a2',
'Last refresh: {{value0}}',
{
value0: formatMiniMaxRelativeRefresh(miniMaxRateLimits.updatedAt, now)
}
)}
{t('auto.components.settings.AccountsPane.53f7b8c7a2', 'Last refresh: {{value0}}', {
value0: formatMiniMaxRelativeRefresh(miniMaxRateLimits.updatedAt, now, t, intlLocale)
})}
</p>
) : null}
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.AccountsPane.31d24a4e87',
'Cookie expires when you sign out in the browser.'
)}
@@ -263,14 +262,14 @@ export function MiniMaxCredentials({
</SearchableSetting>
<SearchableSetting
title={translate('auto.components.settings.AccountsPane.83b6a1f7c4', 'MiniMax API key')}
title={t('auto.components.settings.AccountsPane.83b6a1f7c4', 'MiniMax API key')}
description={
host.label === null
? translate(
? t(
'auto.components.settings.AccountsPane.apiKeySelectedEndpoint',
'Paste the API key from your MiniMax console → API keys. Stored locally and sent to the selected MiniMax endpoint for usage refreshes. The API key takes priority over the cookie.'
)
: translate(
: t(
'auto.components.settings.usageCredentialHost.miniMaxApiKeyDescription',
'Paste the API key from your MiniMax console → API keys. Stored on {{host}} and sent from there to the selected MiniMax endpoint for usage refreshes. The API key takes priority over the cookie.',
{ host: host.label }
@@ -282,7 +281,7 @@ export function MiniMaxCredentials({
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<Label htmlFor="minimax-api-key">
{translate('auto.components.settings.AccountsPane.83b6a1f7c4', 'MiniMax API key')}
{t('auto.components.settings.AccountsPane.83b6a1f7c4', 'MiniMax API key')}
</Label>
<Badge
variant={miniMaxApiKeyConfigured ? 'secondary' : 'outline'}
@@ -294,17 +293,14 @@ export function MiniMaxCredentials({
<LockOpen className="size-3" />
)}
{miniMaxApiKeyConfigured
? translate('auto.components.settings.AccountsPane.73ea15f24b', 'Saved')
: translate('auto.components.settings.AccountsPane.23afe8f226', 'Not saved')}
? t('auto.components.settings.AccountsPane.73ea15f24b', 'Saved')
: t('auto.components.settings.AccountsPane.23afe8f226', 'Not saved')}
</Badge>
</div>
</div>
<UnsealedCredentialNotice
protection={miniMaxApiKeyProtection}
credentialName={translate(
'auto.components.settings.AccountsPane.83b6a1f7c4',
'MiniMax API key'
)}
credentialName={t('auto.components.settings.AccountsPane.83b6a1f7c4', 'MiniMax API key')}
/>
{apiKeyNotice ? <p className="text-xs text-muted-foreground">{apiKeyNotice}</p> : null}
<div className="flex gap-2">
@@ -314,7 +310,7 @@ export function MiniMaxCredentials({
disabled={apiKeyReplaceLocked}
value={miniMaxApiKeyDraft}
onChange={(e) => setMiniMaxApiKeyDraft(e.target.value)}
placeholder={translate(
placeholder={t(
'auto.components.settings.AccountsPane.4f2c8a7e1b',
'Paste your MiniMax API key'
)}
@@ -329,8 +325,8 @@ export function MiniMaxCredentials({
>
{miniMaxCredentialBusy ? <Loader2 className="size-3 animate-spin" /> : null}
{miniMaxApiKeyConfigured
? translate('auto.components.settings.AccountsPane.f38b9cc4bd', 'Replace')
: translate('auto.components.settings.AccountsPane.590a3130f9', 'Save')}
? t('auto.components.settings.AccountsPane.f38b9cc4bd', 'Replace')
: t('auto.components.settings.AccountsPane.590a3130f9', 'Save')}
</Button>
{miniMaxApiKeyConfigured ? (
<Button
@@ -340,12 +336,12 @@ export function MiniMaxCredentials({
disabled={apiKeyLocked}
className="h-7 shrink-0 text-xs text-muted-foreground hover:text-foreground"
>
{translate('auto.components.settings.AccountsPane.a7b1e3c5d2', 'Forget key')}
{t('auto.components.settings.AccountsPane.a7b1e3c5d2', 'Forget key')}
</Button>
) : null}
</div>
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.AccountsPane.apiKeyInstructions',
'Copy the API key from your MiniMax console → API keys. A saved API key takes priority over the cookie; use Forget key to switch back to the cookie.'
)}
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react'
import { toast } from 'sonner'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { Badge } from '../../../ui/badge'
import { Button } from '../../../ui/button'
import { Input } from '../../../ui/input'
@@ -11,6 +11,7 @@ type CredentialStatus = boolean | 'loading' | 'unavailable'
/** Keeps late initial reads from replacing a completed credential mutation. */
export function OpenCodeGoCredentials({ onSaved }: { onSaved: () => void }): React.JSX.Element {
const t = useTranslate()
const [draft, setDraft] = useState('')
const [credentialStatus, setCredentialStatus] = useState<CredentialStatus>('loading')
const [busy, setBusy] = useState(false)
@@ -48,7 +49,7 @@ export function OpenCodeGoCredentials({ onSaved }: { onSaved: () => void }): Rea
setDraft('')
onSaved()
} catch {
toast.error(translate('sessionHistory.settings.saveError', 'Could not save. Try again.'))
toast.error(t('sessionHistory.settings.saveError', 'Could not save. Try again.'))
} finally {
setBusy(false)
}
@@ -56,11 +57,11 @@ export function OpenCodeGoCredentials({ onSaved }: { onSaved: () => void }): Rea
return (
<SearchableSetting
title={translate(
title={t(
'auto.components.settings.AccountsPane.opencodeGo.apiKey.title',
'OpenCode Go API Key'
)}
description={translate(
description={t(
'auto.components.settings.AccountsPane.opencodeGo.apiKey.description',
'Optional override. Orca otherwise uses the key OpenCode saved when you ran /connect, then OPENCODE_API_KEY.'
)}
@@ -69,25 +70,22 @@ export function OpenCodeGoCredentials({ onSaved }: { onSaved: () => void }): Rea
>
<div className="flex items-center gap-2">
<Label htmlFor="opencode-go-api-key">
{translate(
{t(
'auto.components.settings.AccountsPane.opencodeGo.apiKey.label',
'OpenCode Go API key'
)}
</Label>
<Badge variant={configured ? 'secondary' : 'outline'}>
{credentialStatus === 'loading'
? translate(
'auto.components.settings.AccountsPane.opencodeGo.apiKey.checking',
'Checking…'
)
? t('auto.components.settings.AccountsPane.opencodeGo.apiKey.checking', 'Checking…')
: credentialStatus === 'unavailable'
? translate(
? t(
'auto.components.settings.AccountsPane.opencodeGo.apiKey.unavailable',
'Status unavailable'
)
: configured
? translate('auto.components.settings.AccountsPane.73ea15f24b', 'Saved')
: translate('auto.components.settings.AccountsPane.23afe8f226', 'Not saved')}
? t('auto.components.settings.AccountsPane.73ea15f24b', 'Saved')
: t('auto.components.settings.AccountsPane.23afe8f226', 'Not saved')}
</Badge>
</div>
<div className="flex gap-2">
@@ -97,7 +95,7 @@ export function OpenCodeGoCredentials({ onSaved }: { onSaved: () => void }): Rea
value={draft}
disabled={busy}
onChange={(event) => setDraft(event.target.value)}
placeholder={translate(
placeholder={t(
'auto.components.settings.AccountsPane.opencodeGo.apiKey.placeholder',
'Leave blank to use the key saved by /connect or OPENCODE_API_KEY'
)}
@@ -110,8 +108,8 @@ export function OpenCodeGoCredentials({ onSaved }: { onSaved: () => void }): Rea
onClick={() => void updateCredential(false)}
>
{configured
? translate('auto.components.settings.AccountsPane.f38b9cc4bd', 'Replace')
: translate('auto.components.settings.AccountsPane.590a3130f9', 'Save')}
? t('auto.components.settings.AccountsPane.f38b9cc4bd', 'Replace')
: t('auto.components.settings.AccountsPane.590a3130f9', 'Save')}
</Button>
{configured && (
<Button
@@ -120,12 +118,12 @@ export function OpenCodeGoCredentials({ onSaved }: { onSaved: () => void }): Rea
disabled={busy}
onClick={() => void updateCredential(true)}
>
{translate('auto.components.settings.AccountsPane.b398b834c9', 'Clear')}
{t('auto.components.settings.AccountsPane.b398b834c9', 'Clear')}
</Button>
)}
</div>
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.AccountsPane.opencodeGo.apiKey.help',
'Used for OpenCode Go usage in the status bar. The session cookie below is only needed for legacy console (OpenCode Black) accounts.'
)}
@@ -1,4 +1,5 @@
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import type { RuntimeClientTarget } from '../../../../../../shared/runtime-client-target'
import { UsageCredentialsHostOutdatedError } from '@/runtime/provider-accounts/usage/runtime-usage-credentials-client'
@@ -23,29 +24,26 @@ export function usageCredentialAccessAfterError(error: unknown): 'outdated' | 'u
export function usageCredentialAccessNotice(
host: UsageCredentialHost,
access: UsageCredentialAccess
access: UsageCredentialAccess,
t: Translate = translate
): string | null {
if (host.label === null) {
return null
}
if (access === 'checking') {
return translate(
'auto.components.settings.usageCredentialHost.checking',
'Checking {{host}}…',
{
host: host.label
}
)
return t('auto.components.settings.usageCredentialHost.checking', 'Checking {{host}}…', {
host: host.label
})
}
if (access === 'outdated') {
return translate(
return t(
'auto.components.settings.usageCredentialHost.outdated',
'Update Orca on {{host}} to save this key there.',
{ host: host.label }
)
}
if (access === 'unreachable') {
return translate(
return t(
'auto.components.settings.usageCredentialHost.unreachable',
'Could not reach {{host}}, so whether a key is saved there is unknown.',
{ host: host.label }
@@ -57,18 +55,19 @@ export function usageCredentialAccessNotice(
/** A remote host's saved key it cannot use: sealed by its other Orca app, or just unreadable. */
export function usageCredentialUnreadableNotice(
host: UsageCredentialHost,
unreadable: 'sealed-elsewhere' | 'unreadable' | undefined
unreadable: 'sealed-elsewhere' | 'unreadable' | undefined,
t: Translate = translate
): string | null {
if (host.label === null || unreadable === undefined) {
return null
}
return unreadable === 'sealed-elsewhere'
? translate(
? t(
'auto.components.settings.usageCredentialHost.sealedElsewhereForgettable',
'Saved by another Orca app on {{host}}, which this one cannot read. Change it in that app, or forget it here.',
{ host: host.label }
)
: translate(
: t(
'auto.components.settings.usageCredentialHost.unreadableKey',
'Orca on {{host}} cannot read the saved key. Replace it or forget it.',
{ host: host.label }
@@ -83,21 +82,27 @@ export function usageCredentialSealedElsewhere(
return host.label !== null && unreadable === 'sealed-elsewhere'
}
export function usageCredentialStoredLabel(host: UsageCredentialHost): string {
export function usageCredentialStoredLabel(
host: UsageCredentialHost,
t: Translate = translate
): string {
return host.label === null
? translate('auto.components.settings.AccountsPane.0b8c1c7e02', 'Stored locally')
: translate('auto.components.settings.usageCredentialHost.storedOnHost', 'Stored on {{host}}', {
? t('auto.components.settings.AccountsPane.0b8c1c7e02', 'Stored locally')
: t('auto.components.settings.usageCredentialHost.storedOnHost', 'Stored on {{host}}', {
host: host.label
})
}
export function zcodePlanKeyDescription(host: UsageCredentialHost): string {
export function zcodePlanKeyDescription(
host: UsageCredentialHost,
t: Translate = translate
): string {
return host.label === null
? translate(
? t(
'auto.components.settings.ZcodePlanAccountsSection.keyDescription',
'Paste the API key from the selected console’s API Keys page. Stored locally, encrypted when the OS supports it, and sent only to that site for usage refreshes.'
)
: translate(
: t(
'auto.components.settings.ZcodePlanAccountsSection.keyOnHostDescription',
'Paste the API key from the selected console’s API Keys page. Stored on {{host}} and sent from there only to that site for usage refreshes.',
{ host: host.label }
@@ -8,11 +8,13 @@ import {
usageCredentialAccessAfterError,
type UsageCredentialAccess
} from './usage-credential-host'
import { useTranslate } from '@/i18n/use-translate'
export type MiniMaxCredentialsModel = ReturnType<typeof useMiniMaxCredentials>
/** Key by owner so switching credential hosts resets drafts. */
export function useMiniMaxCredentials(owner: RuntimeClientTarget) {
const t = useTranslate()
const recordFeatureInteraction = useAppStore((s) => s.recordFeatureInteraction)
const [status, setStatus] = useState<MiniMaxCredentialsStatus | null>(null)
const [access, setAccess] = useState<UsageCredentialAccess>(
@@ -48,17 +50,20 @@ export function useMiniMaxCredentials(owner: RuntimeClientTarget) {
}
}, [ownerEnvironmentId])
const actions = createMiniMaxCredentialActions({
owner,
miniMaxApiKeyDraft,
setMiniMaxApiKeyDraft,
miniMaxCookieDraft,
setMiniMaxCookieDraft,
setStatus,
setAccess,
setMiniMaxCredentialBusy,
recordFeatureInteraction
})
const actions = createMiniMaxCredentialActions(
{
owner,
miniMaxApiKeyDraft,
setMiniMaxApiKeyDraft,
miniMaxCookieDraft,
setMiniMaxCookieDraft,
setStatus,
setAccess,
setMiniMaxCredentialBusy,
recordFeatureInteraction
},
t
)
return {
access,
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call'
import type { GlobalSettings } from '../../../../../../shared/global-settings-types'
import type { RuntimeClientTarget } from '../../../../../../shared/runtime-client-target'
@@ -34,6 +34,7 @@ export function useUsageHostSettings(
localSettings: GlobalSettings,
updateLocalSettings: (updates: Partial<GlobalSettings>) => Promise<void> | void
): UsageHostSettingsModel {
const t = useTranslate()
const ownerEnvironmentId = owner.kind === 'environment' ? owner.environmentId : null
const [remote, setRemote] = useState<{
environmentId: string
@@ -90,7 +91,7 @@ export function useUsageHostSettings(
})
} catch (error) {
toast.error(
translate(
t(
'auto.components.settings.useUsageHostSettings.updateFailed',
'Could not update the setting on {{host}}.',
{ host: hostLabel ?? '' }
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react'
import { toast } from 'sonner'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { useAppStore } from '../../../../store/index'
import type { ZcodePlanCredentialsStatus } from '../../../../../../shared/zcode-plan-sites'
import type { RuntimeClientTarget } from '../../../../../../shared/runtime-client-target'
@@ -20,6 +20,7 @@ export function useZcodePlanCredentials(
updatedAt: number | undefined,
owner: RuntimeClientTarget = LOCAL_OWNER
) {
const t = useTranslate()
const recordFeatureInteraction = useAppStore((s) => s.recordFeatureInteraction)
const [status, setStatus] = useState<ZcodePlanCredentialsStatus | null>(null)
const [access, setAccess] = useState<UsageCredentialAccess>(
@@ -86,7 +87,7 @@ export function useZcodePlanCredentials(
recordFeatureInteraction('usage-tracking')
if (action === 'save') {
toast.success(
translate(
t(
'auto.components.settings.ZcodePlanAccountsSection.keySaved',
'GLM Coding Plan API key saved.'
)
@@ -97,7 +98,7 @@ export function useZcodePlanCredentials(
setAccess('outdated')
}
toast.error(
translate(
t(
'auto.components.settings.ZcodePlanAccountsSection.keySaveFailed',
'GLM Coding Plan credential update failed.'
),
@@ -1,4 +1,5 @@
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { formatResetDuration } from '../../../../../../shared/rate-limit-reset-format'
import type { ProviderRateLimits, RateLimitWindow } from '../../../../../../shared/rate-limit-types'
import { Badge } from '../../../ui/badge'
@@ -10,14 +11,14 @@ export type ZcodeUsageWindowRow = {
window: RateLimitWindow
}
function windowLabel(kind: ZcodeUsageWindowKind): string {
function windowLabel(kind: ZcodeUsageWindowKind, t: Translate = translate): string {
if (kind === 'session') {
return translate('auto.components.settings.ZcodePlanAccountsSection.window.session', '5 hours')
return t('auto.components.settings.ZcodePlanAccountsSection.window.session', '5 hours')
}
if (kind === 'weekly') {
return translate('auto.components.settings.ZcodePlanAccountsSection.window.weekly', 'Weekly')
return t('auto.components.settings.ZcodePlanAccountsSection.window.weekly', 'Weekly')
}
return translate('auto.components.settings.ZcodePlanAccountsSection.window.mcp', 'MCP monthly')
return t('auto.components.settings.ZcodePlanAccountsSection.window.mcp', 'MCP monthly')
}
function formatWindowReset(window: RateLimitWindow, now: number): string | null {
@@ -50,6 +51,7 @@ export function ZcodeUsageWindowView({
row: ZcodeUsageWindowRow
now: number
}): React.JSX.Element {
const t = useTranslate()
const resetLabel = formatWindowReset(row.window, now)
return (
<div className="flex items-center gap-2 text-xs">
@@ -57,9 +59,9 @@ export function ZcodeUsageWindowView({
<span className="tabular-nums">{Math.round(row.window.usedPercent)}%</span>
</Badge>
<span className="text-muted-foreground">
{windowLabel(row.kind)}
{windowLabel(row.kind, t)}
{resetLabel
? translate(
? t(
'auto.components.settings.ZcodePlanAccountsSection.resetIn',
' — resets in {{value0}}',
{ value0: resetLabel }
@@ -32,6 +32,7 @@ import {
} from './permissions/AgentPermissionsSetting'
import type { SettingsHostScope } from '../host-scope/settings-host-scope'
import { SettingsGroup } from '../section-layout/SettingsGroup'
import { useTranslate } from '@/i18n/use-translate'
type AgentSettingProps = {
settings: GlobalSettings
@@ -56,6 +57,7 @@ export function AgentsPane({
wslDistros,
wslCapabilitiesLoading
}: AgentsPaneProps): React.JSX.Element {
const t = useTranslate()
return (
<div className="space-y-8">
<AgentLaunchSettingsSection
@@ -85,15 +87,15 @@ export function AgentsPane({
/>
<AgentStatusHooksSetting settings={settings} updateSettings={updateSettings} />
<SettingsSwitchRow
label={getClaudeSubagentNavigationTitle()}
description={getClaudeSubagentNavigationDescription()}
label={getClaudeSubagentNavigationTitle(t)}
description={getClaudeSubagentNavigationDescription(t)}
checked={settings.claudeSubagentNavigationEnabled !== false}
onChange={() =>
updateSettings({
claudeSubagentNavigationEnabled: settings.claudeSubagentNavigationEnabled === false
})
}
ariaLabel={getClaudeSubagentNavigationTitle()}
ariaLabel={getClaudeSubagentNavigationTitle(t)}
/>
{!isPairedWebClientWindow() ? (
<>
@@ -119,40 +121,43 @@ function renderAgentPermissions(props: AgentPermissionsSettingProps): React.Reac
}
export function AgentStatusHooksSetting({ settings, updateSettings }: AgentSettingProps) {
const t = useTranslate()
const enabled = settings.agentStatusHooksEnabled !== false
return (
<SettingsSwitchRow
label={getAgentStatusHooksTitle()}
description={getAgentStatusHooksDescription()}
label={getAgentStatusHooksTitle(t)}
description={getAgentStatusHooksDescription(t)}
checked={enabled}
onChange={() => updateSettings({ agentStatusHooksEnabled: !enabled })}
ariaLabel={getAgentStatusHooksTitle()}
ariaLabel={getAgentStatusHooksTitle(t)}
/>
)
}
export function AgentWorkspaceTrustSetting({ settings, updateSettings }: AgentSettingProps) {
const t = useTranslate()
const enabled = settings.agentWorkspaceTrustEnabled !== false
return (
<SettingsSwitchRow
label={getAgentWorkspaceTrustTitle()}
description={getAgentWorkspaceTrustDescription()}
label={getAgentWorkspaceTrustTitle(t)}
description={getAgentWorkspaceTrustDescription(t)}
checked={enabled}
onChange={() => updateSettings({ agentWorkspaceTrustEnabled: !enabled })}
ariaLabel={getAgentWorkspaceTrustTitle()}
ariaLabel={getAgentWorkspaceTrustTitle(t)}
/>
)
}
export function AgentGeneratedTabTitlesSetting({ settings, updateSettings }: AgentSettingProps) {
const t = useTranslate()
const enabled = settings.tabAutoGenerateTitle === true
return (
<SettingsSwitchRow
label={getAgentGeneratedTabTitlesTitle()}
description={getAgentGeneratedTabTitlesDescription()}
label={getAgentGeneratedTabTitlesTitle(t)}
description={getAgentGeneratedTabTitlesDescription(t)}
checked={enabled}
onChange={() => updateSettings({ tabAutoGenerateTitle: !enabled })}
ariaLabel={getAgentGeneratedTabTitlesTitle()}
ariaLabel={getAgentGeneratedTabTitlesTitle(t)}
/>
)
}
@@ -13,6 +13,7 @@ import {
normalizeComputerAwakeMode,
type ComputerAwakeMode
} from '../../../../../../shared/computer-awake-mode'
import { useTranslate } from '../../../../i18n/use-translate'
type AgentAwakeSettingProps = {
settings: GlobalSettings
@@ -23,8 +24,9 @@ export function AgentAwakeSetting({
settings,
updateSettings
}: AgentAwakeSettingProps): React.JSX.Element {
const title = getAgentAwakeTitle()
const description = getAgentAwakeDescription()
const t = useTranslate()
const title = getAgentAwakeTitle(t)
const description = getAgentAwakeDescription(undefined, t)
const mode = normalizeComputerAwakeMode(
settings.computerAwakeMode,
settings.keepComputerAwakeWhileAgentsRun
@@ -37,7 +39,7 @@ export function AgentAwakeSetting({
<SearchableSetting
title={title}
description={description}
keywords={getAgentAwakeSearchKeywords()}
keywords={getAgentAwakeSearchKeywords(undefined, t)}
>
<div className="flex items-start justify-between gap-4 py-3">
<div className="min-w-0 flex-1 space-y-0.5">
@@ -52,15 +54,15 @@ export function AgentAwakeSetting({
options={[
{
value: 'on',
label: getAgentAwakeModeLabel('on')
label: getAgentAwakeModeLabel('on', t)
},
{
value: 'auto',
label: getAgentAwakeModeLabel('auto')
label: getAgentAwakeModeLabel('auto', t)
},
{
value: 'off',
label: getAgentAwakeModeLabel('off')
label: getAgentAwakeModeLabel('off', t)
}
]}
/>
@@ -7,7 +7,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '.
import { getAgentCacheTimerSearchEntries } from './agent-cache-timer-search'
import { SearchableSetting } from '../../section-layout/SearchableSetting'
import { SettingsSwitch } from '../../form-controls/SettingsFormControls'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
type AgentCacheTimerSectionProps = {
@@ -19,23 +19,18 @@ export function AgentCacheTimerSection({
settings,
updateSettings
}: AgentCacheTimerSectionProps): React.JSX.Element {
const t = useTranslate()
return (
<SettingsGroup
title={translate(
'auto.components.settings.AgentCacheTimerSection.a137f8854d',
'Prompt Cache Timer'
)}
description={translate(
title={t('auto.components.settings.AgentCacheTimerSection.a137f8854d', 'Prompt Cache Timer')}
description={t(
'auto.components.settings.AgentCacheTimerSection.fe590653c1',
'Claude caches your conversation to reduce costs. When idle too long the cache expires and the next message resends full context at higher cost. This shows a countdown so you know when to resume.'
)}
>
<SearchableSetting
title={translate(
'auto.components.settings.AgentCacheTimerSection.b4e7302944',
'Cache Timer'
)}
description={translate(
title={t('auto.components.settings.AgentCacheTimerSection.b4e7302944', 'Cache Timer')}
description={t(
'auto.components.settings.AgentCacheTimerSection.9c20253679',
'Show a countdown after a Claude agent becomes idle.'
)}
@@ -50,24 +45,18 @@ export function AgentCacheTimerSection({
<div className="flex items-center gap-2">
<Timer className="size-4 text-muted-foreground" />
<Label>
{translate(
'auto.components.settings.AgentCacheTimerSection.b4e7302944',
'Cache Timer'
)}
{t('auto.components.settings.AgentCacheTimerSection.b4e7302944', 'Cache Timer')}
</Label>
</div>
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.AgentCacheTimerSection.487b176240',
'Show a countdown in the sidebar after a Claude agent becomes idle.'
)}
</p>
</div>
<SettingsSwitch
ariaLabel={translate(
'auto.components.settings.AgentCacheTimerSection.b4e7302944',
'Cache Timer'
)}
ariaLabel={t('auto.components.settings.AgentCacheTimerSection.b4e7302944', 'Cache Timer')}
checked={settings.promptCacheTimerEnabled}
onChange={() => {
const enabling = !settings.promptCacheTimerEnabled
@@ -81,11 +70,8 @@ export function AgentCacheTimerSection({
{settings.promptCacheTimerEnabled && (
<SearchableSetting
title={translate(
'auto.components.settings.AgentCacheTimerSection.a2a8962138',
'Timer Duration'
)}
description={translate(
title={t('auto.components.settings.AgentCacheTimerSection.a2a8962138', 'Timer Duration')}
description={t(
'auto.components.settings.AgentCacheTimerSection.80c454e8a6',
"Match this to your provider's cache TTL."
)}
@@ -94,13 +80,10 @@ export function AgentCacheTimerSection({
>
<div className="min-w-0 flex-1 space-y-0.5">
<Label>
{translate(
'auto.components.settings.AgentCacheTimerSection.a2a8962138',
'Timer Duration'
)}
{t('auto.components.settings.AgentCacheTimerSection.a2a8962138', 'Timer Duration')}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.AgentCacheTimerSection.8b9e202e0a',
"Match this to your provider's cache TTL. The default is 5 minutes."
)}
@@ -115,13 +98,10 @@ export function AgentCacheTimerSection({
</SelectTrigger>
<SelectContent>
<SelectItem value="300000">
{translate(
'auto.components.settings.AgentCacheTimerSection.54395ecd7c',
'5 minutes'
)}
{t('auto.components.settings.AgentCacheTimerSection.54395ecd7c', '5 minutes')}
</SelectItem>
<SelectItem value="3600000">
{translate('auto.components.settings.AgentCacheTimerSection.05de84a104', '1 hour')}
{t('auto.components.settings.AgentCacheTimerSection.05de84a104', '1 hour')}
</SelectItem>
</SelectContent>
</Select>
@@ -10,6 +10,7 @@ import {
getCodexHistorySharingUnsupported
} from './codex-history-sharing-copy'
import { useCodexHistorySharingHostSetting } from './use-codex-history-sharing-host-setting'
import { useTranslate } from '@/i18n/use-translate'
export function CodexHistorySharingSetting({
settings,
@@ -21,6 +22,7 @@ export function CodexHistorySharingSetting({
/** The host whose Codex launches write the history links. */
hostScope: SettingsHostScope
}): React.JSX.Element {
const t = useTranslate()
const environmentId = runtimeTargetEnvironmentId(hostScope.target)
const hostName = useAppStateValue((state) =>
environmentId
@@ -37,11 +39,11 @@ export function CodexHistorySharingSetting({
const enabled = setting.state === 'ready' ? setting.enabled : true
return (
<SettingsSwitchRow
label={getCodexHistorySharingTitle()}
label={getCodexHistorySharingTitle(t)}
description={
setting.state === 'unsupported'
? getCodexHistorySharingUnsupported(hostLabel)
: getCodexHistorySharingDescription()
? getCodexHistorySharingUnsupported(hostLabel, t)
: getCodexHistorySharingDescription(t)
}
checked={enabled}
disabled={setting.state !== 'ready' || !hostScope.available}
@@ -1,4 +1,5 @@
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import type { ComputerAwakeMode } from '../../../../../../shared/computer-awake-mode'
import { searchKeywords } from '../../search/settings-search-keywords'
@@ -10,38 +11,39 @@ const AGENT_AWAKE_DESCRIPTION_MAC_KEY =
const AGENT_AWAKE_DESCRIPTION_DEFAULT_KEY =
'auto.components.settings.agent-awake-copy.modeDescriptionDefault'
export function getAgentAwakeTitle(): string {
return translate(AGENT_AWAKE_TITLE_KEY, 'Keep computer awake')
export function getAgentAwakeTitle(t: Translate = translate): string {
return t(AGENT_AWAKE_TITLE_KEY, 'Keep computer awake')
}
export function getAgentAwakeModeLabel(mode: ComputerAwakeMode): string {
export function getAgentAwakeModeLabel(mode: ComputerAwakeMode, t: Translate = translate): string {
if (mode === 'on') {
return translate('auto.components.settings.AgentAwakeSetting.on', 'On')
return t('auto.components.settings.AgentAwakeSetting.on', 'On')
}
if (mode === 'auto') {
return translate('auto.components.settings.AgentAwakeSetting.auto', 'Agent')
return t('auto.components.settings.AgentAwakeSetting.auto', 'Agent')
}
return translate('auto.components.settings.AgentAwakeSetting.off', 'Off')
return t('auto.components.settings.AgentAwakeSetting.off', 'Off')
}
export function getAgentAwakeDescription(
userAgent = typeof navigator === 'undefined' ? '' : navigator.userAgent
userAgent = typeof navigator === 'undefined' ? '' : navigator.userAgent,
t: Translate = translate
): string {
if (userAgent.includes('Windows')) {
return translate(
return t(
AGENT_AWAKE_DESCRIPTION_WINDOWS_KEY,
"Choose On, Agent, or Off. Agent mode stays awake while agents are working; lid-close behavior follows this device's power settings."
)
}
// macOS caffeinate can't hold a closed lid awake; Linux blocks logind's lid switch.
if (userAgent.includes('Mac')) {
return translate(
return t(
AGENT_AWAKE_DESCRIPTION_MAC_KEY,
'Choose On, Agent, or Off. Agent mode prevents idle sleep while agents work, so long runs finish with the lid open. Closing the lid still puts this Mac to sleep.'
)
}
return translate(
return t(
AGENT_AWAKE_DESCRIPTION_DEFAULT_KEY,
'Choose On, Agent, or Off. Agent mode stays awake while agents are working. Orca also asks this device to stay awake when the lid is closed, subject to its power policy.'
)
@@ -69,25 +71,30 @@ export function getAgentAwakeLidNote(
}
export function getAgentAwakeSearchKeywords(
userAgent = typeof navigator === 'undefined' ? '' : navigator.userAgent
userAgent = typeof navigator === 'undefined' ? '' : navigator.userAgent,
t?: Translate
): string[] {
const keywords = searchKeywords([
{ key: 'auto.components.settings.agents.search.66b6b82eb4', fallback: 'awake' },
{ key: 'auto.components.settings.agents.search.dbc8aca6b0', fallback: 'sleep' },
{ key: 'auto.components.settings.agents.search.845ad9128a', fallback: 'power' },
{ key: 'auto.components.settings.agents.search.96ba2373b6', fallback: 'agent' },
{ key: 'auto.components.settings.agents.search.48f84d10f1', fallback: 'running' },
{ key: 'auto.components.settings.agents.search.affbf130f6', fallback: 'working' },
{ key: 'auto.components.settings.agents.search.0d1c334987', fallback: 'lid' },
{ key: 'auto.components.settings.agents.search.ff8de8a2ad', fallback: 'display' }
])
const keywords = searchKeywords(
[
{ key: 'auto.components.settings.agents.search.66b6b82eb4', fallback: 'awake' },
{ key: 'auto.components.settings.agents.search.dbc8aca6b0', fallback: 'sleep' },
{ key: 'auto.components.settings.agents.search.845ad9128a', fallback: 'power' },
{ key: 'auto.components.settings.agents.search.96ba2373b6', fallback: 'agent' },
{ key: 'auto.components.settings.agents.search.48f84d10f1', fallback: 'running' },
{ key: 'auto.components.settings.agents.search.affbf130f6', fallback: 'working' },
{ key: 'auto.components.settings.agents.search.0d1c334987', fallback: 'lid' },
{ key: 'auto.components.settings.agents.search.ff8de8a2ad', fallback: 'display' }
],
t
)
return userAgent.includes('Linux')
? [
...keywords,
...searchKeywords([
{ key: 'auto.components.settings.agents.search.f622b8eb2a', fallback: 'linux' }
])
...searchKeywords(
[{ key: 'auto.components.settings.agents.search.f622b8eb2a', fallback: 'linux' }],
t
)
]
: keywords
}
@@ -1,4 +1,5 @@
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { searchKeywords } from '../../search/settings-search-keywords'
const AGENT_GENERATED_TAB_TITLES_TITLE_KEY =
@@ -6,12 +7,12 @@ const AGENT_GENERATED_TAB_TITLES_TITLE_KEY =
const AGENT_GENERATED_TAB_TITLES_DESCRIPTION_KEY =
'auto.components.settings.agent-generated-tab-title-copy.b036c7a409'
export function getAgentGeneratedTabTitlesTitle(): string {
return translate(AGENT_GENERATED_TAB_TITLES_TITLE_KEY, 'Auto-generate tab titles')
export function getAgentGeneratedTabTitlesTitle(t: Translate = translate): string {
return t(AGENT_GENERATED_TAB_TITLES_TITLE_KEY, 'Auto-generate tab titles')
}
export function getAgentGeneratedTabTitlesDescription(): string {
return translate(
export function getAgentGeneratedTabTitlesDescription(t: Translate = translate): string {
return t(
AGENT_GENERATED_TAB_TITLES_DESCRIPTION_KEY,
'Derive short stable tab names from the first known agent prompt. Manual renames always win.'
)
@@ -1,16 +1,17 @@
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { searchKeywords } from '../../search/settings-search-keywords'
const AGENT_STATUS_HOOKS_TITLE_KEY = 'auto.components.settings.agent-status-hooks-copy.7707c15abb'
const AGENT_STATUS_HOOKS_DESCRIPTION_KEY =
'auto.components.settings.agent-status-hooks-copy.a68a642835'
export function getAgentStatusHooksTitle(): string {
return translate(AGENT_STATUS_HOOKS_TITLE_KEY, 'Agent status hooks')
export function getAgentStatusHooksTitle(t: Translate = translate): string {
return t(AGENT_STATUS_HOOKS_TITLE_KEY, 'Agent status hooks')
}
export function getAgentStatusHooksDescription(): string {
return translate(
export function getAgentStatusHooksDescription(t: Translate = translate): string {
return t(
AGENT_STATUS_HOOKS_DESCRIPTION_KEY,
'Shows working, waiting, and done states in Orca. Turn off to remove Orca-managed hooks and stop reinstalling them.'
)
@@ -1,16 +1,17 @@
// @protected-copy: folder trust setting copy.
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { searchKeywords } from '../../search/settings-search-keywords'
export function getAgentWorkspaceTrustTitle(): string {
return translate(
export function getAgentWorkspaceTrustTitle(t: Translate = translate): string {
return t(
'auto.components.settings.agent-workspace-trust-copy.title',
'Trust the folder when Orca starts an agent'
)
}
export function getAgentWorkspaceTrustDescription(): string {
return translate(
export function getAgentWorkspaceTrustDescription(t: Translate = translate): string {
return t(
'auto.components.settings.agent-workspace-trust-copy.description',
'Agents Orca starts can skip their "Do you trust this folder?" prompt, so the project\'s agent hooks and settings run right away. Orca asks once per repository the first time you start an agent there, and you can change the answer in that repository\'s settings. Turn this off to let agents always ask. An agent that asks while running without you watching, such as an orchestration worker, an automation or an agent started from your phone, waits until you answer in its terminal. Folders already trusted stay trusted.'
)
@@ -1,15 +1,13 @@
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { searchKeywords } from '../../search/settings-search-keywords'
export function getClaudeSubagentNavigationTitle(): string {
return translate(
'auto.components.settings.claudeSubagentNavigation.title',
'Claude subagent navigation'
)
export function getClaudeSubagentNavigationTitle(t: Translate = translate): string {
return t('auto.components.settings.claudeSubagentNavigation.title', 'Claude subagent navigation')
}
export function getClaudeSubagentNavigationDescription(): string {
return translate(
export function getClaudeSubagentNavigationDescription(t: Translate = translate): string {
return t(
'auto.components.settings.claudeSubagentNavigation.description',
'Open Claude subagents from the sidebar. Adds a shortcut to Claude’s keybindings.json and loads an Orca mod in new Orca panes. Requires Claude 2.1.300+ and status hooks. Turning off removes Orca’s shortcut; shortcuts you delete stay deleted.'
)
@@ -1,22 +1,20 @@
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { searchKeywords } from '../../search/settings-search-keywords'
export function getCodexHistorySharingTitle(): string {
return translate(
'settings.agents.codexHistorySharing.title',
'Share Codex history across accounts'
)
export function getCodexHistorySharingTitle(t: Translate = translate): string {
return t('settings.agents.codexHistorySharing.title', 'Share Codex history across accounts')
}
export function getCodexHistorySharingDescription(): string {
return translate(
export function getCodexHistorySharingDescription(t: Translate = translate): string {
return t(
'settings.agents.codexHistorySharing.description',
"Lets each Codex account, and your own ~/.codex, resume conversations started on the others. Turn off to keep every account's history separate. Turning it off removes nothing already shared."
)
}
export function getCodexHistorySharingUnsupported(host: string): string {
return translate(
export function getCodexHistorySharingUnsupported(host: string, t: Translate = translate): string {
return t(
'settings.agents.codexHistorySharing.unsupported',
'Update Orca on {{host}} to change this setting there.',
{ host }
@@ -3,7 +3,7 @@ import { Check, ChevronDown, ExternalLink } from 'lucide-react'
import type { TuiAgent } from '../../../../../../shared/agent-launch/catalog/tui-agent'
import { AgentIcon } from '@/lib/agent-catalog'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { Button } from '../../../ui/button'
import { SettingsBadge, SettingsSegmentedControl } from '../../form-controls/SettingsFormControls'
@@ -44,6 +44,7 @@ export function AgentAvailabilityControl({
isEnabled: boolean
onSetEnabled: (enabled: boolean) => void
}): React.JSX.Element {
const t = useTranslate()
const value: AgentAvailability = isEnabled ? 'enabled' : 'disabled'
return (
<SettingsSegmentedControl<AgentAvailability>
@@ -53,20 +54,18 @@ export function AgentAvailabilityControl({
onSetEnabled(next === 'enabled')
}
}}
ariaLabel={translate(
'auto.components.settings.AgentsPane.1c9a9679ec',
'{{value0}} availability',
{ value0: label }
)}
ariaLabel={t('auto.components.settings.AgentsPane.1c9a9679ec', '{{value0}} availability', {
value0: label
})}
size="sm"
options={[
{
value: 'enabled',
label: translate('auto.components.settings.AgentsPane.d4d2a45d63', 'Enabled')
label: t('auto.components.settings.AgentsPane.d4d2a45d63', 'Enabled')
},
{
value: 'disabled',
label: translate('auto.components.settings.AgentsPane.8dc0192e48', 'Disabled')
label: t('auto.components.settings.AgentsPane.8dc0192e48', 'Disabled')
}
]}
/>
@@ -135,6 +134,7 @@ export function AgentCatalogRow({
opensAs,
chatRuntime
}: AgentCatalogRowProps): React.JSX.Element {
const t = useTranslate()
const envSummary = environmentNames?.join(' ') ?? stringifyAgentDefaultEnvDraft(envOverride)
const defaultEnvSummary = stringifyAgentDefaultEnvDraft(defaultEnv)
const [cmdOpen, setCmdOpen] = useState(
@@ -169,7 +169,7 @@ export function AgentCatalogRow({
<span className="text-sm font-medium leading-none">{label}</span>
{!isEnabled && (
<SettingsBadge tone="muted">
{translate('auto.components.settings.AgentsPane.8dc0192e48', 'Disabled')}
{t('auto.components.settings.AgentsPane.8dc0192e48', 'Disabled')}
</SettingsBadge>
)}
</div>
@@ -196,7 +196,7 @@ export function AgentCatalogRow({
className="rounded-full outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<SettingsBadge tone="accent">
{agentPermissionChoiceLabel(permissionChoice.choice, label)}
{agentPermissionChoiceLabel(permissionChoice.choice, label, t)}
</SettingsBadge>
</button>
</div>
@@ -227,16 +227,13 @@ export function AgentCatalogRow({
aria-pressed={isDefault}
title={
isDefault
? translate('settings.agents.clearDefault', 'Clear default agent')
: translate(
'auto.components.settings.AgentsPane.5f986a9b92',
'Set as default'
)
? t('settings.agents.clearDefault', 'Clear default agent')
: t('auto.components.settings.AgentsPane.5f986a9b92', 'Set as default')
}
className="h-7 w-full justify-center gap-1 text-xs"
>
<Check className={cn('size-3', !isDefault && 'invisible')} />
{translate('auto.components.settings.AgentsPane.24e032fa34', 'Default')}
{t('auto.components.settings.AgentsPane.24e032fa34', 'Default')}
</Button>
)}
</div>
@@ -247,8 +244,8 @@ export function AgentCatalogRow({
rel="noopener noreferrer"
title={
isDetected
? translate('auto.components.settings.AgentsPane.fe4d630c94', 'Docs')
: translate('auto.components.settings.AgentsPane.f95b5c79b8', 'Install')
? t('auto.components.settings.AgentsPane.fe4d630c94', 'Docs')
: t('auto.components.settings.AgentsPane.f95b5c79b8', 'Install')
}
className="flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
>
@@ -262,8 +259,8 @@ export function AgentCatalogRow({
onClick={() => setCmdOpen((previous) => !previous)}
aria-label={
cmdOpen
? translate('settings.agents.collapseAgentSettings', 'Collapse agent settings')
: translate('settings.agents.expandAgentSettings', 'Expand agent settings')
? t('settings.agents.collapseAgentSettings', 'Collapse agent settings')
: t('settings.agents.expandAgentSettings', 'Expand agent settings')
}
className="size-7 text-muted-foreground hover:text-foreground"
>
@@ -301,7 +298,7 @@ export function AgentCatalogRow({
onSaveOverride={onSaveOverride}
/>
<p className="mt-2 text-[11px] text-muted-foreground">
{translate(
{t(
'settings.agents.undetectedCommandHint',
'If the {{agent}} CLI is installed outside PATH, enter the full path to its executable on this host.',
{ agent: label }
@@ -342,7 +339,7 @@ export function AgentCatalogRow({
}
note={
permissionChoice?.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 }
@@ -373,7 +370,7 @@ export function AgentCatalogRow({
</div>
)}
<p className="mt-2 text-[11px] text-muted-foreground">
{translate(
{t(
'auto.components.settings.AgentsPane.f9f127d664',
'Override the binary path or name, and edit the default launch arguments or environment for this agent.'
)}
@@ -3,7 +3,7 @@ import type { TuiAgent } from '../../../../../../shared/agent-launch/catalog/tui
import { type AgentCatalogEntry, AgentIcon } from '@/lib/agent-catalog'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
function DefaultAgentPill({
@@ -55,6 +55,7 @@ export function AgentDefaultSetting({
onSetDefault: (agent: TuiAgent | 'blank' | null) => void
permissions: React.ReactNode
}): React.JSX.Element {
const t = useTranslate()
const storedDefaultAgent =
defaultAgent !== null && defaultAgent !== 'blank'
? catalog.find((agent) => agent.id === defaultAgent)
@@ -66,7 +67,7 @@ export function AgentDefaultSetting({
return (
<SettingsGroup
title={translate('auto.components.settings.AgentsPane.385212c7a1', 'Default Agent')}
title={t('auto.components.settings.AgentsPane.385212c7a1', 'Default Agent')}
description={description}
>
<div className="flex flex-wrap gap-2 py-4">
@@ -75,7 +76,7 @@ export function AgentDefaultSetting({
onClick={() => onSetDefault(defaultAgent === null ? 'blank' : null)}
>
{defaultAgent === null && <Check className="size-3.5" />}
{translate('auto.components.settings.AgentsPane.92033495ff', 'Auto')}
{t('auto.components.settings.AgentsPane.92033495ff', 'Auto')}
</DefaultAgentPill>
{defaultAgentPills.map((agent) => {
const isActive = defaultAgent === agent.id
@@ -88,7 +89,7 @@ export function AgentDefaultSetting({
onClick={() => onSetDefault(isActive ? 'blank' : agent.id)}
title={
isUndetected
? translate(
? t(
'auto.components.settings.AgentsPane.storedDefaultUndetected',
'Saved as your default, but not detected right now'
)
@@ -2,7 +2,7 @@ import { Fragment } from 'react'
import { AlertTriangle, RefreshCw } from 'lucide-react'
import type { AgentCatalogEntry } from '@/lib/agent-catalog'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { Button } from '../../../ui/button'
import { AgentCatalogRow, type AgentCatalogRowProps } from './AgentCatalogRow'
import { SettingsBadge } from '../../form-controls/SettingsFormControls'
@@ -30,12 +30,13 @@ export function AgentDetectionCatalog({
onRefresh: () => void
getRowProps: (agent: AgentCatalogEntry, isDetected: boolean) => AgentCatalogRowProps
}): React.JSX.Element {
const t = useTranslate()
return (
<>
{detectedAgents.length === 0 && !detectionPending && !detectionFailed && (
<div className="flex items-center justify-between gap-3 rounded-md border border-dashed border-border/50 px-3 py-3 text-sm text-muted-foreground">
<span>
{translate(
{t(
'auto.components.settings.AgentsPane.noAgentsDetected',
'No agents detected. If one is installed, the probe may have timed out.'
)}
@@ -63,32 +64,28 @@ export function AgentDetectionCatalog({
title={
installed ? (
<span className="flex items-center gap-2">
{translate('auto.components.settings.AgentsPane.02e0143be5', 'Installed')}
{t('auto.components.settings.AgentsPane.02e0143be5', 'Installed')}
<SettingsBadge tone="accent">
{detectedAgents.length}{' '}
{translate('auto.components.settings.AgentsPane.ed3e110e61', 'detected')}
{t('auto.components.settings.AgentsPane.ed3e110e61', 'detected')}
</SettingsBadge>
{activeServerName ? (
<SettingsBadge tone="muted">
{translate(
'auto.components.settings.AgentsPane.03e1a5081a',
'on {{value0}}',
{
value0: activeServerName
}
)}
{t('auto.components.settings.AgentsPane.03e1a5081a', 'on {{value0}}', {
value0: activeServerName
})}
</SettingsBadge>
) : null}
</span>
) : (
<span className="flex items-center gap-2 text-muted-foreground">
{translate(
{t(
'auto.components.settings.AgentsPane.e8da2af684',
'Available to install'
)}
<SettingsBadge tone="muted">
{undetectedAgents.length}{' '}
{translate('auto.components.settings.AgentsPane.024bd95089', 'agents')}
{t('auto.components.settings.AgentsPane.024bd95089', 'agents')}
</SettingsBadge>
</span>
)
@@ -103,11 +100,11 @@ export function AgentDetectionCatalog({
disabled={isRefreshing}
title={
activeServerEnvironmentId
? translate(
? t(
'auto.components.settings.AgentsPane.25a41a9aad',
'Re-detect agents installed on this server'
)
: translate(
: t(
'auto.components.settings.AgentsPane.13647f9f80',
'Re-read your shell PATH and re-detect installed agents'
)
@@ -116,11 +113,8 @@ export function AgentDetectionCatalog({
>
<RefreshCw className={cn('size-3', isRefreshing && 'animate-spin')} />
{isRefreshing
? translate(
'auto.components.settings.AgentsPane.c9b33eb5c0',
'Refreshing…'
)
: translate('auto.components.settings.AgentsPane.0d9e293a02', 'Refresh')}
? t('auto.components.settings.AgentsPane.c9b33eb5c0', 'Refreshing…')
: t('auto.components.settings.AgentsPane.0d9e293a02', 'Refresh')}
</Button>
) : undefined
}
@@ -144,10 +138,7 @@ export function AgentDetectionCatalog({
{detectionPending && !detectionFailed && (
<div className="flex items-center justify-center rounded-md border border-dashed border-border/50 py-6 text-sm text-muted-foreground">
{translate(
'auto.components.settings.AgentsPane.d83834f5e6',
'Detecting installed agents…'
)}
{t('auto.components.settings.AgentsPane.d83834f5e6', 'Detecting installed agents…')}
</div>
)}
@@ -155,7 +146,7 @@ export function AgentDetectionCatalog({
<div className="flex items-start justify-between gap-3 rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-xs text-destructive">
<span className="flex min-w-0 items-start gap-2">
<AlertTriangle className="mt-0.5 size-3.5 shrink-0" />
{translate(
{t(
'auto.components.settings.AgentsPane.remoteDetectionFailed',
'Couldn’t detect installed agents. Check the host connection and try again.'
)}
@@ -168,7 +159,7 @@ export function AgentDetectionCatalog({
className="h-6 shrink-0 gap-1.5 px-2 text-destructive hover:text-destructive"
>
<RefreshCw className="size-3" />
{translate('auto.components.settings.AgentsPane.retryDetection', 'Retry')}
{t('auto.components.settings.AgentsPane.retryDetection', 'Retry')}
</Button>
</div>
)}
@@ -183,6 +174,7 @@ function RefreshButton({
isRefreshing: boolean
onRefresh: () => void
}): React.JSX.Element {
const t = useTranslate()
return (
<Button
type="button"
@@ -194,8 +186,8 @@ function RefreshButton({
>
<RefreshCw className={cn('size-3', isRefreshing && 'animate-spin')} />
{isRefreshing
? translate('auto.components.settings.AgentsPane.c9b33eb5c0', 'Refreshing…')
: translate('auto.components.settings.AgentsPane.0d9e293a02', 'Refresh')}
? t('auto.components.settings.AgentsPane.c9b33eb5c0', 'Refreshing…')
: t('auto.components.settings.AgentsPane.0d9e293a02', 'Refresh')}
</Button>
)
}
@@ -61,7 +61,7 @@ import {
import { chatProviderAgents, useHostStructuredAgentsVersion } from '@/lib/chat-provider-agents'
import { readHostStructuredAgents } from '@/runtime/host-structured-agents'
import { LOCAL_EXECUTION_HOST_ID } from '../../../../../../shared/execution-host'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
export type AgentPermissionsRenderer = (props: AgentPermissionsSettingProps) => React.ReactNode
@@ -94,6 +94,7 @@ export function AgentLaunchSettingsCatalog({
updateSettings: (updates: Partial<GlobalSettings>) => void | Promise<void>
}
}): React.JSX.Element {
const t = useTranslate()
const {
detectedIds: detected,
detectionFailed,
@@ -201,12 +202,12 @@ export function AgentLaunchSettingsCatalog({
const values = { agent: label, mode: agentPermissionChoiceLabel(lifted.choice, label) }
toast.success(
chatSupported
? translate(
? t(
'settings.agents.permissionFromArgs',
'{{agent}} set to {{mode}} in terminals and new chats.',
values
)
: translate(
: t(
'settings.agents.permissionFromArgsTerminals',
'{{agent}} set to {{mode}} in terminals.',
values
@@ -335,7 +336,7 @@ export function AgentLaunchSettingsCatalog({
isTuiAgentEnabled(agent.id, settings.disabledTuiAgents)
)}
catalog={catalog}
description={translate(
description={t(
'settings.agents.hostDefaults',
'Defaults for new agents on the selected host.'
)}
@@ -1,8 +1,9 @@
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { AgentDefaultSetting } from './AgentDefaultSetting'
import { AllAgentsPermissionSetting } from '../../../agent/launch-settings/AgentPermissionChoiceControls'
export function AgentLaunchSettingsLoading(): React.JSX.Element {
const t = useTranslate()
return (
<fieldset disabled className="min-w-0 space-y-8">
<AgentDefaultSetting
@@ -10,7 +11,7 @@ export function AgentLaunchSettingsLoading(): React.JSX.Element {
detectedIds={null}
enabledDetectedAgents={[]}
catalog={[]}
description={translate(
description={t(
'settings.agents.hostDefaults',
'Defaults for new agents on the selected host.'
)}
@@ -21,7 +21,7 @@ import { HostAgentLaunchSettings } from './HostAgentLaunchSettings'
import { SettingsHostScopeUnavailableNotice } from '../../host-scope/SettingsHostScopePicker'
import type { SettingsHostScope } from '../../host-scope/settings-host-scope'
import { runtimeTargetEnvironmentId } from '@/runtime/rpc/targets/runtime-client-target'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call'
import {
useAgentPermissionOverrideNotice,
@@ -78,6 +78,7 @@ function LocalAgentLaunchSettings({
updateSettings: (updates: Partial<GlobalSettings>) => void | Promise<void>
renderPermissions: AgentPermissionsRenderer
}): React.JSX.Element {
const t = useTranslate()
const notify = useAgentPermissionOverrideNotice()
const chatOwnSupported = useChatSettingsOwnPermissions()
const tail = useRef<Promise<void> | null>(null)
@@ -95,15 +96,12 @@ function LocalAgentLaunchSettings({
const failed = (agent: TuiAgent | undefined) => (error: unknown) => {
toast.error(
agent
? translate(
? t(
'settings.agents.localSaveFailedAgent',
'{{agent}} settings could not be saved. Try again.',
{ agent: getAgentLabel(agent) }
)
: translate(
'settings.agents.localSaveFailed',
'Agent settings could not be saved. Try again.'
)
: t('settings.agents.localSaveFailed', 'Agent settings could not be saved. Try again.')
)
throw error
}
@@ -1,7 +1,7 @@
import { useState } from 'react'
import { Button } from '../../../ui/button'
import { Input } from '../../../ui/input'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import type { AgentLaunchSettingsMutation } from '../../../../../../shared/agent-launch/choices/agent-launch-settings'
import type { TuiAgent } from '../../../../../../shared/agent-launch/catalog/tui-agent'
import { useAgentLaunchFieldWrite } from '../../../agent/launch-settings/use-agent-launch-field-write'
@@ -15,11 +15,12 @@ export function HostAgentEnvironmentEditor({
names: readonly string[]
mutate: (mutation: AgentLaunchSettingsMutation) => Promise<void>
}): React.JSX.Element {
const t = useTranslate()
const [newName, setNewName] = useState('')
return (
<div className="space-y-2">
<span className="text-xs text-muted-foreground">
{translate('auto.components.settings.AgentsPane.8fbe1f37c1', 'Environment')}
{t('auto.components.settings.AgentsPane.8fbe1f37c1', 'Environment')}
</span>
{names.map((name) => (
<EnvironmentValueRow key={name} agent={agent} name={name} saved mutate={mutate} />
@@ -28,8 +29,8 @@ export function HostAgentEnvironmentEditor({
<Input
value={newName}
onChange={(event) => setNewName(event.target.value)}
aria-label={translate('settings.agents.environmentName', 'Environment variable name')}
placeholder={translate('settings.agents.environmentName', 'Environment variable name')}
aria-label={t('settings.agents.environmentName', 'Environment variable name')}
placeholder={t('settings.agents.environmentName', 'Environment variable name')}
spellCheck={false}
className="h-7"
/>
@@ -66,6 +67,7 @@ function EnvironmentValueRow({
>
) => Promise<void>
}): React.JSX.Element {
const t = useTranslate()
const [value, setValue] = useState('')
const [changed, setChanged] = useState(false)
const { pending, write } = useAgentLaunchFieldWrite()
@@ -91,17 +93,17 @@ function EnvironmentValueRow({
setValue(event.target.value)
setChanged(true)
}}
aria-label={translate('settings.agents.environmentValue', 'Value for {{name}}', { name })}
aria-label={t('settings.agents.environmentValue', 'Value for {{name}}', { name })}
placeholder={
saved
? translate('settings.agents.savedEnvironment', 'Saved value · enter to replace')
: translate('settings.agents.newEnvironment', 'Value')
? t('settings.agents.savedEnvironment', 'Saved value · enter to replace')
: t('settings.agents.newEnvironment', 'Value')
}
spellCheck={false}
className="h-7"
/>
<Button type="button" size="xs" disabled={!changed || pending} onClick={save}>
{translate('settings.agents.save', 'Save')}
{t('settings.agents.save', 'Save')}
</Button>
{saved ? (
<Button
@@ -113,7 +115,7 @@ function EnvironmentValueRow({
write('remove', () => mutate({ type: 'environment-remove', agent, name }))
}
>
{translate('settings.agents.removeEnvironment', 'Remove')}
{t('settings.agents.removeEnvironment', 'Remove')}
</Button>
) : null}
</div>
@@ -23,7 +23,7 @@ import {
useAgentPermissionOverrideNotice,
type SelectPermissionOverrides
} from '../permissions/agent-permission-override-notice'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
type ReadState =
| { kind: 'loading' | 'unsupported' | 'failed' }
@@ -45,6 +45,7 @@ export function HostAgentLaunchSettings({
hostName: string
renderPermissions: AgentPermissionsRenderer
}): React.JSX.Element {
const t = useTranslate()
const owner = useMemo(
() => ({ environmentId, pairingRevision }),
[environmentId, pairingRevision]
@@ -160,18 +161,18 @@ export function HostAgentLaunchSettings({
<div className="space-y-2 text-sm text-muted-foreground">
<p>
{read.kind === 'unsupported'
? translate(
? t(
'settings.agents.updateHost',
'Update this server to edit its agent launch settings.'
)
: translate(
: t(
'settings.agents.readFailed',
'Couldn’t read agent settings. Check the host connection and try again.'
)}
</p>
{read.kind === 'failed' ? (
<Button type="button" variant="ghost" size="xs" onClick={() => void refresh()}>
{translate('auto.components.settings.AgentsPane.retryDetection', 'Retry')}
{t('auto.components.settings.AgentsPane.retryDetection', 'Retry')}
</Button>
) : null}
</div>
@@ -195,7 +196,7 @@ export function HostAgentLaunchSettings({
</fieldset>
{saveFailed ? (
<p role="alert" className="text-xs text-destructive">
{translate(
{t(
'settings.agents.saveFailed',
'Couldn’t save agent settings. Check the host connection and try again.'
)}
@@ -1,5 +1,5 @@
import { lazy, Suspense, useState } from 'react'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { useAppStore } from '@/store/index'
import { Button } from '../../../ui/button'
import { SearchableSetting } from '../../section-layout/SearchableSetting'
@@ -13,16 +13,14 @@ const OnboardingInlineCommandTerminal = lazy(() =>
type SetupChoice = 'zai' | 'bigmodel' | 'api-key'
export function ZcodeCliSetupSection(): React.JSX.Element {
const t = useTranslate()
const commandOverride = useAppStore((s) => s.settings?.agentCmdOverrides?.zcode)
const [choice, setChoice] = useState<SetupChoice | null>(null)
const executable = commandOverride?.trim() || 'zcode'
const command = choice === 'api-key' ? executable : `${executable} login ${choice} --no-browser`
const title = translate(
'auto.components.settings.ZcodeCliSetupSection.title',
'Use GLM with ZCode'
)
const description = translate(
const title = t('auto.components.settings.ZcodeCliSetupSection.title', 'Use GLM with ZCode')
const description = t(
'auto.components.settings.ZcodeCliSetupSection.description',
'Sign in to use GLM as a coding agent. The quota API key above only monitors usage; ZCode owns its own sign-in and model selection.'
)
@@ -37,7 +35,7 @@ export function ZcodeCliSetupSection(): React.JSX.Element {
<h4 className="text-xs font-medium">{title}</h4>
<p className="text-xs text-muted-foreground">{description}</p>
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.ZcodeCliSetupSection.help',
'Choose browser sign-in for your plan site, or use ZCode’s masked API-key setup. After signing in, start ZCode and use /model to choose a GLM model.'
)}
@@ -49,7 +47,7 @@ export function ZcodeCliSetupSection(): React.JSX.Element {
disabled={choice !== null}
onClick={() => setChoice('zai')}
>
{translate('auto.components.settings.ZcodeCliSetupSection.zai', 'Z.AI browser sign-in')}
{t('auto.components.settings.ZcodeCliSetupSection.zai', 'Z.AI browser sign-in')}
</Button>
<Button
variant="outline"
@@ -57,10 +55,7 @@ export function ZcodeCliSetupSection(): React.JSX.Element {
disabled={choice !== null}
onClick={() => setChoice('bigmodel')}
>
{translate(
'auto.components.settings.ZcodeCliSetupSection.bigmodel',
'BigModel browser sign-in'
)}
{t('auto.components.settings.ZcodeCliSetupSection.bigmodel', 'BigModel browser sign-in')}
</Button>
<Button
variant="outline"
@@ -68,14 +63,11 @@ export function ZcodeCliSetupSection(): React.JSX.Element {
disabled={choice !== null}
onClick={() => setChoice('api-key')}
>
{translate('auto.components.settings.ZcodeCliSetupSection.apiKey', 'ZCode API-key setup')}
{t('auto.components.settings.ZcodeCliSetupSection.apiKey', 'ZCode API-key setup')}
</Button>
{choice ? (
<Button variant="ghost" size="xs" onClick={() => setChoice(null)}>
{translate(
'auto.components.settings.ZcodeCliSetupSection.close',
'Close setup terminal'
)}
{t('auto.components.settings.ZcodeCliSetupSection.close', 'Close setup terminal')}
</Button>
) : null}
</div>
@@ -83,7 +75,7 @@ export function ZcodeCliSetupSection(): React.JSX.Element {
<Suspense
fallback={
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.ZcodeCliSetupSection.loading',
'Loading sign-in terminal…'
)}
@@ -92,21 +84,21 @@ export function ZcodeCliSetupSection(): React.JSX.Element {
>
<OnboardingInlineCommandTerminal
command={command}
title={translate(
title={t(
'auto.components.settings.ZcodeCliSetupSection.terminalTitle',
'ZCode sign-in'
)}
ariaLabel={translate(
ariaLabel={t(
'auto.components.settings.ZcodeCliSetupSection.terminalLabel',
'ZCode sign-in terminal'
)}
description={
choice === 'api-key'
? translate(
? t(
'auto.components.settings.ZcodeCliSetupSection.apiKeyInstructions',
'Press Enter to start ZCode, then enter /login and choose the API-key option for your plan site. Enter the key in ZCode’s masked prompt.'
)
: translate(
: t(
'auto.components.settings.ZcodeCliSetupSection.oauthInstructions',
'Press Enter to run the sign-in command, then open the authorization URL printed by ZCode. Sign-in is saved on this terminal’s execution host.'
)
@@ -1,6 +1,6 @@
import { ChevronDown } from 'lucide-react'
import type { AgentPermissionMode } from '../../../../../../shared/agent-launch/permissions/tui-agent-permissions'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { Button } from '../../../ui/button'
import {
DropdownMenu,
@@ -28,7 +28,8 @@ export function AgentPermissionModeMenu({
autoDisabled?: boolean
onChange: (mode: Exclude<AgentPermissionMode, 'mixed'>) => void
}): React.JSX.Element {
const ownLabel = translate('settings.agents.permissionEachOwn', "Each agent's own settings")
const t = useTranslate()
const ownLabel = t('settings.agents.permissionEachOwn', "Each agent's own settings")
const choice = mode === 'mixed' ? '' : agentPermissionModeChoice(mode)
return (
<DropdownMenu>
@@ -41,9 +42,7 @@ export function AgentPermissionModeMenu({
className="min-w-0 max-w-full"
>
{choice === '' ? (
<span className="truncate">
{translate('settings.agents.permissionMixed', 'Mixed')}
</span>
<span className="truncate">{t('settings.agents.permissionMixed', 'Mixed')}</span>
) : (
<AgentPermissionChoiceName choice={choice} ownLabel={ownLabel} truncate />
)}
@@ -1,6 +1,7 @@
import { Trans } from 'react-i18next'
import type { AgentPermissionMode } from '../../../../../../shared/agent-launch/permissions/tui-agent-permissions'
import { getIntlLocale, i18n, translate } from '@/i18n/i18n'
import { i18n } from '@/i18n/i18n'
import { useIntlLocale, useTranslate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
import { Tooltip, TooltipContent, TooltipTrigger } from '../../../ui/tooltip'
import { AgentPermissionModeMenu } from './AgentPermissionModeMenu'
@@ -28,6 +29,7 @@ function SupportedAgentsTrigger({
labels: readonly string[]
children?: React.ReactNode
}): React.JSX.Element {
const intlLocale = useIntlLocale()
return (
<Tooltip>
<TooltipTrigger asChild>
@@ -39,7 +41,7 @@ function SupportedAgentsTrigger({
</button>
</TooltipTrigger>
<TooltipContent side="bottom" align="start" sideOffset={6}>
{new Intl.ListFormat(getIntlLocale(), { type: 'conjunction' }).format(labels)}
{new Intl.ListFormat(intlLocale, { type: 'conjunction' }).format(labels)}
</TooltipContent>
</Tooltip>
)
@@ -50,11 +52,12 @@ function AgentPermissionsDescription({
majorityMode,
autoAgentLabels
}: AgentPermissionsSettingProps): React.ReactNode {
const t = useTranslate()
if (mode === 'auto') {
return (
<Trans
i18n={i18n}
defaults={translate(
defaults={t(
'settings.agents.permissionsAutoDescription',
'<agents>Supported agents</agents> approve safe actions on their own.'
)}
@@ -64,28 +67,26 @@ function AgentPermissionsDescription({
}
if (mode === 'mixed') {
return majorityMode
? translate(
? t(
'settings.agents.permissionsMixed',
'Most agents use {{mode}}. Choosing a mode resets the marked exceptions, except custom arguments.',
{ mode: agentPermissionModeLabel(majorityMode) }
{ mode: agentPermissionModeLabel(majorityMode, t) }
)
: translate(
: t(
'settings.agents.permissionsMixedNoMajority',
'Agents use different settings. Choosing a mode applies to all.'
)
}
return translate(
return t(
'settings.agents.permissionsDescription',
'How much agents can do without asking you. Agents with custom launch arguments keep them.'
)
}
export function AgentPermissionsSetting(props: AgentPermissionsSettingProps): React.JSX.Element {
const t = useTranslate()
const { mode, nonAutoMode, onChange, onChangeNonAuto, autoSupported = true } = props
const title = translate(
'auto.components.settings.AgentsPane.agentPermissions',
'Agent Permissions'
)
const title = t('auto.components.settings.AgentsPane.agentPermissions', 'Agent Permissions')
const showOthers = mode === 'auto' && onChangeNonAuto !== undefined
return (
<section className="flex items-start justify-between gap-3">
@@ -96,7 +97,7 @@ export function AgentPermissionsSetting(props: AgentPermissionsSettingProps): Re
</p>
{!autoSupported ? (
<p className="text-xs text-muted-foreground">
{translate(
{t(
'settings.agents.autoUnsupported',
'Approve for me needs a newer Orca on this host.'
)}
@@ -131,7 +132,7 @@ export function AgentPermissionsSetting(props: AgentPermissionsSettingProps): Re
)}
>
<span className="text-xs text-muted-foreground">
{translate('settings.agents.otherAgentsPermissions', 'Other agents')}
{t('settings.agents.otherAgentsPermissions', 'Other agents')}
</span>
<AgentPermissionModeMenu
mode={nonAutoMode ?? 'mixed'}
@@ -140,7 +141,7 @@ export function AgentPermissionsSetting(props: AgentPermissionsSettingProps): Re
onChangeNonAuto(next)
}
}}
label={translate(
label={t(
'settings.agents.otherAgentsPermissionsLabel',
'Permissions for other agents'
)}
@@ -1,5 +1,6 @@
import type { AgentPermissionMode } from '../../../../../../shared/agent-launch/permissions/tui-agent-permissions'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { Button } from '../../../ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '../../../ui/tooltip'
import { SettingsBadge } from '../../form-controls/SettingsFormControls'
@@ -25,20 +26,17 @@ export function AgentPermissionExceptionBadge({
permission: AgentRowPermission
onSelect: () => void
}): React.JSX.Element {
const t = useTranslate()
const { mode } = permission
const text = mode === 'mixed' ? customLabel() : agentPermissionModeLabel(mode)
const text = mode === 'mixed' ? customLabel(t) : agentPermissionModeLabel(mode, t)
const button = (
<button
type="button"
onClick={onSelect}
aria-label={translate(
'settings.agents.editRowPermissions',
'{{agent}} permissions: {{mode}}',
{
agent: label,
mode: text
}
)}
aria-label={t('settings.agents.editRowPermissions', '{{agent}} permissions: {{mode}}', {
agent: label,
mode: text
})}
className="rounded-full outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<SettingsBadge tone="accent">{text}</SettingsBadge>
@@ -51,14 +49,14 @@ export function AgentPermissionExceptionBadge({
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent>
{translate('settings.agents.customArgsTooltip', 'Uses its own launch arguments.')}
{t('settings.agents.customArgsTooltip', 'Uses its own launch arguments.')}
</TooltipContent>
</Tooltip>
)
}
function customLabel(): string {
return translate('settings.agents.customArgsBadge', 'Custom')
function customLabel(t: Translate = translate): string {
return t('settings.agents.customArgsBadge', 'Custom')
}
export function AgentRowPermissionControl({
@@ -70,18 +68,19 @@ export function AgentRowPermissionControl({
permission: AgentRowPermission
controlRef: React.Ref<HTMLDivElement>
}): React.JSX.Element {
const t = useTranslate()
return (
<div ref={controlRef} className="flex flex-wrap items-center gap-x-3 gap-y-1">
<span className="text-xs font-medium">
{translate('settings.agents.rowPermissions', 'Permissions')}
{t('settings.agents.rowPermissions', 'Permissions')}
</span>
{permission.mode === 'mixed' ? (
<span className="flex items-center gap-2 text-xs text-muted-foreground">
{translate('settings.agents.customPermissions', 'Custom arguments')}
{t('settings.agents.customPermissions', 'Custom arguments')}
{permission.reset ? (
<Button type="button" variant="outline" size="xs" onClick={permission.reset.apply}>
{translate('settings.agents.replaceWithPermissionMode', 'Replace with {{mode}}', {
mode: agentPermissionModeLabel(permission.reset.mode)
{t('settings.agents.replaceWithPermissionMode', 'Replace with {{mode}}', {
mode: agentPermissionModeLabel(permission.reset.mode, t)
})}
</Button>
) : null}
@@ -90,7 +89,7 @@ export function AgentRowPermissionControl({
<AgentPermissionModeMenu
mode={permission.mode}
onChange={permission.onChange}
label={translate('settings.agents.rowPermissionsLabel', '{{agent}} permissions', {
label={t('settings.agents.rowPermissionsLabel', '{{agent}} permissions', {
agent: label
})}
autoAvailable={permission.autoAvailable}
@@ -1,10 +1,14 @@
import type { AgentPermissionMode } from '../../../../../../shared/agent-launch/permissions/tui-agent-permissions'
import { agentPermissionChoiceLabel } from '../../../agent/launch-settings/AgentPermissionChoiceControls'
import type { Translate } from '../../../../i18n/use-translate'
export function agentPermissionModeChoice(mode: Exclude<AgentPermissionMode, 'mixed'>) {
return mode === 'manual' ? 'own' : mode === 'yolo' ? 'full-access' : 'auto'
}
export function agentPermissionModeLabel(mode: Exclude<AgentPermissionMode, 'mixed'>): string {
return agentPermissionChoiceLabel(agentPermissionModeChoice(mode), null)
export function agentPermissionModeLabel(
mode: Exclude<AgentPermissionMode, 'mixed'>,
t?: Translate
): string {
return agentPermissionChoiceLabel(agentPermissionModeChoice(mode), null, t)
}
@@ -2,6 +2,7 @@ import { useEffect, useId, useRef } from 'react'
import { toast } from 'sonner'
import { getAgentLabel } from '@/lib/agent-catalog'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { useMountedRef } from '@/hooks/useMountedRef'
import type {
AgentLaunchSettings,
@@ -27,6 +28,7 @@ function modeLabel(choice: AgentPermissionChoice): string {
}
export function useAgentPermissionOverrideNotice() {
const t = useTranslate()
const id = useId()
const mounted = useMountedRef()
const active = useRef<Permissions>(undefined)
@@ -90,14 +92,14 @@ export function useAgentPermissionOverrideNotice() {
const more = (count: number) =>
count > 3 ? (
<div>
{translate('settings.agents.permissionOverrideMore', 'and {{count}} more', {
{t('settings.agents.permissionOverrideMore', 'and {{count}} more', {
count: count - 3
})}
</div>
) : null
toast.success(
retained.length
? translate(
? t(
'settings.agents.permissionOverridesKept',
'{{count}} agents still use their own permission setting:',
{
@@ -105,15 +107,11 @@ export function useAgentPermissionOverrideNotice() {
defaultValue_one: '{{count}} agent still uses their own permission setting:'
}
)
: translate(
'settings.agents.permissionOverridesFollow',
'{{count}} agents now follow {{mode}}',
{
count: vanished.length,
mode,
defaultValue_one: '{{count}} agent now follows {{mode}}'
}
),
: t('settings.agents.permissionOverridesFollow', '{{count}} agents now follow {{mode}}', {
count: vanished.length,
mode,
defaultValue_one: '{{count}} agent now follows {{mode}}'
}),
{
id,
description: (
@@ -128,11 +126,10 @@ export function useAgentPermissionOverrideNotice() {
{more(retained.length)}
{vanished.slice(0, 3).map((agent) => (
<div key={agent}>
{translate(
'settings.agents.permissionOverrideFollows',
'{{agent}} now follows {{mode}}',
{ agent: getAgentLabel(agent), mode }
)}
{t('settings.agents.permissionOverrideFollows', '{{agent}} now follows {{mode}}', {
agent: getAgentLabel(agent),
mode
})}
</div>
))}
{more(vanished.length)}
@@ -142,7 +139,7 @@ export function useAgentPermissionOverrideNotice() {
...(retained.length
? {
action: {
label: translate('settings.agents.permissionOverridesUse', 'Use {{mode}}', {
label: t('settings.agents.permissionOverridesUse', 'Use {{mode}}', {
mode
}),
onClick: () => {
@@ -7,6 +7,7 @@ import {
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../ui/select'
import { SettingsRow, SettingsSegmentedControl } from '../../form-controls/SettingsFormControls'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
type AgentRuntimeSegment = GlobalWindowsRuntimeDefault['kind']
@@ -38,6 +39,7 @@ export function AgentRuntimeSetting({
wslDistros = EMPTY_WSL_DISTROS,
wslCapabilitiesLoading = false
}: AgentRuntimeSettingProps): React.JSX.Element | null {
const t = useTranslate()
if (!wslSupportedPlatform) {
return null
}
@@ -62,16 +64,13 @@ export function AgentRuntimeSetting({
return (
<SettingsRow
label={translate('auto.components.settings.AgentRuntimeSetting.label', 'Agent runtime')}
label={t('auto.components.settings.AgentRuntimeSetting.label', 'Agent runtime')}
alignTop
description={getDescription(runtimeDefault, wslAvailable, wslCapabilitiesLoading)}
description={getDescription(runtimeDefault, wslAvailable, wslCapabilitiesLoading, t)}
control={
<div className="flex w-52 flex-col items-stretch gap-2">
<SettingsSegmentedControl<AgentRuntimeSegment>
ariaLabel={translate(
'auto.components.settings.AgentRuntimeSetting.label',
'Agent runtime'
)}
ariaLabel={t('auto.components.settings.AgentRuntimeSetting.label', 'Agent runtime')}
value={runtimeDefault.kind}
onChange={handleRuntimeChange}
equalWidth
@@ -82,7 +81,7 @@ export function AgentRuntimeSetting({
},
{
value: 'wsl',
label: translate('auto.components.settings.AgentRuntimeSetting.wsl', 'WSL'),
label: t('auto.components.settings.AgentRuntimeSetting.wsl', 'WSL'),
disabled: wslCapabilitiesLoading || !wslAvailable || !nextWslDistro
}
]}
@@ -103,11 +102,8 @@ export function AgentRuntimeSetting({
<SelectValue
placeholder={
wslCapabilitiesLoading
? translate(
'auto.components.settings.AgentRuntimeSetting.loadingWsl',
'Loading WSL'
)
: translate(
? t('auto.components.settings.AgentRuntimeSetting.loadingWsl', 'Loading WSL')
: t(
'auto.components.settings.AgentRuntimeSetting.selectDistro',
'Select distro'
)
@@ -117,7 +113,7 @@ export function AgentRuntimeSetting({
<SelectContent>
{!runtimeDefault.distro ? (
<SelectItem value={NO_DISTRO_VALUE}>
{translate(
{t(
'auto.components.settings.AgentRuntimeSetting.selectDistro',
'Select distro'
)}
@@ -165,27 +161,28 @@ function getVisibleDistroOptions(
function getDescription(
runtimeDefault: GlobalWindowsRuntimeDefault,
wslAvailable: boolean,
wslCapabilitiesLoading: boolean
wslCapabilitiesLoading: boolean,
t: Translate = translate
): string {
if (runtimeDefault.kind === 'windows-host') {
return translate(
return t(
'auto.components.settings.AgentRuntimeSetting.windowsDescription',
'Detect and launch agents on Windows for projects that do not override their runtime.'
)
}
if (!wslAvailable && !wslCapabilitiesLoading) {
return translate(
return t(
'auto.components.settings.AgentRuntimeSetting.wslUnavailable',
'WSL is not available on this machine.'
)
}
if (!runtimeDefault.distro) {
return translate(
return t(
'auto.components.settings.AgentRuntimeSetting.distroRequired',
'Choose a WSL distro before projects can inherit WSL.'
)
}
return translate(
return t(
'auto.components.settings.AgentRuntimeSetting.wslDescription',
'Detect and launch agents in {{value0}} via WSL for projects that do not override their runtime.',
{ value0: runtimeDefault.distro }
@@ -13,6 +13,7 @@ import {
} from './codex-terminal-server-isolation-copy'
import { SearchableSetting } from '../../section-layout/SearchableSetting'
import { SettingsSwitchRow } from '../../form-controls/SettingsFormControls'
import { useTranslate } from '@/i18n/use-translate'
type CodexTerminalServerIsolationSettingProps = {
settings: GlobalSettings
@@ -23,15 +24,16 @@ export function CodexTerminalServerIsolationSetting({
settings,
updateSettings
}: CodexTerminalServerIsolationSettingProps): React.JSX.Element {
const title = getCodexTerminalServerIsolationTitle()
const description = getCodexTerminalServerIsolationDescription()
const t = useTranslate()
const title = getCodexTerminalServerIsolationTitle(t)
const description = getCodexTerminalServerIsolationDescription(t)
const enabled = isCodexTerminalServerIsolationEnabled(settings)
return (
<SearchableSetting
id={CODEX_TERMINAL_SERVER_ISOLATION_SETTINGS_TARGET_ID}
title={title}
description={description}
keywords={getCodexTerminalServerIsolationSearchKeywords()}
keywords={getCodexTerminalServerIsolationSearchKeywords(t)}
// Two independent switches share one search unit, so split them like separate rows.
className="divide-y divide-border"
>
@@ -44,8 +46,8 @@ export function CodexTerminalServerIsolationSetting({
{/* With isolation off, sharing is intentional and needs no warning control. */}
{enabled ? (
<SettingsSwitchRow
label={getCodexSharedServerWarningTitle()}
description={getCodexSharedServerWarningDescription()}
label={getCodexSharedServerWarningTitle(t)}
description={getCodexSharedServerWarningDescription(t)}
checked={isCodexSharedServerWarningEnabled(settings)}
onChange={() =>
void updateSettings({
@@ -1,46 +1,50 @@
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { searchKeywords } from '../../search/settings-search-keywords'
export function getCodexTerminalServerIsolationTitle(): string {
return translate(
export function getCodexTerminalServerIsolationTitle(t: Translate = translate): string {
return t(
'settings.agents.codexTerminalServerIsolation.title',
'Run each Codex terminal on its own server'
)
}
export function getCodexTerminalServerIsolationDescription(): string {
return translate(
export function getCodexTerminalServerIsolationDescription(t: Translate = translate): string {
return t(
'settings.agents.codexTerminalServerIsolation.description',
"Keeps Orca's status and closing tabs working correctly. Turn off to use Codex's shared server and its agents overview. Applies to new terminals."
)
}
export function getCodexSharedServerWarningTitle(): string {
return translate(
export function getCodexSharedServerWarningTitle(t: Translate = translate): string {
return t(
'settings.agents.codexSharedServerWarning.title',
'Warn when a Codex tab shares a server'
)
}
export function getCodexSharedServerWarningDescription(): string {
return translate(
export function getCodexSharedServerWarningDescription(t: Translate = translate): string {
return t(
'settings.agents.codexSharedServerWarning.description',
'Shows a notice on a Codex you started yourself when it shares a server with other tabs, because its agent status may be wrong.'
)
}
export function getCodexTerminalServerIsolationSearchKeywords(): string[] {
return searchKeywords([
{
key: 'auto.components.settings.agents.search.5ded38b843',
fallback: 'codex',
englishOnly: true
},
{ key: 'auto.components.settings.agents.search.7e15b89f6e', fallback: 'server' },
{ key: 'auto.components.settings.agents.search.ff457e1e7b', fallback: 'daemon' },
{ key: 'auto.components.settings.agents.search.9f3becc4e8', fallback: 'shared' },
{ key: 'auto.components.settings.agents.search.34337ed5c7', fallback: 'isolate' },
{ key: 'auto.components.settings.agents.search.9a84e65118', fallback: 'agents overview' },
{ key: 'auto.components.settings.agents.search.6984d4291a', fallback: 'status' }
])
export function getCodexTerminalServerIsolationSearchKeywords(t?: Translate): string[] {
return searchKeywords(
[
{
key: 'auto.components.settings.agents.search.5ded38b843',
fallback: 'codex',
englishOnly: true
},
{ key: 'auto.components.settings.agents.search.7e15b89f6e', fallback: 'server' },
{ key: 'auto.components.settings.agents.search.ff457e1e7b', fallback: 'daemon' },
{ key: 'auto.components.settings.agents.search.9f3becc4e8', fallback: 'shared' },
{ key: 'auto.components.settings.agents.search.34337ed5c7', fallback: 'isolate' },
{ key: 'auto.components.settings.agents.search.9a84e65118', fallback: 'agents overview' },
{ key: 'auto.components.settings.agents.search.6984d4291a', fallback: 'status' }
],
t
)
}
@@ -8,7 +8,8 @@ import { LoadingSpinner } from '@/components/LoadingSpinner'
import { useImmediateMutation } from '@/hooks/useImmediateMutation'
import { useMountedRef } from '@/hooks/useMountedRef'
import { motionSafeScrollBehavior } from '@/hooks/usePrefersReducedMotion'
import { getIntlLocale, translate } from '@/i18n/i18n'
import { translate } from '@/i18n/i18n'
import { useIntlLocale, useTranslate } from '@/i18n/use-translate'
import { getAgentLabel } from '@/lib/agent-catalog'
import { cn } from '@/lib/utils'
import { useAppStore } from '@/store/index'
@@ -41,6 +42,8 @@ type Writer = {
/** After the default changes, names the agents set for themselves to the other surface. */
function useKeptSurfaceNotice(): (surface: AgentDefaultSurface, writer: Writer) => void {
const intlLocale = useIntlLocale()
const t = useTranslate()
const id = useId()
const mounted = useMountedRef()
useEffect(
@@ -61,7 +64,7 @@ function useKeptSurfaceNotice(): (surface: AgentDefaultSurface, writer: Writer)
return
}
const values = {
agents: new Intl.ListFormat(getIntlLocale(), { type: 'conjunction' }).format(
agents: new Intl.ListFormat(intlLocale, { type: 'conjunction' }).format(
kept.map((agent) => getAgentLabel(agent))
),
count: kept.length
@@ -69,12 +72,12 @@ function useKeptSurfaceNotice(): (surface: AgentDefaultSurface, writer: Writer)
const chat = surface === 'chat'
toast.success(
chat
? translate('settings.agents.defaultSurfaceNowChat', 'Agents now open as chats')
: translate('settings.agents.defaultSurfaceNowTerminal', 'Agents now open in the terminal'),
? t('settings.agents.defaultSurfaceNowChat', 'Agents now open as chats')
: t('settings.agents.defaultSurfaceNowTerminal', 'Agents now open in the terminal'),
{
id,
description: chat
? translate(
? t(
'settings.agents.defaultSurfaceKeptTerminal',
'{{agents}} stay in the terminal, as you set.',
{
@@ -83,19 +86,15 @@ function useKeptSurfaceNotice(): (surface: AgentDefaultSurface, writer: Writer)
defaultValue_one: '{{agents}} stays in the terminal, as you set.'
}
)
: translate(
'settings.agents.defaultSurfaceKeptChat',
'{{agents}} stay as chats, as you set.',
{
...values,
count: values.count,
defaultValue_one: '{{agents}} stays as a chat, as you set.'
}
),
: t('settings.agents.defaultSurfaceKeptChat', '{{agents}} stay as chats, as you set.', {
...values,
count: values.count,
defaultValue_one: '{{agents}} stays as a chat, as you set.'
}),
action: {
label: chat
? translate('settings.agents.defaultSurfaceSetAllChat', 'Set all to Chat')
: translate('settings.agents.defaultSurfaceSetAllTerminal', 'Set all to Terminal'),
? t('settings.agents.defaultSurfaceSetAllChat', 'Set all to Chat')
: t('settings.agents.defaultSurfaceSetAllTerminal', 'Set all to Terminal'),
onClick: () =>
void followAgentDefaultSurface({ agents: kept, surface, ...writer }).catch(saveFailed)
}
@@ -174,13 +173,14 @@ export function AgentDefaultSurfaceSection({
settings: GlobalSettings
presentation?: 'tiles' | 'switch'
}): JSX.Element {
const t = useTranslate()
const name = useId()
const notify = useKeptSurfaceNotice()
const updateSettings = useAppStore((s) => s.updateSettingsOrThrow)
const mutation = useImmediateMutation()
const pending = mutation.isPending('default-surface')
const surface = normalizeAgentDefaultSurface(settings.agentDefaultSurface)
const title = translate('settings.agents.defaultSurfaceTitle', 'Open agents as')
const title = t('settings.agents.defaultSurfaceTitle', 'Open agents as')
const choose = (next: AgentDefaultSurface): void => {
const writer = { fallbackSettings: settings, updateSettings }
void mutation.run('default-surface', {
@@ -209,17 +209,17 @@ export function AgentDefaultSurfaceSection({
label={
<span className="text-lg font-semibold">
{surface === 'chat'
? translate('settings.chat.launch.on', 'Chat UI is on')
: translate('settings.chat.launch.off', 'Chat UI is off')}
? t('settings.chat.launch.on', 'Chat UI is on')
: t('settings.chat.launch.off', 'Chat UI is off')}
</span>
}
description={
surface === 'chat'
? translate(
? t(
'settings.chat.launch.chatDescription',
'New agents will launch in chat mode by default.'
)
: translate(
: t(
'settings.chat.launch.cliDescription',
'New agents will launch as CLI by default.'
)
@@ -231,8 +231,8 @@ export function AgentDefaultSurfaceSection({
onClick={() => choose(surface === 'chat' ? 'terminal' : 'chat')}
>
{surface === 'chat'
? translate('settings.chat.launch.turnOff', 'Turn off')
: translate('settings.chat.launch.turnOn', 'Turn on')}
? t('settings.chat.launch.turnOff', 'Turn off')
: t('settings.chat.launch.turnOn', 'Turn on')}
</Button>
}
/>
@@ -244,7 +244,7 @@ export function AgentDefaultSurfaceSection({
<div id={AGENT_DEFAULT_SURFACE_SECTION_ID} className="@container scroll-mt-8">
<SettingsGroup
title={title}
description={translate(
description={t(
'settings.agents.defaultSurfaceDescription',
'The default for agents on this computer.'
)}
@@ -260,8 +260,8 @@ export function AgentDefaultSurfaceSection({
name={name}
pending={pending}
checked={surface === 'chat'}
title={translate('settings.agents.defaultOpensAsChat', 'Chat')}
description={translate(
title={t('settings.agents.defaultOpensAsChat', 'Chat')}
description={t(
'settings.agents.defaultSurfaceChatDescription',
'Rich messages, visuals and images.'
)}
@@ -272,8 +272,8 @@ export function AgentDefaultSurfaceSection({
name={name}
pending={pending}
checked={surface === 'terminal'}
title={translate('settings.agents.defaultSurfaceTerminal', 'Terminal')}
description={translate(
title={t('settings.agents.defaultSurfaceTerminal', 'Terminal')}
description={t(
'settings.agents.defaultSurfaceTerminalDescription',
'Each agent’s own CLI.'
)}
@@ -287,7 +287,7 @@ export function AgentDefaultSurfaceSection({
onClick={showInstalledList}
className="inline-flex items-center gap-0.5 rounded-sm text-[13px] font-medium underline-offset-4 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
>
{translate('settings.agents.defaultSurfacePerAgent', 'Choose per agent')}
{t('settings.agents.defaultSurfacePerAgent', 'Choose per agent')}
<ChevronRight className="size-3.5" />
</button>
</div>
@@ -10,7 +10,7 @@ import {
} from '../../../../../shared/app-icon'
import { Button } from '../../ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/tooltip'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
const APP_ICON_URLS = {
classic: classicIconUrl,
@@ -56,23 +56,24 @@ function IconCycleButton({ label, onClick, children }: IconCycleButtonProps): Re
}
export function AppIconSelector({ value, onChange }: AppIconSelectorProps): React.JSX.Element {
const t = useTranslate()
const selected = normalizeAppIconId(value)
return (
<div className="flex items-center justify-center gap-2">
<IconCycleButton
label={translate('auto.components.settings.AppIconSelector.5f5142a62a', 'Previous icon')}
label={t('auto.components.settings.AppIconSelector.5f5142a62a', 'Previous icon')}
onClick={() => onChange(getOffsetIcon(selected, -1))}
>
<ChevronLeft className="size-4" />
</IconCycleButton>
<img
src={APP_ICON_URLS[selected]}
alt={translate('auto.components.settings.AppIconSelector.415fa76f64', 'Selected app icon')}
alt={t('auto.components.settings.AppIconSelector.415fa76f64', 'Selected app icon')}
className="size-24 rounded-2xl object-contain"
/>
<IconCycleButton
label={translate('auto.components.settings.AppIconSelector.d5a112dc9b', 'Next icon')}
label={t('auto.components.settings.AppIconSelector.d5a112dc9b', 'Next icon')}
onClick={() => onChange(getOffsetIcon(selected, 1))}
>
<ChevronRight className="size-4" />
@@ -4,7 +4,7 @@ import { ChevronRight } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useAppStore } from '../../../store/index'
import { normalizeSettingsSearchQuery } from '../search/settings-search'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
type AppearanceAdvancedDisclosureProps = {
label?: string
@@ -24,6 +24,7 @@ export function AppearanceAdvancedDisclosure({
contentClassName,
children
}: AppearanceAdvancedDisclosureProps): React.JSX.Element {
const t = useTranslate()
const searchQuery = useAppStore((state) => state.settingsSearchQuery)
const isSearching = normalizeSettingsSearchQuery(searchQuery).length > 0
const [open, setOpen] = useState(false)
@@ -46,8 +47,7 @@ export function AppearanceAdvancedDisclosure({
expanded && 'rotate-90'
)}
/>
{label ??
translate('auto.components.settings.AppearanceAdvancedDisclosure.advanced', 'Advanced')}
{label ?? t('auto.components.settings.AppearanceAdvancedDisclosure.advanced', 'Advanced')}
</button>
{expanded ? <div className={cn('pt-1', contentClassName)}>{children}</div> : null}
</div>
@@ -24,7 +24,7 @@ import {
} from './appearance-search'
import { getMenuBarIconEntries, getSystemTrayEntries } from './appearance-system-presence-search'
import { SHOW_UI_LANGUAGE_SETTING, UI_LANGUAGE_CHOICES } from '@/i18n/supported-languages'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import type { UiLanguage } from '../../../../../shared/ui-language'
import { matchesSettingsSearch, normalizeSettingsSearchQuery } from '../search/settings-search'
import { translateSearchKeyword } from '../search/settings-search-keywords'
@@ -56,6 +56,7 @@ export function AppearanceInterfaceSection({
onRequestFontSuggestions,
forceVisiblePrimary = false
}: AppearanceInterfaceSectionProps): React.JSX.Element {
const t = useTranslate()
const searchQuery = useAppStore((state) => state.settingsSearchQuery)
const iconThemes = useIconThemePickerLabels()
const pluginIconThemes = iconThemes.themes
@@ -63,39 +64,33 @@ export function AppearanceInterfaceSection({
const themeSlots = [
{
base: 'light' as const,
title: translate(
'auto.components.settings.AppearanceInterfaceSection.lightTheme',
'Light theme'
)
title: t('auto.components.settings.AppearanceInterfaceSection.lightTheme', 'Light theme')
},
{
base: 'dark' as const,
title: translate(
'auto.components.settings.AppearanceInterfaceSection.darkTheme',
'Dark theme'
)
title: t('auto.components.settings.AppearanceInterfaceSection.darkTheme', 'Dark theme')
}
]
const editorThemeEntry = getCodeEditorThemeEntries()[0]
const editorThemeSlots = [
{
base: 'light' as const,
title: translate('settings.appearance.editorTheme.light', 'Light editor theme'),
description: translate(
title: t('settings.appearance.editorTheme.light', 'Light editor theme'),
description: t(
'settings.appearance.editorTheme.lightDescription',
'Code editor, diff and notebook colors while the app is light.'
)
},
{
base: 'dark' as const,
title: translate('settings.appearance.editorTheme.dark', 'Dark editor theme'),
description: translate(
title: t('settings.appearance.editorTheme.dark', 'Dark editor theme'),
description: t(
'settings.appearance.editorTheme.darkDescription',
'Code editor, diff and notebook colors while the app is dark.'
)
}
]
const iconThemeTitle = translate(
const iconThemeTitle = t(
'auto.components.settings.AppearanceInterfaceSection.pluginIconTheme.title',
'Icon theme'
)
@@ -106,7 +101,7 @@ export function AppearanceInterfaceSection({
const menuBarIconEntry = getMenuBarIconEntries({ showMenuBarIcon: true })[0]
const systemTrayEntry = getSystemTrayEntries({ showSystemTray: true })[0]
const themeEntry = getThemeEntries()[0]
const themeLabel = translate('auto.components.settings.AppearancePane.932ff1fbff', 'Theme')
const themeLabel = t('auto.components.settings.AppearancePane.932ff1fbff', 'Theme')
const titlebarEntry = getTitlebarEntries()[0]
const typographyEntry = getTypographyEntries()[0]
const zoomEntry = getZoomEntries()[0]
@@ -118,7 +113,7 @@ export function AppearanceInterfaceSection({
...getMenuBarIconEntries({ showMenuBarIcon: isDesktopMac })
]
const showAdvanced = !isSearching || matchesSettingsSearch(searchQuery, advancedEntries)
const languageTitle = translate('settings.appearance.language.title', 'Language')
const languageTitle = t('settings.appearance.language.title', 'Language')
return (
<div className="divide-y divide-border/40">
@@ -145,7 +140,7 @@ export function AppearanceInterfaceSection({
<SearchableSetting
key={slot.base}
title={slot.title}
description={translate(
description={t(
'auto.components.settings.AppearanceInterfaceSection.themeSlot.description',
'App colors for this mode.'
)}
@@ -156,7 +151,9 @@ export function AppearanceInterfaceSection({
slot.base,
...translateSearchKeyword(
'auto.components.settings.appearance.search.matchTerminal',
'match terminal'
'match terminal',
undefined,
t
)
]}
forceVisible={forceVisiblePrimary}
@@ -201,7 +198,7 @@ export function AppearanceInterfaceSection({
{pluginIconThemes.length > 0 ? (
<SearchableSetting
title={iconThemeTitle}
description={translate(
description={t(
'auto.components.settings.AppearanceInterfaceSection.pluginIconTheme.description',
'File and folder icons from an enabled plugin.'
)}
@@ -223,7 +220,7 @@ export function AppearanceInterfaceSection({
pluginIconThemes.find((theme) => theme.id === value)?.id ?? null
})
}
searchPlaceholder={translate(
searchPlaceholder={t(
'auto.components.settings.AppearanceInterfaceSection.pluginIconTheme.search',
'Search icon themes…'
)}
@@ -281,19 +278,19 @@ export function AppearanceInterfaceSection({
) : null}
<SearchableSetting
title={translate('auto.components.settings.AppearancePane.5e6d7aba8d', 'UI Zoom')}
title={t('auto.components.settings.AppearancePane.5e6d7aba8d', 'UI Zoom')}
description={zoomEntry?.description}
keywords={zoomEntry?.keywords ?? ['zoom', 'scale', 'shortcut']}
forceVisible={forceVisiblePrimary}
>
<SettingsRow
label={translate('auto.components.settings.AppearancePane.5e6d7aba8d', 'UI Zoom')}
label={t('auto.components.settings.AppearancePane.5e6d7aba8d', 'UI Zoom')}
// Explain the shortcut and terminal exception; the zoom buttons already explain scaling.
description={
<>
<ShortcutHintList combos={zoomInKeyCombos} /> /{' '}
<ShortcutHintList combos={zoomOutKeyCombos} />{' '}
{translate(
{t(
'auto.components.settings.AppearancePane.ef89200c1f',
'when not in a terminal pane.'
)}
@@ -304,13 +301,13 @@ export function AppearanceInterfaceSection({
</SearchableSetting>
<SearchableSetting
title={translate('auto.components.settings.AppearancePane.102d6b5f9b', 'IDE Font')}
title={t('auto.components.settings.AppearancePane.102d6b5f9b', 'IDE Font')}
description={typographyEntry?.description}
keywords={typographyEntry?.keywords ?? ['font', 'typeface', 'typography']}
forceVisible={forceVisiblePrimary}
>
<SettingsRow
label={translate('auto.components.settings.AppearancePane.102d6b5f9b', 'IDE Font')}
label={t('auto.components.settings.AppearancePane.102d6b5f9b', 'IDE Font')}
control={
<FontAutocomplete
value={settings.appFontFamily}
@@ -329,18 +326,12 @@ export function AppearanceInterfaceSection({
<AppearanceAdvancedDisclosure showTopBorder={false}>
<div className="divide-y divide-border/40">
<SearchableSetting
title={translate(
'auto.components.settings.AppearancePane.9868f39007',
'Titlebar App Name'
)}
title={t('auto.components.settings.AppearancePane.9868f39007', 'Titlebar App Name')}
description={titlebarEntry?.description}
keywords={titlebarEntry?.keywords ?? ['titlebar', 'orca', 'app', 'name']}
>
<SettingsSwitchRow
label={translate(
'auto.components.settings.AppearancePane.9868f39007',
'Titlebar App Name'
)}
label={t('auto.components.settings.AppearancePane.9868f39007', 'Titlebar App Name')}
checked={settings.showTitlebarAppName}
onChange={() =>
updateSettings({ showTitlebarAppName: !settings.showTitlebarAppName })
@@ -356,7 +347,7 @@ export function AppearanceInterfaceSection({
{isDesktopWindows ? (
<SearchableSetting
title={translate(
title={t(
'auto.components.settings.AppearancePane.2edf606c46',
'Minimize to Tray on Close'
)}
@@ -364,12 +355,12 @@ export function AppearanceInterfaceSection({
keywords={systemTrayEntry?.keywords ?? ['tray', 'minimize', 'close']}
>
<SettingsSwitchRow
label={translate(
label={t(
'auto.components.settings.AppearancePane.2edf606c46',
'Minimize to Tray on Close'
)}
// Explain the non-obvious platform restriction and keep-running consequence.
description={translate(
description={t(
'auto.components.settings.AppearancePane.b707773a0d',
'When enabled, closing the window keeps Orca running in the system tray instead of quitting.'
)}
@@ -383,14 +374,14 @@ export function AppearanceInterfaceSection({
{isDesktopMac ? (
<SearchableSetting
title={translate('settings.appearance.menuBarIcon.title', 'Show Menu Bar Icon')}
title={t('settings.appearance.menuBarIcon.title', 'Show Menu Bar Icon')}
description={menuBarIconEntry?.description}
keywords={menuBarIconEntry?.keywords ?? ['menu bar', 'status item', 'activity']}
>
<SettingsSwitchRow
label={translate('settings.appearance.menuBarIcon.title', 'Show Menu Bar Icon')}
label={t('settings.appearance.menuBarIcon.title', 'Show Menu Bar Icon')}
// Hiding the status item preserves Dock activation and close-keeps-running behavior.
description={translate(
description={t(
'settings.appearance.menuBarIcon.description',
'Keep an Orca shortcut and activity indicator in the macOS menu bar.'
)}
@@ -39,7 +39,7 @@ import { normalizeAppIconId } from '../../../../../shared/app-icon'
import { getRendererAppPlatform } from '@/lib/renderer-app-platform'
import { isWebClientLocation } from '@/lib/web-client-location'
import { SHOW_UI_LANGUAGE_SETTING } from '@/i18n/supported-languages'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { resolveInterfaceSectionSummary } from './appearance-interface-summary'
@@ -74,6 +74,7 @@ export function AppearancePane({
terminalConfigImport,
warpThemes
}: AppearancePaneProps): React.JSX.Element {
const t = useTranslate()
const searchQuery = useAppStore((state) => state.settingsSearchQuery)
const appearanceAccordionDeepLink = useAppStore((state) => state.appearanceAccordionDeepLink)
const clearAppearanceAccordionDeepLink = useAppStore(
@@ -114,19 +115,13 @@ export function AppearancePane({
cancelAnimationFrame(frameId)
}
}, [appearanceAccordionDeepLink, clearAppearanceAccordionDeepLink])
const interfaceTitle = translate(
'auto.components.settings.AppearancePane.interfaceTitle',
'Interface'
)
const terminalTitle = translate(
'auto.components.settings.AppearancePane.terminalTitle',
'Terminal'
)
const windowSidebarTitle = translate(
const interfaceTitle = t('auto.components.settings.AppearancePane.interfaceTitle', 'Interface')
const terminalTitle = t('auto.components.settings.AppearancePane.terminalTitle', 'Terminal')
const windowSidebarTitle = t(
'auto.components.settings.AppearancePane.windowSidebarTitle',
'Window & Sidebar'
)
const windowSidebarSummary = translate(
const windowSidebarSummary = t(
'auto.components.settings.AppearancePane.windowSidebarSummary',
'Sidebar, status bar, and file explorer'
)
@@ -196,10 +191,10 @@ export function AppearancePane({
})
}
const interfaceSummary = resolveInterfaceSectionSummary(settings)
const interfaceSummary = resolveInterfaceSectionSummary(settings, t)
const terminalSummary = `${
settings.terminalFontFamily ||
translate('auto.components.settings.AppearancePane.terminalDefaultFont', 'Default font')
t('auto.components.settings.AppearancePane.terminalDefaultFont', 'Default font')
} · ${settings.terminalFontSize}px`
return (
@@ -272,8 +267,8 @@ export function AppearancePane({
{appIconMatches ? (
<SettingsGroup>
<SearchableSetting
title={translate('auto.components.settings.AppearancePane.ca1590d42f', 'App Icon')}
description={translate(
title={t('auto.components.settings.AppearancePane.ca1590d42f', 'App Icon')}
description={t(
'auto.components.settings.AppearancePane.0cd9b8228f',
'Choose the app icon shown in the Dock and window switcher.'
)}
@@ -2,17 +2,18 @@ import type React from 'react'
import type { ShortcutKeyComboDetails } from '@/hooks/useShortcutLabel'
import { ShortcutKeyCombo } from '../../ShortcutKeyCombo'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
export function ShortcutHintList({
combos
}: {
combos: ShortcutKeyComboDetails[]
}): React.JSX.Element {
const t = useTranslate()
if (combos.length === 0) {
return (
<span className="text-xs text-muted-foreground">
{translate('auto.components.settings.AppearancePane.3057983501', 'Unassigned')}
{t('auto.components.settings.AppearancePane.3057983501', 'Unassigned')}
</span>
)
}
@@ -28,7 +28,7 @@ import {
import { LeftSidebarAppearanceSetting } from './LeftSidebarAppearanceSetting'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { matchesSettingsSearch, normalizeSettingsSearchQuery } from '../search/settings-search'
type AppearanceWindowSidebarSectionProps = {
@@ -68,6 +68,7 @@ export function AppearanceWindowSidebarSection({
updateSettings,
forceVisiblePrimary = false
}: AppearanceWindowSidebarSectionProps): React.JSX.Element {
const t = useTranslate()
const searchQuery = useAppStore((state) => state.settingsSearchQuery)
const isSearching = normalizeSettingsSearchQuery(searchQuery).length > 0
const statusBarItems = useAppStore((state) => state.statusBarItems)
@@ -82,12 +83,9 @@ export function AppearanceWindowSidebarSection({
const sidebarEntries = getSidebarEntries()
const workspaceCardLayoutEntry = getWorkspaceCardLayoutEntry()
const layoutEntries = getLayoutEntries()
const followSymlinkEntry = getFollowSymlinkedDirectoriesEntry()
const statusBarTitle = translate(
'auto.components.settings.AppearancePane.3e4175e5c6',
'Status Bar'
)
const statusBarDescription = translate(
const followSymlinkEntry = getFollowSymlinkedDirectoriesEntry(t)
const statusBarTitle = t('auto.components.settings.AppearancePane.3e4175e5c6', 'Status Bar')
const statusBarDescription = t(
'auto.components.settings.AppearancePane.statusBarDescription',
'Choose which indicators appear in the status bar.'
)
@@ -154,14 +152,14 @@ export function AppearanceWindowSidebarSection({
options={[
{
value: 'used',
label: translate(
label: t(
'auto.components.settings.AppearanceWindowSidebarSection.usagePercentageDisplayUsed',
'Used'
)
},
{
value: 'remaining',
label: translate(
label: t(
'auto.components.settings.AppearanceWindowSidebarSection.usagePercentageDisplayRemaining',
'Remaining'
)
@@ -205,7 +203,7 @@ export function AppearanceWindowSidebarSection({
{showSidebarAdvanced ? (
<div className="space-y-3">
<SettingsSubsectionHeader
title={translate('auto.components.settings.AppearancePane.dc29f3cc0d', 'Sidebar')}
title={t('auto.components.settings.AppearancePane.dc29f3cc0d', 'Sidebar')}
/>
<div className="ml-4 divide-y divide-border/40">
<SearchableSetting
@@ -226,14 +224,14 @@ export function AppearanceWindowSidebarSection({
options={[
{
value: 'detailed',
label: translate(
label: t(
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.cc17bd443b',
'Detailed'
)
},
{
value: 'compact',
label: translate(
label: t(
'auto.components.sidebar.SidebarWorkspaceOptionsMenu.25105b28cb',
'Compact'
)
@@ -245,7 +243,7 @@ export function AppearanceWindowSidebarSection({
</SearchableSetting>
<SearchableSetting
title={translate(
title={t(
'auto.components.settings.AppearancePane.cf81907069',
'Show Tasks Button'
)}
@@ -253,7 +251,7 @@ export function AppearanceWindowSidebarSection({
keywords={sidebarEntries[0]?.keywords ?? ['tasks', 'sidebar', 'button']}
>
<SettingsSwitchRow
label={translate(
label={t(
'auto.components.settings.AppearancePane.cf81907069',
'Show Tasks Button'
)}
@@ -265,7 +263,7 @@ export function AppearanceWindowSidebarSection({
</SearchableSetting>
<SearchableSetting
title={translate(
title={t(
'auto.components.settings.AppearancePane.511f270ebb',
'Show Automations Button'
)}
@@ -275,7 +273,7 @@ export function AppearanceWindowSidebarSection({
}
>
<SettingsSwitchRow
label={translate(
label={t(
'auto.components.settings.AppearancePane.511f270ebb',
'Show Automations Button'
)}
@@ -289,7 +287,7 @@ export function AppearanceWindowSidebarSection({
</SearchableSetting>
<SearchableSetting
title={translate(
title={t(
'auto.components.settings.AppearancePane.9da1020447',
'Show Orca Mobile Button'
)}
@@ -297,12 +295,12 @@ export function AppearanceWindowSidebarSection({
keywords={sidebarEntries[2]?.keywords ?? ['mobile', 'phone', 'sidebar']}
>
<SettingsSwitchRow
label={translate(
label={t(
'auto.components.settings.AppearancePane.9da1020447',
'Show Orca Mobile Button'
)}
// Explain the remaining Toolbox route so hiding the shortcut cannot imply feature removal.
description={translate(
description={t(
'auto.components.settings.AppearancePane.61d842eca0',
'Show the Orca Mobile shortcut in the sidebar. It remains available from Toolbox.'
)}
@@ -319,12 +317,12 @@ export function AppearanceWindowSidebarSection({
keywords={getShowPinnedWorktreesInGroupsEntry().keywords}
>
<SettingsSwitchRow
label={translate(
label={t(
'auto.components.settings.AppearancePane.showPinnedWorktreesInGroups.title',
'Also show pinned worktrees in their original lists'
)}
// This opt-in duplicates pinned worktrees into natural groups while preserving Pinned.
description={translate(
description={t(
'auto.components.settings.AppearancePane.showPinnedWorktreesInGroups.description',
'Pinned worktrees stay in Pinned and also appear in All, Project, Status, and PR.'
)}
@@ -345,16 +343,13 @@ export function AppearanceWindowSidebarSection({
{showFileExplorerAdvanced ? (
<div className="space-y-3">
<SettingsSubsectionHeader
title={translate(
'auto.components.settings.AppearancePane.d496901cd0',
'File Explorer'
)}
title={t('auto.components.settings.AppearancePane.d496901cd0', 'File Explorer')}
/>
<div className="ml-4 divide-y divide-border/40">
<SearchableSetting
title={
layoutEntries[0]?.title ??
translate(
t(
'auto.components.settings.AppearancePane.0fafabcf35',
'Show Git-Ignored Files'
)
@@ -363,11 +358,11 @@ export function AppearanceWindowSidebarSection({
keywords={layoutEntries[0]?.keywords ?? ['git', 'gitignore', 'ignored']}
>
<SettingsSwitchRow
label={translate(
label={t(
'auto.components.settings.AppearancePane.0fafabcf35',
'Show Git-Ignored Files'
)}
description={translate(
description={t(
'auto.components.settings.AppearancePane.gitIgnoredGlossary',
'Files matched by .gitignore.'
)}
@@ -6,7 +6,7 @@ import {
DEFAULT_LEFT_SIDEBAR_TINT_OPACITY,
MAX_LEFT_SIDEBAR_TINT_OPACITY
} from '../../../../../shared/left-sidebar-appearance'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import {
ColorField,
NumberField,
@@ -23,15 +23,16 @@ export function LeftSidebarAppearanceSetting({
settings,
updateSettings
}: LeftSidebarAppearanceSettingProps): React.JSX.Element {
const t = useTranslate()
return (
<div className="space-y-2">
<SettingsRow
alignTop
label={translate(
label={t(
'auto.components.settings.AppearancePane.leftSidebarAppearance.title',
'Left Sidebar Appearance'
)}
description={translate(
description={t(
'auto.components.settings.AppearancePane.leftSidebarAppearance.tintDescription',
'Keep the left sidebar default or tint it. To follow the terminal, pick Match Terminal as the Light or Dark theme.'
)}
@@ -40,21 +41,21 @@ export function LeftSidebarAppearanceSetting({
size="sm"
value={settings.leftSidebarAppearanceMode === 'tinted' ? 'tinted' : 'default'}
onChange={(leftSidebarAppearanceMode) => updateSettings({ leftSidebarAppearanceMode })}
ariaLabel={translate(
ariaLabel={t(
'auto.components.settings.AppearancePane.leftSidebarAppearance.title',
'Left Sidebar Appearance'
)}
options={[
{
value: 'default',
label: translate(
label: t(
'auto.components.settings.AppearancePane.leftSidebarAppearance.default',
'Default'
)
},
{
value: 'tinted',
label: translate(
label: t(
'auto.components.settings.AppearancePane.leftSidebarAppearance.tinted',
'Tinted'
)
@@ -66,11 +67,11 @@ export function LeftSidebarAppearanceSetting({
{settings.leftSidebarAppearanceMode === 'tinted' ? (
<div className="space-y-2">
<ColorField
label={translate(
label={t(
'auto.components.settings.AppearancePane.leftSidebarAppearance.tintColor',
'Sidebar Tint'
)}
description={translate(
description={t(
'auto.components.settings.AppearancePane.leftSidebarAppearance.tintColorDescription',
'The color mixed into the left sidebar surface.'
)}
@@ -79,11 +80,11 @@ export function LeftSidebarAppearanceSetting({
onChange={(leftSidebarTintColor) => updateSettings({ leftSidebarTintColor })}
/>
<NumberField
label={translate(
label={t(
'auto.components.settings.AppearancePane.leftSidebarAppearance.tintOpacity',
'Tint Strength'
)}
description={translate(
description={t(
'auto.components.settings.AppearancePane.leftSidebarAppearance.tintOpacityDescription',
'Controls how strongly the tint is mixed into the sidebar.'
)}
@@ -3,7 +3,7 @@ import type { CtrlTabOrderMode } from '../../../../../shared/tab-types'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../ui/select'
import { SearchableSetting } from '../section-layout/SearchableSetting'
import { SettingsRow } from '../form-controls/SettingsFormControls'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
export function RecentTabOrderControl({
ctrlTabOrderMode,
@@ -14,10 +14,11 @@ export function RecentTabOrderControl({
keywords?: string[]
updateSettings: (updates: { ctrlTabOrderMode?: CtrlTabOrderMode }) => Promise<void> | void
}): React.JSX.Element {
const t = useTranslate()
return (
<SearchableSetting
title={translate('auto.components.settings.RecentTabOrderControl.7a546f2309', 'Tab Order')}
description={translate(
title={t('auto.components.settings.RecentTabOrderControl.7a546f2309', 'Tab Order')}
description={t(
'auto.components.settings.RecentTabOrderControl.a867a0889f',
'Recent or tab strip.'
)}
@@ -25,7 +26,7 @@ export function RecentTabOrderControl({
className="max-w-none"
>
<SettingsRow
label={translate('auto.components.settings.RecentTabOrderControl.7a546f2309', 'Tab Order')}
label={t('auto.components.settings.RecentTabOrderControl.7a546f2309', 'Tab Order')}
control={
<Select
value={ctrlTabOrderMode}
@@ -38,16 +39,10 @@ export function RecentTabOrderControl({
</SelectTrigger>
<SelectContent>
<SelectItem value="mru">
{translate(
'auto.components.settings.RecentTabOrderControl.6e6a3fcc61',
'Most recent'
)}
{t('auto.components.settings.RecentTabOrderControl.6e6a3fcc61', 'Most recent')}
</SelectItem>
<SelectItem value="sequential">
{translate(
'auto.components.settings.RecentTabOrderControl.3b17c81ede',
'Tab strip order'
)}
{t('auto.components.settings.RecentTabOrderControl.3b17c81ede', 'Tab strip order')}
</SelectItem>
</SelectContent>
</Select>
@@ -2,7 +2,7 @@ import type { GlobalSettings } from '../../../../../shared/global-settings-types
import type { SettingsSearchEntry } from '../search/settings-search'
import { SearchableSetting } from '../section-layout/SearchableSetting'
import { SettingsSwitchRow } from '../form-controls/SettingsFormControls'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
export function ReduceVisualEffectsSetting({
entry,
@@ -13,7 +13,8 @@ export function ReduceVisualEffectsSetting({
enabled: boolean
updateSettings: (updates: Partial<GlobalSettings>) => void
}): React.JSX.Element {
const title = translate('settings.appearance.reduceVisualEffects.title', 'Reduce Visual Effects')
const t = useTranslate()
const title = t('settings.appearance.reduceVisualEffects.title', 'Reduce Visual Effects')
return (
<SearchableSetting
title={title}
@@ -8,9 +8,10 @@ import {
UI_ZOOM_MAX as ZOOM_MAX,
zoomLevelToPercent
} from '../../../../../shared/ui-zoom-level'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
export function UIZoomControl(): React.JSX.Element {
const t = useTranslate()
const [zoomLevel, setZoomLevel] = useState(() => window.api.ui.getZoomLevel())
useEffect(() => {
@@ -55,7 +56,7 @@ export function UIZoomControl(): React.JSX.Element {
className="ml-1 gap-1.5"
>
<RotateCcw className="size-3" />
{translate('auto.components.settings.UIZoomControl.c2c64b24d0', 'Reset')}
{t('auto.components.settings.UIZoomControl.c2c64b24d0', 'Reset')}
</Button>
</div>
)
@@ -16,31 +16,40 @@ import { useMountedRef } from '@/hooks/useMountedRef'
import { readPrefersReducedTransparency } from '@/hooks/use-reduce-visual-effects'
import { activeWindowMaterial, rendererWindowMaterialHost } from '@/lib/window-material-runtime'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
type WindowMaterialSettingProps = {
settings: GlobalSettings
updateSettings: (updates: Partial<GlobalSettings>) => void
}
function windowMaterialLabel(material: WindowMaterial, host: WindowMaterialHost): string {
function windowMaterialLabel(
material: WindowMaterial,
host: WindowMaterialHost,
t: Translate = translate
): string {
if (material === 'off') {
return translate('settings.windowMaterial.off', 'Opaque')
return t('settings.windowMaterial.off', 'Opaque')
}
if (material === 'translucent') {
return translate('settings.windowMaterial.translucent', 'Translucent')
return t('settings.windowMaterial.translucent', 'Translucent')
}
return host.platform === 'win32'
? translate('settings.windowMaterial.acrylic', 'Acrylic blur')
: translate('settings.windowMaterial.blur', 'Blurred')
? t('settings.windowMaterial.acrylic', 'Acrylic blur')
: t('settings.windowMaterial.blur', 'Blurred')
}
function windowMaterialHint(settings: GlobalSettings, host: WindowMaterialHost): string {
const description = translate(
function windowMaterialHint(
settings: GlobalSettings,
host: WindowMaterialHost,
t: Translate = translate
): string {
const description = t(
'settings.windowMaterial.description',
'What shows behind translucent terminals. Requires restart.'
)
if (selectableWindowMaterials(host).length === 1) {
return translate(
return t(
'settings.windowMaterial.windowsUnsupported',
'Requires Windows 11 version 22H2 or later.'
)
@@ -49,19 +58,19 @@ function windowMaterialHint(settings: GlobalSettings, host: WindowMaterialHost):
return description
}
if (settings.reduceVisualEffects === true || readPrefersReducedTransparency()) {
return translate(
return t(
'settings.windowMaterial.reducedEffects',
'Off while Reduce Visual Effects or the system Reduce Transparency setting is on.'
)
}
if ((settings.terminalBackgroundOpacity ?? 1) >= 1) {
return translate(
return t(
'settings.windowMaterial.needsOpacity',
'Lower Background Opacity below 1 to see the desktop through terminals.'
)
}
return host.platform === 'linux'
? translate(
? t(
'settings.windowMaterial.linuxCompositor',
'Your compositor decides whether to blur it. Requires restart.'
)
@@ -69,6 +78,7 @@ function windowMaterialHint(settings: GlobalSettings, host: WindowMaterialHost):
}
function RestartBanner(): React.JSX.Element {
const t = useTranslate()
const [relaunching, setRelaunching] = useState(false)
const mountedRef = useMountedRef()
@@ -90,13 +100,10 @@ function RestartBanner(): React.JSX.Element {
<div className="flex items-center justify-between gap-3 rounded-md border border-yellow-500/50 bg-yellow-500/10 px-3 py-2.5">
<div className="min-w-0 flex-1 space-y-0.5">
<p className="text-sm font-medium text-yellow-700 dark:text-yellow-300">
{translate(
'auto.components.settings.TerminalWindowSection.c65bb9ce63',
'Restart required'
)}
{t('auto.components.settings.TerminalWindowSection.c65bb9ce63', 'Restart required')}
</p>
<p className="text-xs text-muted-foreground">
{translate(
{t(
'settings.windowMaterial.restartDescription',
'Restart Orca to apply the window material change.'
)}
@@ -111,8 +118,8 @@ function RestartBanner(): React.JSX.Element {
>
<RotateCw className={`size-3 ${relaunching ? 'animate-spin' : ''}`} />
{relaunching
? translate('auto.components.settings.TerminalWindowSection.907131d741', 'Restarting…')
: translate('auto.components.settings.TerminalWindowSection.8abdab9f7c', 'Restart now')}
? t('auto.components.settings.TerminalWindowSection.907131d741', 'Restarting…')
: t('auto.components.settings.TerminalWindowSection.8abdab9f7c', 'Restart now')}
</Button>
</div>
)
@@ -123,9 +130,10 @@ export function WindowMaterialSetting({
settings,
updateSettings
}: WindowMaterialSettingProps): React.JSX.Element {
const t = useTranslate()
const host = rendererWindowMaterialHost()
const selectable = selectableWindowMaterials(host)
const label = translate('settings.windowMaterial.title', 'Window Material')
const label = t('settings.windowMaterial.title', 'Window Material')
// Why compare resolved values: opacity and Reduce Visual Effects also decide the material.
const restartPending =
resolveWindowMaterial(settings, host, readPrefersReducedTransparency()) !==
@@ -135,7 +143,7 @@ export function WindowMaterialSetting({
<>
<SettingsRow
label={label}
description={windowMaterialHint(settings, host)}
description={windowMaterialHint(settings, host, t)}
control={
<Select
value={hostWindowMaterial(windowMaterialPreference(settings, host.platform), host)}
@@ -153,7 +161,7 @@ export function WindowMaterialSetting({
<SelectContent>
{selectable.map((material) => (
<SelectItem key={material} value={material}>
{windowMaterialLabel(material, host)}
{windowMaterialLabel(material, host, t)}
</SelectItem>
))}
</SelectContent>
@@ -5,31 +5,38 @@ import {
UI_LANGUAGE_CHOICES
} from '@/i18n/supported-languages'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
function resolveThemeSummary(theme: GlobalSettings['theme']): string {
function resolveThemeSummary(theme: GlobalSettings['theme'], t: Translate = translate): string {
if (theme === 'system') {
return translate('auto.components.settings.AppearancePane.fb0e0b4453', 'System')
return t('auto.components.settings.AppearancePane.fb0e0b4453', 'System')
}
if (theme === 'light') {
return translate('auto.components.settings.AppearancePane.fd89b5487c', 'Light')
return t('auto.components.settings.AppearancePane.fd89b5487c', 'Light')
}
return translate('auto.components.settings.AppearancePane.7d26ccabe8', 'Dark')
return t('auto.components.settings.AppearancePane.7d26ccabe8', 'Dark')
}
function resolveLanguageSummary(uiLanguage: GlobalSettings['uiLanguage']): string {
function resolveLanguageSummary(
uiLanguage: GlobalSettings['uiLanguage'],
t: Translate = translate
): string {
const choice = UI_LANGUAGE_CHOICES.find((entry) => entry.value === uiLanguage)
if (choice == null) {
return translate('settings.appearance.language.system', 'System')
return t('settings.appearance.language.system', 'System')
}
return getUiLanguageChoiceLabel(choice, translate)
}
export function resolveInterfaceSectionSummary(settings: GlobalSettings): string {
export function resolveInterfaceSectionSummary(
settings: GlobalSettings,
t: Translate = translate
): string {
const fontSummary =
settings.appFontFamily ||
translate('auto.components.settings.AppearancePane.interfaceDefaultFont', 'Default font')
t('auto.components.settings.AppearancePane.interfaceDefaultFont', 'Default font')
if (!SHOW_UI_LANGUAGE_SETTING) {
return `${resolveThemeSummary(settings.theme)} · ${fontSummary}`
return `${resolveThemeSummary(settings.theme, t)} · ${fontSummary}`
}
return `${resolveThemeSummary(settings.theme)} · ${resolveLanguageSummary(settings.uiLanguage)} · ${fontSummary}`
return `${resolveThemeSummary(settings.theme, t)} · ${resolveLanguageSummary(settings.uiLanguage, t)} · ${fontSummary}`
}
@@ -3,6 +3,7 @@ import { getTerminalAppearanceSearchEntries } from '../terminal/terminal-search'
import { getSidebarEntries } from './appearance-sidebar-search'
import { createLocalizedCatalog } from '@/i18n/localized-catalog'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { translateSearchKeyword } from '../search/settings-search-keywords'
import { SHOW_UI_LANGUAGE_SETTING } from '@/i18n/supported-languages'
import { getStatusBarToggles } from './appearance-status-bar-search'
@@ -144,25 +145,38 @@ export const getTypographyEntries = createLocalizedCatalog((): SettingsSearchEnt
}
])
export const getFollowSymlinkedDirectoriesEntry = (): SettingsSearchEntry => ({
title: translate('settings.followSymlinkedDirectories', 'Follow symlinked directories'),
description: translate(
export const getFollowSymlinkedDirectoriesEntry = (
t: Translate = translate
): SettingsSearchEntry => ({
title: t('settings.followSymlinkedDirectories', 'Follow symlinked directories'),
description: t(
'settings.followSymlinkedDirectoriesDescription',
'Search linked folders in Quick Open and expand local linked folders in File Explorer.'
),
keywords: [
...translateSearchKeyword('settings.followSymlinkedDirectoriesKeywords.symlink', 'symlink'),
...translateSearchKeyword(
'settings.followSymlinkedDirectoriesKeywords.symlink',
'symlink',
undefined,
t
),
...translateSearchKeyword(
'settings.followSymlinkedDirectoriesKeywords.linkedFolder',
'linked folder'
'linked folder',
undefined,
t
),
...translateSearchKeyword(
'settings.followSymlinkedDirectoriesKeywords.quickOpen',
'quick open'
'quick open',
undefined,
t
),
...translateSearchKeyword(
'settings.followSymlinkedDirectoriesKeywords.fileExplorer',
'file explorer'
'file explorer',
undefined,
t
)
]
})
@@ -5,7 +5,7 @@ import {
chatTerminalControlledTooltip,
getChatContrastEntriesByKey
} from './chat-appearance-search'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { Slider } from '../../../ui/slider'
import { SearchableSetting } from '../../section-layout/SearchableSetting'
import { SettingsDisabledControlTooltip } from '../../form-controls/SettingsDisabledControlTooltip'
@@ -22,6 +22,7 @@ export function AppearanceChatContrastControls({
onChange,
forceVisiblePrimary
}: ChatContrastControlsProps): React.JSX.Element {
const t = useTranslate()
const entries = getChatContrastEntriesByKey()
const { contrast, matchTerminalInterface: matching } = appearance
return (
@@ -45,7 +46,7 @@ export function AppearanceChatContrastControls({
>
<SettingsRow
label={entries.contrast.title}
description={matching ? chatTerminalControlledHint() : entries.contrast.description}
description={matching ? chatTerminalControlledHint(t) : entries.contrast.description}
control={
<ChatContrastSlider contrast={contrast} onChange={onChange} disabled={matching} />
}
@@ -63,6 +64,7 @@ function ChatContrastSlider({
contrast: number
disabled: boolean
}): React.JSX.Element {
const t = useTranslate()
const [draft, setDraft] = useState({ savedContrast: contrast, value: contrast })
// Keep the thumb mounted so committed keyboard changes retain focus.
if (draft.savedContrast !== contrast) {
@@ -71,10 +73,10 @@ function ChatContrastSlider({
return (
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">
{translate('settings.appearance.chat.softer', 'Softer')}
{t('settings.appearance.chat.softer', 'Softer')}
</span>
<SettingsDisabledControlTooltip
reason={disabled ? chatTerminalControlledTooltip() : undefined}
reason={disabled ? chatTerminalControlledTooltip(t) : undefined}
>
<div className="w-40">
<Slider
@@ -83,14 +85,14 @@ function ChatContrastSlider({
max={150}
step={1}
value={[draft.value]}
thumbLabels={[translate('settings.appearance.chat.contrast', 'Contrast')]}
thumbLabels={[t('settings.appearance.chat.contrast', 'Contrast')]}
onValueChange={([value]) => setDraft({ savedContrast: contrast, value })}
onValueCommit={([value]) => onChange({ contrast: value })}
/>
</div>
</SettingsDisabledControlTooltip>
<span className="text-xs text-muted-foreground">
{translate('settings.appearance.chat.sharper', 'Sharper')}
{t('settings.appearance.chat.sharper', 'Sharper')}
</span>
<span className="w-8 text-right text-xs text-muted-foreground tabular-nums">
{draft.value}
@@ -6,7 +6,7 @@ import {
} from '../../../../../../shared/native-chat/native-chat-appearance-settings'
import { useAppStore } from '../../../../store/index'
import { formatPrimaryShortcutLabel } from '@/hooks/useShortcutLabel'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { AppearanceChatContrastControls } from './AppearanceChatContrastControls'
import { ChatFontFamilySetting } from './ChatFontFamilySetting'
import { Button } from '../../../ui/button'
@@ -40,12 +40,13 @@ export function AppearanceChatSection({
onRequestFontSuggestions,
forceVisiblePrimary = false
}: AppearanceChatSectionProps): React.JSX.Element {
const t = useTranslate()
const appearance = resolveNativeChatAppearanceSettings(settings.nativeChatAppearance)
const matching = appearance.matchTerminalInterface
const keybindings = useAppStore((state) => state.keybindings)
const increase = formatPrimaryShortcutLabel('zoom.in', keybindings)
const decrease = formatPrimaryShortcutLabel('zoom.out', keybindings)
const entries = getChatAppearanceEntriesByKey({ increase, decrease })
const entries = getChatAppearanceEntriesByKey({ increase, decrease }, t)
const update = (updates: NativeChatAppearanceSettings): void => {
void writeNativeChatAppearance(
(current) => normalizeNativeChatAppearanceSettings({ ...current, ...updates }),
@@ -73,15 +74,15 @@ export function AppearanceChatSection({
>
<NumberField
label={entries.textSize.title}
description={matching ? chatTerminalControlledHint() : entries.textSize.description}
description={matching ? chatTerminalControlledHint(t) : entries.textSize.description}
value={appearance.fontSize}
defaultValue={matching ? undefined : 14}
disabled={matching}
disabledReason={chatTerminalControlledTooltip()}
disabledReason={chatTerminalControlledTooltip(t)}
min={12}
max={20}
integer
suffix={translate('settings.appearance.chat.pixels', 'px')}
suffix={t('settings.appearance.chat.pixels', 'px')}
onChange={(fontSize) => update({ fontSize })}
/>
</SearchableSetting>
@@ -92,15 +93,15 @@ export function AppearanceChatSection({
>
<NumberField
label={entries.codeTextSize.title}
description={matching ? chatTerminalControlledHint() : entries.codeTextSize.description}
description={matching ? chatTerminalControlledHint(t) : entries.codeTextSize.description}
value={appearance.codeFontSize}
defaultValue={matching ? undefined : 12}
disabled={matching}
disabledReason={chatTerminalControlledTooltip()}
disabledReason={chatTerminalControlledTooltip(t)}
min={10}
max={18}
integer
suffix={translate('settings.appearance.chat.pixels', 'px')}
suffix={t('settings.appearance.chat.pixels', 'px')}
onChange={(codeFontSize) => update({ codeFontSize })}
/>
</SearchableSetting>
@@ -116,7 +117,7 @@ export function AppearanceChatSection({
<SettingsSegmentedControl
value={appearance.width}
onChange={(width) => update({ width })}
options={getChatWidthOptions()}
options={getChatWidthOptions(t)}
ariaLabel={entries.width.title}
/>
}
@@ -138,7 +139,7 @@ export function AppearanceChatSection({
void writeNativeChatAppearance(resetNativeChatAppearanceSettings, updateSettings)
}
>
{translate('settings.appearance.chat.reset', 'Reset')}
{t('settings.appearance.chat.reset', 'Reset')}
</Button>
}
/>
@@ -1,7 +1,7 @@
import { useState } from 'react'
import type { GlobalSettings } from '../../../../../../shared/global-settings-types'
import { resolveNativeChatAppearanceSettings } from '../../../../../../shared/native-chat/native-chat-appearance-settings'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import {
NATIVE_CHAT_APPEARANCE_ROOT_CLASS,
useNativeChatAppearanceStyle
@@ -31,10 +31,11 @@ export function ChatFontFamilySetting({
onRequestFontSuggestions?: () => void
forceVisible?: boolean
}): React.JSX.Element {
const t = useTranslate()
const appearance = resolveNativeChatAppearanceSettings(settings.nativeChatAppearance)
const matching = appearance.matchTerminalInterface
const [previewFamily, setPreviewFamily] = useState<string | null>(null)
const entry = getChatAppearanceEntriesByKey().fontFamily
const entry = getChatAppearanceEntriesByKey(undefined, t).fontFamily
const style = useNativeChatAppearanceStyle({
...settings,
nativeChatAppearance: {
@@ -42,16 +43,16 @@ export function ChatFontFamilySetting({
fontFamily: previewFamily ?? appearance.fontFamily
}
})
const defaultLabel = translate('settings.appearance.chat.inheritFont', 'Same as IDE font')
const defaultLabel = t('settings.appearance.chat.inheritFont', 'Same as IDE font')
return (
<SearchableSetting id={entry.targetSectionId} {...entry} forceVisible={forceVisible}>
<SettingsRow
label={entry.title}
description={matching ? chatTerminalControlledHint() : entry.description}
description={matching ? chatTerminalControlledHint(t) : entry.description}
control={
<SettingsDisabledControlTooltip
reason={matching ? chatTerminalControlledTooltip() : undefined}
reason={matching ? chatTerminalControlledTooltip(t) : undefined}
>
<FontAutocomplete
value={appearance.fontFamily}
@@ -69,14 +70,11 @@ export function ChatFontFamilySetting({
}
/>
<p
aria-label={translate('settings.appearance.chat.fontPreviewLabel', 'Chat font preview')}
aria-label={t('settings.appearance.chat.fontPreviewLabel', 'Chat font preview')}
className={`${NATIVE_CHAT_APPEARANCE_ROOT_CLASS} mb-3 rounded-md border border-border px-3 py-2 text-sm text-chat-foreground`}
style={style}
>
{translate(
'settings.appearance.chat.fontPreview',
'The quick brown fox jumps over the lazy dog.'
)}{' '}
{t('settings.appearance.chat.fontPreview', 'The quick brown fox jumps over the lazy dog.')}{' '}
<code className="font-mono text-chat-code">fontFamily</code>
</p>
</SearchableSetting>
@@ -5,12 +5,12 @@ import { translateSearchKeyword } from '../../search/settings-search-keywords'
import type { SettingsSearchEntry } from '../../search/settings-search'
import { formatPrimaryShortcutLabel } from '@/hooks/useShortcutLabel'
export function chatTerminalControlledHint(): string {
return translate('settings.appearance.chat.terminalControlledHint', 'Set by terminal interface.')
export function chatTerminalControlledHint(t: Translate = translate): string {
return t('settings.appearance.chat.terminalControlledHint', 'Set by terminal interface.')
}
export function chatTerminalControlledTooltip(): string {
return translate(
export function chatTerminalControlledTooltip(t: Translate = translate): string {
return t(
'settings.appearance.chat.terminalControlledTooltip',
'Matching your terminal interface. Turn off Match terminal interface to change this.'
)
@@ -99,13 +99,16 @@ const getChatAppearanceCatalog = createLocalizedCatalog(
}) satisfies Record<string, SettingsSearchEntry>
)
export function getChatAppearanceEntriesByKey(shortcuts?: { increase: string; decrease: string }) {
export function getChatAppearanceEntriesByKey(
shortcuts?: { increase: string; decrease: string },
t: Translate = translate
) {
const entries = getChatAppearanceCatalog()
return {
...entries,
textSize: {
...entries.textSize,
description: translate(
description: t(
'settings.appearance.chat.textSizeDescription',
'Messages, tool activity and the message box. {{increase}} / {{decrease}} in a chat change this too.',
{
@@ -6,7 +6,7 @@ import { Popover, PopoverAnchor, PopoverContent } from '../../../ui/popover'
import { ScrollArea } from '../../../ui/scroll-area'
import { filterFontSuggestions, getRenderedFontSuggestions } from './font-suggestion-filter'
import { useInstalledFontsUnavailable } from './installed-font-list'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { buildAppFontFamily } from '@/lib/app-font-family'
// Why: these OS-shipped fonts draw pictographs or Greek for Latin letters, so their own name would be unreadable.
@@ -55,6 +55,7 @@ export function FontAutocomplete({
onRequestSuggestions,
onPreviewFontFamily
}: FontAutocompleteProps): React.JSX.Element {
const t = useTranslate()
const [query, setQuery] = useState(value)
const [prevValue, setPrevValue] = useState(value)
const [open, setOpen] = useState(false)
@@ -297,14 +298,11 @@ export function FontAutocomplete({
focusInput()
}}
className="rounded-sm p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
aria-label={translate(
aria-label={t(
'auto.components.settings.SettingsFormControls.a4ff6143f8',
'Clear font selection'
)}
title={translate(
'auto.components.settings.SettingsFormControls.74bcecd5ec',
'Clear'
)}
title={t('auto.components.settings.SettingsFormControls.74bcecd5ec', 'Clear')}
>
<CircleX className="size-3.5" />
</button>
@@ -325,14 +323,11 @@ export function FontAutocomplete({
}
}}
className="rounded-sm p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
aria-label={translate(
aria-label={t(
'auto.components.settings.SettingsFormControls.c766f8ac75',
'Toggle font suggestions'
)}
title={translate(
'auto.components.settings.SettingsFormControls.b55371ea18',
'Fonts'
)}
title={t('auto.components.settings.SettingsFormControls.b55371ea18', 'Fonts')}
>
<ChevronsUpDown className="size-3.5" />
</button>
@@ -390,7 +385,7 @@ export function FontAutocomplete({
))
) : (
<div className="px-3 py-3 text-sm text-muted-foreground">
{translate(
{t(
'auto.components.settings.SettingsFormControls.42a4d15a30',
'No matching fonts.'
)}
@@ -400,7 +395,7 @@ export function FontAutocomplete({
</ScrollArea>
{installedFontsUnavailable ? (
<p className="border-t border-border px-3 py-2 text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.FontAutocomplete.installedFontsUnavailable',
'Couldn’t list installed fonts. Type any installed font name to use it.'
)}
@@ -15,6 +15,7 @@ import {
} from '../../form-controls/SettingsSearchableSelect'
import { SettingsSegmentedControl } from '../../form-controls/SettingsFormControls'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { usePluginTerminalThemes, usePluginThemes } from '@/store/plugins/appearance/plugin-themes'
import { PluginThemeSwatch, ThemeColorSwatch } from '../../../plugins/page/PluginThemeSwatch'
import {
@@ -35,36 +36,36 @@ type BuiltInTheme = GlobalSettings['theme']
const BUILT_IN = 'built-in'
const FOLLOW_APP = 'follow-app'
function browseThemesLabel(): string {
return translate('auto.components.settings.AppearanceThemeControl.browseThemes', 'Browse themes')
function browseThemesLabel(t: Translate = translate): string {
return t('auto.components.settings.AppearanceThemeControl.browseThemes', 'Browse themes')
}
function builtInLabel(): string {
return translate(
function builtInLabel(t: Translate = translate): string {
return t(
'auto.components.settings.AppearanceInterfaceSection.pluginIconTheme.builtIn',
'Built-in'
)
}
function searchThemesLabel(): string {
return translate('auto.components.settings.AppearanceThemeControl.searchThemes', 'Search themes…')
function searchThemesLabel(t: Translate = translate): string {
return t('auto.components.settings.AppearanceThemeControl.searchThemes', 'Search themes…')
}
function noThemesLabel(): string {
return translate(
function noThemesLabel(t: Translate = translate): string {
return t(
'auto.components.settings.AppearanceThemeControl.noThemes',
'No themes match your search.'
)
}
/** Desktop only: the web client has no plugin catalog to open. */
function browseThemesActions(): SettingsSearchableSelectAction[] {
function browseThemesActions(t?: Translate): SettingsSearchableSelectAction[] {
return isWebClientLocation()
? []
: [
{
key: 'browse-themes',
label: browseThemesLabel(),
label: browseThemesLabel(t),
icon: <Store className="size-3.5" />,
onSelect: () => requestPluginBrowse('themes')
}
@@ -74,9 +75,10 @@ function browseThemesActions(): SettingsSearchableSelectAction[] {
/** `labels` holds every theme the picker lists, so two sharing a name show their publishers. */
function pluginThemeGroups(
themes: readonly PluginThemeRegistration[],
labels: ReadonlyMap<string, string> = themeOptionLabels(themes)
labels: ReadonlyMap<string, string> = themeOptionLabels(themes),
t?: Translate
): SettingsSearchableSelectGroup[] {
const pluginNames = disambiguatedContentPluginNames(themes)
const pluginNames = disambiguatedContentPluginNames(themes, t)
return [...Map.groupBy(themes, (theme) => theme.pluginKey).values()].map((group) => ({
key: group[0].pluginKey,
heading: pluginNames.get(group[0].pluginKey) ?? group[0].pluginName,
@@ -91,16 +93,21 @@ function pluginThemeGroups(
function themeOptionLabels(
themes: readonly PluginThemeRegistration[],
catalogThemes: readonly CatalogAppTheme[] = []
catalogThemes: readonly CatalogAppTheme[] = [],
t?: Translate
): ReadonlyMap<string, string> {
return disambiguatedPluginContentLabels([
...themes,
...catalogThemes.map((theme) => ({
id: theme.value,
pluginKey: theme.listing.pluginKey,
label: theme.label
}))
])
return disambiguatedPluginContentLabels(
[
...themes,
...catalogThemes.map((theme) => ({
id: theme.value,
pluginKey: theme.listing.pluginKey,
label: theme.label
}))
],
undefined,
t
)
}
/** System, Dark or Light; the Light theme and Dark theme slots pick the colors for each. */
@@ -115,13 +122,14 @@ export function AppearanceThemeControl({
updateSettings: (updates: Partial<GlobalSettings>) => void
applyTheme: (theme: BuiltInTheme) => void
}): React.JSX.Element {
const t = useTranslate()
return (
<div className="flex items-center gap-2">
{/* Why desktop only: the web client has no plugin catalog to open. */}
{isWebClientLocation() ? null : (
<Button variant="outline" size="sm" onClick={() => requestPluginBrowse('themes')}>
<Store />
{browseThemesLabel()}
{browseThemesLabel(t)}
</Button>
)}
<SettingsSegmentedControl
@@ -134,15 +142,15 @@ export function AppearanceThemeControl({
options={[
{
value: 'system',
label: translate('auto.components.settings.AppearancePane.fb0e0b4453', 'System')
label: t('auto.components.settings.AppearancePane.fb0e0b4453', 'System')
},
{
value: 'dark',
label: translate('auto.components.settings.AppearancePane.7d26ccabe8', 'Dark')
label: t('auto.components.settings.AppearancePane.7d26ccabe8', 'Dark')
},
{
value: 'light',
label: translate('auto.components.settings.AppearancePane.fd89b5487c', 'Light')
label: t('auto.components.settings.AppearancePane.fd89b5487c', 'Light')
}
]}
/>
@@ -163,6 +171,7 @@ export function AppearanceThemeSlotSelect({
settings: GlobalSettings
updateSettings: (updates: Partial<GlobalSettings>) => void
}): React.JSX.Element {
const t = useTranslate()
const allThemes = usePluginThemes()
const themes = allThemes.filter((theme) => theme.base === base)
const catalog = useCatalogAppThemes(base, allThemes, settings.pluginSystemEnabled === true)
@@ -179,14 +188,14 @@ export function AppearanceThemeSlotSelect({
: undefined
const terminalInUse =
terminal !== undefined && isPluginTerminalThemeSelected(settings, terminal, base === 'dark')
const labels = themeOptionLabels(themes, catalog.themes)
const labels = themeOptionLabels(themes, catalog.themes, t)
return (
<div className="flex flex-col items-end gap-1">
<SettingsSearchableSelect
ariaLabel={label}
value={value}
searchPlaceholder={searchThemesLabel()}
emptyText={noThemesLabel()}
searchPlaceholder={searchThemesLabel(t)}
emptyText={noThemesLabel(t)}
onOpenChange={(open) => {
if (open) {
catalog.open()
@@ -209,32 +218,26 @@ export function AppearanceThemeSlotSelect({
{
key: BUILT_IN,
options: [
{ value: BUILT_IN, label: builtInLabel() },
{ value: BUILT_IN, label: builtInLabel(t) },
{
value: MATCH_TERMINAL_APP_THEME,
label: translate(
label: t(
'auto.components.settings.AppearanceThemeControl.matchTerminal',
'Match Terminal'
),
detail: (
<>
<Zap className="mr-1 size-3" aria-hidden />
{translate(
'auto.components.settings.AppearanceThemeControl.dynamic',
'dynamic'
)}
{t('auto.components.settings.AppearanceThemeControl.dynamic', 'dynamic')}
</>
)
}
]
},
...pluginThemeGroups(themes, labels),
...pluginThemeGroups(themes, labels, t),
{
key: 'catalog',
heading: translate(
'auto.components.settings.AppearanceThemeControl.moreThemes',
'More themes'
),
heading: t('auto.components.settings.AppearanceThemeControl.moreThemes', 'More themes'),
options: catalog.themes.map((theme) => ({
value: theme.value,
label: labels.get(theme.value) ?? theme.label,
@@ -248,19 +251,19 @@ export function AppearanceThemeSlotSelect({
catalog.loading ? (
<div className="flex items-center gap-2 px-3 py-1.5 text-xs text-muted-foreground">
<Loader2 className="size-3.5 animate-spin motion-reduce:animate-none" />
{translate(
{t(
'auto.components.settings.AppearanceThemeControl.moreThemesLoading',
'Loading themes…'
)}
</div>
) : null
}
actions={browseThemesActions()}
actions={browseThemesActions(t)}
/>
{catalog.installing ? (
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Loader2 className="size-3.5 animate-spin motion-reduce:animate-none" />
{translate(
{t(
'auto.components.settings.AppearanceThemeControl.installingTheme',
'Installing {{value0}}…',
{ value0: labels.get(catalog.installing.value) ?? catalog.installing.label }
@@ -282,7 +285,7 @@ export function AppearanceThemeSlotSelect({
)
}
>
{translate(
{t(
'auto.components.settings.AppearanceThemeControl.useTerminalTheme',
'Use its terminal colors'
)}
@@ -304,6 +307,7 @@ export function CodeEditorThemeSelect({
settings: GlobalSettings
updateSettings: (updates: Partial<GlobalSettings>) => void
}): React.JSX.Element {
const t = useTranslate()
const themes = usePluginThemes()
const selected = findCodeEditorTheme(
base === 'dark' ? settings.editorThemeDark : settings.editorThemeLight,
@@ -313,8 +317,8 @@ export function CodeEditorThemeSelect({
<SettingsSearchableSelect
ariaLabel={label}
value={selected === null ? FOLLOW_APP : typeof selected === 'string' ? selected : selected.id}
searchPlaceholder={searchThemesLabel()}
emptyText={noThemesLabel()}
searchPlaceholder={searchThemesLabel(t)}
emptyText={noThemesLabel(t)}
onValueChange={(value) =>
updateSettings(
editorThemeSlotUpdate(
@@ -331,25 +335,25 @@ export function CodeEditorThemeSelect({
options: [
{
value: FOLLOW_APP,
label: translate('settings.appearance.editorTheme.followApp', 'Same as app theme')
label: t('settings.appearance.editorTheme.followApp', 'Same as app theme')
}
]
},
{
key: BUILT_IN,
heading: builtInLabel(),
heading: builtInLabel(t),
options: [
{
value: 'light',
label: translate('auto.components.settings.AppearancePane.fd89b5487c', 'Light')
label: t('auto.components.settings.AppearancePane.fd89b5487c', 'Light')
},
{
value: 'dark',
label: translate('auto.components.settings.AppearancePane.7d26ccabe8', 'Dark')
label: t('auto.components.settings.AppearancePane.7d26ccabe8', 'Dark')
}
]
},
...pluginThemeGroups(themes)
...pluginThemeGroups(themes, undefined, t)
]}
/>
)
@@ -7,6 +7,7 @@ import { buildPluginListEntries, needsNewerOrca } from '../../../plugins/list/pl
import { applyPluginAppThemeToSlot } from '../../../plugins/state/plugin-theme-apply'
import { createPluginPagePreviewCache } from '../../../plugins/state/use-plugin-catalog'
import { usePluginInstallAction } from '../../../plugins/state/use-plugin-install-action'
import type { Translate } from '../../../../i18n/use-translate'
type CatalogSnapshot = {
installed: readonly PluginHostListEntry[]
@@ -28,9 +29,10 @@ export type CatalogAppTheme = {
export function catalogAppThemes(
snapshot: CatalogSnapshot,
base: 'light' | 'dark',
installedPluginKeys: ReadonlySet<string>
installedPluginKeys: ReadonlySet<string>,
t?: Translate
): CatalogAppTheme[] {
return buildPluginListEntries(snapshot.installed, snapshot.listings)
return buildPluginListEntries(snapshot.installed, snapshot.listings, undefined, t)
.available.flatMap((entry) => {
const { listing } = entry
const release = listing?.release
@@ -1,21 +1,24 @@
import { getUiLanguageChoiceLabel, UI_LANGUAGE_CHOICES } from '@/i18n/supported-languages'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { usePluginIconThemes } from '@/store/plugins/appearance/plugin-icon-themes'
import { usePluginLanguagePacks } from '@/store/plugins/localization/plugin-language-packs'
import { disambiguatedPluginContentLabels } from '../../plugins/presentation/plugin-name-disambiguation'
/** Icon themes with their picker labels; a plugin one sharing a name shows its publisher. */
export function useIconThemePickerLabels() {
const t = useTranslate()
const themes = usePluginIconThemes()
const builtIn = translate(
const builtIn = t(
'auto.components.settings.AppearanceInterfaceSection.pluginIconTheme.builtIn',
'Built-in'
)
return { themes, builtIn, labels: disambiguatedPluginContentLabels(themes, [builtIn]) }
return { themes, builtIn, labels: disambiguatedPluginContentLabels(themes, [builtIn], t) }
}
/** Orca's languages and plugin language packs, labelled as the language picker lists them. */
export function useUiLanguagePickerLabels() {
const t = useTranslate()
const packs = usePluginLanguagePacks()
const choices = UI_LANGUAGE_CHOICES.map((choice) => getUiLanguageChoiceLabel(choice, translate))
const packLabels = disambiguatedPluginContentLabels(
@@ -23,7 +26,8 @@ export function useUiLanguagePickerLabels() {
...pack,
label: pack.displayName ?? `${pack.locale} — ${pack.pluginKey}`
})),
choices
choices,
t
)
return { packs, choices, packLabels }
}
@@ -1,5 +1,6 @@
import type { GlobalSettings } from '../../../../../shared/global-settings-types'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { Label } from '../../ui/label'
import { SearchableSetting } from '../section-layout/SearchableSetting'
import { SettingsSegmentedControl } from '../form-controls/SettingsFormControls'
@@ -14,15 +15,15 @@ const KEYWORDS = [
'hunk'
]
function getTitle(): string {
return translate(
function getTitle(t: Translate = translate): string {
return t(
'auto.components.settings.GeneralEditorSettingsSection.collapseUnchangedTitle',
'Collapse Unchanged Regions'
)
}
function getDescription(): string {
return translate(
function getDescription(t: Translate = translate): string {
return t(
'auto.components.settings.GeneralEditorSettingsSection.collapseUnchangedDescription',
'Show only changed lines and a little surrounding context in a file diff, hiding the rest behind expandable bands. The View All Changes diff always collapses this way.'
)
@@ -35,8 +36,9 @@ export function CollapseUnchangedRegionsSetting({
settings: GlobalSettings
updateSettings: (updates: Partial<GlobalSettings>) => void | Promise<void>
}): React.JSX.Element {
const title = getTitle()
const description = getDescription()
const t = useTranslate()
const title = getTitle(t)
const description = getDescription(t)
return (
<SearchableSetting
@@ -58,17 +60,11 @@ export function CollapseUnchangedRegionsSetting({
options={[
{
value: 'off',
label: translate(
'auto.components.settings.GeneralEditorSettingsSection.bf16ef0af2',
'Off'
)
label: t('auto.components.settings.GeneralEditorSettingsSection.bf16ef0af2', 'Off')
},
{
value: 'on',
label: translate(
'auto.components.settings.GeneralEditorSettingsSection.3f6892f307',
'On'
)
label: t('auto.components.settings.GeneralEditorSettingsSection.3f6892f307', 'On')
}
]}
/>
@@ -1,5 +1,5 @@
import type { GlobalSettings } from '../../../../../shared/global-settings-types'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { SearchableSetting } from '../section-layout/SearchableSetting'
import { Label } from '../../ui/label'
import { SettingsSegmentedControl } from '../form-controls/SettingsFormControls'
@@ -13,13 +13,14 @@ export function DiffShowWhitespaceSetting({
settings,
updateSettings
}: DiffShowWhitespaceSettingProps): React.JSX.Element {
const t = useTranslate()
return (
<SearchableSetting
title={translate(
title={t(
'auto.components.settings.GeneralEditorSettingsSection.f1b3ceeb98',
'Diff Show Whitespace'
)}
description={translate(
description={t(
'auto.components.settings.GeneralEditorSettingsSection.94a479cef3',
'Show leading and trailing whitespace differences in diffs.'
)}
@@ -28,20 +29,20 @@ export function DiffShowWhitespaceSetting({
>
<div className="min-w-0 flex-1 space-y-0.5">
<Label>
{translate(
{t(
'auto.components.settings.GeneralEditorSettingsSection.f1b3ceeb98',
'Diff Show Whitespace'
)}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.GeneralEditorSettingsSection.94a479cef3',
'Show leading and trailing whitespace differences in diffs.'
)}
</p>
</div>
<SettingsSegmentedControl
ariaLabel={translate(
ariaLabel={t(
'auto.components.settings.GeneralEditorSettingsSection.f1b3ceeb98',
'Diff Show Whitespace'
)}
@@ -50,17 +51,11 @@ export function DiffShowWhitespaceSetting({
options={[
{
value: 'off',
label: translate(
'auto.components.settings.GeneralEditorSettingsSection.bf16ef0af2',
'Off'
)
label: t('auto.components.settings.GeneralEditorSettingsSection.bf16ef0af2', 'Off')
},
{
value: 'on',
label: translate(
'auto.components.settings.GeneralEditorSettingsSection.3f6892f307',
'On'
)
label: t('auto.components.settings.GeneralEditorSettingsSection.3f6892f307', 'On')
}
]}
/>
@@ -1,6 +1,6 @@
import type React from 'react'
import type { GlobalSettings } from '../../../../../shared/global-settings-types'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { SearchableSetting } from '../section-layout/SearchableSetting'
import { FontAutocomplete } from '../appearance/fonts/FontAutocomplete'
import { SettingsRow } from '../form-controls/SettingsFormControls'
@@ -18,24 +18,19 @@ export function EditorFontFamilySetting({
fontSuggestions,
onRequestFontSuggestions
}: EditorFontFamilySettingProps): React.JSX.Element {
const t = useTranslate()
return (
<SearchableSetting
title={translate(
'auto.components.settings.EditorFontFamilySetting.title',
'Editor Font Family'
)}
description={translate(
title={t('auto.components.settings.EditorFontFamilySetting.title', 'Editor Font Family')}
description={t(
'auto.components.settings.EditorFontFamilySetting.description',
'Font used by file editors and diff views, and by Markdown code when set. Leave empty to follow the terminal font.'
)}
keywords={['editor', 'font', 'typography', 'family', 'code', 'cjk']}
>
<SettingsRow
label={translate(
'auto.components.settings.EditorFontFamilySetting.title',
'Editor Font Family'
)}
description={translate(
label={t('auto.components.settings.EditorFontFamilySetting.title', 'Editor Font Family')}
description={t(
'auto.components.settings.EditorFontFamilySetting.description',
'Font used by file editors and diff views, and by Markdown code when set. Leave empty to follow the terminal font.'
)}
@@ -44,7 +39,7 @@ export function EditorFontFamilySetting({
value={settings.editorFontFamily ?? ''}
suggestions={fontSuggestions}
onRequestSuggestions={onRequestFontSuggestions}
placeholder={translate(
placeholder={t(
'auto.components.settings.EditorFontFamilySetting.placeholder',
'Same as terminal font'
)}
@@ -1,20 +1,18 @@
import type { GlobalSettings } from '../../../../../shared/global-settings-types'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { SearchableSetting } from '../section-layout/SearchableSetting'
import { SettingsSwitchRow } from '../form-controls/SettingsFormControls'
export function getFormatOnSaveSearchEntry() {
export function getFormatOnSaveSearchEntry(t: Translate = translate) {
return {
title: translate('settings.editorFormatOnSave.title', 'Format on Save'),
description: translate(
title: t('settings.editorFormatOnSave.title', 'Format on Save'),
description: t(
'settings.editorFormatOnSave.description',
'Format text before manual saves and auto save. Configure a formatter command in Settings > Projects. Off by default.'
),
keywords: [
translate(
'settings.editorFormatOnSave.keywords',
'formatter format on save prettier biome gofmt'
)
t('settings.editorFormatOnSave.keywords', 'formatter format on save prettier biome gofmt')
]
}
}
@@ -26,7 +24,8 @@ export function EditorFormatOnSaveSetting({
settings: GlobalSettings
updateSettings: (updates: Partial<GlobalSettings>) => void
}): React.JSX.Element {
const entry = getFormatOnSaveSearchEntry()
const t = useTranslate()
const entry = getFormatOnSaveSearchEntry(t)
return (
<SearchableSetting {...entry}>
<SettingsSwitchRow
@@ -1,24 +1,31 @@
import type { GlobalSettings } from '../../../../../shared/global-settings-types'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { SearchableSetting } from '../section-layout/SearchableSetting'
import { SettingsSwitchRow } from '../form-controls/SettingsFormControls'
import { translateSearchKeyword } from '../search/settings-search-keywords'
export function getInsertFinalNewlineSearchEntry() {
export function getInsertFinalNewlineSearchEntry(t: Translate = translate) {
return {
title: translate('settings.editorInsertFinalNewline.title', 'Insert Final Newline'),
description: translate(
title: t('settings.editorInsertFinalNewline.title', 'Insert Final Newline'),
description: t(
'settings.editorInsertFinalNewline.description',
'Add a line break when saving a non-empty code file that does not end with one. Off by default.'
),
keywords: [
...translateSearchKeyword('settings.editorInsertFinalNewline.keywordNewline', 'newline', {
aliases: ['final newline', 'trailing newline']
}),
...translateSearchKeyword(
'settings.editorInsertFinalNewline.keywordNewline',
'newline',
{
aliases: ['final newline', 'trailing newline']
},
t
),
...translateSearchKeyword(
'settings.editorInsertFinalNewline.keywordEndOfFile',
'end of file',
{ aliases: ['eof', 'save'] }
{ aliases: ['eof', 'save'] },
t
)
]
}
@@ -31,7 +38,8 @@ export function EditorInsertFinalNewlineSetting({
settings: GlobalSettings
updateSettings: (updates: Partial<GlobalSettings>) => void
}): React.JSX.Element {
const entry = getInsertFinalNewlineSearchEntry()
const t = useTranslate()
const entry = getInsertFinalNewlineSearchEntry(t)
return (
<SearchableSetting {...entry}>
<SettingsSwitchRow
@@ -15,7 +15,7 @@ import {
type EditorLigaturesMode
} from '@/lib/editor-typography'
import { supportsCodeFontSmoothing, type CodeFontSmoothing } from '@/lib/code-font-smoothing'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { SearchableSetting } from '../section-layout/SearchableSetting'
import {
NumberField,
@@ -34,6 +34,7 @@ export function EditorTypographySettings({
settings,
updateSettings
}: EditorTypographySettingsProps): React.JSX.Element {
const t = useTranslate()
const entries = getEditorTypographySearchEntries()
const editorFontFamily = resolveEditorFontFamily(settings)
const ligatures = resolveEditorLigaturesMode(settings)
@@ -50,7 +51,7 @@ export function EditorTypographySettings({
integer
// Shows the terminal size an empty field follows.
placeholder={String(settings.terminalFontSize)}
suffix={translate('auto.components.settings.editorTypography.fontSizeUnit', 'px')}
suffix={t('auto.components.settings.editorTypography.fontSizeUnit', 'px')}
onChange={(value) => updateSettings({ editorFontSize: value })}
onClear={() => updateSettings({ editorFontSize: undefined })}
/>
@@ -80,10 +81,7 @@ export function EditorTypographySettings({
min={0}
step={0.1}
clearOnZero
placeholder={translate(
'auto.components.settings.editorTypography.lineHeightAuto',
'Auto'
)}
placeholder={t('auto.components.settings.editorTypography.lineHeightAuto', 'Auto')}
onChange={(value) => updateSettings({ editorLineHeight: value })}
onClear={() => updateSettings({ editorLineHeight: undefined })}
/>
@@ -95,12 +93,12 @@ export function EditorTypographySettings({
description={
ligatures === 'auto'
? fontFamilyHasKnownLigatures(editorFontFamily)
? translate(
? t(
'auto.components.settings.editorTypography.ligaturesAutoEnabled',
'Auto - enabled for "{{value0}}".',
{ value0: editorFontFamily }
)
: translate(
: t(
'auto.components.settings.editorTypography.ligaturesAutoDisabled',
'Auto - disabled for "{{value0}}".',
{ value0: editorFontFamily }
@@ -115,18 +113,15 @@ export function EditorTypographySettings({
options={[
{
value: 'off',
label: translate('auto.components.settings.editorTypography.ligaturesOff', 'Off')
label: t('auto.components.settings.editorTypography.ligaturesOff', 'Off')
},
{
value: 'auto',
label: translate(
'auto.components.settings.editorTypography.ligaturesAuto',
'Auto'
)
label: t('auto.components.settings.editorTypography.ligaturesAuto', 'Auto')
},
{
value: 'on',
label: translate('auto.components.settings.editorTypography.ligaturesOn', 'On')
label: t('auto.components.settings.editorTypography.ligaturesOn', 'On')
}
]}
/>
@@ -147,14 +142,14 @@ export function EditorTypographySettings({
options={[
{
value: 'grayscale',
label: translate(
label: t(
'auto.components.settings.editorTypography.fontSmoothingGrayscale',
'Grayscale'
)
},
{
value: 'native',
label: translate(
label: t(
'auto.components.settings.editorTypography.fontSmoothingNative',
'Native'
)
@@ -1,5 +1,5 @@
import type { GlobalSettings } from '../../../../../shared/global-settings-types'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { SearchableSetting } from '../section-layout/SearchableSetting'
import { Label } from '../../ui/label'
import { SettingsSegmentedControl } from '../form-controls/SettingsFormControls'
@@ -13,13 +13,14 @@ export function EditorWordWrapSetting({
settings,
updateSettings
}: EditorWordWrapSettingProps): React.JSX.Element {
const t = useTranslate()
return (
<SearchableSetting
title={translate(
title={t(
'auto.components.settings.GeneralEditorSettingsSection.7ddd66fede',
'Editor Word Wrap'
)}
description={translate(
description={t(
'auto.components.settings.GeneralEditorSettingsSection.9b18de6eea',
'Wrap long lines in file editors instead of requiring horizontal scrolling.'
)}
@@ -28,20 +29,20 @@ export function EditorWordWrapSetting({
>
<div className="min-w-0 flex-1 space-y-0.5">
<Label>
{translate(
{t(
'auto.components.settings.GeneralEditorSettingsSection.7ddd66fede',
'Editor Word Wrap'
)}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.GeneralEditorSettingsSection.9b18de6eea',
'Wrap long lines in file editors instead of requiring horizontal scrolling.'
)}
</p>
</div>
<SettingsSegmentedControl
ariaLabel={translate(
ariaLabel={t(
'auto.components.settings.GeneralEditorSettingsSection.7ddd66fede',
'Editor Word Wrap'
)}
@@ -50,17 +51,11 @@ export function EditorWordWrapSetting({
options={[
{
value: 'off',
label: translate(
'auto.components.settings.GeneralEditorSettingsSection.bf16ef0af2',
'Off'
)
label: t('auto.components.settings.GeneralEditorSettingsSection.bf16ef0af2', 'Off')
},
{
value: 'on',
label: translate(
'auto.components.settings.GeneralEditorSettingsSection.3f6892f307',
'On'
)
label: t('auto.components.settings.GeneralEditorSettingsSection.3f6892f307', 'On')
}
]}
/>
@@ -12,7 +12,7 @@ import { Label } from '../../ui/label'
import { SearchableSetting } from '../section-layout/SearchableSetting'
import { CollapseUnchangedRegionsSetting } from './CollapseUnchangedRegionsSetting'
import { SettingsSegmentedControl, SettingsSwitchRow } from '../form-controls/SettingsFormControls'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { RichMarkdownSpellcheckSetting } from './RichMarkdownSpellcheckSetting'
import { MarkdownDefaultViewSetting } from './MarkdownDefaultViewSetting'
import { DiffShowWhitespaceSetting } from './DiffShowWhitespaceSetting'
@@ -41,6 +41,7 @@ export function GeneralEditorSettingsSection({
fontSuggestions,
onRequestFontSuggestions
}: GeneralEditorSettingsSectionProps): React.JSX.Element {
const t = useTranslate()
const [autoSaveDelayDraftState, setAutoSaveDelayDraftState] = useState(() =>
createAutoSaveDelayDraftState(settings.editorAutoSaveDelayMs)
)
@@ -88,11 +89,8 @@ export function GeneralEditorSettingsSection({
return (
<SettingsGroup
key="editor"
title={translate(
'auto.components.settings.GeneralEditorSettingsSection.45c6e85c4d',
'Editor'
)}
description={translate(
title={t('auto.components.settings.GeneralEditorSettingsSection.45c6e85c4d', 'Editor')}
description={t(
'auto.components.settings.GeneralEditorSettingsSection.d21136d9ef',
'Configure how Orca persists file edits.'
)}
@@ -101,22 +99,22 @@ export function GeneralEditorSettingsSection({
<EditorInsertFinalNewlineSetting settings={settings} updateSettings={updateSettings} />
<SearchableSetting
title={translate(
title={t(
'auto.components.settings.GeneralEditorSettingsSection.0df2e4fd12',
'Auto Save Files'
)}
description={translate(
description={t(
'auto.components.settings.GeneralEditorSettingsSection.70bb30feb1',
'Save editor and editable diff changes automatically after a short pause.'
)}
keywords={['autosave', 'save']}
>
<SettingsSwitchRow
label={translate(
label={t(
'auto.components.settings.GeneralEditorSettingsSection.0df2e4fd12',
'Auto Save Files'
)}
description={translate(
description={t(
'auto.components.settings.GeneralEditorSettingsSection.70bb30feb1',
'Save editor and editable diff changes automatically after a short pause.'
)}
@@ -126,11 +124,11 @@ export function GeneralEditorSettingsSection({
</SearchableSetting>
<SearchableSetting
title={translate(
title={t(
'auto.components.settings.GeneralEditorSettingsSection.d6cf227ca0',
'Auto Save Delay'
)}
description={translate(
description={t(
'auto.components.settings.GeneralEditorSettingsSection.1bec6d8318',
'How long Orca waits after your last edit before saving automatically.'
)}
@@ -139,24 +137,24 @@ export function GeneralEditorSettingsSection({
>
<div className="min-w-0 flex-1 space-y-0.5">
<Label>
{translate(
{t(
'auto.components.settings.GeneralEditorSettingsSection.d6cf227ca0',
'Auto Save Delay'
)}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.GeneralEditorSettingsSection.8112cd6dcf',
'How long Orca waits after your last edit before saving automatically. First launch defaults to'
)}{' '}
{DEFAULT_EDITOR_AUTO_SAVE_DELAY_MS}{' '}
{translate('auto.components.settings.GeneralEditorSettingsSection.fc5c5306ff', 'ms.')}
{t('auto.components.settings.GeneralEditorSettingsSection.fc5c5306ff', 'ms.')}
</p>
</div>
<div className="flex shrink-0 items-center gap-2">
<Input
type="number"
aria-label={translate(
aria-label={t(
'auto.components.settings.GeneralEditorSettingsSection.d6cf227ca0',
'Auto Save Delay'
)}
@@ -174,17 +172,17 @@ export function GeneralEditorSettingsSection({
className="number-input-clean w-28 text-right tabular-nums"
/>
<span className="text-xs text-muted-foreground">
{translate('auto.components.settings.GeneralEditorSettingsSection.a5db1d3975', 'ms')}
{t('auto.components.settings.GeneralEditorSettingsSection.a5db1d3975', 'ms')}
</span>
</div>
</SearchableSetting>
<SearchableSetting
title={translate(
title={t(
'auto.components.settings.GeneralEditorSettingsSection.7311f67ee7',
'Default Diff View'
)}
description={translate(
description={t(
'auto.components.settings.GeneralEditorSettingsSection.b492397d34',
'Preferred presentation format for showing git diffs by default.'
)}
@@ -193,20 +191,20 @@ export function GeneralEditorSettingsSection({
>
<div className="min-w-0 flex-1 space-y-0.5">
<Label>
{translate(
{t(
'auto.components.settings.GeneralEditorSettingsSection.7311f67ee7',
'Default Diff View'
)}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.GeneralEditorSettingsSection.b492397d34',
'Preferred presentation format for showing git diffs by default.'
)}
</p>
</div>
<SettingsSegmentedControl
ariaLabel={translate(
ariaLabel={t(
'auto.components.settings.GeneralEditorSettingsSection.7311f67ee7',
'Default Diff View'
)}
@@ -215,14 +213,11 @@ export function GeneralEditorSettingsSection({
options={[
{
value: 'inline',
label: translate(
'auto.components.settings.GeneralEditorSettingsSection.05b6df93b3',
'Inline'
)
label: t('auto.components.settings.GeneralEditorSettingsSection.05b6df93b3', 'Inline')
},
{
value: 'side-by-side',
label: translate(
label: t(
'auto.components.settings.GeneralEditorSettingsSection.12cbc0d0d6',
'Side-by-side'
)
@@ -247,11 +242,11 @@ export function GeneralEditorSettingsSection({
<CollapseUnchangedRegionsSetting settings={settings} updateSettings={updateSettings} />
<SearchableSetting
title={translate(
title={t(
'auto.components.settings.GeneralEditorSettingsSection.8f1afdfbd8',
'Diff Word Wrap'
)}
description={translate(
description={t(
'auto.components.settings.GeneralEditorSettingsSection.4aa4d9fb73',
'Wrap long lines in diff editors instead of requiring horizontal scrolling.'
)}
@@ -260,20 +255,20 @@ export function GeneralEditorSettingsSection({
>
<div className="min-w-0 flex-1 space-y-0.5">
<Label>
{translate(
{t(
'auto.components.settings.GeneralEditorSettingsSection.8f1afdfbd8',
'Diff Word Wrap'
)}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.GeneralEditorSettingsSection.4aa4d9fb73',
'Wrap long lines in diff editors instead of requiring horizontal scrolling.'
)}
</p>
</div>
<SettingsSegmentedControl
ariaLabel={translate(
ariaLabel={t(
'auto.components.settings.GeneralEditorSettingsSection.8f1afdfbd8',
'Diff Word Wrap'
)}
@@ -282,28 +277,22 @@ export function GeneralEditorSettingsSection({
options={[
{
value: 'off',
label: translate(
'auto.components.settings.GeneralEditorSettingsSection.bf16ef0af2',
'Off'
)
label: t('auto.components.settings.GeneralEditorSettingsSection.bf16ef0af2', 'Off')
},
{
value: 'on',
label: translate(
'auto.components.settings.GeneralEditorSettingsSection.3f6892f307',
'On'
)
label: t('auto.components.settings.GeneralEditorSettingsSection.3f6892f307', 'On')
}
]}
/>
</SearchableSetting>
<SearchableSetting
title={translate(
title={t(
'auto.components.settings.GeneralEditorSettingsSection.1de48ad940',
'Default Diff File Tree'
)}
description={translate(
description={t(
'auto.components.settings.GeneralEditorSettingsSection.1b87897af9',
'Show or hide the file tree when opening combined diff views.'
)}
@@ -312,20 +301,20 @@ export function GeneralEditorSettingsSection({
>
<div className="min-w-0 flex-1 space-y-0.5">
<Label>
{translate(
{t(
'auto.components.settings.GeneralEditorSettingsSection.1de48ad940',
'Default Diff File Tree'
)}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.GeneralEditorSettingsSection.1b87897af9',
'Show or hide the file tree when opening combined diff views.'
)}
</p>
</div>
<SettingsSegmentedControl
ariaLabel={translate(
ariaLabel={t(
'auto.components.settings.GeneralEditorSettingsSection.1de48ad940',
'Default Diff File Tree'
)}
@@ -336,39 +325,27 @@ export function GeneralEditorSettingsSection({
options={[
{
value: 'shown',
label: translate(
'auto.components.settings.GeneralEditorSettingsSection.73a09aad63',
'Shown'
)
label: t('auto.components.settings.GeneralEditorSettingsSection.73a09aad63', 'Shown')
},
{
value: 'hidden',
label: translate(
'auto.components.settings.GeneralEditorSettingsSection.5a1ea6eaa2',
'Hidden'
)
label: t('auto.components.settings.GeneralEditorSettingsSection.5a1ea6eaa2', 'Hidden')
}
]}
/>
</SearchableSetting>
<SearchableSetting
title={translate(
'auto.components.settings.GeneralEditorSettingsSection.6690b1ffb9',
'Minimap'
)}
description={translate(
title={t('auto.components.settings.GeneralEditorSettingsSection.6690b1ffb9', 'Minimap')}
description={t(
'auto.components.settings.GeneralEditorSettingsSection.51161d1647',
'Show the minimap overview when editing a file.'
)}
keywords={['minimap', 'overview', 'code', 'scroll']}
>
<SettingsSwitchRow
label={translate(
'auto.components.settings.GeneralEditorSettingsSection.6690b1ffb9',
'Minimap'
)}
description={translate(
label={t('auto.components.settings.GeneralEditorSettingsSection.6690b1ffb9', 'Minimap')}
description={t(
'auto.components.settings.GeneralEditorSettingsSection.51161d1647',
'Show the minimap overview when editing a file.'
)}
@@ -382,22 +359,22 @@ export function GeneralEditorSettingsSection({
<RichMarkdownSpellcheckSetting settings={settings} updateSettings={updateSettings} />
<SearchableSetting
title={translate(
title={t(
'auto.components.settings.GeneralEditorSettingsSection.4edc104f0f',
'Markdown Review Notes'
)}
description={translate(
description={t(
'auto.components.settings.GeneralEditorSettingsSection.5f02e6fb21',
'Show local markdown review note controls in markdown files.'
)}
keywords={['markdown', 'review', 'notes', 'annotations', 'agents']}
>
<SettingsSwitchRow
label={translate(
label={t(
'auto.components.settings.GeneralEditorSettingsSection.4edc104f0f',
'Markdown Review Notes'
)}
description={translate(
description={t(
'auto.components.settings.GeneralEditorSettingsSection.f80603d293',
'Show markdown note controls in every markdown view and include markdown notes in agent handoff actions.'
)}
@@ -4,7 +4,7 @@ import {
normalizeMarkdownDefaultViewMode,
type MarkdownDefaultViewMode
} from '../../../../../shared/markdown-default-view-mode'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { useAppStore } from '@/store'
import { Label } from '../../ui/label'
import { SearchableSetting } from '../section-layout/SearchableSetting'
@@ -17,13 +17,14 @@ type MarkdownDefaultViewSettingProps = {
export function MarkdownDefaultViewSetting({
settings
}: MarkdownDefaultViewSettingProps): React.JSX.Element {
const t = useTranslate()
// Why: the store action keeps already-open Markdown tabs on the view they show.
const setMarkdownDefaultViewMode = useAppStore((s) => s.setMarkdownDefaultViewMode)
const title = translate(
const title = t(
'auto.components.settings.GeneralEditorSettingsSection.markdownDefaultView',
'Default Markdown View'
)
const description = translate(
const description = t(
'auto.components.settings.GeneralEditorSettingsSection.markdownDefaultViewDesc',
'The view Markdown files open in. Right-click the view switcher in a Markdown tab to change it there.'
)
@@ -46,11 +47,11 @@ export function MarkdownDefaultViewSetting({
options={[
{
value: 'source',
label: translate('auto.components.editor.EditorViewToggle.4d6ccb7ba6', 'Source')
label: t('auto.components.editor.EditorViewToggle.4d6ccb7ba6', 'Source')
},
{
value: 'rich',
label: translate('auto.components.editor.EditorViewToggle.aff15f94f5', 'Rich Editor')
label: t('auto.components.editor.EditorViewToggle.aff15f94f5', 'Rich Editor')
}
]}
/>
@@ -1,6 +1,6 @@
import type React from 'react'
import type { GlobalSettings } from '../../../../../shared/global-settings-types'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { SearchableSetting } from '../section-layout/SearchableSetting'
import { SettingsSwitchRow } from '../form-controls/SettingsFormControls'
@@ -13,26 +13,27 @@ export function RichMarkdownSpellcheckSetting({
settings,
updateSettings
}: RichMarkdownSpellcheckSettingProps): React.JSX.Element {
const t = useTranslate()
const enabled = settings.richMarkdownSpellcheckEnabled ?? true
return (
<SearchableSetting
title={translate(
title={t(
'auto.components.settings.GeneralEditorSettingsSection.b82f86d7d2',
'Rich Markdown Spellcheck'
)}
description={translate(
description={t(
'auto.components.settings.GeneralEditorSettingsSection.5195f0b9ef',
'Show browser spelling underlines and suggestions while editing rich Markdown.'
)}
keywords={['spellcheck', 'spell check', 'spelling', 'markdown', 'red underline']}
>
<SettingsSwitchRow
label={translate(
label={t(
'auto.components.settings.GeneralEditorSettingsSection.b82f86d7d2',
'Rich Markdown Spellcheck'
)}
description={translate(
description={t(
'auto.components.settings.GeneralEditorSettingsSection.5195f0b9ef',
'Show browser spelling underlines and suggestions while editing rich Markdown.'
)}
@@ -1,7 +1,7 @@
import { Smartphone } from 'lucide-react'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../ui/select'
import { AUTO_RESTORE_FIT_OPTIONS, autoRestoreValueFromMs } from './mobile-auto-restore-options'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
type MobileAutoRestoreFitSectionProps = {
autoRestoreFitMs: number | null
@@ -12,19 +12,17 @@ export function MobileAutoRestoreFitSection({
autoRestoreFitMs,
onAutoRestoreFitChange
}: MobileAutoRestoreFitSectionProps): React.JSX.Element {
const t = useTranslate()
return (
<div>
<div className="mb-3 flex items-center gap-2">
<Smartphone className="size-4 text-muted-foreground" />
<span className="text-sm font-medium">
{translate(
'auto.components.settings.MobilePane.ee56f1c7e4',
'When you leave the mobile app'
)}
{t('auto.components.settings.MobilePane.ee56f1c7e4', 'When you leave the mobile app')}
</span>
</div>
<p className="text-muted-foreground mb-3 text-xs">
{translate(
{t(
'auto.components.settings.MobilePane.5e2c7a1f84',
"While you're using a terminal on your phone, Orca shrinks it to fit your phone screen. When you close the app or switch away, this controls whether it stays at phone size (so interactive CLI tools don't reflow) or resizes back to your desktop. You can always use Restore or Restore all on the banner to resize manually."
)}
@@ -18,6 +18,7 @@ import { MobilePairingSetupSection } from './pairing/MobilePairingSetupSection'
import { MobileRelayMintFailureNotice } from '../../mobile/mobile-relay-mint-failure-notice'
import { WindowsFirewallNotice } from '../../mobile/WindowsFirewallNotice'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import {
canMintMobilePairingOffer,
type MobilePairingConnectionMode
@@ -29,6 +30,7 @@ import { usePairingOfferExpiry } from '../../mobile/use-pairing-offer-expiry'
import { shouldOpenMobilePairingAddress } from './mobile-pane-search'
export function MobilePane(): React.JSX.Element {
const t = useTranslate()
const autoRestoreFitMs = useAppStore((s) => s.settings?.mobileAutoRestoreFitMs ?? null)
const updateSettings = useAppStore((s) => s.updateSettings)
const [qrDataUrl, setQrDataUrl] = useState<string | null>(null)
@@ -389,10 +391,7 @@ export function MobilePane(): React.JSX.Element {
<span className="sr-only" role="status" aria-live="polite">
{pairingUrl != null && !loading
? translate(
'auto.components.settings.MobilePane.pairingCodeReady',
'Pairing code ready'
)
? t('auto.components.settings.MobilePane.pairingCodeReady', 'Pairing code ready')
: ''}
</span>
@@ -6,7 +6,7 @@ import {
getMobileOverviewSearchEntry,
getMobileSidebarShortcutSearchEntry
} from './mobile-settings-search'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { useAppStore } from '@/store/index'
import { MobileRelayBetaNotice } from './pairing/MobileRelayBetaNotice'
import { openAppHttpLink } from '@/lib/browser/navigation/app-http-link-open'
@@ -15,6 +15,7 @@ const ORCA_IOS_APP_STORE_URL = 'https://apps.apple.com/app/orca-ide/id6766130217
const ORCA_ANDROID_APK_URL = 'https://www.onorca.dev/download/android'
export function MobileSettingsPane(): React.JSX.Element {
const t = useTranslate()
const showMobileButton = useAppStore((s) => s.settings?.showMobileButton !== false)
const updateSettings = useAppStore((s) => s.updateSettings)
@@ -22,8 +23,8 @@ export function MobileSettingsPane(): React.JSX.Element {
<div className="space-y-8">
<SettingsGroup>
<SearchableSetting
title={translate('auto.components.settings.MobileSettingsPane.e7a3ae8c4e', 'Mobile')}
description={translate(
title={t('auto.components.settings.MobileSettingsPane.e7a3ae8c4e', 'Mobile')}
description={t(
'auto.components.settings.MobileSettingsPane.174f4a3c6d',
'Control terminals and agents from your phone.'
)}
@@ -32,7 +33,7 @@ export function MobileSettingsPane(): React.JSX.Element {
>
<div className="space-y-2 text-xs text-muted-foreground">
<p>
{translate(
{t(
'auto.components.settings.MobileSettingsPane.installIntro',
'Install Orca Mobile from the'
)}{' '}
@@ -41,7 +42,7 @@ export function MobileSettingsPane(): React.JSX.Element {
onClick={(event) => openAppHttpLink(ORCA_IOS_APP_STORE_URL, { event })}
className="cursor-pointer underline underline-offset-2 hover:text-foreground"
>
{translate('auto.components.settings.MobileSettingsPane.b5a2ed83ff', 'App Store')}
{t('auto.components.settings.MobileSettingsPane.b5a2ed83ff', 'App Store')}
</button>
{' · '}
<button
@@ -50,26 +51,20 @@ export function MobileSettingsPane(): React.JSX.Element {
onClick={(event) => openAppHttpLink(ORCA_ANDROID_APK_URL, { event })}
className="cursor-pointer underline underline-offset-2 hover:text-foreground"
>
{translate(
'auto.components.settings.MobileSettingsPane.androidApkLabel',
'Android APK'
)}
{t('auto.components.settings.MobileSettingsPane.androidApkLabel', 'Android APK')}
</button>
{translate(
'auto.components.settings.MobileSettingsPane.installOutro',
', then pair below.'
)}
{t('auto.components.settings.MobileSettingsPane.installOutro', ', then pair below.')}
</p>
<MobileRelayBetaNotice />
</div>
</SearchableSetting>
<SearchableSetting
title={translate(
title={t(
'auto.components.settings.MobileSettingsPane.1de96ec8a6',
'Show Orca Mobile Button'
)}
description={translate(
description={t(
'auto.components.settings.MobileSettingsPane.682293cadf',
'Show the Orca Mobile button at the top of the left sidebar.'
)}
@@ -77,11 +72,11 @@ export function MobileSettingsPane(): React.JSX.Element {
>
{/* Why: the in-page removal toast points users to Settings > Mobile. */}
<SettingsSwitchRow
label={translate(
label={t(
'auto.components.settings.MobileSettingsPane.1de96ec8a6',
'Show Orca Mobile Button'
)}
description={translate(
description={t(
'auto.components.settings.MobileSettingsPane.d4f2b65f30',
'Show the Orca Mobile shortcut in the sidebar.'
)}
@@ -2,7 +2,7 @@ import type { GlobalSettings } from '../../../../../../shared/global-settings-ty
import { getEmulatorOpeningMode } from '../../../../../../shared/emulator-opening-preferences'
import { SettingsRow } from '../../form-controls/SettingsFormControls'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../ui/select'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { simulatorRuntimeLabel } from '@/lib/simulator-runtime-label'
type Device = { name: string; udid: string; runtime?: string; isAvailable?: boolean }
@@ -13,6 +13,7 @@ type Props = {
updateSettings: (updates: Partial<GlobalSettings>) => Promise<void>
}
export function EmulatorOpeningSettings({ settings, devices, enabled, updateSettings }: Props) {
const t = useTranslate()
const mode = getEmulatorOpeningMode(settings)
const fixed = settings.mobileEmulatorDefaultDeviceUdid ?? ''
const selected = devices.find((device) => device.udid === fixed)
@@ -21,8 +22,8 @@ export function EmulatorOpeningSettings({ settings, devices, enabled, updateSett
<>
<SettingsRow
alignTop
label={translate('emulator.opening.when', 'When opening')}
description={translate(
label={t('emulator.opening.when', 'When opening')}
description={t(
'emulator.opening.description',
'Applies to new emulator tabs on this computer. Existing workspace sessions stay connected.'
)}
@@ -41,13 +42,11 @@ export function EmulatorOpeningSettings({ settings, devices, enabled, updateSett
</SelectTrigger>
<SelectContent position="popper" align="end">
<SelectItem value="last-used">
{translate('emulator.opening.lastUsed', 'Last used')}
</SelectItem>
<SelectItem value="ask">
{translate('emulator.opening.ask', 'Ask every time')}
{t('emulator.opening.lastUsed', 'Last used')}
</SelectItem>
<SelectItem value="ask">{t('emulator.opening.ask', 'Ask every time')}</SelectItem>
<SelectItem value="specific">
{translate('emulator.opening.specific', 'Specific device')}
{t('emulator.opening.specific', 'Specific device')}
</SelectItem>
</SelectContent>
</Select>
@@ -56,8 +55,8 @@ export function EmulatorOpeningSettings({ settings, devices, enabled, updateSett
{mode === 'specific' && (
<SettingsRow
alignTop
label={translate('emulator.opening.fixed', 'Default device')}
description={translate(
label={t('emulator.opening.fixed', 'Default device')}
description={t(
'emulator.opening.fixedDescription',
'Choose an existing device. A missing or unavailable device prompts for another selection instead of starting a different platform.'
)}
@@ -70,7 +69,7 @@ export function EmulatorOpeningSettings({ settings, devices, enabled, updateSett
}
>
<SelectTrigger size="sm" className="w-56 max-w-full">
<SelectValue placeholder={translate('emulator.opening.choose', 'Choose device')}>
<SelectValue placeholder={t('emulator.opening.choose', 'Choose device')}>
{selected
? [selected.name, simulatorRuntimeLabel(selected.runtime)]
.filter(Boolean)
@@ -81,7 +80,7 @@ export function EmulatorOpeningSettings({ settings, devices, enabled, updateSett
<SelectContent position="popper" align="end">
{missing && (
<SelectItem value={fixed} disabled>
{fixed} · {translate('emulator.opening.notFound', 'Not found')}
{fixed} · {t('emulator.opening.notFound', 'Not found')}
</SelectItem>
)}
{devices.map((device) => (
@@ -3,7 +3,7 @@ import { ORCA_CLI_SKILL_NAME } from '@/lib/agent-feature-skill-names'
import { useMobileEmulatorAgentSetupState } from '../../../emulator-pane/use-mobile-emulator-agent-setup-state'
import { AgentSkillSetupPanel } from '../../../skills/setup/AgentSkillSetupPanel'
import { MobileEmulatorExamples } from './MobileEmulatorExamples'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
const EMULATOR_CLI_COMMANDS = [
@@ -14,16 +14,17 @@ const EMULATOR_CLI_COMMANDS = [
] as const
export function MobileEmulatorAgentControlRow(): React.JSX.Element {
const t = useTranslate()
const setup = useMobileEmulatorAgentSetupState(true)
const activeSkillRuntime = setup.skillRuntime
return (
<SettingsGroup
title={translate(
title={t(
'auto.components.settings.MobileEmulatorAgentControlRow.2a674aa810',
'Agent Mobile Emulator Control'
)}
description={translate(
description={t(
'auto.components.settings.MobileEmulatorAgentControlRow.ff4b7e65d6',
'Let coding agents control the active mobile emulator with Orca CLI commands.'
)}
@@ -32,11 +33,11 @@ export function MobileEmulatorAgentControlRow(): React.JSX.Element {
<div className="py-3">
<AgentSkillSetupPanel
variant="inline"
title={translate(
title={t(
'auto.components.settings.MobileEmulatorAgentControlRow.67e19ee03c',
'Orca CLI skill'
)}
description={translate(
description={t(
'auto.components.settings.MobileEmulatorAgentControlRow.d94ca6a623',
'Enables agents to use Orca CLI commands, including mobile emulator control.'
)}
@@ -58,14 +59,14 @@ export function MobileEmulatorAgentControlRow(): React.JSX.Element {
<div className="flex items-center gap-2">
<Import className="size-3.5 text-muted-foreground" />
<p className="text-sm font-medium">
{translate(
{t(
'auto.components.settings.MobileEmulatorAgentControlRow.c7f3fe0a6e',
'Common emulator commands'
)}
</p>
</div>
<p className="mt-1 text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.MobileEmulatorAgentControlRow.8af7a8bc38',
'Commands target the active emulator for the current worktree. Coordinates are normalized from 0..1.'
)}
@@ -2,7 +2,7 @@ import { CheckCircle2, CircleAlert, FolderOpen, X } from 'lucide-react'
import type React from 'react'
import { toast } from 'sonner'
import { Button } from '../../../ui/button'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { emulatorMessageText } from '@/lib/emulator-error-copy'
import { openAppHttpLink } from '@/lib/browser/navigation/app-http-link-open'
@@ -65,6 +65,7 @@ export function MobileEmulatorAvailabilityDetails({
configuredPath,
onSetAndroidSdkPath
}: MobileEmulatorAvailabilityDetailsProps): React.JSX.Element | null {
const t = useTranslate()
if (!availability) {
return null
}
@@ -84,7 +85,7 @@ export function MobileEmulatorAvailabilityDetails({
toast.error(
error instanceof Error
? error.message
: translate(
: t(
'auto.components.settings.MobileEmulatorSdkStatus.63fe73a1ea',
'Could not update Android SDK folder.'
)
@@ -99,7 +100,7 @@ export function MobileEmulatorAvailabilityDetails({
toast.error(
error instanceof Error
? error.message
: translate(
: t(
'auto.components.settings.MobileEmulatorSdkStatus.63fe73a1ea',
'Could not update Android SDK folder.'
)
@@ -112,19 +113,16 @@ export function MobileEmulatorAvailabilityDetails({
<div className="divide-y divide-border/40 rounded-md border border-border/50 px-3">
<ToolchainStatusRow
ok={android.sdkFound}
title={translate(
'auto.components.settings.MobileEmulatorSdkStatus.027cbf668a',
'Android SDK'
)}
title={t('auto.components.settings.MobileEmulatorSdkStatus.027cbf668a', 'Android SDK')}
detail={
android.sdkFound ? (
<>
{configuredPath
? translate(
? t(
'auto.components.settings.MobileEmulatorSdkStatus.f6d080d128',
'Using configured path'
)
: translate(
: t(
'auto.components.settings.MobileEmulatorSdkStatus.7fe4bd5907',
'Detected at'
)}{' '}
@@ -132,7 +130,7 @@ export function MobileEmulatorAvailabilityDetails({
</>
) : (
emulatorMessageText(android) ||
translate(
t(
'auto.components.settings.MobileEmulatorSdkStatus.2784f0b22d',
'Not found. Install Android Studio, then create a Virtual Device.'
)
@@ -147,7 +145,7 @@ export function MobileEmulatorAvailabilityDetails({
variant="outline"
onClick={(event) => openAppHttpLink(ANDROID_STUDIO_URL, { event })}
>
{translate(
{t(
'auto.components.settings.MobileEmulatorSdkStatus.b94ff260e6',
'Download Android Studio'
)}
@@ -161,7 +159,7 @@ export function MobileEmulatorAvailabilityDetails({
className={sdkPathActionClassName}
>
<FolderOpen className="size-3" />
{translate(
{t(
'auto.components.settings.MobileEmulatorSdkStatus.18925b082d',
'Locate SDK folder'
)}
@@ -175,10 +173,7 @@ export function MobileEmulatorAvailabilityDetails({
className={sdkPathActionClassName}
>
<X className="size-3" />
{translate(
'auto.components.settings.MobileEmulatorSdkStatus.8c52684db8',
'Clear'
)}
{t('auto.components.settings.MobileEmulatorSdkStatus.8c52684db8', 'Clear')}
</Button>
) : null}
</>
@@ -188,18 +183,18 @@ export function MobileEmulatorAvailabilityDetails({
{showIos ? (
<ToolchainStatusRow
ok={iosOk}
title={translate(
title={t(
'auto.components.settings.MobileEmulatorSdkStatus.76eb88b88e',
'iOS Simulator (Xcode)'
)}
detail={
iosOk
? translate('auto.components.settings.MobileEmulatorSdkStatus.c6f3ea4f12', 'Ready')
? t('auto.components.settings.MobileEmulatorSdkStatus.c6f3ea4f12', 'Ready')
: emulatorMessageText(
[availability.simctl, availability.serveSim].find((check) => check?.message) ??
{}
) ||
translate(
t(
'auto.components.settings.MobileEmulatorSdkStatus.e4f14b50d7',
'Install Xcode and add an iOS Simulator runtime.'
)
@@ -4,6 +4,7 @@ import { cn } from '@/lib/utils'
import { Button } from '../../../ui/button'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../../ui/tooltip'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
const EMULATOR_EXAMPLE_PROMPTS = [
'Using Orca CLI, attach to the active iPhone simulator, sign in with the test account, complete onboarding, and tell me where the flow feels confusing.',
@@ -36,6 +37,7 @@ type MobileEmulatorExamplesProps = {
export function MobileEmulatorExamples({
variant = 'card'
}: MobileEmulatorExamplesProps): React.JSX.Element {
const t = useTranslate()
return (
<div
className={cn(
@@ -45,14 +47,14 @@ export function MobileEmulatorExamples({
<div className="flex items-center gap-2">
<Sparkles className="size-3.5 text-muted-foreground" />
<p className="text-sm font-medium">
{translate(
{t(
'auto.components.settings.MobileEmulatorExamples.0820b3f84f',
'Try it — example prompts'
)}
</p>
</div>
<p className="mt-1 text-xs text-muted-foreground">
{translate(
{t(
'auto.components.settings.MobileEmulatorExamples.4daa95f25a',
'Paste any of these into Claude Code, Codex, or another agent in a project where the Orca CLI skill is installed.'
)}
@@ -64,9 +66,9 @@ export function MobileEmulatorExamples({
className="flex items-start gap-2 rounded-lg border border-border bg-background px-3 py-2"
>
<p className="flex-1 text-[11px] leading-relaxed text-foreground/90">
{translate('auto.components.settings.MobileEmulatorExamples.b525ff2b12', '"')}
{t('auto.components.settings.MobileEmulatorExamples.b525ff2b12', '"')}
{prompt}
{translate('auto.components.settings.MobileEmulatorExamples.d151e25078', '"')}
{t('auto.components.settings.MobileEmulatorExamples.d151e25078', '"')}
</p>
<TooltipProvider delayDuration={250}>
<Tooltip>
@@ -75,7 +77,7 @@ export function MobileEmulatorExamples({
type="button"
variant="ghost"
size="icon-xs"
aria-label={translate(
aria-label={t(
'auto.components.settings.MobileEmulatorExamples.c12b253997',
'Copy example prompt'
)}
@@ -85,7 +87,7 @@ export function MobileEmulatorExamples({
</Button>
</TooltipTrigger>
<TooltipContent side="left" sideOffset={6}>
{translate('auto.components.settings.MobileEmulatorExamples.edf13dd03b', 'Copy')}
{t('auto.components.settings.MobileEmulatorExamples.edf13dd03b', 'Copy')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
@@ -14,6 +14,7 @@ import { SettingsGroup } from '../../section-layout/SettingsGroup'
import { SettingsSwitchRow } from '../../form-controls/SettingsFormControls'
import { getMobileEmulatorSearchEntries } from './mobile-emulator-search'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { emulatorErrorText, emulatorMessageText } from '@/lib/emulator-error-copy'
import { useUiLanguageRevision } from '@/i18n/use-ui-language-revision'
@@ -41,19 +42,20 @@ type MobileEmulatorSettingsPaneProps = {
updateSettings: (updates: Partial<GlobalSettings>) => Promise<void>
}
function statusText(availability: EmulatorAvailability | null, enabled: boolean): string {
function statusText(
availability: EmulatorAvailability | null,
enabled: boolean,
t: Translate = translate
): string {
if (!enabled) {
return translate('auto.components.settings.MobileEmulatorSettingsPane.a4f1c82d90', 'Disabled')
return t('auto.components.settings.MobileEmulatorSettingsPane.a4f1c82d90', 'Disabled')
}
if (!availability) {
return translate(
'auto.components.settings.MobileEmulatorSettingsPane.b5e2d93e01',
'Checking...'
)
return t('auto.components.settings.MobileEmulatorSettingsPane.b5e2d93e01', 'Checking...')
}
return availability.available
? translate('auto.components.settings.MobileEmulatorSettingsPane.c6f3ea4f12', 'Ready')
: translate('auto.components.settings.MobileEmulatorSettingsPane.d704fb5023', 'Needs setup')
? t('auto.components.settings.MobileEmulatorSettingsPane.c6f3ea4f12', 'Ready')
: t('auto.components.settings.MobileEmulatorSettingsPane.d704fb5023', 'Needs setup')
}
function statusBadgeClassName(availability: EmulatorAvailability | null, enabled: boolean): string {
@@ -68,20 +70,23 @@ function statusBadgeClassName(availability: EmulatorAvailability | null, enabled
: 'border-destructive/30 bg-destructive/10 text-destructive'
}
function availabilityDetail(availability: EmulatorAvailability | null): string {
function availabilityDetail(
availability: EmulatorAvailability | null,
t: Translate = translate
): string {
if (!availability) {
return translate(
return t(
'auto.components.settings.MobileEmulatorSettingsPane.06b06429c6',
'Checking Android SDK and iOS Simulator support.'
)
}
if (availability.available) {
return availability.devices.length === 1
? translate(
? t(
'auto.components.settings.MobileEmulatorSettingsPane.6d1483d4a0',
'1 emulator device detected.'
)
: translate(
: t(
'auto.components.settings.MobileEmulatorSettingsPane.0a452d4d3b',
'{{value0}} emulator devices detected.',
{ value0: availability.devices.length }
@@ -95,6 +100,7 @@ export function MobileEmulatorSettingsPane({
settings,
updateSettings
}: MobileEmulatorSettingsPaneProps): React.JSX.Element {
const t = useTranslate()
useUiLanguageRevision()
const [availability, setAvailability] = useState<EmulatorAvailability | null>(null)
const [refreshing, setRefreshing] = useState(false)
@@ -139,11 +145,11 @@ export function MobileEmulatorSettingsPane({
<div className="space-y-8">
<SettingsGroup>
<SearchableSetting
title={translate(
title={t(
'auto.components.settings.MobileEmulatorSettingsPane.6593c9ddd3',
'Mobile Emulator'
)}
description={translate(
description={t(
'auto.components.settings.MobileEmulatorSettingsPane.bc39d0f115',
'Configure mobile emulator support for Orca and coding agents.'
)}
@@ -151,11 +157,11 @@ export function MobileEmulatorSettingsPane({
className="divide-y divide-border"
>
<SettingsSwitchRow
label={translate(
label={t(
'auto.components.settings.MobileEmulatorSettingsPane.700ddbf9b1',
'Enable Mobile Emulator'
)}
description={translate(
description={t(
'auto.components.settings.MobileEmulatorSettingsPane.f9af91ea26',
'Shows the New Mobile Emulator action and allows agents to attach to the active emulator.'
)}
@@ -167,12 +173,14 @@ export function MobileEmulatorSettingsPane({
<div className="flex items-start gap-4">
<div className="min-w-0 flex-1 space-y-0.5">
<Label>
{translate(
{t(
'auto.components.settings.MobileEmulatorSettingsPane.ae1612c58c',
'Availability'
)}
</Label>
<p className="text-xs text-muted-foreground">{availabilityDetail(availability)}</p>
<p className="text-xs text-muted-foreground">
{availabilityDetail(availability, t)}
</p>
</div>
<div className="flex shrink-0 items-center gap-2">
<Badge
@@ -180,13 +188,13 @@ export function MobileEmulatorSettingsPane({
className={cn('text-[11px]', statusBadgeClassName(availability, enabled))}
>
{refreshing ? <Loader2 className="size-3 animate-spin" /> : null}
{statusText(availability, enabled)}
{statusText(availability, enabled, t)}
</Badge>
<Button
type="button"
variant="outline"
size="icon-xs"
aria-label={translate(
aria-label={t(
'auto.components.settings.MobileEmulatorSettingsPane.8aec2f99a0',
'Refresh emulator availability'
)}
@@ -225,11 +233,11 @@ export function MobileEmulatorSettingsPane({
{enabled ? (
<SearchableSetting
title={translate(
title={t(
'auto.components.settings.MobileEmulatorSettingsPane.f2f8d97bb6',
'Agent Mobile Emulator Control'
)}
description={translate(
description={t(
'auto.components.settings.MobileEmulatorSettingsPane.19d39113b6',
'Let coding agents control the active mobile emulator with Orca CLI commands.'
)}

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