mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
refactor(i18n): read render-path translations from useTranslate in settings (repository, browser, terminal, runtime environments, ssh) (#39356)
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:
@@ -849,37 +849,6 @@
|
||||
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
|
||||
4 src/renderer/src/components/settings/browser/BrowserDefaultZoomSetting.tsx
|
||||
7 src/renderer/src/components/settings/browser/BrowserHomePageSetting.tsx
|
||||
4 src/renderer/src/components/settings/browser/BrowserPane.tsx
|
||||
4 src/renderer/src/components/settings/browser/BrowserSearchEngineSetting.tsx
|
||||
5 src/renderer/src/components/settings/browser/BrowserSitePermissionsSetting.tsx
|
||||
12 src/renderer/src/components/settings/browser/BrowserUserAgentSetting.tsx
|
||||
9 src/renderer/src/components/settings/browser/KagiSessionLinkForm.tsx
|
||||
2 src/renderer/src/components/settings/browser/extensions/BrowserExtensionsExperimentalSetting.tsx
|
||||
16 src/renderer/src/components/settings/browser/extensions/BrowserExtensionsSection.tsx
|
||||
7 src/renderer/src/components/settings/browser/extensions/BrowserExtensionsWebStoreInstall.tsx
|
||||
15 src/renderer/src/components/settings/browser/passwords/BrowserPasswordEntryDialog.tsx
|
||||
2 src/renderer/src/components/settings/browser/passwords/BrowserPasswordImportMenu.tsx
|
||||
10 src/renderer/src/components/settings/browser/passwords/BrowserPasswordList.tsx
|
||||
2 src/renderer/src/components/settings/browser/passwords/BrowserPasswordNeverList.tsx
|
||||
6 src/renderer/src/components/settings/browser/passwords/BrowserPasswordPreferences.tsx
|
||||
21 src/renderer/src/components/settings/browser/passwords/BrowserPasswordsSection.tsx
|
||||
7 src/renderer/src/components/settings/browser/profiles/BrowserNewProfileDialog.tsx
|
||||
11 src/renderer/src/components/settings/browser/profiles/BrowserProfileRow.tsx
|
||||
6 src/renderer/src/components/settings/browser/profiles/BrowserProfileScopeSetting.tsx
|
||||
13 src/renderer/src/components/settings/browser/profiles/BrowserSessionCookiesSection.tsx
|
||||
4 src/renderer/src/components/settings/browser/routing/BrowserClientHostedRemoteSetting.tsx
|
||||
3 src/renderer/src/components/settings/browser/routing/BrowserLinkRoutingSetting.tsx
|
||||
2 src/renderer/src/components/settings/browser/routing/BrowserLocalhostWorktreeLabelsSetting.tsx
|
||||
6 src/renderer/src/components/settings/browser/routing/BrowserSshWorkspaceRoutingSetting.tsx
|
||||
11 src/renderer/src/components/settings/browser/routing/BrowserTerminalLinkActionsSetting.tsx
|
||||
3 src/renderer/src/components/settings/browser/use/BrowserUseComputerUseNotice.tsx
|
||||
13 src/renderer/src/components/settings/browser/use/BrowserUseCookieImportStep.tsx
|
||||
1 src/renderer/src/components/settings/browser/use/BrowserUseEnableSwitch.tsx
|
||||
6 src/renderer/src/components/settings/browser/use/BrowserUseExamples.tsx
|
||||
6 src/renderer/src/components/settings/browser/use/BrowserUsePane.tsx
|
||||
2 src/renderer/src/components/settings/browser/use/BrowserUseSkillStep.tsx
|
||||
3 src/renderer/src/components/settings/chat/ChatBehaviorSetting.tsx
|
||||
10 src/renderer/src/components/settings/chat/ChatNamingSetting.tsx
|
||||
2 src/renderer/src/components/settings/chat/ChatPermissionSetting.tsx
|
||||
@@ -944,64 +913,6 @@
|
||||
9 src/renderer/src/components/settings/quick-commands/QuickCommandsPane.tsx
|
||||
4 src/renderer/src/components/settings/quick-commands/QuickCommandsScopeFilter.tsx
|
||||
4 src/renderer/src/components/settings/quick-commands/QuickCommandsToolbar.tsx
|
||||
8 src/renderer/src/components/settings/repository/RepositoryFormatterSection.tsx
|
||||
18 src/renderer/src/components/settings/repository/RepositoryGitHubAccountSection.tsx
|
||||
6 src/renderer/src/components/settings/repository/RepositoryJiraProjectSection.tsx
|
||||
19 src/renderer/src/components/settings/repository/RepositoryPane.tsx
|
||||
1 src/renderer/src/components/settings/repository/RepositoryRemoveProjectButton.tsx
|
||||
9 src/renderer/src/components/settings/repository/execution/ProjectWindowsRuntimeSetting.tsx
|
||||
12 src/renderer/src/components/settings/repository/execution/RepositoryForkSyncSection.tsx
|
||||
17 src/renderer/src/components/settings/repository/execution/RepositoryHookPolicySettings.tsx
|
||||
16 src/renderer/src/components/settings/repository/execution/RepositoryHookScriptSetting.tsx
|
||||
12 src/renderer/src/components/settings/repository/execution/RepositoryHooksSection.tsx
|
||||
6 src/renderer/src/components/settings/repository/execution/RepositoryHooksYamlStatus.tsx
|
||||
7 src/renderer/src/components/settings/repository/execution/RepositoryHostSetupActions.tsx
|
||||
16 src/renderer/src/components/settings/repository/execution/RepositoryHostSetupsSection.tsx
|
||||
7 src/renderer/src/components/settings/repository/execution/RepositoryIssueCommandSetting.tsx
|
||||
2 src/renderer/src/components/settings/repository/execution/RepositoryWindowsRuntimeSection.tsx
|
||||
20 src/renderer/src/components/settings/repository/execution/RepositoryWorktreeDefaultsSection.tsx
|
||||
7 src/renderer/src/components/settings/repository/execution/SparsePresetSettingsSection.tsx
|
||||
14 src/renderer/src/components/settings/repository/execution/WorktreeSymlinksSection.tsx
|
||||
22 src/renderer/src/components/settings/repository/execution/repository-host-add-project-steps.tsx
|
||||
1 src/renderer/src/components/settings/repository/execution/sparse-preset-directory-preview.tsx
|
||||
9 src/renderer/src/components/settings/repository/execution/sparse-preset-settings-row.tsx
|
||||
4 src/renderer/src/components/settings/repository/identity/RepositoryIconColorSection.tsx
|
||||
3 src/renderer/src/components/settings/repository/identity/RepositoryIconEmojiPicker.tsx
|
||||
4 src/renderer/src/components/settings/repository/identity/RepositoryIconPicker.tsx
|
||||
13 src/renderer/src/components/settings/repository/identity/RepositoryIconTabs.tsx
|
||||
4 src/renderer/src/components/settings/repository/mcp/McpConfigFileRow.tsx
|
||||
9 src/renderer/src/components/settings/repository/mcp/McpConfigSection.tsx
|
||||
1 src/renderer/src/components/settings/repository/mcp/McpMissingConfigList.tsx
|
||||
6 src/renderer/src/components/settings/repository/source-control/RepositorySourceControlAiActionRows.tsx
|
||||
5 src/renderer/src/components/settings/repository/source-control/RepositorySourceControlAiCustomCommand.tsx
|
||||
5 src/renderer/src/components/settings/repository/source-control/RepositorySourceControlAiEnablement.tsx
|
||||
5 src/renderer/src/components/settings/repository/source-control/RepositorySourceControlAiHostedReviewDefaults.tsx
|
||||
1 src/renderer/src/components/settings/repository/source-control/RepositorySourceControlAiSection.tsx
|
||||
3 src/renderer/src/components/settings/runtime-environments/catalog/RuntimeEnvironmentsPane.tsx
|
||||
4 src/renderer/src/components/settings/runtime-environments/catalog/runtime-default-host-row.tsx
|
||||
11 src/renderer/src/components/settings/runtime-environments/catalog/runtime-environment-dialogs.tsx
|
||||
3 src/renderer/src/components/settings/runtime-environments/catalog/use-runtime-environment-connection-actions.ts
|
||||
6 src/renderer/src/components/settings/runtime-environments/catalog/use-runtime-environment-mutation-actions.ts
|
||||
22 src/renderer/src/components/settings/runtime-environments/managed-servers/managed-server-row-parts.tsx
|
||||
18 src/renderer/src/components/settings/runtime-environments/managed-servers/runtime-install-server-dialog.tsx
|
||||
11 src/renderer/src/components/settings/runtime-environments/pairing/RuntimeAccessGrantList.tsx
|
||||
1 src/renderer/src/components/settings/runtime-environments/pairing/RuntimePairingGeneratedUrlRows.tsx
|
||||
37 src/renderer/src/components/settings/runtime-environments/pairing/RuntimePairingGeneratorForm.tsx
|
||||
9 src/renderer/src/components/settings/runtime-environments/pairing/RuntimePairingUrlGenerator.tsx
|
||||
2 src/renderer/src/components/settings/runtime-environments/pairing/runtime-add-server-dialog.tsx
|
||||
9 src/renderer/src/components/settings/runtime-environments/pairing/runtime-share-computer-button.tsx
|
||||
3 src/renderer/src/components/settings/runtime-environments/pairing/use-runtime-pairing-url-copy.ts
|
||||
10 src/renderer/src/components/settings/runtime-environments/runtime-servers-header-actions.tsx
|
||||
4 src/renderer/src/components/settings/runtime-environments/server-rows/runtime-server-list-row.tsx
|
||||
4 src/renderer/src/components/settings/runtime-environments/server-rows/runtime-server-list.tsx
|
||||
15 src/renderer/src/components/settings/runtime-environments/server-rows/runtime-server-row-details.tsx
|
||||
10 src/renderer/src/components/settings/runtime-environments/server-rows/runtime-server-row-menu.tsx
|
||||
1 src/renderer/src/components/settings/runtime-environments/server-rows/runtime-server-row-state.ts
|
||||
8 src/renderer/src/components/settings/runtime-environments/server-rows/runtime-server-row.tsx
|
||||
11 src/renderer/src/components/settings/runtime-environments/ssh-access/RuntimeSshAccessControl.tsx
|
||||
3 src/renderer/src/components/settings/runtime-environments/ssh-access/runtime-ssh-access-dialog.tsx
|
||||
12 src/renderer/src/components/settings/runtime-environments/updates/GeneralRemoteServerUpdates.tsx
|
||||
15 src/renderer/src/components/settings/runtime-environments/updates/RemoteServerUpdateDialog.tsx
|
||||
2 src/renderer/src/components/settings/session-history/SessionHistoryComputerRow.tsx
|
||||
4 src/renderer/src/components/settings/session-history/SessionHistoryServerRow.tsx
|
||||
7 src/renderer/src/components/settings/session-history/SessionHistorySettingsPane.tsx
|
||||
@@ -1034,58 +945,14 @@
|
||||
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
|
||||
8 src/renderer/src/components/settings/ssh/SshPane.tsx
|
||||
18 src/renderer/src/components/settings/ssh/SshPaneToolbar.tsx
|
||||
7 src/renderer/src/components/settings/ssh/server-actions/SshHostDeltaMoveDialog.tsx
|
||||
4 src/renderer/src/components/settings/ssh/server-actions/SshHostKeepServerVersionDialog.tsx
|
||||
5 src/renderer/src/components/settings/ssh/server-actions/SshHostMoveAnywayAction.tsx
|
||||
19 src/renderer/src/components/settings/ssh/server-actions/SshHostRecoverServerAction.tsx
|
||||
4 src/renderer/src/components/settings/ssh/server-actions/SshHostRestartServerAction.tsx
|
||||
9 src/renderer/src/components/settings/ssh/server-actions/SshTargetServerStatus.tsx
|
||||
2 src/renderer/src/components/settings/ssh/targets/SshDestructiveActionDialog.tsx
|
||||
19 src/renderer/src/components/settings/ssh/targets/SshHostAdvancedFields.tsx
|
||||
4 src/renderer/src/components/settings/ssh/targets/SshManagedHostRemoveDialog.tsx
|
||||
5 src/renderer/src/components/settings/ssh/targets/SshRestoreRemovedHostsDialog.tsx
|
||||
11 src/renderer/src/components/settings/ssh/targets/SshTargetCard.tsx
|
||||
9 src/renderer/src/components/settings/ssh/targets/SshTargetDestructiveActions.tsx
|
||||
18 src/renderer/src/components/settings/ssh/targets/SshTargetForm.tsx
|
||||
3 src/renderer/src/components/settings/ssh/targets/SshTargetList.tsx
|
||||
1 src/renderer/src/components/settings/ssh/targets/SshTargetsEmptyState.tsx
|
||||
1 src/renderer/src/components/settings/ssh/targets/use-ssh-bulk-remove.ts
|
||||
3 src/renderer/src/components/settings/ssh/targets/use-ssh-config-sync.ts
|
||||
6 src/renderer/src/components/settings/ssh/targets/use-ssh-target-tests.ts
|
||||
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
|
||||
2 src/renderer/src/components/settings/tasks/TaskSourceShowInTasksStep.tsx
|
||||
11 src/renderer/src/components/settings/tasks/TaskSourceSimpleSetup.tsx
|
||||
10 src/renderer/src/components/settings/tasks/TasksPane.tsx
|
||||
26 src/renderer/src/components/settings/terminal/TerminalAdvancedSection.tsx
|
||||
37 src/renderer/src/components/settings/terminal/TerminalInteractionSection.tsx
|
||||
22 src/renderer/src/components/settings/terminal/TerminalPane.tsx
|
||||
25 src/renderer/src/components/settings/terminal/TerminalWindowSection.tsx
|
||||
20 src/renderer/src/components/settings/terminal/appearance/TerminalAdvancedTypographyControls.tsx
|
||||
5 src/renderer/src/components/settings/terminal/appearance/TerminalAppearanceSection.tsx
|
||||
12 src/renderer/src/components/settings/terminal/appearance/TerminalContrastSetting.tsx
|
||||
14 src/renderer/src/components/settings/terminal/appearance/TerminalCursorAppearanceSection.tsx
|
||||
4 src/renderer/src/components/settings/terminal/appearance/TerminalFontSizeSetting.tsx
|
||||
8 src/renderer/src/components/settings/terminal/appearance/TerminalPaneAppearanceSection.tsx
|
||||
16 src/renderer/src/components/settings/terminal/appearance/TerminalRenderingSection.tsx
|
||||
5 src/renderer/src/components/settings/terminal/appearance/TerminalSettingsPreview.tsx
|
||||
11 src/renderer/src/components/settings/terminal/configuration/TerminalConfigImportModal.tsx
|
||||
21 src/renderer/src/components/settings/terminal/platform/TerminalMacKeyboardSection.tsx
|
||||
25 src/renderer/src/components/settings/terminal/platform/TerminalWindowsAdministratorSection.tsx
|
||||
15 src/renderer/src/components/settings/terminal/platform/TerminalWindowsShellSection.tsx
|
||||
6 src/renderer/src/components/settings/terminal/session-management/ManageSessionKillDialog.tsx
|
||||
2 src/renderer/src/components/settings/terminal/session-management/ManageSessionsSection.tsx
|
||||
10 src/renderer/src/components/settings/terminal/session-management/ManageSessionsTable.tsx
|
||||
16 src/renderer/src/components/settings/terminal/session-management/TerminalLimitSetting.tsx
|
||||
15 src/renderer/src/components/settings/terminal/themes/TerminalThemePicker.tsx
|
||||
23 src/renderer/src/components/settings/terminal/themes/TerminalThemeSections.tsx
|
||||
1 src/renderer/src/components/settings/terminal/themes/WarpThemeImportButton.tsx
|
||||
22 src/renderer/src/components/settings/terminal/themes/WarpThemeImportModal.tsx
|
||||
1 src/renderer/src/components/settings/terminal/themes/YamlThemeImportButton.tsx
|
||||
4 src/renderer/src/components/settings/terminal/themes/useWarpThemeImport.ts
|
||||
25 src/renderer/src/components/settings/updates/GeneralUpdateSettingsSection.tsx
|
||||
4 src/renderer/src/components/settings/updates/ReleaseBuildPicker.tsx
|
||||
17 src/renderer/src/components/settings/updates/ReleaseChannelSection.tsx
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { Translate } from '@/i18n/use-translate'
|
||||
import {
|
||||
browserExtensionHostWarning,
|
||||
browserExtensionPermissionWarning,
|
||||
@@ -132,14 +133,15 @@ function featureLabel(feature: UnsupportedBrowserExtensionFeature): string {
|
||||
}
|
||||
|
||||
/** One calm line naming what the extension asks for that Orca cannot do yet, or null. */
|
||||
export function describeUnsupportedBrowserExtensionFeatures(permissions: string[]): string | null {
|
||||
export function describeUnsupportedBrowserExtensionFeatures(
|
||||
permissions: string[],
|
||||
t: Translate = translate
|
||||
): string | null {
|
||||
const features = unsupportedBrowserExtensionFeatures(permissions)
|
||||
if (features.length === 0) {
|
||||
return null
|
||||
}
|
||||
return translate(
|
||||
'browserExtensions.unsupported.line',
|
||||
'Not available in Orca yet: {{features}}.',
|
||||
{ features: features.map(featureLabel).join(', ') }
|
||||
)
|
||||
return t('browserExtensions.unsupported.line', 'Not available in Orca yet: {{features}}.', {
|
||||
features: features.map(featureLabel).join(', ')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,40 +4,35 @@ import {
|
||||
type BrowserSitePermission
|
||||
} from '../../../../shared/browser-site-permission'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { Translate } from '@/i18n/use-translate'
|
||||
|
||||
/** The setting's name, e.g. in Settings → Browser → Site permissions. */
|
||||
export function browserSitePermissionLabel(permission: BrowserSitePermission): string {
|
||||
export function browserSitePermissionLabel(
|
||||
permission: BrowserSitePermission,
|
||||
t: Translate = translate
|
||||
): string {
|
||||
switch (permission) {
|
||||
case 'camera':
|
||||
return translate('auto.components.browserPane.sitePermission.label.camera', 'Camera')
|
||||
return t('auto.components.browserPane.sitePermission.label.camera', 'Camera')
|
||||
case 'microphone':
|
||||
return translate('auto.components.browserPane.sitePermission.label.microphone', 'Microphone')
|
||||
return t('auto.components.browserPane.sitePermission.label.microphone', 'Microphone')
|
||||
case 'location':
|
||||
return translate('auto.components.browserPane.sitePermission.label.location', 'Location')
|
||||
return t('auto.components.browserPane.sitePermission.label.location', 'Location')
|
||||
case 'notifications':
|
||||
return translate(
|
||||
'auto.components.browserPane.sitePermission.label.notifications',
|
||||
'Notifications'
|
||||
)
|
||||
return t('auto.components.browserPane.sitePermission.label.notifications', 'Notifications')
|
||||
case 'clipboard':
|
||||
return translate('auto.components.browserPane.sitePermission.label.clipboard', 'Clipboard')
|
||||
return t('auto.components.browserPane.sitePermission.label.clipboard', 'Clipboard')
|
||||
case 'midi':
|
||||
return translate('auto.components.browserPane.sitePermission.label.midi', 'MIDI devices')
|
||||
return t('auto.components.browserPane.sitePermission.label.midi', 'MIDI devices')
|
||||
case 'window-management':
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.browserPane.sitePermission.label.windowManagement',
|
||||
'Window management'
|
||||
)
|
||||
case 'idle-detection':
|
||||
return translate(
|
||||
'auto.components.browserPane.sitePermission.label.idleDetection',
|
||||
'Device use'
|
||||
)
|
||||
return t('auto.components.browserPane.sitePermission.label.idleDetection', 'Device use')
|
||||
case 'local-network':
|
||||
return translate(
|
||||
'auto.components.browserPane.sitePermission.label.localNetwork',
|
||||
'Local network'
|
||||
)
|
||||
return t('auto.components.browserPane.sitePermission.label.localNetwork', 'Local network')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,9 +85,9 @@ export function browserSitePermissionRequestLine(permission: BrowserSitePermissi
|
||||
}
|
||||
|
||||
/** The origin as people read it: the host for https, the full origin otherwise. */
|
||||
export function browserSitePermissionSiteName(origin: string): string {
|
||||
export function browserSitePermissionSiteName(origin: string, t: Translate = translate): string {
|
||||
if (origin === BROWSER_SITE_PERMISSION_LOCAL_FILES_ORIGIN) {
|
||||
return translate('auto.components.browserPane.sitePermission.localFiles', 'Local files')
|
||||
return t('auto.components.browserPane.sitePermission.localFiles', 'Local files')
|
||||
}
|
||||
return origin.startsWith('https://') ? origin.slice('https://'.length) : origin
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { Translate } from '@/i18n/use-translate'
|
||||
import {
|
||||
hasOfficialPluginPublisher,
|
||||
splitQualifiedPluginKey
|
||||
@@ -8,8 +9,12 @@ import {
|
||||
type PluginNameCandidate
|
||||
} from '../../../../../shared/plugins/plugin-name-clashes'
|
||||
|
||||
export function pluginNameWithPublisher(name: string, publisher: string): string {
|
||||
return translate(
|
||||
export function pluginNameWithPublisher(
|
||||
name: string,
|
||||
publisher: string,
|
||||
t: Translate = translate
|
||||
): string {
|
||||
return t(
|
||||
'auto.components.plugins.pluginNameDisambiguation.nameWithPublisher',
|
||||
'{{value0}} ({{value1}})',
|
||||
{ value0: name, value1: publisher }
|
||||
@@ -18,14 +23,15 @@ export function pluginNameWithPublisher(name: string, publisher: string): string
|
||||
|
||||
/** Each item's name as a list shows it: a clashing non-official one gains " (publisher)". */
|
||||
export function disambiguatedPluginNames(
|
||||
items: readonly PluginNameCandidate[]
|
||||
items: readonly PluginNameCandidate[],
|
||||
t?: Translate
|
||||
): Map<string, string> {
|
||||
const clashing = pluginNameClashKeys(items)
|
||||
return new Map(
|
||||
items.map((item) => [
|
||||
item.key,
|
||||
clashing.has(item.key) && item.publisher
|
||||
? pluginNameWithPublisher(item.name, item.publisher)
|
||||
? pluginNameWithPublisher(item.name, item.publisher, t)
|
||||
: item.name
|
||||
])
|
||||
)
|
||||
@@ -48,26 +54,31 @@ export type PluginContentLabelItem = { id: string; pluginKey: string; label: str
|
||||
*/
|
||||
export function disambiguatedPluginContentLabels(
|
||||
items: readonly PluginContentLabelItem[],
|
||||
plainLabels: readonly string[] = []
|
||||
plainLabels: readonly string[] = [],
|
||||
t?: Translate
|
||||
): Map<string, string> {
|
||||
return disambiguatedPluginNames([
|
||||
...plainLabels.map((label, index) => ({
|
||||
key: `\0built-in:${index}`,
|
||||
name: label,
|
||||
publisher: null,
|
||||
official: true
|
||||
})),
|
||||
...items.map((item) => ({
|
||||
key: item.id,
|
||||
name: item.label,
|
||||
...contentPublisher(item.pluginKey)
|
||||
}))
|
||||
])
|
||||
return disambiguatedPluginNames(
|
||||
[
|
||||
...plainLabels.map((label, index) => ({
|
||||
key: `\0built-in:${index}`,
|
||||
name: label,
|
||||
publisher: null,
|
||||
official: true
|
||||
})),
|
||||
...items.map((item) => ({
|
||||
key: item.id,
|
||||
name: item.label,
|
||||
...contentPublisher(item.pluginKey)
|
||||
}))
|
||||
],
|
||||
t
|
||||
)
|
||||
}
|
||||
|
||||
/** Plugin names for the plugins behind some content, keyed by plugin key. */
|
||||
export function disambiguatedContentPluginNames(
|
||||
items: readonly { pluginKey: string; pluginName: string }[]
|
||||
items: readonly { pluginKey: string; pluginName: string }[],
|
||||
t?: Translate
|
||||
): Map<string, string> {
|
||||
const byPlugin = new Map(items.map((item) => [item.pluginKey, item.pluginName]))
|
||||
return disambiguatedPluginNames(
|
||||
@@ -75,7 +86,8 @@ export function disambiguatedContentPluginNames(
|
||||
key: pluginKey,
|
||||
name,
|
||||
...contentPublisher(pluginKey)
|
||||
}))
|
||||
})),
|
||||
t
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { SettingsSearchEntry } from '../search/settings-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 { getNewWorktreeCardStyleSearchEntry } from './new-worktree-card-style-search-entry'
|
||||
import { getEphemeralVmsSearchEntry } from '../virtual-machines/recipes/ephemeral-vms-search'
|
||||
@@ -198,35 +199,26 @@ function findEntry(title: string): SettingsSearchEntry {
|
||||
return entry
|
||||
}
|
||||
|
||||
export function getExperimentalSearchEntry() {
|
||||
export function getExperimentalSearchEntry(t: Translate = translate) {
|
||||
return {
|
||||
pet: findEntry(translate('auto.components.settings.experimental.search.87d99e634b', 'Pet')),
|
||||
pet: findEntry(t('auto.components.settings.experimental.search.87d99e634b', 'Pet')),
|
||||
agentDashboard: findEntry(
|
||||
translate(
|
||||
'auto.components.settings.experimental.search.agentDashboard.title',
|
||||
'Agent Dashboard'
|
||||
)
|
||||
t('auto.components.settings.experimental.search.agentDashboard.title', 'Agent Dashboard')
|
||||
),
|
||||
terminalAttention: findEntry(
|
||||
translate('auto.components.settings.experimental.search.9e4ddf776d', 'Terminal attention')
|
||||
t('auto.components.settings.experimental.search.9e4ddf776d', 'Terminal attention')
|
||||
),
|
||||
agentHibernation: findEntry(
|
||||
translate(
|
||||
'auto.components.settings.experimental.search.agentHibernation.title',
|
||||
'Agent sleep'
|
||||
)
|
||||
t('auto.components.settings.experimental.search.agentHibernation.title', 'Agent sleep')
|
||||
),
|
||||
newWorktreeCardStyle: findEntry(
|
||||
translate(
|
||||
'auto.components.settings.experimental.search.newWorktreeCardStyle.title',
|
||||
'New card style'
|
||||
)
|
||||
t('auto.components.settings.experimental.search.newWorktreeCardStyle.title', 'New card style')
|
||||
),
|
||||
ephemeralVms: findEntry(
|
||||
translate('auto.components.settings.ephemeralVms.search.cloudVmTitle', 'Cloud VM')
|
||||
t('auto.components.settings.ephemeralVms.search.cloudVmTitle', 'Cloud VM')
|
||||
),
|
||||
browserExtensions: findEntry(
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.experimental.search.browserExtensions.title',
|
||||
'Browser extensions'
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import { Label } from '../../ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../ui/select'
|
||||
import { SearchableSetting } from '../section-layout/SearchableSetting'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
type BrowserDefaultZoomSettingProps = {
|
||||
value: number
|
||||
@@ -17,15 +17,13 @@ export function BrowserDefaultZoomSetting({
|
||||
value,
|
||||
onChange
|
||||
}: BrowserDefaultZoomSettingProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const selectedZoomLevel = normalizeBrowserPageZoomLevel(value)
|
||||
|
||||
return (
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.BrowserDefaultZoomSetting.265597101f',
|
||||
'Default Zoom'
|
||||
)}
|
||||
description={translate(
|
||||
title={t('auto.components.settings.BrowserDefaultZoomSetting.265597101f', 'Default Zoom')}
|
||||
description={t(
|
||||
'auto.components.settings.BrowserDefaultZoomSetting.siteDefault',
|
||||
"Zoom for sites you haven't zoomed."
|
||||
)}
|
||||
@@ -34,13 +32,10 @@ export function BrowserDefaultZoomSetting({
|
||||
>
|
||||
<div className="space-y-0.5">
|
||||
<Label>
|
||||
{translate(
|
||||
'auto.components.settings.BrowserDefaultZoomSetting.265597101f',
|
||||
'Default Zoom'
|
||||
)}
|
||||
{t('auto.components.settings.BrowserDefaultZoomSetting.265597101f', 'Default Zoom')}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.BrowserDefaultZoomSetting.siteDefault',
|
||||
"Zoom for sites you haven't zoomed."
|
||||
)}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Button } from '../../ui/button'
|
||||
import { Input } from '../../ui/input'
|
||||
import { Label } from '../../ui/label'
|
||||
import { SearchableSetting } from '../section-layout/SearchableSetting'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
type BrowserHomePageSettingProps = {
|
||||
value: string
|
||||
@@ -18,13 +18,11 @@ export function BrowserHomePageSetting({
|
||||
onChange,
|
||||
onSave
|
||||
}: BrowserHomePageSettingProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.BrowserHomePageSetting.70224e37b1',
|
||||
'Default Home Page'
|
||||
)}
|
||||
description={translate(
|
||||
title={t('auto.components.settings.BrowserHomePageSetting.70224e37b1', 'Default Home Page')}
|
||||
description={t(
|
||||
'auto.components.settings.BrowserHomePageSetting.6a37540f4b',
|
||||
'URL opened when creating a new browser tab. Leave empty to open a blank tab.'
|
||||
)}
|
||||
@@ -33,13 +31,10 @@ export function BrowserHomePageSetting({
|
||||
>
|
||||
<div className="min-w-0 shrink space-y-0.5">
|
||||
<Label>
|
||||
{translate(
|
||||
'auto.components.settings.BrowserHomePageSetting.70224e37b1',
|
||||
'Default Home Page'
|
||||
)}
|
||||
{t('auto.components.settings.BrowserHomePageSetting.70224e37b1', 'Default Home Page')}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.BrowserHomePageSetting.6a37540f4b',
|
||||
'URL opened when creating a new browser tab. Leave empty to open a blank tab.'
|
||||
)}
|
||||
@@ -58,10 +53,7 @@ export function BrowserHomePageSetting({
|
||||
if (normalized && normalized !== ORCA_BROWSER_BLANK_URL) {
|
||||
onSave(normalized)
|
||||
toast.success(
|
||||
translate(
|
||||
'auto.components.settings.BrowserHomePageSetting.c6cbd1c105',
|
||||
'Home page saved.'
|
||||
)
|
||||
t('auto.components.settings.BrowserHomePageSetting.c6cbd1c105', 'Home page saved.')
|
||||
)
|
||||
}
|
||||
}}
|
||||
@@ -69,7 +61,7 @@ export function BrowserHomePageSetting({
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
placeholder={translate(
|
||||
placeholder={t(
|
||||
'auto.components.settings.BrowserHomePageSetting.37a30c5bfd',
|
||||
'https://google.com'
|
||||
)}
|
||||
@@ -79,7 +71,7 @@ export function BrowserHomePageSetting({
|
||||
className="h-7 w-52 text-xs"
|
||||
/>
|
||||
<Button type="submit" size="sm" variant="outline" className="h-7 text-xs">
|
||||
{translate('auto.components.settings.BrowserHomePageSetting.d4ddcd0056', 'Save')}
|
||||
{t('auto.components.settings.BrowserHomePageSetting.d4ddcd0056', 'Save')}
|
||||
</Button>
|
||||
</form>
|
||||
</SearchableSetting>
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
} from '../../../../../shared/execution-host'
|
||||
import { isMacUserAgent } from '@/lib/user-agent-platform'
|
||||
import { isWebClientLocation } from '@/lib/web-client-location'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { useUiLanguageRevision } from '@/i18n/use-ui-language-revision'
|
||||
import { resolveAvailableBrowserSessionHostId } from './profiles/browser-session-host-selection'
|
||||
import { getDefaultBrowserProfileForHost } from '@/store/slices/browser/hosts/browser-host-state'
|
||||
@@ -62,6 +62,7 @@ export function BrowserPane({
|
||||
updateSettings,
|
||||
onOpenComputerUse
|
||||
}: BrowserPaneProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const languageRevision = useUiLanguageRevision()
|
||||
const searchQuery = useAppStore((s) => s.settingsSearchQuery)
|
||||
const browserDefaultUrl = useAppStore((s) => s.browserDefaultUrl)
|
||||
@@ -126,33 +127,37 @@ export function BrowserPane({
|
||||
const showSitePermissions = matchesSettingsSearch(searchQuery, [browserSearchEntries[11]])
|
||||
const showProfileScope = matchesSettingsSearch(searchQuery, [browserSearchEntries[12]])
|
||||
const showLinkRoutingBackground = matchesSettingsSearch(searchQuery, [browserSearchEntries[13]])
|
||||
const showPasswords = matchesSettingsSearch(searchQuery, [getBrowserPasswordsSearchEntry()])
|
||||
const showPasswords = matchesSettingsSearch(searchQuery, [getBrowserPasswordsSearchEntry(t)])
|
||||
const showBrowserUse = matchesSettingsSearch(searchQuery, getBrowserUsePaneSearchEntries())
|
||||
const isMac = isMacUserAgent()
|
||||
const linkRoutingDescription = getBrowserLinkRoutingDescription(
|
||||
{ isMac },
|
||||
settings.openLinksInAppModifierInverts === true
|
||||
settings.openLinksInAppModifierInverts === true,
|
||||
t
|
||||
)
|
||||
const hostLabelOverrides = useMemo(() => getHostDisplayLabelOverrides(settings), [settings])
|
||||
const browserSessionHostOptions = useMemo(
|
||||
() =>
|
||||
buildSidebarHostOptions({
|
||||
repos,
|
||||
sshTargetLabels,
|
||||
sshConnectionStates,
|
||||
settings,
|
||||
runtimeEnvironments,
|
||||
runtimeStatusByEnvironmentId,
|
||||
hostLabelOverrides
|
||||
})
|
||||
buildSidebarHostOptions(
|
||||
{
|
||||
repos,
|
||||
sshTargetLabels,
|
||||
sshConnectionStates,
|
||||
settings,
|
||||
runtimeEnvironments,
|
||||
runtimeStatusByEnvironmentId,
|
||||
hostLabelOverrides
|
||||
},
|
||||
t
|
||||
)
|
||||
.filter((host) => host.kind === 'local' || host.kind === 'runtime')
|
||||
.map((host) => ({
|
||||
id: host.id,
|
||||
label: host.label,
|
||||
detail:
|
||||
host.kind === 'local'
|
||||
? translate('auto.components.settings.BrowserPane.86b7c83fee', 'This computer')
|
||||
: translate(
|
||||
? t('auto.components.settings.BrowserPane.86b7c83fee', 'This computer')
|
||||
: t(
|
||||
'auto.components.settings.BrowserPane.c0f85056d9',
|
||||
'Browser profiles on this Orca server.'
|
||||
)
|
||||
@@ -166,7 +171,8 @@ export function BrowserPane({
|
||||
runtimeEnvironments,
|
||||
runtimeStatusByEnvironmentId,
|
||||
hostLabelOverrides,
|
||||
languageRevision
|
||||
languageRevision,
|
||||
t
|
||||
]
|
||||
)
|
||||
const settingsFocusedHostId = getSettingsFocusedExecutionHostId(settings)
|
||||
@@ -340,8 +346,8 @@ export function BrowserPane({
|
||||
|
||||
{showClientHostedRemote || showSshWorkspaceRouting ? (
|
||||
<SettingsGroup
|
||||
title={translate('settings.browser.remoteBrowsing.heading', 'Remote browsing')}
|
||||
description={translate(
|
||||
title={t('settings.browser.remoteBrowsing.heading', 'Remote browsing')}
|
||||
description={t(
|
||||
'settings.browser.remoteBrowsing.headingDescription',
|
||||
'Where remote workspace pages render, and where their network traffic leaves from.'
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '.
|
||||
import { SEARCH_ENGINE_LABELS, type SearchEngine } from '../../../../../shared/browser-url'
|
||||
import { SearchableSetting } from '../section-layout/SearchableSetting'
|
||||
import { KagiSessionLinkForm } from './KagiSessionLinkForm'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
type BrowserSearchEngineSettingProps = {
|
||||
selectedSearchEngine: SearchEngine
|
||||
@@ -14,10 +14,11 @@ export function BrowserSearchEngineSetting({
|
||||
selectedSearchEngine,
|
||||
onSearchEngineChange
|
||||
}: BrowserSearchEngineSettingProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<SearchableSetting
|
||||
title={translate('auto.components.settings.BrowserPane.0d9c987f21', 'Default Search Engine')}
|
||||
description={translate(
|
||||
title={t('auto.components.settings.BrowserPane.0d9c987f21', 'Default Search Engine')}
|
||||
description={t(
|
||||
'auto.components.settings.BrowserPane.7b225c78f5',
|
||||
'Search engine used when typing non-URL text in the address bar.'
|
||||
)}
|
||||
@@ -38,10 +39,10 @@ export function BrowserSearchEngineSetting({
|
||||
>
|
||||
<div className="space-y-0.5">
|
||||
<Label>
|
||||
{translate('auto.components.settings.BrowserPane.0d9c987f21', 'Default Search Engine')}
|
||||
{t('auto.components.settings.BrowserPane.0d9c987f21', 'Default Search Engine')}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.BrowserPane.3e46903ad4',
|
||||
'Used when typing non-URL text in the address bar.'
|
||||
)}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { SettingsRow } from '../form-controls/SettingsFormControls'
|
||||
import { getBrowserSitePermissionsSearchEntry } from './browser-site-permissions-search'
|
||||
import { LOCAL_EXECUTION_HOST_ID } from '../../../../../shared/execution-host'
|
||||
import { useAppStore } from '../../../store/index'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
function SitePermissionRow({
|
||||
entry,
|
||||
@@ -29,7 +29,8 @@ function SitePermissionRow({
|
||||
permission: BrowserSitePermission
|
||||
onChange: (permission: BrowserSitePermission, value: string) => void
|
||||
}): React.JSX.Element {
|
||||
const label = browserSitePermissionLabel(permission)
|
||||
const t = useTranslate()
|
||||
const label = browserSitePermissionLabel(permission, t)
|
||||
return (
|
||||
<SettingsRow
|
||||
label={label}
|
||||
@@ -43,13 +44,13 @@ function SitePermissionRow({
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="allow">
|
||||
{translate('settings.browser.sitePermissions.allow', 'Allow')}
|
||||
{t('settings.browser.sitePermissions.allow', 'Allow')}
|
||||
</SelectItem>
|
||||
<SelectItem value="block">
|
||||
{translate('settings.browser.sitePermissions.block', 'Block')}
|
||||
{t('settings.browser.sitePermissions.block', 'Block')}
|
||||
</SelectItem>
|
||||
<SelectItem value="ask">
|
||||
{translate('settings.browser.sitePermissions.ask', 'Ask again')}
|
||||
{t('settings.browser.sitePermissions.ask', 'Ask again')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -62,6 +63,7 @@ const NO_PROFILES: readonly BrowserSessionProfile[] = []
|
||||
|
||||
/** Remembered answers live on this computer, so only its own browser profiles are listed. */
|
||||
export function BrowserSitePermissionsSetting(): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const profiles =
|
||||
useAppStore((s) => s.browserSessionProfilesByHostId[LOCAL_EXECUTION_HOST_ID]) ?? NO_PROFILES
|
||||
const fetchBrowserSessionProfiles = useAppStore((s) => s.fetchBrowserSessionProfiles)
|
||||
@@ -129,7 +131,7 @@ export function BrowserSitePermissionsSetting(): React.JSX.Element {
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
className="max-w-48 shrink-0"
|
||||
aria-label={translate('settings.browser.sitePermissions.profile', 'Browser profile')}
|
||||
aria-label={t('settings.browser.sitePermissions.profile', 'Browser profile')}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
@@ -146,7 +148,7 @@ export function BrowserSitePermissionsSetting(): React.JSX.Element {
|
||||
|
||||
{entries?.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'settings.browser.sitePermissions.empty',
|
||||
'No sites have been allowed or blocked yet.'
|
||||
)}
|
||||
@@ -157,7 +159,7 @@ export function BrowserSitePermissionsSetting(): React.JSX.Element {
|
||||
{entries.map((entry) => (
|
||||
<div key={entry.origin} className="rounded-md border border-border/70 px-3 py-1">
|
||||
<p className="truncate pt-2 text-sm font-medium" title={entry.origin}>
|
||||
{browserSitePermissionSiteName(entry.origin)}
|
||||
{browserSitePermissionSiteName(entry.origin, t)}
|
||||
</p>
|
||||
{BROWSER_SITE_PERMISSIONS.filter((permission) => entry.decisions[permission]).map(
|
||||
(permission) => (
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
} from '../../../../../shared/browser-user-agent-mode'
|
||||
import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../../../shared/execution-host'
|
||||
import { BROWSER_USER_AGENT_SETTINGS_TARGET_ID } from '@/lib/settings-navigation-types'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { SearchableSetting } from '../section-layout/SearchableSetting'
|
||||
import { SettingsRow, SettingsSegmentedControl } from '../form-controls/SettingsFormControls'
|
||||
|
||||
@@ -16,11 +16,12 @@ type BrowserUserAgentSettingProps = {
|
||||
export function BrowserUserAgentSetting({
|
||||
hostId
|
||||
}: BrowserUserAgentSettingProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const [status, setStatus] = useState<BrowserIdentityModeStatus | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const title = translate('settings.browser.userAgent.title', 'Browser identity')
|
||||
const description = translate(
|
||||
const title = t('settings.browser.userAgent.title', 'Browser identity')
|
||||
const description = t(
|
||||
'settings.browser.userAgent.description',
|
||||
'Choose the user agent for every browser profile and page. Changes take effect after a restart.'
|
||||
)
|
||||
@@ -57,9 +58,7 @@ export function BrowserUserAgentSetting({
|
||||
.identitySet(mode)
|
||||
.then((result) => {
|
||||
if (!result) {
|
||||
setError(
|
||||
translate('settings.browser.userAgent.unavailable', 'Browser identity is unavailable.')
|
||||
)
|
||||
setError(t('settings.browser.userAgent.unavailable', 'Browser identity is unavailable.'))
|
||||
} else if (!result.ok) {
|
||||
setError(result.error.message)
|
||||
} else {
|
||||
@@ -74,7 +73,7 @@ export function BrowserUserAgentSetting({
|
||||
if (!isLocal) {
|
||||
control = (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'settings.browser.userAgent.remoteUnsupported',
|
||||
'Manage browser identity on the remote host with the Orca CLI.'
|
||||
)}
|
||||
@@ -83,7 +82,7 @@ export function BrowserUserAgentSetting({
|
||||
} else if (!status) {
|
||||
control = (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{error ?? translate('settings.browser.userAgent.loading', 'Loading…')}
|
||||
{error ?? t('settings.browser.userAgent.loading', 'Loading…')}
|
||||
</span>
|
||||
)
|
||||
} else if (status.identity.configuredMode === null) {
|
||||
@@ -91,13 +90,13 @@ export function BrowserUserAgentSetting({
|
||||
control = (
|
||||
<div className="space-y-1 text-right">
|
||||
<div className="text-xs text-destructive">
|
||||
{translate(
|
||||
{t(
|
||||
'settings.browser.userAgent.resetRequired',
|
||||
'Identity data must be reset explicitly before it can be changed.'
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'settings.browser.userAgent.resetRequiredCommand',
|
||||
'Reset it from the command line: orca browser identity set --mode <mode> --reset'
|
||||
)}
|
||||
@@ -116,8 +115,8 @@ export function BrowserUserAgentSetting({
|
||||
{
|
||||
value: 'clean',
|
||||
disabled: saving,
|
||||
label: translate('settings.browser.userAgent.optionClean', 'Cleaned'),
|
||||
tooltip: translate(
|
||||
label: t('settings.browser.userAgent.optionClean', 'Cleaned'),
|
||||
tooltip: t(
|
||||
'settings.browser.userAgent.optionCleanTooltip',
|
||||
'Removes Orca and Electron tokens to match imported Chrome sessions.'
|
||||
)
|
||||
@@ -125,8 +124,8 @@ export function BrowserUserAgentSetting({
|
||||
{
|
||||
value: 'native',
|
||||
disabled: saving,
|
||||
label: translate('settings.browser.userAgent.optionNative', 'Native'),
|
||||
tooltip: translate(
|
||||
label: t('settings.browser.userAgent.optionNative', 'Native'),
|
||||
tooltip: t(
|
||||
'settings.browser.userAgent.optionNativeTooltip',
|
||||
"Keeps Electron's built-in identity for sites that reject the cleaned identity."
|
||||
)
|
||||
@@ -135,7 +134,7 @@ export function BrowserUserAgentSetting({
|
||||
/>
|
||||
{status.identity.restartRequired ? (
|
||||
<div className="text-[11px] text-muted-foreground">
|
||||
{translate('settings.browser.userAgent.restartRequired', 'Restart required')}
|
||||
{t('settings.browser.userAgent.restartRequired', 'Restart required')}
|
||||
</div>
|
||||
) : null}
|
||||
{error ? <div className="text-[11px] text-destructive">{error}</div> : null}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { normalizeKagiSessionLink } from '../../../../../shared/browser-url'
|
||||
import { useAppStore } from '../../../store/index'
|
||||
import { Button } from '../../ui/button'
|
||||
import { Input } from '../../ui/input'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
export type KagiSessionLinkDraftState = {
|
||||
persisted: string
|
||||
@@ -26,6 +26,7 @@ export function resolveKagiSessionLinkDraftState(
|
||||
}
|
||||
|
||||
export function KagiSessionLinkForm(): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const browserKagiSessionLink = useAppStore((s) => s.browserKagiSessionLink)
|
||||
const setBrowserKagiSessionLink = useAppStore((s) => s.setBrowserKagiSessionLink)
|
||||
const persistedDraft = browserKagiSessionLink ?? ''
|
||||
@@ -49,17 +50,14 @@ export function KagiSessionLinkForm(): React.JSX.Element {
|
||||
setBrowserKagiSessionLink(null)
|
||||
setDraftState(createKagiSessionLinkDraftState(''))
|
||||
toast.success(
|
||||
translate(
|
||||
'auto.components.settings.KagiSessionLinkForm.9f741627a7',
|
||||
'Kagi session link cleared.'
|
||||
)
|
||||
t('auto.components.settings.KagiSessionLinkForm.9f741627a7', 'Kagi session link cleared.')
|
||||
)
|
||||
return
|
||||
}
|
||||
const normalized = normalizeKagiSessionLink(trimmed)
|
||||
if (!normalized) {
|
||||
toast.error(
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.KagiSessionLinkForm.0911d5fa4c',
|
||||
'Enter a Kagi private session link from https://kagi.com/search?token=...'
|
||||
)
|
||||
@@ -69,10 +67,7 @@ export function KagiSessionLinkForm(): React.JSX.Element {
|
||||
setBrowserKagiSessionLink(normalized)
|
||||
setDraftState(createKagiSessionLinkDraftState(normalized))
|
||||
toast.success(
|
||||
translate(
|
||||
'auto.components.settings.KagiSessionLinkForm.3e5b7c6c25',
|
||||
'Kagi session link saved.'
|
||||
)
|
||||
t('auto.components.settings.KagiSessionLinkForm.3e5b7c6c25', 'Kagi session link saved.')
|
||||
)
|
||||
}
|
||||
|
||||
@@ -85,7 +80,7 @@ export function KagiSessionLinkForm(): React.JSX.Element {
|
||||
}}
|
||||
>
|
||||
<p className="max-w-72 text-right text-[11px] leading-snug text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.KagiSessionLinkForm.81409d9362',
|
||||
'Optional private session link for Kagi auth.'
|
||||
)}
|
||||
@@ -95,7 +90,7 @@ export function KagiSessionLinkForm(): React.JSX.Element {
|
||||
type="password"
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
placeholder={translate(
|
||||
placeholder={t(
|
||||
'auto.components.settings.KagiSessionLinkForm.e383683485',
|
||||
'https://kagi.com/search?token=...'
|
||||
)}
|
||||
@@ -103,14 +98,14 @@ export function KagiSessionLinkForm(): React.JSX.Element {
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
autoComplete="off"
|
||||
aria-label={translate(
|
||||
aria-label={t(
|
||||
'auto.components.settings.KagiSessionLinkForm.ff450194cd',
|
||||
'Kagi private session link'
|
||||
)}
|
||||
className="h-7 w-72 text-xs"
|
||||
/>
|
||||
<Button type="submit" size="sm" variant="outline" className="h-7 text-xs">
|
||||
{translate('auto.components.settings.KagiSessionLinkForm.d5c8b94c5b', 'Save')}
|
||||
{t('auto.components.settings.KagiSessionLinkForm.d5c8b94c5b', 'Save')}
|
||||
</Button>
|
||||
{browserKagiSessionLink ? (
|
||||
<Button
|
||||
@@ -122,14 +117,14 @@ export function KagiSessionLinkForm(): React.JSX.Element {
|
||||
setBrowserKagiSessionLink(null)
|
||||
setDraftState(createKagiSessionLinkDraftState(''))
|
||||
toast.success(
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.KagiSessionLinkForm.9f741627a7',
|
||||
'Kagi session link cleared.'
|
||||
)
|
||||
)
|
||||
}}
|
||||
>
|
||||
{translate('auto.components.settings.KagiSessionLinkForm.92f0b4e472', 'Clear')}
|
||||
{t('auto.components.settings.KagiSessionLinkForm.92f0b4e472', 'Clear')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
+5
-4
@@ -1,5 +1,5 @@
|
||||
import type { GlobalSettings } from '../../../../../../shared/global-settings-types'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { Label } from '../../../ui/label'
|
||||
import { SearchableSetting } from '../../section-layout/SearchableSetting'
|
||||
import { SettingsSwitch } from '../../form-controls/SettingsFormControls'
|
||||
@@ -14,7 +14,8 @@ export function BrowserExtensionsExperimentalSetting({
|
||||
settings,
|
||||
updateSettings
|
||||
}: BrowserExtensionsExperimentalSettingProps): React.JSX.Element {
|
||||
const entry = getExperimentalSearchEntry().browserExtensions
|
||||
const t = useTranslate()
|
||||
const entry = getExperimentalSearchEntry(t).browserExtensions
|
||||
const enabled = settings.experimentalBrowserExtensions === true
|
||||
|
||||
return (
|
||||
@@ -29,7 +30,7 @@ export function BrowserExtensionsExperimentalSetting({
|
||||
<div className="min-w-0 shrink space-y-0.5">
|
||||
<Label>{entry.title}</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.ExperimentalPane.browserExtensions.copy',
|
||||
'Adds Settings › Browser › Extensions for local browser profiles.'
|
||||
)}
|
||||
@@ -37,7 +38,7 @@ export function BrowserExtensionsExperimentalSetting({
|
||||
</div>
|
||||
<SettingsSwitch
|
||||
checked={enabled}
|
||||
ariaLabel={translate(
|
||||
ariaLabel={t(
|
||||
'auto.components.settings.ExperimentalPane.browserExtensions.toggleLabel',
|
||||
'Toggle browser extensions'
|
||||
)}
|
||||
|
||||
+22
-24
@@ -6,7 +6,7 @@ import type {
|
||||
BrowserExtensionListing,
|
||||
BrowserExtensionListResult
|
||||
} from '../../../../../../shared/browser/extensions/browser-extension-types'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { Button } from '../../../ui/button'
|
||||
import { Label } from '../../../ui/label'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../../../ui/tooltip'
|
||||
@@ -40,11 +40,12 @@ function ExtensionRow({
|
||||
onReviewUpdate: () => void
|
||||
onRemove: () => void
|
||||
}): React.JSX.Element {
|
||||
const unsupported = describeUnsupportedBrowserExtensionFeatures(extension.permissions)
|
||||
const t = useTranslate()
|
||||
const unsupported = describeUnsupportedBrowserExtensionFeatures(extension.permissions, t)
|
||||
const sourceLabel =
|
||||
extension.source === 'unpacked'
|
||||
? translate('settings.browser.extensions.sourceUnpacked', 'Unpacked')
|
||||
: translate('settings.browser.extensions.sourceWebStore', 'Chrome Web Store')
|
||||
? t('settings.browser.extensions.sourceUnpacked', 'Unpacked')
|
||||
: t('settings.browser.extensions.sourceWebStore', 'Chrome Web Store')
|
||||
return (
|
||||
<div className="flex w-full items-center gap-3 rounded-md border border-border/70 px-3 py-2.5">
|
||||
{extension.iconDataUrl ? (
|
||||
@@ -59,7 +60,7 @@ function ExtensionRow({
|
||||
</p>
|
||||
{extension.heldUpdateVersion ? (
|
||||
<p className="text-2xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'settings.browser.extensions.heldUpdate',
|
||||
'Version {{version}} is waiting for more access.',
|
||||
{ version: extension.heldUpdateVersion }
|
||||
@@ -69,7 +70,7 @@ function ExtensionRow({
|
||||
{unsupported ? <p className="text-2xs text-muted-foreground">{unsupported}</p> : null}
|
||||
{extension.loadError ? (
|
||||
<p className="text-2xs break-words text-destructive">
|
||||
{translate('settings.browser.extensions.loadError', 'Could not start: {{error}}', {
|
||||
{t('settings.browser.extensions.loadError', 'Could not start: {{error}}', {
|
||||
error: extension.loadError
|
||||
})}
|
||||
</p>
|
||||
@@ -78,25 +79,21 @@ function ExtensionRow({
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
{extension.heldUpdateVersion ? (
|
||||
<Button variant="outline" size="xs" onClick={onReviewUpdate} disabled={busy}>
|
||||
{translate('settings.browser.extensions.reviewAccess', 'Review access')}
|
||||
{t('settings.browser.extensions.reviewAccess', 'Review access')}
|
||||
</Button>
|
||||
) : null}
|
||||
{extension.hasOptionsPage && extension.enabled ? (
|
||||
<Button variant="ghost" size="xs" onClick={onOpenOptions} disabled={busy}>
|
||||
<Settings2 className="size-3" />
|
||||
{translate('settings.browser.extensions.options', 'Options')}
|
||||
{t('settings.browser.extensions.options', 'Options')}
|
||||
</Button>
|
||||
) : null}
|
||||
<SettingsSwitch
|
||||
checked={extension.enabled}
|
||||
disabled={busy}
|
||||
ariaLabel={translate(
|
||||
'settings.browser.extensions.toggleLabel',
|
||||
'Turn {{name}} on or off',
|
||||
{
|
||||
name: extension.name
|
||||
}
|
||||
)}
|
||||
ariaLabel={t('settings.browser.extensions.toggleLabel', 'Turn {{name}} on or off', {
|
||||
name: extension.name
|
||||
})}
|
||||
onChange={onToggle}
|
||||
/>
|
||||
{extension.source === 'unpacked' ? (
|
||||
@@ -107,13 +104,13 @@ function ExtensionRow({
|
||||
size="icon-sm"
|
||||
onClick={onReload}
|
||||
disabled={busy}
|
||||
aria-label={translate('settings.browser.extensions.reload', 'Reload from folder')}
|
||||
aria-label={t('settings.browser.extensions.reload', 'Reload from folder')}
|
||||
>
|
||||
<RotateCw />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={4}>
|
||||
{translate('settings.browser.extensions.reload', 'Reload from folder')}
|
||||
{t('settings.browser.extensions.reload', 'Reload from folder')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
@@ -124,13 +121,13 @@ function ExtensionRow({
|
||||
size="icon-sm"
|
||||
onClick={onRemove}
|
||||
disabled={busy}
|
||||
aria-label={translate('settings.browser.extensions.remove', 'Remove')}
|
||||
aria-label={t('settings.browser.extensions.remove', 'Remove')}
|
||||
>
|
||||
<Trash2 />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={4}>
|
||||
{translate('settings.browser.extensions.remove', 'Remove')}
|
||||
{t('settings.browser.extensions.remove', 'Remove')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -142,6 +139,7 @@ export function BrowserExtensionsSection({
|
||||
profileId,
|
||||
profileLabel
|
||||
}: BrowserExtensionsSectionProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const confirm = useConfirmationDialog()
|
||||
const entry = getBrowserExtensionsSearchEntry()
|
||||
const [listing, setListing] = useState<{
|
||||
@@ -199,11 +197,11 @@ export function BrowserExtensionsSection({
|
||||
|
||||
const remove = async (extension: BrowserExtensionListing): Promise<void> => {
|
||||
const confirmed = await confirm({
|
||||
title: translate('settings.browser.extensions.removeTitle', 'Remove {{name}}?', {
|
||||
title: t('settings.browser.extensions.removeTitle', 'Remove {{name}}?', {
|
||||
name: extension.name
|
||||
}),
|
||||
description: browserExtensionRemovalDescription(extension.source),
|
||||
confirmLabel: translate('settings.browser.extensions.remove', 'Remove'),
|
||||
confirmLabel: t('settings.browser.extensions.remove', 'Remove'),
|
||||
confirmVariant: 'destructive',
|
||||
surface: 'security'
|
||||
})
|
||||
@@ -226,7 +224,7 @@ export function BrowserExtensionsSection({
|
||||
<div className="min-w-0 space-y-0.5">
|
||||
<Label>{entry.title}</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'settings.browser.extensions.profileDescription',
|
||||
'Extensions turned on here run in tabs that use the {{profile}} profile.',
|
||||
{ profile: profileLabel }
|
||||
@@ -241,7 +239,7 @@ export function BrowserExtensionsSection({
|
||||
onClick={() => void loadUnpacked()}
|
||||
>
|
||||
<FolderOpen className="size-3" />
|
||||
{translate('settings.browser.extensions.loadUnpacked', 'Load unpacked…')}
|
||||
{t('settings.browser.extensions.loadUnpacked', 'Load unpacked…')}
|
||||
</Button>
|
||||
</div>
|
||||
<BrowserExtensionsWebStoreInstall
|
||||
@@ -251,7 +249,7 @@ export function BrowserExtensionsSection({
|
||||
/>
|
||||
{active && result.extensions.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'settings.browser.extensions.empty',
|
||||
'No extensions installed. Add one from the Chrome Web Store above.'
|
||||
)}
|
||||
|
||||
+9
-8
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { CHROME_WEB_STORE_HOME_URL } from '../../../../../../shared/browser/extensions/browser-extension-webstore'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { useAppStore } from '@/store/index'
|
||||
import { LoadingSpinner } from '../../../LoadingSpinner'
|
||||
import { Button } from '../../../ui/button'
|
||||
@@ -19,6 +19,7 @@ export function BrowserExtensionsWebStoreInstall({
|
||||
disabled,
|
||||
onFinished
|
||||
}: BrowserExtensionsWebStoreInstallProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const [input, setInput] = useState('')
|
||||
const [installing, setInstalling] = useState(false)
|
||||
const openBrowserProfileTab = useAppStore((state) => state.openBrowserProfileTabInActiveWorkspace)
|
||||
@@ -34,7 +35,7 @@ export function BrowserExtensionsWebStoreInstall({
|
||||
if (result.status === 'installed') {
|
||||
setInput('')
|
||||
toast.success(
|
||||
translate('settings.browser.extensions.webStore.added', 'Added {{name}}', {
|
||||
t('settings.browser.extensions.webStore.added', 'Added {{name}}', {
|
||||
name: result.name
|
||||
})
|
||||
)
|
||||
@@ -52,7 +53,7 @@ export function BrowserExtensionsWebStoreInstall({
|
||||
closeSettingsPage()
|
||||
} else {
|
||||
toast.error(
|
||||
translate(
|
||||
t(
|
||||
'settings.browser.extensions.webStore.openFailed',
|
||||
'Open a workspace to browse the Chrome Web Store.'
|
||||
)
|
||||
@@ -73,11 +74,11 @@ export function BrowserExtensionsWebStoreInstall({
|
||||
value={input}
|
||||
onChange={(event) => setInput(event.target.value)}
|
||||
disabled={disabled || installing}
|
||||
placeholder={translate(
|
||||
placeholder={t(
|
||||
'settings.browser.extensions.webStore.placeholder',
|
||||
'Chrome Web Store link or extension ID'
|
||||
)}
|
||||
aria-label={translate(
|
||||
aria-label={t(
|
||||
'settings.browser.extensions.webStore.inputLabel',
|
||||
'Add from Chrome Web Store'
|
||||
)}
|
||||
@@ -94,8 +95,8 @@ export function BrowserExtensionsWebStoreInstall({
|
||||
>
|
||||
{installing ? <LoadingSpinner className="size-4" /> : null}
|
||||
{installing
|
||||
? translate('settings.browser.extensions.webStore.adding', 'Adding…')
|
||||
: translate('settings.browser.extensions.webStore.add', 'Add')}
|
||||
? t('settings.browser.extensions.webStore.adding', 'Adding…')
|
||||
: t('settings.browser.extensions.webStore.add', 'Add')}
|
||||
</Button>
|
||||
</form>
|
||||
<Button
|
||||
@@ -106,7 +107,7 @@ export function BrowserExtensionsWebStoreInstall({
|
||||
disabled={disabled}
|
||||
onClick={() => void openStore()}
|
||||
>
|
||||
{translate('settings.browser.extensions.webStore.browse', 'Browse the Chrome Web Store')}
|
||||
{t('settings.browser.extensions.webStore.browse', 'Browse the Chrome Web Store')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
+17
-18
@@ -13,6 +13,7 @@ import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import type {
|
||||
BrowserPasswordEntryEditResult,
|
||||
BrowserPasswordEntryInput
|
||||
@@ -88,6 +89,7 @@ function BrowserPasswordEntryForm({
|
||||
start: BrowserPasswordEntryFormStart
|
||||
onDone: (saved: boolean) => void
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const [draft, setDraft] = useState<BrowserPasswordEntryInput>(
|
||||
start.mode === 'edit'
|
||||
? { site: start.site, username: start.username, password: start.password, note: start.note }
|
||||
@@ -137,11 +139,11 @@ function BrowserPasswordEntryForm({
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{start.mode === 'edit'
|
||||
? translate('settings.browser.passwords.form.editTitle', 'Edit password')
|
||||
: translate('settings.browser.passwords.form.addTitle', 'Add password')}
|
||||
? t('settings.browser.passwords.form.editTitle', 'Edit password')
|
||||
: t('settings.browser.passwords.form.addTitle', 'Add password')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{translate(
|
||||
{t(
|
||||
'settings.browser.passwords.form.description',
|
||||
'Orca fills it when you pick it under a sign-in field on this site.'
|
||||
)}
|
||||
@@ -150,15 +152,12 @@ function BrowserPasswordEntryForm({
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="browser-password-site">
|
||||
{translate('settings.browser.passwords.form.site', 'Site')}
|
||||
{t('settings.browser.passwords.form.site', 'Site')}
|
||||
</Label>
|
||||
<Input
|
||||
id="browser-password-site"
|
||||
value={draft.site}
|
||||
placeholder={translate(
|
||||
'settings.browser.passwords.form.sitePlaceholder',
|
||||
'example.com'
|
||||
)}
|
||||
placeholder={t('settings.browser.passwords.form.sitePlaceholder', 'example.com')}
|
||||
disabled={start.mode === 'edit'}
|
||||
autoFocus={start.mode === 'add'}
|
||||
onChange={(event) => set('site')(event.target.value)}
|
||||
@@ -166,12 +165,12 @@ function BrowserPasswordEntryForm({
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="browser-password-username">
|
||||
{translate('settings.browser.passwords.form.username', 'Username')}
|
||||
{t('settings.browser.passwords.form.username', 'Username')}
|
||||
</Label>
|
||||
<Input
|
||||
id="browser-password-username"
|
||||
value={draft.username}
|
||||
placeholder={translate(
|
||||
placeholder={t(
|
||||
'settings.browser.passwords.form.usernamePlaceholder',
|
||||
'Email or username'
|
||||
)}
|
||||
@@ -181,7 +180,7 @@ function BrowserPasswordEntryForm({
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="browser-password-password">
|
||||
{translate('settings.browser.passwords.form.password', 'Password')}
|
||||
{t('settings.browser.passwords.form.password', 'Password')}
|
||||
</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
@@ -198,15 +197,15 @@ function BrowserPasswordEntryForm({
|
||||
onClick={() => setShowPassword((shown) => !shown)}
|
||||
aria-label={
|
||||
showPassword
|
||||
? translate('settings.browser.passwords.hide', 'Hide password')
|
||||
: translate('settings.browser.passwords.show', 'Show password')
|
||||
? t('settings.browser.passwords.hide', 'Hide password')
|
||||
: t('settings.browser.passwords.show', 'Show password')
|
||||
}
|
||||
>
|
||||
{showPassword ? <EyeOff /> : <Eye />}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-2xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'settings.browser.passwords.form.currentHint',
|
||||
'Save the password you use on this site today.'
|
||||
)}
|
||||
@@ -215,7 +214,7 @@ function BrowserPasswordEntryForm({
|
||||
{showNote ? (
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="browser-password-note">
|
||||
{translate('settings.browser.passwords.form.note', 'Note')}
|
||||
{t('settings.browser.passwords.form.note', 'Note')}
|
||||
</Label>
|
||||
<Textarea
|
||||
id="browser-password-note"
|
||||
@@ -227,17 +226,17 @@ function BrowserPasswordEntryForm({
|
||||
) : (
|
||||
<Button type="button" variant="ghost" size="xs" onClick={() => setShowNote(true)}>
|
||||
<Plus />
|
||||
{translate('settings.browser.passwords.form.addNote', 'Add a note')}
|
||||
{t('settings.browser.passwords.form.addNote', 'Add a note')}
|
||||
</Button>
|
||||
)}
|
||||
{error ? <p className="text-xs text-destructive">{error}</p> : null}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="ghost" size="sm" onClick={() => onDone(false)}>
|
||||
{translate('settings.browser.passwords.form.cancel', 'Cancel')}
|
||||
{t('settings.browser.passwords.form.cancel', 'Cancel')}
|
||||
</Button>
|
||||
<Button type="submit" size="sm" disabled={!canSave}>
|
||||
{translate('settings.browser.passwords.form.save', 'Save')}
|
||||
{t('settings.browser.passwords.form.save', 'Save')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
+4
-3
@@ -9,7 +9,7 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
// Families whose `Login Data` Orca can read; Firefox and Safari keep passwords elsewhere.
|
||||
@@ -26,6 +26,7 @@ export function BrowserPasswordImportMenu({
|
||||
onImportCsv: () => void
|
||||
onImportBrowser: (source: BrowserPasswordImportSource) => void
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const detectedBrowsers = useAppStore((s) => s.detectedBrowsers)
|
||||
const detectedBrowsersHost = useAppStore((s) => s.detectedBrowsersHost)
|
||||
const fetchDetectedBrowsers = useAppStore((s) => s.fetchDetectedBrowsers)
|
||||
@@ -39,13 +40,13 @@ export function BrowserPasswordImportMenu({
|
||||
<DropdownMenu onOpenChange={(open) => open && void fetchDetectedBrowsers()}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="button" variant="outline" size="xs" disabled={disabled}>
|
||||
{translate('settings.browser.passwords.import', 'Import')}
|
||||
{t('settings.browser.passwords.import', 'Import')}
|
||||
<ChevronDown />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-60">
|
||||
<DropdownMenuItem onSelect={onImportCsv}>
|
||||
{translate('settings.browser.passwords.importCsv', 'From a CSV file…')}
|
||||
{t('settings.browser.passwords.importCsv', 'From a CSV file…')}
|
||||
</DropdownMenuItem>
|
||||
{importableBrowsers.map((browser) => (
|
||||
<Fragment key={browser.family}>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useIntlLocale, useTranslate, type Translate } from '@/i18n/use-translate'
|
||||
import { formatUiRelativeTime } from '@/i18n/relative-time-format'
|
||||
import type { BrowserPasswordEntrySummary } from '../../../../../../shared/browser-passwords-types'
|
||||
import { browserPasswordHostOf } from './browser-password-import-toast'
|
||||
@@ -38,8 +39,8 @@ function groupBySite(entries: BrowserPasswordEntrySummary[]): SiteGroup[] {
|
||||
)
|
||||
}
|
||||
|
||||
function usernameText(entry: BrowserPasswordEntrySummary): string {
|
||||
return entry.username || translate('settings.browser.passwords.noUsername', '(no username)')
|
||||
function usernameText(entry: BrowserPasswordEntrySummary, t: Translate = translate): string {
|
||||
return entry.username || t('settings.browser.passwords.noUsername', '(no username)')
|
||||
}
|
||||
|
||||
function SiteTile({ host }: { host: string }): React.JSX.Element {
|
||||
@@ -56,12 +57,13 @@ function SiteTile({ host }: { host: string }): React.JSX.Element {
|
||||
|
||||
/** Host, plus the scheme when it is plain http so it can't pass for the https login. */
|
||||
function SiteName({ group }: { group: SiteGroup }): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<div className="truncate text-sm">
|
||||
{group.host}
|
||||
{group.origin.startsWith('http:') ? (
|
||||
<span className="text-2xs text-muted-foreground">
|
||||
{translate('settings.browser.passwords.httpSuffix', ' · http')}
|
||||
{t('settings.browser.passwords.httpSuffix', ' · http')}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -81,15 +83,17 @@ function PasswordRow({
|
||||
now: number
|
||||
actions: BrowserPasswordRowActions
|
||||
}): React.JSX.Element {
|
||||
const account = translate('settings.browser.passwords.account', '{{username}} on {{site}}', {
|
||||
username: usernameText(entry),
|
||||
const t = useTranslate()
|
||||
const intlLocale = useIntlLocale()
|
||||
const account = t('settings.browser.passwords.account', '{{username}} on {{site}}', {
|
||||
username: usernameText(entry, t),
|
||||
site: group.host
|
||||
})
|
||||
const lastUsed =
|
||||
entry.lastUsedAt === null
|
||||
? null
|
||||
: translate('settings.browser.passwords.lastUsed', 'Used {{when}}', {
|
||||
when: formatUiRelativeTime(entry.lastUsedAt - now)
|
||||
: t('settings.browser.passwords.lastUsed', 'Used {{when}}', {
|
||||
when: formatUiRelativeTime(entry.lastUsedAt - now, intlLocale)
|
||||
})
|
||||
return (
|
||||
<div className="group/pw flex items-center gap-3 rounded-md px-2 py-2 transition-colors hover:bg-accent/60 focus-within:bg-accent/60">
|
||||
@@ -97,7 +101,7 @@ function PasswordRow({
|
||||
<div className="min-w-0 flex-1">
|
||||
{nested ? null : <SiteName group={group} />}
|
||||
<div className={nested ? 'truncate text-sm' : 'truncate text-xs text-muted-foreground'}>
|
||||
{usernameText(entry)}
|
||||
{usernameText(entry, t)}
|
||||
</div>
|
||||
</div>
|
||||
{lastUsed ? <div className="shrink-0 text-2xs text-muted-foreground">{lastUsed}</div> : null}
|
||||
@@ -107,14 +111,10 @@ function PasswordRow({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
aria-label={translate(
|
||||
'settings.browser.passwords.copyFor',
|
||||
'Copy password for {{account}}',
|
||||
{
|
||||
account
|
||||
}
|
||||
)}
|
||||
title={translate('settings.browser.passwords.copy', 'Copy password')}
|
||||
aria-label={t('settings.browser.passwords.copyFor', 'Copy password for {{account}}', {
|
||||
account
|
||||
})}
|
||||
title={t('settings.browser.passwords.copy', 'Copy password')}
|
||||
onClick={() => actions.onCopy(entry)}
|
||||
>
|
||||
<Copy />
|
||||
@@ -123,10 +123,10 @@ function PasswordRow({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
aria-label={translate('settings.browser.passwords.editFor', 'Edit {{account}}', {
|
||||
aria-label={t('settings.browser.passwords.editFor', 'Edit {{account}}', {
|
||||
account
|
||||
})}
|
||||
title={translate('settings.browser.passwords.edit', 'Edit')}
|
||||
title={t('settings.browser.passwords.edit', 'Edit')}
|
||||
onClick={() => actions.onEdit(entry)}
|
||||
>
|
||||
<Pencil />
|
||||
@@ -137,7 +137,7 @@ function PasswordRow({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
aria-label={translate(
|
||||
aria-label={t(
|
||||
'settings.browser.passwords.moreActions',
|
||||
'More actions for {{account}}',
|
||||
{ account }
|
||||
@@ -149,7 +149,7 @@ function PasswordRow({
|
||||
<DropdownMenuContent align="end" className="min-w-36">
|
||||
<DropdownMenuItem variant="destructive" onSelect={() => actions.onDelete(entry)}>
|
||||
<Trash2 />
|
||||
{translate('settings.browser.passwords.delete', 'Delete…')}
|
||||
{t('settings.browser.passwords.delete', 'Delete…')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -168,6 +168,7 @@ export function BrowserPasswordList({
|
||||
now: number
|
||||
actions: BrowserPasswordRowActions
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
// Why a capped scroll box: a vault can hold hundreds of logins and must not push the rest of Settings away.
|
||||
<div className="scrollbar-sleek max-h-[min(26rem,55vh)] divide-y divide-border/50 overflow-y-auto overscroll-contain rounded-md border border-border/50 p-1">
|
||||
@@ -188,7 +189,7 @@ export function BrowserPasswordList({
|
||||
<div className="min-w-0 flex-1">
|
||||
<SiteName group={group} />
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{translate('settings.browser.passwords.accountCount', '{{count}} accounts', {
|
||||
{t('settings.browser.passwords.accountCount', '{{count}} accounts', {
|
||||
count: group.entries.length
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { browserPasswordHostOf } from './browser-password-import-toast'
|
||||
|
||||
/** Sites the user told the save offer to skip; removing one lets Orca offer again there. */
|
||||
@@ -12,6 +12,7 @@ export function BrowserPasswordNeverList({
|
||||
origins: string[]
|
||||
onRemove: (origin: string) => void
|
||||
}): React.JSX.Element | null {
|
||||
const t = useTranslate()
|
||||
if (origins.length === 0) {
|
||||
return null
|
||||
}
|
||||
@@ -20,7 +21,7 @@ export function BrowserPasswordNeverList({
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button type="button" variant="ghost" size="xs">
|
||||
<ChevronRight className="transition-transform in-data-[state=open]:rotate-90" />
|
||||
{translate('settings.browser.passwords.neverHeading', 'Never saved for {{count}} sites', {
|
||||
{t('settings.browser.passwords.neverHeading', 'Never saved for {{count}} sites', {
|
||||
count: origins.length
|
||||
})}
|
||||
</Button>
|
||||
@@ -31,7 +32,7 @@ export function BrowserPasswordNeverList({
|
||||
<div key={origin} className="flex items-center gap-3 py-1.5 pl-5">
|
||||
<div className="min-w-0 flex-1 truncate text-xs">{browserPasswordHostOf(origin)}</div>
|
||||
<Button type="button" variant="ghost" size="xs" onClick={() => onRemove(origin)}>
|
||||
{translate('settings.browser.passwords.removeNever', 'Remove')}
|
||||
{t('settings.browser.passwords.removeNever', 'Remove')}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
+8
-10
@@ -1,4 +1,4 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import type { BrowserPasswordReauthMethod } from '../../../../../../shared/browser-passwords-types'
|
||||
import type { GlobalSettings } from '../../../../../../shared/global-settings-types'
|
||||
import { SettingsSwitchRow } from '../../form-controls/SettingsFormControls'
|
||||
@@ -16,13 +16,14 @@ export function BrowserPasswordPreferences({
|
||||
updateSettings: (updates: Partial<GlobalSettings>) => void
|
||||
reauthMethod: BrowserPasswordReauthMethod | null
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const offerToSave = settings.browserPasswordsOfferToSave !== false
|
||||
const fillRequiresReauth = settings.browserPasswordsFillRequiresReauth === true
|
||||
return (
|
||||
<div className="divide-y divide-border/50">
|
||||
<SettingsSwitchRow
|
||||
label={translate('settings.browser.passwords.offerToSave', 'Offer to save passwords')}
|
||||
description={translate(
|
||||
label={t('settings.browser.passwords.offerToSave', 'Offer to save passwords')}
|
||||
description={t(
|
||||
'settings.browser.passwords.offerToSaveDescription',
|
||||
'After you sign in to a site in Orca’s browser, offer to save the login.'
|
||||
)}
|
||||
@@ -30,22 +31,19 @@ export function BrowserPasswordPreferences({
|
||||
onChange={() => updateSettings({ browserPasswordsOfferToSave: !offerToSave })}
|
||||
/>
|
||||
<SettingsSwitchRow
|
||||
label={translate(
|
||||
'settings.browser.passwords.fillReauth',
|
||||
'Ask for Touch ID before filling'
|
||||
)}
|
||||
label={t('settings.browser.passwords.fillReauth', 'Ask for Touch ID before filling')}
|
||||
description={
|
||||
reauthMethod === 'touch-id'
|
||||
? translate(
|
||||
? t(
|
||||
'settings.browser.passwords.fillReauthDescription',
|
||||
'For a shared computer. Showing or copying a password already asks.'
|
||||
)
|
||||
: reauthMethod === 'touch-id-unavailable'
|
||||
? translate(
|
||||
? t(
|
||||
'settings.browser.passwords.reauthUnavailable',
|
||||
'Touch ID can’t be used right now, so Orca won’t show, copy or fill a password that needs it.'
|
||||
)
|
||||
: translate(
|
||||
: t(
|
||||
'settings.browser.passwords.noReauth',
|
||||
'This computer can’t confirm it’s you with Touch ID, so passwords show and fill without a check.'
|
||||
)
|
||||
|
||||
+28
-32
@@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useConfirmationDialog } from '@/components/confirmation-dialog-context'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import type {
|
||||
BrowserPasswordEntrySummary,
|
||||
BrowserPasswordImportResult,
|
||||
@@ -53,7 +53,8 @@ type BrowserPasswordsSectionProps = {
|
||||
|
||||
/** Saved logins for the profile picked under Session & Cookies, as Extensions does. */
|
||||
export function BrowserPasswordsSection(props: BrowserPasswordsSectionProps): React.JSX.Element {
|
||||
const entry = getBrowserPasswordsSearchEntry()
|
||||
const t = useTranslate()
|
||||
const entry = getBrowserPasswordsSearchEntry(t)
|
||||
return (
|
||||
<SearchableSetting
|
||||
id="browser-passwords"
|
||||
@@ -73,6 +74,7 @@ function BrowserPasswordsProfile({
|
||||
settings,
|
||||
updateSettings
|
||||
}: BrowserPasswordsSectionProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const api = window.api.browserPasswords
|
||||
const confirm = useConfirmationDialog()
|
||||
const [view, setView] = useState<VaultView>({
|
||||
@@ -123,7 +125,7 @@ function BrowserPasswordsProfile({
|
||||
void runImport(() => api.importFromBrowser({ browserProfileId, ...source }))
|
||||
|
||||
const authFailed = (): void => {
|
||||
toast.error(translate('settings.browser.passwords.authFailed', 'Could not confirm it was you'))
|
||||
toast.error(t('settings.browser.passwords.authFailed', 'Could not confirm it was you'))
|
||||
}
|
||||
|
||||
const openAdd = (): void => {
|
||||
@@ -158,7 +160,7 @@ function BrowserPasswordsProfile({
|
||||
const result = await api.copyPassword({ browserProfileId, entryId: item.id })
|
||||
if (result.ok) {
|
||||
toast.success(
|
||||
translate(
|
||||
t(
|
||||
'settings.browser.passwords.copied',
|
||||
'Password copied. Orca clears it from the clipboard in 2 minutes.'
|
||||
)
|
||||
@@ -170,20 +172,19 @@ function BrowserPasswordsProfile({
|
||||
|
||||
const remove = async (item: BrowserPasswordEntrySummary): Promise<void> => {
|
||||
const confirmed = await confirm({
|
||||
title: translate(
|
||||
title: t(
|
||||
'settings.browser.passwords.deleteConfirmTitle',
|
||||
'Delete the {{site}} password for {{username}}?',
|
||||
{
|
||||
site: browserPasswordHostOf(item.origin),
|
||||
username:
|
||||
item.username || translate('settings.browser.passwords.noUsername', '(no username)')
|
||||
username: item.username || t('settings.browser.passwords.noUsername', '(no username)')
|
||||
}
|
||||
),
|
||||
description: translate(
|
||||
description: t(
|
||||
'settings.browser.passwords.deleteConfirmDescription',
|
||||
'Orca won’t fill it anymore. This can’t be undone.'
|
||||
),
|
||||
confirmLabel: translate('settings.browser.passwords.deleteConfirm', 'Delete'),
|
||||
confirmLabel: t('settings.browser.passwords.deleteConfirm', 'Delete'),
|
||||
confirmVariant: 'destructive'
|
||||
})
|
||||
if (confirmed && (await api.deleteEntry({ browserProfileId, entryId: item.id }))) {
|
||||
@@ -199,20 +200,17 @@ function BrowserPasswordsProfile({
|
||||
|
||||
const deleteAll = async (): Promise<void> => {
|
||||
const confirmed = await confirm({
|
||||
title: translate(
|
||||
'settings.browser.passwords.deleteAllConfirmTitle',
|
||||
'Delete all password data?'
|
||||
),
|
||||
description: translate(
|
||||
title: t('settings.browser.passwords.deleteAllConfirmTitle', 'Delete all password data?'),
|
||||
description: t(
|
||||
'settings.browser.passwords.deleteAllConfirmDescription',
|
||||
'Every saved password in the {{profile}} profile is permanently deleted. This can’t be undone.',
|
||||
{ profile: profileLabel }
|
||||
),
|
||||
confirmLabel: translate('settings.browser.passwords.deleteAll', 'Delete data'),
|
||||
confirmLabel: t('settings.browser.passwords.deleteAll', 'Delete data'),
|
||||
confirmVariant: 'destructive'
|
||||
})
|
||||
if (confirmed && (await api.deleteAll({ browserProfileId }))) {
|
||||
toast.success(translate('settings.browser.passwords.deletedAll', 'Password data deleted'))
|
||||
toast.success(t('settings.browser.passwords.deletedAll', 'Password data deleted'))
|
||||
await refresh()
|
||||
}
|
||||
}
|
||||
@@ -226,9 +224,9 @@ function BrowserPasswordsProfile({
|
||||
<>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0 space-y-0.5">
|
||||
<Label>{getBrowserPasswordsSearchEntry().title}</Label>
|
||||
<Label>{getBrowserPasswordsSearchEntry(t).title}</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'settings.browser.passwords.profileDescription',
|
||||
'Saved logins for the {{profile}} profile. Orca fills one only when you pick it under a sign-in field.',
|
||||
{ profile: profileLabel }
|
||||
@@ -249,7 +247,7 @@ function BrowserPasswordsProfile({
|
||||
onClick={openAdd}
|
||||
>
|
||||
<Plus />
|
||||
{translate('settings.browser.passwords.add', 'Add')}
|
||||
{t('settings.browser.passwords.add', 'Add')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -257,7 +255,7 @@ function BrowserPasswordsProfile({
|
||||
{unavailableReason ? (
|
||||
<div className="rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2.5">
|
||||
<p className="text-sm">
|
||||
{translate(
|
||||
{t(
|
||||
'settings.browser.passwords.unavailableTitle',
|
||||
'Saved passwords are off on this computer'
|
||||
)}
|
||||
@@ -277,10 +275,10 @@ function BrowserPasswordsProfile({
|
||||
<KeyRound className="size-5 text-muted-foreground/60" />
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm">
|
||||
{translate('settings.browser.passwords.emptyTitle', 'No saved passwords yet')}
|
||||
{t('settings.browser.passwords.emptyTitle', 'No saved passwords yet')}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'settings.browser.passwords.empty',
|
||||
'Orca offers to save a login after you sign in. You can also bring them from another browser or a CSV file.'
|
||||
)}
|
||||
@@ -295,12 +293,10 @@ function BrowserPasswordsProfile({
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder={translate(
|
||||
'settings.browser.passwords.search',
|
||||
'Search {{count}} passwords',
|
||||
{ count: view.entries.length }
|
||||
)}
|
||||
aria-label={translate('settings.browser.passwords.searchLabel', 'Search passwords')}
|
||||
placeholder={t('settings.browser.passwords.search', 'Search {{count}} passwords', {
|
||||
count: view.entries.length
|
||||
})}
|
||||
aria-label={t('settings.browser.passwords.searchLabel', 'Search passwords')}
|
||||
density="compact"
|
||||
/>
|
||||
) : null}
|
||||
@@ -316,7 +312,7 @@ function BrowserPasswordsProfile({
|
||||
/>
|
||||
) : (
|
||||
<p className="py-4 text-center text-xs text-muted-foreground">
|
||||
{translate('settings.browser.passwords.noMatches', 'No saved passwords match.')}
|
||||
{t('settings.browser.passwords.noMatches', 'No saved passwords match.')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -332,15 +328,15 @@ function BrowserPasswordsProfile({
|
||||
{ready ? (
|
||||
<SettingsRow
|
||||
className="border-t border-border/50"
|
||||
label={translate('settings.browser.passwords.deleteAllTitle', 'Delete all password data')}
|
||||
description={translate(
|
||||
label={t('settings.browser.passwords.deleteAllTitle', 'Delete all password data')}
|
||||
description={t(
|
||||
'settings.browser.passwords.deleteAllDescription',
|
||||
'Every saved password, note and never-save site in the {{profile}} profile.',
|
||||
{ profile: profileLabel }
|
||||
)}
|
||||
control={
|
||||
<Button type="button" variant="destructive" size="xs" onClick={() => void deleteAll()}>
|
||||
{translate('settings.browser.passwords.deleteAllAction', 'Delete…')}
|
||||
{t('settings.browser.passwords.deleteAllAction', 'Delete…')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,31 +1,72 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { Translate } from '@/i18n/use-translate'
|
||||
import type { SettingsSearchEntry } from '../../search/settings-search'
|
||||
import { translateSearchKeyword } from '../../search/settings-search-keywords'
|
||||
|
||||
export function getBrowserPasswordsTitle(): string {
|
||||
return translate('settings.browser.passwords.title', 'Passwords')
|
||||
export function getBrowserPasswordsTitle(t: Translate = translate): string {
|
||||
return t('settings.browser.passwords.title', 'Passwords')
|
||||
}
|
||||
|
||||
export function getBrowserPasswordsDescription(): string {
|
||||
return translate(
|
||||
export function getBrowserPasswordsDescription(t: Translate = translate): string {
|
||||
return t(
|
||||
'settings.browser.passwords.description',
|
||||
'Saved logins for Orca’s browser: add, import, edit and delete them.'
|
||||
)
|
||||
}
|
||||
|
||||
export function getBrowserPasswordsSearchEntry(): SettingsSearchEntry {
|
||||
export function getBrowserPasswordsSearchEntry(t?: Translate): SettingsSearchEntry {
|
||||
return {
|
||||
title: getBrowserPasswordsTitle(),
|
||||
description: getBrowserPasswordsDescription(),
|
||||
title: getBrowserPasswordsTitle(t),
|
||||
description: getBrowserPasswordsDescription(t),
|
||||
keywords: [
|
||||
...translateSearchKeyword('auto.components.settings.browser.search.2d2d995c58', 'browser'),
|
||||
...translateSearchKeyword('settings.browser.passwords.searchKeywords.passwords', 'passwords'),
|
||||
...translateSearchKeyword('settings.browser.passwords.searchKeywords.logins', 'logins'),
|
||||
...translateSearchKeyword('settings.browser.passwords.searchKeywords.autofill', 'autofill'),
|
||||
...translateSearchKeyword('settings.browser.passwords.searchKeywords.import', 'import'),
|
||||
...translateSearchKeyword('settings.browser.passwords.searchKeywords.csv', 'csv'),
|
||||
...translateSearchKeyword('settings.browser.passwords.searchKeywords.vault', 'vault'),
|
||||
...translateSearchKeyword('settings.browser.passwords.searchKeywords.touchId', 'touch id')
|
||||
...translateSearchKeyword(
|
||||
'auto.components.settings.browser.search.2d2d995c58',
|
||||
'browser',
|
||||
undefined,
|
||||
t
|
||||
),
|
||||
...translateSearchKeyword(
|
||||
'settings.browser.passwords.searchKeywords.passwords',
|
||||
'passwords',
|
||||
undefined,
|
||||
t
|
||||
),
|
||||
...translateSearchKeyword(
|
||||
'settings.browser.passwords.searchKeywords.logins',
|
||||
'logins',
|
||||
undefined,
|
||||
t
|
||||
),
|
||||
...translateSearchKeyword(
|
||||
'settings.browser.passwords.searchKeywords.autofill',
|
||||
'autofill',
|
||||
undefined,
|
||||
t
|
||||
),
|
||||
...translateSearchKeyword(
|
||||
'settings.browser.passwords.searchKeywords.import',
|
||||
'import',
|
||||
undefined,
|
||||
t
|
||||
),
|
||||
...translateSearchKeyword(
|
||||
'settings.browser.passwords.searchKeywords.csv',
|
||||
'csv',
|
||||
undefined,
|
||||
t
|
||||
),
|
||||
...translateSearchKeyword(
|
||||
'settings.browser.passwords.searchKeywords.vault',
|
||||
'vault',
|
||||
undefined,
|
||||
t
|
||||
),
|
||||
...translateSearchKeyword(
|
||||
'settings.browser.passwords.searchKeywords.touchId',
|
||||
'touch id',
|
||||
undefined,
|
||||
t
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Input } from '../../../ui/input'
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '../../../ui/dialog'
|
||||
import { useAppStore } from '../../../../store/index'
|
||||
import { useMountedRef } from '@/hooks/useMountedRef'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
type BrowserNewProfileDialogProps = {
|
||||
open: boolean
|
||||
@@ -16,6 +16,7 @@ export function BrowserNewProfileDialog({
|
||||
open,
|
||||
onOpenChange
|
||||
}: BrowserNewProfileDialogProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const mountedRef = useMountedRef()
|
||||
const [newProfileName, setNewProfileName] = useState('')
|
||||
const [isCreatingProfile, setIsCreatingProfile] = useState(false)
|
||||
@@ -37,7 +38,7 @@ export function BrowserNewProfileDialog({
|
||||
<DialogContent className="sm:max-w-sm" showCloseButton={false}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base">
|
||||
{translate('auto.components.settings.BrowserPane.8481ee0331', 'New Browser Profile')}
|
||||
{t('auto.components.settings.BrowserPane.8481ee0331', 'New Browser Profile')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form
|
||||
@@ -58,7 +59,7 @@ export function BrowserNewProfileDialog({
|
||||
if (profile) {
|
||||
handleClose()
|
||||
toast.success(
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.BrowserPane.8f22b7580d',
|
||||
'Profile "{{value0}}" created.',
|
||||
{ value0: profile.label }
|
||||
@@ -66,10 +67,7 @@ export function BrowserNewProfileDialog({
|
||||
)
|
||||
} else {
|
||||
toast.error(
|
||||
translate(
|
||||
'auto.components.settings.BrowserPane.612f7f6861',
|
||||
'Failed to create profile.'
|
||||
)
|
||||
t('auto.components.settings.BrowserPane.612f7f6861', 'Failed to create profile.')
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
@@ -82,22 +80,19 @@ export function BrowserNewProfileDialog({
|
||||
<Input
|
||||
value={newProfileName}
|
||||
onChange={(e) => setNewProfileName(e.target.value)}
|
||||
placeholder={translate(
|
||||
'auto.components.settings.BrowserPane.7d4c0a2aa4',
|
||||
'Profile name'
|
||||
)}
|
||||
placeholder={t('auto.components.settings.BrowserPane.7d4c0a2aa4', 'Profile name')}
|
||||
autoFocus
|
||||
maxLength={50}
|
||||
className="mb-3"
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" size="sm" onClick={handleClose}>
|
||||
{translate('auto.components.settings.BrowserPane.81ff774667', 'Cancel')}
|
||||
{t('auto.components.settings.BrowserPane.81ff774667', 'Cancel')}
|
||||
</Button>
|
||||
<Button type="submit" size="sm" disabled={!newProfileName.trim() || isCreatingProfile}>
|
||||
{isCreatingProfile
|
||||
? translate('auto.components.settings.BrowserPane.7b649a578a', 'Creating…')
|
||||
: translate('auto.components.settings.BrowserPane.64898ecdab', 'Create')}
|
||||
? t('auto.components.settings.BrowserPane.7b649a578a', 'Creating…')
|
||||
: t('auto.components.settings.BrowserPane.64898ecdab', 'Create')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
@@ -21,7 +21,7 @@ import { BrowserCookieImportDisclosure } from '../../../BrowserCookieImportDiscl
|
||||
import { BrowserCookieImportMachineNotice } from '../../../BrowserCookieImportMachineNotice'
|
||||
import { useAppStore } from '../../../../store/index'
|
||||
import { BROWSER_FAMILY_LABELS } from '../../../../../../shared/constants'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { browserCookieImportFailureText } from '@/lib/browser/cookies/browser-cookie-import-error-copy'
|
||||
|
||||
type DetectedBrowser = {
|
||||
@@ -53,6 +53,7 @@ export function BrowserProfileRow({
|
||||
onSelect,
|
||||
isDefault
|
||||
}: BrowserProfileRowProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const isImporting = importState?.profileId === profile.id && importState.status === 'importing'
|
||||
const fetchDetectedBrowsers = useAppStore((s) => s.fetchDetectedBrowsers)
|
||||
|
||||
@@ -68,7 +69,7 @@ export function BrowserProfileRow({
|
||||
emitBrowserCookieImportToast(
|
||||
result.summary,
|
||||
browserProfile
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.BrowserProfileRow.a3f8c2d1e0b4',
|
||||
'Imported {{value0}} cookies from {{value1}} ({{value2}}) into {{value3}}.',
|
||||
{
|
||||
@@ -78,7 +79,7 @@ export function BrowserProfileRow({
|
||||
value3: profile.label
|
||||
}
|
||||
)
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.BrowserProfileRow.b4e9d3f2a1c5',
|
||||
'Imported {{value0}} cookies from {{value1}} into {{value2}}.',
|
||||
{
|
||||
@@ -99,7 +100,7 @@ export function BrowserProfileRow({
|
||||
if (result.ok) {
|
||||
emitBrowserCookieImportToast(
|
||||
result.summary,
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.BrowserProfileRow.b4c167764d',
|
||||
'Imported {{value0}} cookies from file into {{value1}}.',
|
||||
{ value0: result.summary.importedCookies, value1: profile.label }
|
||||
@@ -113,7 +114,7 @@ export function BrowserProfileRow({
|
||||
|
||||
const sourceLabel = profile.source
|
||||
? `${BROWSER_FAMILY_LABELS[profile.source.browserFamily] ?? profile.source.browserFamily}${profile.source.profileName ? ` (${profile.source.profileName})` : ''}`
|
||||
: translate('auto.components.settings.BrowserProfileRow.796d846483', 'No cookies imported')
|
||||
: t('auto.components.settings.BrowserProfileRow.796d846483', 'No cookies imported')
|
||||
|
||||
// Use role=button because nested dropdown/trash buttons make a native button invalid and trigger hydration warnings.
|
||||
return (
|
||||
@@ -138,7 +139,7 @@ export function BrowserProfileRow({
|
||||
<span className="truncate text-sm font-medium">{profile.label}</span>
|
||||
{isActive ? (
|
||||
<span className="shrink-0 rounded border border-border/50 px-1.5 text-[10px] font-medium leading-4 text-foreground/80">
|
||||
{translate('auto.components.settings.BrowserProfileRow.c29648fe5b', 'Active')}
|
||||
{t('auto.components.settings.BrowserProfileRow.c29648fe5b', 'Active')}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -165,7 +166,7 @@ export function BrowserProfileRow({
|
||||
) : (
|
||||
<Import className="size-3" />
|
||||
)}
|
||||
{translate('auto.components.settings.BrowserProfileRow.cdec84552f', 'Import Cookies')}
|
||||
{t('auto.components.settings.BrowserProfileRow.cdec84552f', 'Import Cookies')}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
@@ -174,11 +175,9 @@ export function BrowserProfileRow({
|
||||
browser.profiles.length > 1 ? (
|
||||
<DropdownMenuSub key={browser.family}>
|
||||
<DropdownMenuSubTrigger>
|
||||
{translate(
|
||||
'auto.components.settings.BrowserProfileRow.c5a273a809',
|
||||
'From {{value0}}',
|
||||
{ value0: browser.label }
|
||||
)}
|
||||
{t('auto.components.settings.BrowserProfileRow.c5a273a809', 'From {{value0}}', {
|
||||
value0: browser.label
|
||||
})}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
@@ -200,17 +199,15 @@ export function BrowserProfileRow({
|
||||
key={browser.family}
|
||||
onSelect={() => void handleImportFromBrowser(browser.family)}
|
||||
>
|
||||
{translate(
|
||||
'auto.components.settings.BrowserProfileRow.c5a273a809',
|
||||
'From {{value0}}',
|
||||
{ value0: browser.label }
|
||||
)}
|
||||
{t('auto.components.settings.BrowserProfileRow.c5a273a809', 'From {{value0}}', {
|
||||
value0: browser.label
|
||||
})}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
)}
|
||||
{detectedBrowsers.length > 0 && <DropdownMenuSeparator />}
|
||||
<DropdownMenuItem onSelect={() => void handleImportFromFile()}>
|
||||
{translate('auto.components.settings.BrowserProfileRow.ebb78dfd6f', 'From File…')}
|
||||
{t('auto.components.settings.BrowserProfileRow.ebb78dfd6f', 'From File…')}
|
||||
</DropdownMenuItem>
|
||||
<BrowserCookieImportDisclosure />
|
||||
</DropdownMenuContent>
|
||||
@@ -225,7 +222,7 @@ export function BrowserProfileRow({
|
||||
const ok = await useAppStore.getState().clearDefaultSessionCookies()
|
||||
if (ok) {
|
||||
toast.success(
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.BrowserProfileRow.2d4bea7f35',
|
||||
'Default cookies cleared.'
|
||||
)
|
||||
@@ -244,7 +241,7 @@ export function BrowserProfileRow({
|
||||
const ok = await useAppStore.getState().deleteBrowserSessionProfile(profile.id)
|
||||
if (ok) {
|
||||
toast.success(
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.BrowserProfileRow.8e636cae25',
|
||||
'Profile "{{value0}}" removed.',
|
||||
{ value0: profile.label }
|
||||
|
||||
+8
-7
@@ -1,4 +1,4 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import type { GlobalSettings } from '../../../../../../shared/global-settings-types'
|
||||
import { SearchableSetting } from '../../section-layout/SearchableSetting'
|
||||
import { SettingsRow, SettingsSegmentedControl } from '../../form-controls/SettingsFormControls'
|
||||
@@ -13,6 +13,7 @@ export function BrowserProfileScopeSetting({
|
||||
settings,
|
||||
updateSettings
|
||||
}: BrowserProfileScopeSettingProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const { title, description, keywords } = getBrowserProfileScopeSearchEntry()
|
||||
return (
|
||||
<SearchableSetting
|
||||
@@ -33,24 +34,24 @@ export function BrowserProfileScopeSetting({
|
||||
options={[
|
||||
{
|
||||
value: 'shared',
|
||||
label: translate('settings.browser.profileScope.optionShared', 'Shared'),
|
||||
tooltip: translate(
|
||||
label: t('settings.browser.profileScope.optionShared', 'Shared'),
|
||||
tooltip: t(
|
||||
'settings.browser.profileScope.optionSharedTooltip',
|
||||
'New tabs use the Default profile (or the one picked under Session & Cookies).'
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'repo',
|
||||
label: translate('settings.browser.profileScope.optionRepo', 'Per repo'),
|
||||
tooltip: translate(
|
||||
label: t('settings.browser.profileScope.optionRepo', 'Per repo'),
|
||||
tooltip: t(
|
||||
'settings.browser.profileScope.optionRepoTooltip',
|
||||
"Each repo's workspaces share one profile, created on first use."
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'worktree',
|
||||
label: translate('settings.browser.profileScope.optionWorktree', 'Per workspace'),
|
||||
tooltip: translate(
|
||||
label: t('settings.browser.profileScope.optionWorktree', 'Per workspace'),
|
||||
tooltip: t(
|
||||
'settings.browser.profileScope.optionWorktreeTooltip',
|
||||
'Each workspace gets its own profile, deleted with the workspace.'
|
||||
)
|
||||
|
||||
+27
-18
@@ -7,7 +7,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '.
|
||||
import { SearchableSetting } from '../../section-layout/SearchableSetting'
|
||||
import { BrowserProfileRow, type BrowserProfileRowProps } from './BrowserProfileRow'
|
||||
import type { ExecutionHostId } from '../../../../../../shared/execution-host'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { translateSearchKeyword } from '../../search/settings-search-keywords'
|
||||
|
||||
type BrowserSessionHostOption = {
|
||||
@@ -43,6 +43,7 @@ export function BrowserSessionCookiesSection({
|
||||
onSelectDefaultProfile,
|
||||
onSelectProfile
|
||||
}: BrowserSessionCookiesSectionProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const activeProfile = [defaultProfile, ...nonDefaultProfiles].find(
|
||||
(profile) => profile?.id === activeProfileId
|
||||
)
|
||||
@@ -55,14 +56,24 @@ export function BrowserSessionCookiesSection({
|
||||
return (
|
||||
<SearchableSetting
|
||||
id="browser-session-cookies"
|
||||
title={translate('auto.components.settings.BrowserPane.113cd2dc9b', 'Session & Cookies')}
|
||||
description={translate(
|
||||
title={t('auto.components.settings.BrowserPane.113cd2dc9b', 'Session & Cookies')}
|
||||
description={t(
|
||||
'auto.components.settings.BrowserPane.aa1074bfe9',
|
||||
'Manage browser profiles and import cookies from Chrome, Edge, Comet, or other browsers.'
|
||||
)}
|
||||
keywords={[
|
||||
...translateSearchKeyword('auto.components.settings.browser.search.8a489aab8d', 'google'),
|
||||
...translateSearchKeyword('browser.googleCookieClear.signInKeyword', 'sign-in'),
|
||||
...translateSearchKeyword(
|
||||
'auto.components.settings.browser.search.8a489aab8d',
|
||||
'google',
|
||||
undefined,
|
||||
t
|
||||
),
|
||||
...translateSearchKeyword(
|
||||
'browser.googleCookieClear.signInKeyword',
|
||||
'sign-in',
|
||||
undefined,
|
||||
t
|
||||
),
|
||||
'cookies',
|
||||
'session',
|
||||
'import',
|
||||
@@ -77,21 +88,19 @@ export function BrowserSessionCookiesSection({
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="space-y-0.5">
|
||||
<Label>
|
||||
{translate('auto.components.settings.BrowserPane.2d66a6efb5', 'Session & Cookies')}
|
||||
</Label>
|
||||
<Label>{t('auto.components.settings.BrowserPane.2d66a6efb5', 'Session & Cookies')}</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.BrowserPane.cd47bc9622',
|
||||
'Select a default profile for new browser tabs. Import cookies and switch profiles per-tab via the'
|
||||
)}{' '}
|
||||
<strong>···</strong>{' '}
|
||||
{translate('auto.components.settings.BrowserPane.e4aaf8051b', 'toolbar menu.')}
|
||||
{t('auto.components.settings.BrowserPane.e4aaf8051b', 'toolbar menu.')}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" size="xs" onClick={onAddProfile} className="shrink-0 gap-1.5">
|
||||
<Plus className="size-3" />
|
||||
{translate('auto.components.settings.BrowserPane.6f2584b39e', 'Add Profile')}
|
||||
{t('auto.components.settings.BrowserPane.6f2584b39e', 'Add Profile')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -99,11 +108,11 @@ export function BrowserSessionCookiesSection({
|
||||
<div className="flex items-center justify-between gap-3 rounded-md border border-border/70 px-3 py-2">
|
||||
<div className="min-w-0 space-y-0.5">
|
||||
<Label className="text-xs">
|
||||
{translate('auto.components.settings.BrowserPane.5e19a692f7', 'Host')}
|
||||
{t('auto.components.settings.BrowserPane.5e19a692f7', 'Host')}
|
||||
</Label>
|
||||
<p className="truncate text-[11px] text-muted-foreground">
|
||||
{selectedHost?.detail ??
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.BrowserPane.6480776a03',
|
||||
'Browser profiles for the selected host.'
|
||||
)}
|
||||
@@ -134,7 +143,7 @@ export function BrowserSessionCookiesSection({
|
||||
id: 'default',
|
||||
scope: 'default',
|
||||
partition: '',
|
||||
label: translate('auto.components.settings.BrowserPane.4399c77caa', 'Default'),
|
||||
label: t('auto.components.settings.BrowserPane.4399c77caa', 'Default'),
|
||||
source: null
|
||||
}
|
||||
}
|
||||
@@ -157,16 +166,16 @@ export function BrowserSessionCookiesSection({
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3 pt-3">
|
||||
<div className="min-w-0 space-y-0.5">
|
||||
<Label>{translate('browser.googleCookieClear.heading', 'Google Cookies')}</Label>
|
||||
<Label>{t('browser.googleCookieClear.heading', 'Google Cookies')}</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'browser.googleCookieClear.explanation',
|
||||
"Google ties its sign-ins to the browser they were made in, so Orca doesn't import Google cookies from other browsers. If Google is stuck on the wrong account or keeps signing you out, clear Google cookies from the active profile, then sign in to Google again in Orca. Other sites stay signed in."
|
||||
)}
|
||||
</p>
|
||||
{googleCookieClear.available === false ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'browser.googleCookieClear.unavailable',
|
||||
'Unavailable for this profile on the selected host.'
|
||||
)}
|
||||
@@ -180,7 +189,7 @@ export function BrowserSessionCookiesSection({
|
||||
disabled={!googleCookieClear.available || googleCookieClear.busy}
|
||||
onClick={() => void googleCookieClear.onClick()}
|
||||
>
|
||||
{translate('browser.googleCookieClear.action', 'Clear Google cookies…')}
|
||||
{t('browser.googleCookieClear.action', 'Clear Google cookies…')}
|
||||
</Button>
|
||||
</div>
|
||||
</SearchableSetting>
|
||||
|
||||
+8
-10
@@ -1,4 +1,4 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import type { GlobalSettings } from '../../../../../../shared/global-settings-types'
|
||||
import { BROWSER_CLIENT_HOSTED_REMOTE_SETTINGS_TARGET_ID } from '@/lib/settings-navigation-types'
|
||||
import { SearchableSetting } from '../../section-layout/SearchableSetting'
|
||||
@@ -17,8 +17,9 @@ export function BrowserClientHostedRemoteSetting({
|
||||
settings,
|
||||
updateSettings
|
||||
}: BrowserClientHostedRemoteSettingProps): React.JSX.Element {
|
||||
const title = getBrowserClientHostedRemoteTitle()
|
||||
const description = getBrowserClientHostedRemoteDescription()
|
||||
const t = useTranslate()
|
||||
const title = getBrowserClientHostedRemoteTitle(t)
|
||||
const description = getBrowserClientHostedRemoteDescription(t)
|
||||
|
||||
return (
|
||||
<SearchableSetting
|
||||
@@ -53,19 +54,16 @@ export function BrowserClientHostedRemoteSetting({
|
||||
options={[
|
||||
{
|
||||
value: 'device',
|
||||
label: translate('settings.browser.clientHostedRemote.optionDevice', 'This device'),
|
||||
tooltip: translate(
|
||||
label: t('settings.browser.clientHostedRemote.optionDevice', 'This device'),
|
||||
tooltip: t(
|
||||
'settings.browser.clientHostedRemote.optionDeviceTooltip',
|
||||
'Pages render on this desktop, so input and popups behave natively.'
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'server',
|
||||
label: translate(
|
||||
'settings.browser.clientHostedRemote.optionServer',
|
||||
'Server (streamed)'
|
||||
),
|
||||
tooltip: translate(
|
||||
label: t('settings.browser.clientHostedRemote.optionServer', 'Server (streamed)'),
|
||||
tooltip: t(
|
||||
'settings.browser.clientHostedRemote.optionServerTooltip',
|
||||
'Pages render on the remote server and stream to this device.'
|
||||
)
|
||||
|
||||
+3
-1
@@ -2,6 +2,7 @@ import type { GlobalSettings } from '../../../../../../shared/global-settings-ty
|
||||
import { SearchableSetting } from '../../section-layout/SearchableSetting'
|
||||
import { SettingsSwitchRow } from '../../form-controls/SettingsFormControls'
|
||||
import { getLinkRoutingBackgroundSearchEntry } from './browser-link-routing-copy'
|
||||
import { useTranslate } from '../../../../i18n/use-translate'
|
||||
|
||||
type BrowserLinkRoutingBackgroundSettingProps = {
|
||||
settings: Pick<GlobalSettings, 'openLinksInAppInBackground'>
|
||||
@@ -12,7 +13,8 @@ export function BrowserLinkRoutingBackgroundSetting({
|
||||
settings,
|
||||
updateSettings
|
||||
}: BrowserLinkRoutingBackgroundSettingProps): React.JSX.Element {
|
||||
const entry = getLinkRoutingBackgroundSearchEntry()
|
||||
const t = useTranslate()
|
||||
const entry = getLinkRoutingBackgroundSearchEntry(t)
|
||||
|
||||
return (
|
||||
<SearchableSetting
|
||||
|
||||
+4
-2
@@ -5,6 +5,7 @@ import {
|
||||
getLinkRoutingModifierDescription,
|
||||
getLinkRoutingModifierTitle
|
||||
} from './browser-link-routing-copy'
|
||||
import { useTranslate } from '../../../../i18n/use-translate'
|
||||
|
||||
type BrowserLinkRoutingModifierSettingProps = {
|
||||
settings: Pick<GlobalSettings, 'openLinksInApp' | 'openLinksInAppModifierInverts'>
|
||||
@@ -17,9 +18,10 @@ export function BrowserLinkRoutingModifierSetting({
|
||||
isMac,
|
||||
updateSettings
|
||||
}: BrowserLinkRoutingModifierSettingProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const openLinksInApp = settings.openLinksInApp === true
|
||||
const title = getLinkRoutingModifierTitle(openLinksInApp)
|
||||
const description = getLinkRoutingModifierDescription({ openLinksInApp, isMac })
|
||||
const title = getLinkRoutingModifierTitle(openLinksInApp, t)
|
||||
const description = getLinkRoutingModifierDescription({ openLinksInApp, isMac }, t)
|
||||
|
||||
return (
|
||||
<SearchableSetting
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { GlobalSettings } from '../../../../../../shared/global-settings-ty
|
||||
import { Label } from '../../../ui/label'
|
||||
import { Switch } from '../../../ui/switch'
|
||||
import { SearchableSetting } from '../../section-layout/SearchableSetting'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
type BrowserLinkRoutingSettingProps = {
|
||||
settings: GlobalSettings
|
||||
@@ -17,9 +17,10 @@ export function BrowserLinkRoutingSetting({
|
||||
isMac,
|
||||
updateSettings
|
||||
}: BrowserLinkRoutingSettingProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<SearchableSetting
|
||||
title={translate('auto.components.settings.BrowserPane.d3eb69c0aa', 'Link Routing')}
|
||||
title={t('auto.components.settings.BrowserPane.d3eb69c0aa', 'Link Routing')}
|
||||
description={linkRoutingDescription}
|
||||
keywords={[
|
||||
'browser',
|
||||
@@ -35,13 +36,11 @@ export function BrowserLinkRoutingSetting({
|
||||
className="flex items-center justify-between gap-4 py-3"
|
||||
>
|
||||
<div className="space-y-0.5">
|
||||
<Label>
|
||||
{translate('auto.components.settings.BrowserPane.d3eb69c0aa', 'Link Routing')}
|
||||
</Label>
|
||||
<Label>{t('auto.components.settings.BrowserPane.d3eb69c0aa', 'Link Routing')}</Label>
|
||||
<p className="text-xs text-muted-foreground">{linkRoutingDescription}</p>
|
||||
</div>
|
||||
<Switch
|
||||
aria-label={translate('auto.components.settings.BrowserPane.d3eb69c0aa', 'Link Routing')}
|
||||
aria-label={t('auto.components.settings.BrowserPane.d3eb69c0aa', 'Link Routing')}
|
||||
checked={settings.openLinksInApp}
|
||||
onCheckedChange={(checked) =>
|
||||
updateSettings({
|
||||
|
||||
+4
-3
@@ -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 { SettingsSwitchRow } from '../../form-controls/SettingsFormControls'
|
||||
|
||||
@@ -12,11 +12,12 @@ export function BrowserLocalhostWorktreeLabelsSetting({
|
||||
settings,
|
||||
updateSettings
|
||||
}: BrowserLocalhostWorktreeLabelsSettingProps): React.JSX.Element {
|
||||
const title = translate(
|
||||
const t = useTranslate()
|
||||
const title = t(
|
||||
'auto.components.settings.BrowserLocalhostWorktreeLabelsSetting.8ac8c3ad19',
|
||||
'Localhost Worktree Labels'
|
||||
)
|
||||
const description = translate(
|
||||
const description = t(
|
||||
'auto.components.settings.BrowserLocalhostWorktreeLabelsSetting.1db3c8b983',
|
||||
'Open workspace ports as worktree-specific Orca localhost URLs so browser tabs are easier to tell apart.'
|
||||
)
|
||||
|
||||
+10
-12
@@ -1,4 +1,4 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { useAppStore } from '@/store/index'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { BROWSER_SSH_WORKSPACE_ROUTING_SETTINGS_TARGET_ID } from '@/lib/settings-navigation-types'
|
||||
@@ -22,8 +22,9 @@ export function BrowserSshWorkspaceRoutingSetting({
|
||||
settings,
|
||||
updateSettings
|
||||
}: BrowserSshWorkspaceRoutingSettingProps): React.JSX.Element {
|
||||
const title = getBrowserSshWorkspaceRoutingTitle()
|
||||
const description = getBrowserSshWorkspaceRoutingDescription()
|
||||
const t = useTranslate()
|
||||
const title = getBrowserSshWorkspaceRoutingTitle(t)
|
||||
const description = getBrowserSshWorkspaceRoutingDescription(t)
|
||||
const sshTargetLabels = useAppStore((s) => s.sshTargetLabels)
|
||||
const disabledTargetIds = settings.browserSshWorkspaceRoutingDisabledTargetIds ?? []
|
||||
|
||||
@@ -60,19 +61,16 @@ export function BrowserSshWorkspaceRoutingSetting({
|
||||
options={[
|
||||
{
|
||||
value: 'host',
|
||||
label: translate('settings.browser.sshWorkspaceRouting.optionHost', 'SSH host'),
|
||||
tooltip: translate(
|
||||
label: t('settings.browser.sshWorkspaceRouting.optionHost', 'SSH host'),
|
||||
tooltip: t(
|
||||
'settings.browser.sshWorkspaceRouting.optionHostTooltip',
|
||||
"Traffic and DNS go through the workspace's SSH host."
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'device',
|
||||
label: translate(
|
||||
'settings.browser.sshWorkspaceRouting.optionDevice',
|
||||
'This device'
|
||||
),
|
||||
tooltip: translate(
|
||||
label: t('settings.browser.sshWorkspaceRouting.optionDevice', 'This device'),
|
||||
tooltip: t(
|
||||
'settings.browser.sshWorkspaceRouting.optionDeviceTooltip',
|
||||
"Pages browse from this machine's network."
|
||||
)
|
||||
@@ -84,7 +82,7 @@ export function BrowserSshWorkspaceRoutingSetting({
|
||||
{disabledTargetIds.length > 0 ? (
|
||||
<div className="mt-2 flex flex-col gap-1.5">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'settings.browser.sshWorkspaceRouting.disabledHosts',
|
||||
'Hosts browsing from this device instead:'
|
||||
)}
|
||||
@@ -105,7 +103,7 @@ export function BrowserSshWorkspaceRoutingSetting({
|
||||
})
|
||||
}
|
||||
>
|
||||
{translate('settings.browser.sshWorkspaceRouting.useHost', 'Use SSH host')}
|
||||
{t('settings.browser.sshWorkspaceRouting.useHost', 'Use SSH host')}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
+13
-12
@@ -1,5 +1,5 @@
|
||||
import type { GlobalSettings } from '../../../../../../shared/global-settings-types'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { BROWSER_TERMINAL_LINK_ACTIONS_SETTINGS_TARGET_ID } from '@/lib/settings-navigation-types'
|
||||
import { SearchableSetting } from '../../section-layout/SearchableSetting'
|
||||
import { SettingsRow, SettingsSegmentedControl } from '../../form-controls/SettingsFormControls'
|
||||
@@ -26,48 +26,49 @@ export function BrowserTerminalLinkActionsSetting({
|
||||
isMac,
|
||||
updateSettings
|
||||
}: BrowserTerminalLinkActionsSettingProps): React.JSX.Element {
|
||||
const title = translate(
|
||||
const t = useTranslate()
|
||||
const title = t(
|
||||
'auto.components.settings.BrowserTerminalLinkActionsSetting.title',
|
||||
'Terminal URL clicks'
|
||||
)
|
||||
const description = translate(
|
||||
const description = t(
|
||||
'auto.components.settings.BrowserTerminalLinkActionsSetting.descriptionV2',
|
||||
'Control clicks on detected URLs printed in terminal panes and chat transcripts.'
|
||||
)
|
||||
const behavior = terminalLinkClickBehaviorFor(settings)
|
||||
const plainClickLabel = translate(
|
||||
const plainClickLabel = t(
|
||||
'auto.components.settings.BrowserTerminalLinkActionsSetting.plainClickLabel',
|
||||
'Plain click'
|
||||
)
|
||||
const plainClickDescription = translate(
|
||||
const plainClickDescription = t(
|
||||
'auto.components.settings.BrowserTerminalLinkActionsSetting.plainClickDescription',
|
||||
'Choose whether a left-click shows actions, opens the URL, or leaves it to the terminal. Cmd/Ctrl-click always opens directly.'
|
||||
)
|
||||
const plainClickAriaLabel = translate(
|
||||
const plainClickAriaLabel = t(
|
||||
'auto.components.settings.BrowserTerminalLinkActionsSetting.plainClickAriaLabel',
|
||||
'Plain click URL behavior'
|
||||
)
|
||||
const middleClickLabel = translate(
|
||||
const middleClickLabel = t(
|
||||
'auto.components.settings.BrowserTerminalLinkActionsSetting.middleClickLabel',
|
||||
'Middle click'
|
||||
)
|
||||
const middleClickDescription = translate(
|
||||
const middleClickDescription = t(
|
||||
'auto.components.settings.BrowserTerminalLinkActionsSetting.middleClickDescription',
|
||||
'Choose what a mouse-wheel click does on a detected terminal URL.'
|
||||
)
|
||||
const middleClickAriaLabel = translate(
|
||||
const middleClickAriaLabel = t(
|
||||
'auto.components.settings.BrowserTerminalLinkActionsSetting.middleClickAriaLabel',
|
||||
'Middle click'
|
||||
)
|
||||
const actionsLabel = translate(
|
||||
const actionsLabel = t(
|
||||
'auto.components.settings.BrowserTerminalLinkActionsSetting.actionsLabel',
|
||||
'Actions'
|
||||
)
|
||||
const openUrlLabel = translate(
|
||||
const openUrlLabel = t(
|
||||
'auto.components.settings.BrowserTerminalLinkActionsSetting.openUrlLabel',
|
||||
'Open URL'
|
||||
)
|
||||
const leaveToTerminalLabel = translate(
|
||||
const leaveToTerminalLabel = t(
|
||||
'auto.components.settings.BrowserTerminalLinkActionsSetting.leaveToTerminalLabel',
|
||||
'Leave to terminal'
|
||||
)
|
||||
|
||||
+5
-4
@@ -1,12 +1,13 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { Translate } from '@/i18n/use-translate'
|
||||
|
||||
export function getBrowserClientHostedRemoteTitle(): string {
|
||||
return translate('settings.browser.clientHostedRemote.rowTitle', 'Remote server workspaces')
|
||||
export function getBrowserClientHostedRemoteTitle(t: Translate = translate): string {
|
||||
return t('settings.browser.clientHostedRemote.rowTitle', 'Remote server workspaces')
|
||||
}
|
||||
|
||||
// New pages only: placement is fixed per page generation, so live pages are never migrated.
|
||||
export function getBrowserClientHostedRemoteDescription(): string {
|
||||
return translate(
|
||||
export function getBrowserClientHostedRemoteDescription(t: Translate = translate): string {
|
||||
return t(
|
||||
'settings.browser.clientHostedRemote.rowDescription',
|
||||
'Where pages render. Traffic always goes through the remote server. Applies to new pages only.'
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { Translate } from '@/i18n/use-translate'
|
||||
import type { SettingsSearchEntry } from '../../search/settings-search'
|
||||
import { translateSearchKeyword } from '../../search/settings-search-keywords'
|
||||
|
||||
@@ -9,15 +10,16 @@ export function getBrowserLinkRoutingShortcutLabel(platform: { isMac: boolean })
|
||||
// Modifier inversion changes always-copy; use complete localized variants because sentence suffixes cannot be safely stitched across locales.
|
||||
export function getBrowserLinkRoutingDescription(
|
||||
platform: { isMac: boolean },
|
||||
modifierInverts = false
|
||||
modifierInverts = false,
|
||||
t: Translate = translate
|
||||
): string {
|
||||
if (modifierInverts) {
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.BrowserLinkRoutingSetting.descriptionBase',
|
||||
'Open http(s) links from anywhere in Orca in its built-in browser. Sign-in pages still open in your system browser.'
|
||||
)
|
||||
}
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.BrowserLinkRoutingSetting.description',
|
||||
'Open http(s) links from anywhere in Orca in its built-in browser. Sign-in pages and {{shortcut}} always use your system browser.',
|
||||
{ shortcut: getBrowserLinkRoutingShortcutLabel(platform) }
|
||||
@@ -25,72 +27,94 @@ export function getBrowserLinkRoutingDescription(
|
||||
}
|
||||
|
||||
/** Share destination copy with search, which indexes the alternate title because its routing mode is fixed to false. */
|
||||
export function getLinkRoutingModifierTitle(openLinksInApp: boolean): string {
|
||||
export function getLinkRoutingModifierTitle(
|
||||
openLinksInApp: boolean,
|
||||
t: Translate = translate
|
||||
): string {
|
||||
return openLinksInApp
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.BrowserLinkRoutingModifierSetting.titleSystem',
|
||||
'Hold Shift to open in your web browser'
|
||||
)
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.BrowserLinkRoutingModifierSetting.titleOrca',
|
||||
'Hold Shift to open in Orca'
|
||||
)
|
||||
}
|
||||
|
||||
// Orca describes enabled inversion; while disabled the chord still opens the system browser.
|
||||
export function getLinkRoutingModifierDescription({
|
||||
openLinksInApp,
|
||||
isMac
|
||||
}: {
|
||||
openLinksInApp: boolean
|
||||
isMac: boolean
|
||||
}): string {
|
||||
export function getLinkRoutingModifierDescription(
|
||||
{
|
||||
openLinksInApp,
|
||||
isMac
|
||||
}: {
|
||||
openLinksInApp: boolean
|
||||
isMac: boolean
|
||||
},
|
||||
t: Translate = translate
|
||||
): string {
|
||||
const chord = isMac ? '⇧⌘' : 'Shift+Ctrl'
|
||||
return openLinksInApp
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.BrowserLinkRoutingModifierSetting.descriptionSystem',
|
||||
'Links open in Orca, so {{chord}}+click sends one to your system browser instead.',
|
||||
{ chord }
|
||||
)
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.BrowserLinkRoutingModifierSetting.descriptionOrca',
|
||||
"Links open in your system browser. When enabled, {{chord}}+click opens one in Orca's built-in browser instead.",
|
||||
{ chord }
|
||||
)
|
||||
}
|
||||
|
||||
function getLinkRoutingBackgroundTitle(): string {
|
||||
return translate(
|
||||
function getLinkRoutingBackgroundTitle(t: Translate = translate): string {
|
||||
return t(
|
||||
'auto.components.settings.BrowserLinkRoutingBackgroundSetting.title',
|
||||
'Open links in the background'
|
||||
)
|
||||
}
|
||||
|
||||
function getLinkRoutingBackgroundDescription(): string {
|
||||
return translate(
|
||||
function getLinkRoutingBackgroundDescription(t: Translate = translate): string {
|
||||
return t(
|
||||
'auto.components.settings.BrowserLinkRoutingBackgroundSetting.description',
|
||||
'Links from the terminal, chat, and markdown join the browser without taking focus from where you clicked.'
|
||||
)
|
||||
}
|
||||
|
||||
export function getLinkRoutingBackgroundSearchEntry(): SettingsSearchEntry {
|
||||
export function getLinkRoutingBackgroundSearchEntry(t?: Translate): SettingsSearchEntry {
|
||||
return {
|
||||
title: getLinkRoutingBackgroundTitle(),
|
||||
description: getLinkRoutingBackgroundDescription(),
|
||||
title: getLinkRoutingBackgroundTitle(t),
|
||||
description: getLinkRoutingBackgroundDescription(t),
|
||||
keywords: [
|
||||
...translateSearchKeyword('auto.components.settings.browser.search.2d2d995c58', 'browser'),
|
||||
...translateSearchKeyword('auto.components.settings.browser.search.bea27bac4b', 'links'),
|
||||
...translateSearchKeyword(
|
||||
'auto.components.settings.browser.search.2d2d995c58',
|
||||
'browser',
|
||||
undefined,
|
||||
t
|
||||
),
|
||||
...translateSearchKeyword(
|
||||
'auto.components.settings.browser.search.bea27bac4b',
|
||||
'links',
|
||||
undefined,
|
||||
t
|
||||
),
|
||||
...translateSearchKeyword(
|
||||
'auto.components.settings.browser.search.linkRoutingBackground.background',
|
||||
'background'
|
||||
'background',
|
||||
undefined,
|
||||
t
|
||||
),
|
||||
...translateSearchKeyword(
|
||||
'auto.components.settings.browser.search.linkRoutingBackground.focus',
|
||||
'focus'
|
||||
'focus',
|
||||
undefined,
|
||||
t
|
||||
),
|
||||
...translateSearchKeyword(
|
||||
'auto.components.settings.browser.search.linkRoutingBackground.tab',
|
||||
'tab'
|
||||
'tab',
|
||||
undefined,
|
||||
t
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
+5
-4
@@ -1,11 +1,12 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { Translate } from '@/i18n/use-translate'
|
||||
|
||||
export function getBrowserSshWorkspaceRoutingTitle(): string {
|
||||
return translate('settings.browser.sshWorkspaceRouting.rowTitle', 'SSH workspaces')
|
||||
export function getBrowserSshWorkspaceRoutingTitle(t: Translate = translate): string {
|
||||
return t('settings.browser.sshWorkspaceRouting.rowTitle', 'SSH workspaces')
|
||||
}
|
||||
|
||||
export function getBrowserSshWorkspaceRoutingDescription(): string {
|
||||
return translate(
|
||||
export function getBrowserSshWorkspaceRoutingDescription(t: Translate = translate): string {
|
||||
return t(
|
||||
'settings.browser.sshWorkspaceRouting.rowDescription',
|
||||
'Where browser traffic leaves from. Pages always render on this device.'
|
||||
)
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import { MousePointerClick } from 'lucide-react'
|
||||
import { Button } from '../../../ui/button'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
export function BrowserUseComputerUseNotice({
|
||||
onOpenComputerUse
|
||||
}: {
|
||||
onOpenComputerUse: () => void
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<div className="rounded-xl border border-border/60 bg-card/50 p-4">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start">
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<p className="text-sm font-medium">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.BrowserUseComputerUseNotice.333984cf90',
|
||||
'Use an existing browser session'
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.BrowserUseComputerUseNotice.79209b37b9',
|
||||
'If cookie import is not the right fit, Computer Use can control local apps and may use existing logged-in browser sessions where applicable. Install the Orca Computer Use skill; macOS also requires privacy permissions.'
|
||||
)}
|
||||
@@ -32,7 +33,7 @@ export function BrowserUseComputerUseNotice({
|
||||
className="shrink-0 gap-1.5 self-start"
|
||||
>
|
||||
<MousePointerClick className="size-3.5" />
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.BrowserUseComputerUseNotice.15b5e680ba',
|
||||
'Open Computer Use'
|
||||
)}
|
||||
|
||||
@@ -19,7 +19,7 @@ import { useAppStore } from '../../../../store/index'
|
||||
import { SearchableSetting } from '../../section-layout/SearchableSetting'
|
||||
import { StepBadge } from '../../setup-guide/SetupStepBadge'
|
||||
import { getBrowserUsePaneSearchEntries } from './browser-use-search'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { browserCookieImportFailureText } from '@/lib/browser/cookies/browser-cookie-import-error-copy'
|
||||
|
||||
type BrowserUseCookieImportStepProps = {
|
||||
@@ -35,6 +35,7 @@ export function BrowserUseCookieImportStep({
|
||||
sourceLabel,
|
||||
onConfigureMoreBrowsers
|
||||
}: BrowserUseCookieImportStepProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const detectedBrowsers = useAppStore((s) => s.detectedBrowsers)
|
||||
const fetchDetectedBrowsers = useAppStore((s) => s.fetchDetectedBrowsers)
|
||||
|
||||
@@ -50,7 +51,7 @@ export function BrowserUseCookieImportStep({
|
||||
const browser = detectedBrowsers.find((b) => b.family === browserFamily)
|
||||
emitBrowserCookieImportToast(
|
||||
result.summary,
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.BrowserUsePane.2ea4617e3a',
|
||||
'Imported {{value0}} cookies from {{value1}}{{value2}}.',
|
||||
{
|
||||
@@ -71,7 +72,7 @@ export function BrowserUseCookieImportStep({
|
||||
if (result.ok) {
|
||||
emitBrowserCookieImportToast(
|
||||
result.summary,
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.BrowserUsePane.8f2675c2f3',
|
||||
'Imported {{value0}} cookies from file.',
|
||||
{ value0: result.summary.importedCookies }
|
||||
@@ -85,11 +86,8 @@ export function BrowserUseCookieImportStep({
|
||||
|
||||
return (
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.BrowserUsePane.2eb906706c',
|
||||
'Import Browser Cookies'
|
||||
)}
|
||||
description={translate(
|
||||
title={t('auto.components.settings.BrowserUsePane.2eb906706c', 'Import Browser Cookies')}
|
||||
description={t(
|
||||
'auto.components.settings.BrowserUsePane.af8c83ed61',
|
||||
'Import cookies from Chrome, Edge, or other browsers so agents can reuse your logins.'
|
||||
)}
|
||||
@@ -103,20 +101,17 @@ export function BrowserUseCookieImportStep({
|
||||
/>
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<p className="text-sm font-medium">
|
||||
{translate(
|
||||
'auto.components.settings.BrowserUsePane.2eb906706c',
|
||||
'Import Browser Cookies'
|
||||
)}
|
||||
{t('auto.components.settings.BrowserUsePane.2eb906706c', 'Import Browser Cookies')}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.BrowserUsePane.72d4815523',
|
||||
'Bring your existing logins into Orca so agents can reach authenticated pages. Imports into the default profile.'
|
||||
)}
|
||||
</p>
|
||||
{sourceLabel ? (
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.BrowserUsePane.112f70adc4',
|
||||
'Last imported from {{value0}}',
|
||||
{ value0: sourceLabel }
|
||||
@@ -129,7 +124,7 @@ export function BrowserUseCookieImportStep({
|
||||
onClick={onConfigureMoreBrowsers}
|
||||
className="text-[11px] text-muted-foreground underline underline-offset-2 hover:text-foreground"
|
||||
>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.BrowserUsePane.67d9a53f47',
|
||||
'Manage profiles for separate logins'
|
||||
)}
|
||||
@@ -156,8 +151,8 @@ export function BrowserUseCookieImportStep({
|
||||
<Import className="size-3.5" />
|
||||
)}
|
||||
{cookiesImported
|
||||
? translate('auto.components.settings.BrowserUsePane.0462565413', 'Re-import')
|
||||
: translate('auto.components.settings.BrowserUsePane.2ccfc9cff8', 'Import')}
|
||||
? t('auto.components.settings.BrowserUsePane.0462565413', 'Re-import')
|
||||
: t('auto.components.settings.BrowserUsePane.2ccfc9cff8', 'Import')}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
@@ -166,11 +161,9 @@ export function BrowserUseCookieImportStep({
|
||||
browser.profiles.length > 1 ? (
|
||||
<DropdownMenuSub key={browser.family}>
|
||||
<DropdownMenuSubTrigger>
|
||||
{translate(
|
||||
'auto.components.settings.BrowserUsePane.5301857d88',
|
||||
'From {{value0}}',
|
||||
{ value0: browser.label }
|
||||
)}
|
||||
{t('auto.components.settings.BrowserUsePane.5301857d88', 'From {{value0}}', {
|
||||
value0: browser.label
|
||||
})}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
@@ -192,17 +185,15 @@ export function BrowserUseCookieImportStep({
|
||||
key={browser.family}
|
||||
onSelect={() => void handleImportFromBrowser(browser.family)}
|
||||
>
|
||||
{translate(
|
||||
'auto.components.settings.BrowserUsePane.5301857d88',
|
||||
'From {{value0}}',
|
||||
{ value0: browser.label }
|
||||
)}
|
||||
{t('auto.components.settings.BrowserUsePane.5301857d88', 'From {{value0}}', {
|
||||
value0: browser.label
|
||||
})}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
)}
|
||||
{detectedBrowsers.length > 0 ? <DropdownMenuSeparator /> : null}
|
||||
<DropdownMenuItem onSelect={() => void handleImportFromFile()}>
|
||||
{translate('auto.components.settings.BrowserUsePane.be6df68384', 'From File…')}
|
||||
{t('auto.components.settings.BrowserUsePane.be6df68384', 'From File…')}
|
||||
</DropdownMenuItem>
|
||||
<BrowserCookieImportDisclosure />
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
export function BrowserUseEnableSwitch({
|
||||
enabled,
|
||||
@@ -7,10 +7,11 @@ export function BrowserUseEnableSwitch({
|
||||
enabled: boolean
|
||||
onToggle: () => void
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
aria-label={translate(
|
||||
aria-label={t(
|
||||
'auto.components.settings.BrowserUseEnableSwitch.aea3f45349',
|
||||
'Enable Agent Browser Use'
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { toast } from 'sonner'
|
||||
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 EXAMPLE_PROMPTS: string[] = [
|
||||
'Using Orca CLI, open https://github.com/notifications and click the first unread pull request.',
|
||||
@@ -28,19 +29,17 @@ async function handleCopyText(text: string, label: string): Promise<void> {
|
||||
}
|
||||
|
||||
export function BrowserUseExamples(): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<div className="rounded-xl border border-border/60 bg-card/50 p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="size-3.5 text-muted-foreground" />
|
||||
<p className="text-sm font-medium">
|
||||
{translate(
|
||||
'auto.components.settings.BrowserUseExamples.2a180694f7',
|
||||
'Try it — example prompts'
|
||||
)}
|
||||
{t('auto.components.settings.BrowserUseExamples.2a180694f7', 'Try it — example prompts')}
|
||||
</p>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.BrowserUseExamples.c5325e91f6',
|
||||
'Paste any of these into Claude Code, Codex, or another agent in a project where the skill is installed.'
|
||||
)}
|
||||
@@ -52,9 +51,9 @@ export function BrowserUseExamples(): React.JSX.Element {
|
||||
className="flex items-start gap-2 rounded-lg border border-border/50 bg-background/60 px-3 py-2"
|
||||
>
|
||||
<p className="flex-1 text-[11px] leading-relaxed text-foreground/90">
|
||||
{translate('auto.components.settings.BrowserUseExamples.59722f31b4', '"')}
|
||||
{t('auto.components.settings.BrowserUseExamples.59722f31b4', '"')}
|
||||
{prompt}
|
||||
{translate('auto.components.settings.BrowserUseExamples.b84807f228', '"')}
|
||||
{t('auto.components.settings.BrowserUseExamples.b84807f228', '"')}
|
||||
</p>
|
||||
<TooltipProvider delayDuration={250}>
|
||||
<Tooltip>
|
||||
@@ -63,7 +62,7 @@ export function BrowserUseExamples(): React.JSX.Element {
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() => void handleCopyText(prompt, 'prompt')}
|
||||
aria-label={translate(
|
||||
aria-label={t(
|
||||
'auto.components.settings.BrowserUseExamples.1188e56af4',
|
||||
'Copy example prompt'
|
||||
)}
|
||||
@@ -72,7 +71,7 @@ export function BrowserUseExamples(): React.JSX.Element {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" sideOffset={6}>
|
||||
{translate('auto.components.settings.BrowserUseExamples.1199258ace', 'Copy')}
|
||||
{t('auto.components.settings.BrowserUseExamples.1199258ace', 'Copy')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -17,7 +17,7 @@ import { BrowserUseComputerUseNotice } from './BrowserUseComputerUseNotice'
|
||||
import { BrowserUseEnableSwitch } from './BrowserUseEnableSwitch'
|
||||
import { BrowserUseSkillStep } from './BrowserUseSkillStep'
|
||||
import { BrowserUseCookieImportStep } from './BrowserUseCookieImportStep'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
type BrowserUseSetupProps = {
|
||||
onConfigureMoreBrowsers?: () => void
|
||||
@@ -28,6 +28,7 @@ export function BrowserUseSetup({
|
||||
onConfigureMoreBrowsers,
|
||||
onOpenComputerUse
|
||||
}: BrowserUseSetupProps = {}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const searchQuery = useAppStore((s) => s.settingsSearchQuery)
|
||||
const browserSessionProfiles = useAppStore((s) => s.browserSessionProfiles)
|
||||
const fetchBrowserSessionProfiles = useAppStore((s) => s.fetchBrowserSessionProfiles)
|
||||
@@ -89,10 +90,10 @@ export function BrowserUseSetup({
|
||||
<div className="flex items-center justify-between gap-4 py-3">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">
|
||||
{translate('auto.components.settings.BrowserUsePane.b8a1f2d84d', 'Agent Browser Use')}
|
||||
{t('auto.components.settings.BrowserUsePane.b8a1f2d84d', 'Agent Browser Use')}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.BrowserUsePane.96b91c6349',
|
||||
'Let coding agents drive this browser with your logins.'
|
||||
)}
|
||||
@@ -111,10 +112,10 @@ export function BrowserUseSetup({
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-semibold">
|
||||
{translate('auto.components.settings.BrowserUsePane.b8a1f2d84d', 'Agent Browser Use')}
|
||||
{t('auto.components.settings.BrowserUsePane.b8a1f2d84d', 'Agent Browser Use')}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.BrowserUsePane.finishSteps',
|
||||
'Let coding agents drive this browser with your logins. Finish the steps below.'
|
||||
)}
|
||||
@@ -143,11 +144,11 @@ export function BrowserUseSetup({
|
||||
|
||||
{showSkillStep ? (
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
title={t(
|
||||
'auto.components.settings.BrowserUsePane.2d6ead9ab2',
|
||||
'Install Browser Use Skill'
|
||||
)}
|
||||
description={translate(
|
||||
description={t(
|
||||
'auto.components.settings.BrowserUsePane.68ea76eb71',
|
||||
"Install the Browser Use skill so agents can operate Orca's browser."
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ORCA_CLI_SKILL_NAME } from '@/lib/agent-feature-skill-names'
|
||||
import { AgentSkillSetupPanel } from '../../../skills/setup/AgentSkillSetupPanel'
|
||||
import { StepBadge } from '../../setup-guide/SetupStepBadge'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
type Props = {
|
||||
skillDetected: boolean
|
||||
@@ -24,14 +24,12 @@ export function BrowserUseSkillStep({
|
||||
onBeforeInstall,
|
||||
onRecheck
|
||||
}: Props): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<AgentSkillSetupPanel
|
||||
variant="inline"
|
||||
title={translate(
|
||||
'auto.components.settings.BrowserUseSkillStep.459e24eebc',
|
||||
'Browser Use skill'
|
||||
)}
|
||||
description={translate(
|
||||
title={t('auto.components.settings.BrowserUseSkillStep.459e24eebc', 'Browser Use skill')}
|
||||
description={t(
|
||||
'auto.components.settings.BrowserUseSkillStep.0871b6998d',
|
||||
"Enables agents to navigate and verify pages in Orca's browser."
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { Translate } from '@/i18n/use-translate'
|
||||
|
||||
export type SettingOwnership =
|
||||
| 'client-default'
|
||||
@@ -13,58 +14,58 @@ type SettingOwnershipSummary = {
|
||||
description: string
|
||||
}
|
||||
|
||||
function buildSummaries(): Record<string, SettingOwnershipSummary> {
|
||||
function buildSummaries(t: Translate = translate): Record<string, SettingOwnershipSummary> {
|
||||
return {
|
||||
sourceControlAiDefaults: {
|
||||
ownership: 'client-default',
|
||||
label: translate('auto.components.settings.settingOwnership.clientDefault', 'Client default'),
|
||||
description: translate(
|
||||
label: t('auto.components.settings.settingOwnership.clientDefault', 'Client default'),
|
||||
description: t(
|
||||
'auto.components.settings.settingOwnership.sourceControlAiDefaults',
|
||||
'Recipes, prompts, and hosted-review defaults are shared by this client; model choices and discovery stay scoped to the host where the agent runs.'
|
||||
)
|
||||
},
|
||||
repositorySourceControlAi: {
|
||||
ownership: 'project-host-setup',
|
||||
label: translate(
|
||||
label: t(
|
||||
'auto.components.settings.settingOwnership.projectOnThisHost',
|
||||
'Project on this host'
|
||||
),
|
||||
description: translate(
|
||||
description: t(
|
||||
'auto.components.settings.settingOwnership.repositorySourceControlAi',
|
||||
'These overrides apply to this project setup and inherit the client Source Control AI defaults until customized.'
|
||||
)
|
||||
},
|
||||
agentLaunchDefaults: {
|
||||
ownership: 'host-collection',
|
||||
label: translate('auto.components.settings.settingOwnership.providerHost', 'Provider host'),
|
||||
description: translate(
|
||||
label: t('auto.components.settings.settingOwnership.providerHost', 'Provider host'),
|
||||
description: t(
|
||||
'auto.components.settings.settingOwnership.agentLaunchDefaults',
|
||||
'Default agent, command overrides, CLI arguments, and launch environment are saved on the selected host.'
|
||||
)
|
||||
},
|
||||
terminalQuickCommands: {
|
||||
ownership: 'host-collection',
|
||||
label: translate(
|
||||
label: t(
|
||||
'auto.components.settings.settingOwnership.hostCollectionProjectScopes',
|
||||
'Host collection + project scopes'
|
||||
),
|
||||
description: translate(
|
||||
description: t(
|
||||
'auto.components.settings.settingOwnership.terminalQuickCommandHostCollections',
|
||||
'Commands are saved on the selected Orca host, then scoped globally or to a project setup. Commands from this device also remain available in remote workspaces.'
|
||||
)
|
||||
},
|
||||
workspaceDirectory: {
|
||||
ownership: 'host-override',
|
||||
label: translate('auto.components.settings.settingOwnership.hostOverride', 'Host override'),
|
||||
description: translate(
|
||||
label: t('auto.components.settings.settingOwnership.hostOverride', 'Host override'),
|
||||
description: t(
|
||||
'auto.components.settings.settingOwnership.workspaceDirectory',
|
||||
'The client default is inherited until a host needs its own worktree directory.'
|
||||
)
|
||||
},
|
||||
providerAccounts: {
|
||||
ownership: 'provider-host',
|
||||
label: translate('auto.components.settings.settingOwnership.providerHost', 'Provider host'),
|
||||
description: translate(
|
||||
label: t('auto.components.settings.settingOwnership.providerHost', 'Provider host'),
|
||||
description: t(
|
||||
'auto.components.settings.settingOwnership.providerAccounts',
|
||||
'Credentials and account checks belong to the local client or selected remote server that owns the provider integration.'
|
||||
)
|
||||
@@ -83,6 +84,9 @@ const SUMMARY_KEYS = [
|
||||
|
||||
export type SettingOwnershipKey = (typeof SUMMARY_KEYS)[number]
|
||||
|
||||
export function getSettingOwnershipSummary(key: SettingOwnershipKey): SettingOwnershipSummary {
|
||||
return buildSummaries()[key]
|
||||
export function getSettingOwnershipSummary(
|
||||
key: SettingOwnershipKey,
|
||||
t?: Translate
|
||||
): SettingOwnershipSummary {
|
||||
return buildSummaries(t)[key]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
editorFormatterSchema
|
||||
} from '../../../../../shared/editor-formatter'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate, type Translate } from '@/i18n/use-translate'
|
||||
import { Button } from '../../ui/button'
|
||||
import { Input } from '../../ui/input'
|
||||
import { Label } from '../../ui/label'
|
||||
@@ -12,18 +13,15 @@ import { Textarea } from '../../ui/textarea'
|
||||
import { SearchableSetting } from '../section-layout/SearchableSetting'
|
||||
import { SettingsGroup } from '../section-layout/SettingsGroup'
|
||||
|
||||
export function getRepositoryFormatterSearchEntry() {
|
||||
export function getRepositoryFormatterSearchEntry(t: Translate = translate) {
|
||||
return {
|
||||
title: translate('settings.repositoryFormatter.title', 'Formatter Command'),
|
||||
description: translate(
|
||||
title: t('settings.repositoryFormatter.title', 'Formatter Command'),
|
||||
description: t(
|
||||
'settings.repositoryFormatter.description',
|
||||
'Use this project’s formatter for Format on Save.'
|
||||
),
|
||||
keywords: [
|
||||
translate(
|
||||
'settings.editorFormatOnSave.keywords',
|
||||
'formatter format on save prettier biome gofmt'
|
||||
)
|
||||
t('settings.editorFormatOnSave.keywords', 'formatter format on save prettier biome gofmt')
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -37,6 +35,7 @@ export function RepositoryFormatterSection({
|
||||
forceVisible?: boolean
|
||||
updateRepo: (repoId: string, updates: Pick<Repo, 'editorFormatter'>) => void | Promise<boolean>
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const commandId = useId()
|
||||
const includeId = useId()
|
||||
const sourceKey = JSON.stringify(repo.editorFormatter)
|
||||
@@ -52,7 +51,7 @@ export function RepositoryFormatterSection({
|
||||
setCommand(repo.editorFormatter?.command ?? '')
|
||||
setInclude(repo.editorFormatter?.include.join('\n') ?? DEFAULT_FORMATTER_INCLUDE)
|
||||
}
|
||||
const entry = getRepositoryFormatterSearchEntry()
|
||||
const entry = getRepositoryFormatterSearchEntry(t)
|
||||
const changed =
|
||||
command !== (repo.editorFormatter?.command ?? '') ||
|
||||
include !== (repo.editorFormatter?.include.join('\n') ?? DEFAULT_FORMATTER_INCLUDE)
|
||||
@@ -70,9 +69,7 @@ export function RepositoryFormatterSection({
|
||||
})
|
||||
const saved = await updateRepo(repo.id, { editorFormatter })
|
||||
if (saved === false) {
|
||||
setError(
|
||||
translate('settings.repositoryFormatter.saveFailed', 'Could not save formatter settings.')
|
||||
)
|
||||
setError(t('settings.repositoryFormatter.saveFailed', 'Could not save formatter settings.'))
|
||||
}
|
||||
} catch (reason) {
|
||||
setError(reason instanceof Error ? reason.message : String(reason))
|
||||
@@ -93,13 +90,13 @@ export function RepositoryFormatterSection({
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={commandId}>
|
||||
{translate('settings.repositoryFormatter.command', 'Command')}
|
||||
{t('settings.repositoryFormatter.command', 'Command')}
|
||||
</Label>
|
||||
<Input
|
||||
id={commandId}
|
||||
value={command}
|
||||
onChange={(event) => setCommand(event.target.value)}
|
||||
placeholder={translate(
|
||||
placeholder={t(
|
||||
'settings.repositoryFormatter.placeholder',
|
||||
'npx --no-install prettier --stdin-filepath "${file}"'
|
||||
)}
|
||||
@@ -108,7 +105,7 @@ export function RepositoryFormatterSection({
|
||||
aria-invalid={Boolean(error)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'settings.repositoryFormatter.commandHelp',
|
||||
'Runs on the file’s host in the workspace root. Reads the buffer from stdin and must print only formatted text to stdout. Use ${file} or ${relativeFile} for the path. Arguments are passed directly, without a shell. Do not use --write. Leave empty to disable for this project.'
|
||||
)}
|
||||
@@ -116,7 +113,7 @@ export function RepositoryFormatterSection({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={includeId}>
|
||||
{translate('settings.repositoryFormatter.include', 'Include globs (one per line)')}
|
||||
{t('settings.repositoryFormatter.include', 'Include globs (one per line)')}
|
||||
</Label>
|
||||
<Textarea
|
||||
id={includeId}
|
||||
@@ -127,7 +124,7 @@ export function RepositoryFormatterSection({
|
||||
disabled={saving}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'settings.repositoryFormatter.includeHelp',
|
||||
'Match paths relative to the workspace root. No matching glob means no formatting. * and ** skip dot-prefixed names; list them explicitly, e.g. .github/**/*.yml. Enable Format on Save in General > Editor to use this command.'
|
||||
)}
|
||||
@@ -140,8 +137,8 @@ export function RepositoryFormatterSection({
|
||||
) : null}
|
||||
<Button type="submit" size="sm" disabled={!changed || saving}>
|
||||
{saving
|
||||
? translate('settings.repositoryFormatter.saving', 'Saving…')
|
||||
: translate('settings.repositoryFormatter.save', 'Save formatter')}
|
||||
? t('settings.repositoryFormatter.saving', 'Saving…')
|
||||
: t('settings.repositoryFormatter.save', 'Save formatter')}
|
||||
</Button>
|
||||
</form>
|
||||
</SettingsGroup>
|
||||
|
||||
+35
-40
@@ -9,7 +9,7 @@ import { SearchableSetting } from '../section-layout/SearchableSetting'
|
||||
import { useAppStore } from '@/store/index'
|
||||
import { runtimeTargetForOwnerEnvironment } from '@/runtime/rpc/targets/runtime-client-target'
|
||||
import { getRepoExecutionHostId, parseExecutionHostId } from '../../../../../shared/execution-host'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { searchKeywords } from '../search/settings-search-keywords'
|
||||
import {
|
||||
isGhAccountBindingEnforced,
|
||||
@@ -35,6 +35,7 @@ export function RepositoryGitHubAccountSection({
|
||||
updateRepo,
|
||||
forceVisible
|
||||
}: RepositoryGitHubAccountSectionProps): React.JSX.Element | null {
|
||||
const t = useTranslate()
|
||||
const [inventory, setInventory] = useState<GhAccountBindingInventory | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
@@ -116,26 +117,26 @@ export function RepositoryGitHubAccountSection({
|
||||
if (!validation.ok) {
|
||||
setError(
|
||||
validation.error === 'gh_multi_account_unsupported'
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.RepositoryGitHubAccountSection.unsupported',
|
||||
'This runtime’s GitHub CLI is too old for account binding (needs gh ≥ 2.40).'
|
||||
)
|
||||
: validation.error === 'gh_multi_account_capability_unknown'
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.RepositoryGitHubAccountSection.capabilityUnknown',
|
||||
'Could not determine whether this runtime’s GitHub CLI supports account binding. Retry.'
|
||||
)
|
||||
: validation.error === 'gh_bound_account_not_keyring'
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.RepositoryGitHubAccountSection.envToken',
|
||||
'Environment-token accounts cannot be bound. Use a keyring login.'
|
||||
)
|
||||
: validation.error === 'invalid_binding'
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.RepositoryGitHubAccountSection.invalidBinding',
|
||||
'That GitHub account binding is invalid.'
|
||||
)
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.RepositoryGitHubAccountSection.unavailable',
|
||||
'That GitHub account is unavailable on this runtime.'
|
||||
)
|
||||
@@ -147,7 +148,7 @@ export function RepositoryGitHubAccountSection({
|
||||
const result = await Promise.resolve(updateRepo(repo.id, { ghAccount: resolved }))
|
||||
if (result === false) {
|
||||
setError(
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.RepositoryGitHubAccountSection.saveFailed',
|
||||
'Could not save the GitHub account binding.'
|
||||
)
|
||||
@@ -167,39 +168,36 @@ export function RepositoryGitHubAccountSection({
|
||||
|
||||
return (
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.RepositoryGitHubAccountSection.title',
|
||||
'GitHub Account'
|
||||
)}
|
||||
description={translate(
|
||||
title={t('auto.components.settings.RepositoryGitHubAccountSection.title', 'GitHub Account')}
|
||||
description={t(
|
||||
'auto.components.settings.RepositoryGitHubAccountSection.description',
|
||||
'Use a specific gh login for this project’s GitHub issue and pull request calls.'
|
||||
)}
|
||||
keywords={searchKeywords([
|
||||
repo.displayName,
|
||||
'github',
|
||||
'gh account',
|
||||
'github account',
|
||||
'login',
|
||||
'token',
|
||||
{
|
||||
key: 'auto.components.settings.repository.search.githubAccountKeyword',
|
||||
fallback: 'github account'
|
||||
}
|
||||
])}
|
||||
keywords={searchKeywords(
|
||||
[
|
||||
repo.displayName,
|
||||
'github',
|
||||
'gh account',
|
||||
'github account',
|
||||
'login',
|
||||
'token',
|
||||
{
|
||||
key: 'auto.components.settings.repository.search.githubAccountKeyword',
|
||||
fallback: 'github account'
|
||||
}
|
||||
],
|
||||
t
|
||||
)}
|
||||
className="space-y-3 py-3"
|
||||
forceVisible={forceVisible}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<div className="text-sm font-semibold">
|
||||
{translate(
|
||||
'auto.components.settings.RepositoryGitHubAccountSection.title',
|
||||
'GitHub Account'
|
||||
)}
|
||||
{t('auto.components.settings.RepositoryGitHubAccountSection.title', 'GitHub Account')}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryGitHubAccountSection.longDescription',
|
||||
'Orca injects a short-lived token from the selected keyring login into each gh call for this project. Project View stays on the ambient login.'
|
||||
)}
|
||||
@@ -210,10 +208,7 @@ export function RepositoryGitHubAccountSection({
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-1">
|
||||
<span id={selectLabelId} className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.RepositoryGitHubAccountSection.selectLabel',
|
||||
'Account'
|
||||
)}
|
||||
{t('auto.components.settings.RepositoryGitHubAccountSection.selectLabel', 'Account')}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -221,11 +216,11 @@ export function RepositoryGitHubAccountSection({
|
||||
size="icon"
|
||||
onClick={() => void loadInventory(true)}
|
||||
disabled={loading || saving}
|
||||
aria-label={translate(
|
||||
aria-label={t(
|
||||
'auto.components.settings.RepositoryGitHubAccountSection.refreshAccounts',
|
||||
'Refresh GitHub accounts'
|
||||
)}
|
||||
title={translate(
|
||||
title={t(
|
||||
'auto.components.settings.RepositoryGitHubAccountSection.refreshAccounts',
|
||||
'Refresh GitHub accounts'
|
||||
)}
|
||||
@@ -261,7 +256,7 @@ export function RepositoryGitHubAccountSection({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={AMBIENT_VALUE}>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryGitHubAccountSection.ambient',
|
||||
'Default (ambient gh login)'
|
||||
)}
|
||||
@@ -269,7 +264,7 @@ export function RepositoryGitHubAccountSection({
|
||||
{repo.ghAccount && !selectedInInventory ? (
|
||||
// Retain the missing bound key for display, but disable reselection.
|
||||
<SelectItem value={selectedKey} disabled>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryGitHubAccountSection.unavailableOption',
|
||||
'{{user}} @ {{host}} (unavailable)',
|
||||
{ user: repo.ghAccount.user, host: repo.ghAccount.host }
|
||||
@@ -295,7 +290,7 @@ export function RepositoryGitHubAccountSection({
|
||||
|
||||
{capabilityUnsupported ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryGitHubAccountSection.capabilityBlocked',
|
||||
'Binding is unavailable until this runtime’s GitHub CLI supports `gh auth token --user` (gh ≥ 2.40).'
|
||||
)}
|
||||
@@ -303,7 +298,7 @@ export function RepositoryGitHubAccountSection({
|
||||
) : null}
|
||||
{capabilityUnknown ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryGitHubAccountSection.capabilityUnknownHint',
|
||||
'Could not determine GitHub CLI account-binding support on this runtime. Clear the binding or retry.'
|
||||
)}
|
||||
@@ -311,7 +306,7 @@ export function RepositoryGitHubAccountSection({
|
||||
) : null}
|
||||
{notEnforced ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryGitHubAccountSection.notEnforced',
|
||||
'Saved, but not enforced by this runtime (mixed-version host).'
|
||||
)}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useAppStore } from '@/store/index'
|
||||
import { useJiraProjectInventory } from '../../use-jira-project-inventory'
|
||||
import { JiraProjectPicker } from '../../jira-project-picker'
|
||||
import { SearchableSetting } from '../section-layout/SearchableSetting'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
export function RepositoryJiraProjectSection({
|
||||
repo,
|
||||
@@ -18,6 +18,7 @@ export function RepositoryJiraProjectSection({
|
||||
updateRepo: (id: string, updates: { defaultJiraProject: JiraProjectReference | null }) => unknown
|
||||
forceVisible?: boolean
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const hostId = getRepoExecutionHostId(repo)
|
||||
const source = useMemo(() => taskSourceRuntimeTarget({ hostId }), [hostId])
|
||||
const inventory = useJiraProjectInventory(source, true)
|
||||
@@ -30,7 +31,7 @@ export function RepositoryJiraProjectSection({
|
||||
const result = await updateRepo(repo.id, { defaultJiraProject: project })
|
||||
if (result === false) {
|
||||
throw new Error(
|
||||
translate(
|
||||
t(
|
||||
'components.RepositoryJiraProjectSection.saveFailed',
|
||||
'Could not save the default Jira project.'
|
||||
)
|
||||
@@ -46,7 +47,7 @@ export function RepositoryJiraProjectSection({
|
||||
(echoed?.siteId ?? null) !== (project?.siteId ?? null)
|
||||
) {
|
||||
throw new Error(
|
||||
translate(
|
||||
t(
|
||||
'components.RepositoryJiraProjectSection.runtimeUpdate',
|
||||
'Update this runtime to save a default Jira project.'
|
||||
)
|
||||
@@ -58,11 +59,11 @@ export function RepositoryJiraProjectSection({
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
const label = translate('components.RepositoryJiraProjectSection.title', 'Default Jira project')
|
||||
const label = t('components.RepositoryJiraProjectSection.title', 'Default Jira project')
|
||||
return (
|
||||
<SearchableSetting
|
||||
title={label}
|
||||
description={translate(
|
||||
description={t(
|
||||
'components.RepositoryJiraProjectSection.description',
|
||||
'Default the Jira issue picker and Tasks list to this project. You can change it for each worktree.'
|
||||
)}
|
||||
@@ -76,12 +77,12 @@ export function RepositoryJiraProjectSection({
|
||||
value={repo.defaultJiraProject ?? null}
|
||||
onChange={(project) => void applyProject(project)}
|
||||
label={label}
|
||||
emptyLabel={translate('components.RepositoryJiraProjectSection.none', 'No default project')}
|
||||
emptyLabel={t('components.RepositoryJiraProjectSection.none', 'No default project')}
|
||||
disabled={inventory.loading || saving}
|
||||
/>
|
||||
{!inventory.loading && !inventory.connected && !inventory.error ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'components.RepositoryJiraProjectSection.connect',
|
||||
'Connect Jira in Integrations to choose a project.'
|
||||
)}
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
RepositoryAgentTrustSection
|
||||
} from './RepositoryAgentTrustSection'
|
||||
import { RepositoryGitHubAccountSection } from './RepositoryGitHubAccountSection'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { RepositoryWindowsRuntimeSection } from './execution/RepositoryWindowsRuntimeSection'
|
||||
import { matchesRepositoryIdentitySearch } from './identity/repository-identity-search'
|
||||
import { RepositoryWorktreeDefaultsSection } from './execution/RepositoryWorktreeDefaultsSection'
|
||||
@@ -102,6 +102,7 @@ export function RepositoryPane({
|
||||
updateProject,
|
||||
writeSourceControlAiSettings
|
||||
}: RepositoryPaneProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const isFolder = isFolderRepo(repo)
|
||||
// Bind writes to the switcher-selected owner; same-ID/self-paired repos must not use focused-host fallback.
|
||||
const selectedHostId = getRepoExecutionHostId(repo)
|
||||
@@ -179,20 +180,17 @@ export function RepositoryPane({
|
||||
|
||||
const allEntries = getRepositoryPaneSearchEntries(repo, { isLocalWindowsProject })
|
||||
const identityEntryTitles = new Set([
|
||||
translate('auto.components.settings.repository.search.7e1e456a95', 'Display Name'),
|
||||
translate('auto.components.settings.repository.search.b24f00294a', 'Project Icon'),
|
||||
translate('components.RepositoryJiraProjectSection.title', 'Default Jira project'),
|
||||
translate('auto.components.settings.repository.search.githubAccount', 'GitHub Account'),
|
||||
t('auto.components.settings.repository.search.7e1e456a95', 'Display Name'),
|
||||
t('auto.components.settings.repository.search.b24f00294a', 'Project Icon'),
|
||||
t('components.RepositoryJiraProjectSection.title', 'Default Jira project'),
|
||||
t('auto.components.settings.repository.search.githubAccount', 'GitHub Account'),
|
||||
getRepositoryAgentTrustSearchEntry().title,
|
||||
translate(
|
||||
'auto.components.settings.repository.search.keepForkUpToDate',
|
||||
'Keep Fork Up to Date'
|
||||
),
|
||||
translate('auto.components.settings.repository.search.094adbe930', 'Default Worktree Base'),
|
||||
translate('auto.components.settings.repository.search.443d127b5a', 'Worktree Location'),
|
||||
translate('auto.components.settings.repository.search.externalWorktrees', 'External worktrees'),
|
||||
translate('auto.components.settings.repository.search.projectRuntime', 'Project Runtime'),
|
||||
translate('auto.components.settings.repository.search.c5266c2c9d', 'Remove Project')
|
||||
t('auto.components.settings.repository.search.keepForkUpToDate', 'Keep Fork Up to Date'),
|
||||
t('auto.components.settings.repository.search.094adbe930', 'Default Worktree Base'),
|
||||
t('auto.components.settings.repository.search.443d127b5a', 'Worktree Location'),
|
||||
t('auto.components.settings.repository.search.externalWorktrees', 'External worktrees'),
|
||||
t('auto.components.settings.repository.search.projectRuntime', 'Project Runtime'),
|
||||
t('auto.components.settings.repository.search.c5266c2c9d', 'Remove Project')
|
||||
])
|
||||
const identityEntries = allEntries.filter((entry) => identityEntryTitles.has(entry.title))
|
||||
const sparsePresetEntries = allEntries.filter((entry) =>
|
||||
@@ -234,20 +232,20 @@ export function RepositoryPane({
|
||||
forceFullPaneForRepoMatch || matchesSettingsSearch(searchQuery, identityEntries) ? (
|
||||
<SettingsGroup
|
||||
key="identity"
|
||||
title={translate('auto.components.settings.RepositoryPane.499a437335', 'Identity')}
|
||||
title={t('auto.components.settings.RepositoryPane.499a437335', 'Identity')}
|
||||
description={
|
||||
<>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryPane.b0a0c14a1c',
|
||||
'Project-specific display details for the sidebar and tabs.'
|
||||
)}
|
||||
<span className="block">
|
||||
{translate('auto.components.settings.RepositoryPane.323debba71', 'Type:')}{' '}
|
||||
{t('auto.components.settings.RepositoryPane.323debba71', 'Type:')}{' '}
|
||||
<span className="text-foreground">{getRepoKindLabel(repo)}</span>
|
||||
</span>
|
||||
{isFolder ? (
|
||||
<span className="block">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryPane.ee5a290616',
|
||||
'Opened as folder. Git features are unavailable for this workspace.'
|
||||
)}
|
||||
@@ -265,8 +263,8 @@ export function RepositoryPane({
|
||||
}
|
||||
>
|
||||
<SearchableSetting
|
||||
title={translate('auto.components.settings.RepositoryPane.c7ef4415de', 'Display Name')}
|
||||
description={translate(
|
||||
title={t('auto.components.settings.RepositoryPane.c7ef4415de', 'Display Name')}
|
||||
description={t(
|
||||
'auto.components.settings.RepositoryPane.b0a0c14a1c',
|
||||
'Project-specific display details for the sidebar and tabs.'
|
||||
)}
|
||||
@@ -275,7 +273,7 @@ export function RepositoryPane({
|
||||
forceVisible={forceFullPaneForRepoMatch}
|
||||
>
|
||||
<Label htmlFor={`repo-display-name-${repo.id}`} className="text-sm font-semibold">
|
||||
{translate('auto.components.settings.RepositoryPane.c7ef4415de', 'Display Name')}
|
||||
{t('auto.components.settings.RepositoryPane.c7ef4415de', 'Display Name')}
|
||||
</Label>
|
||||
<RepoSettingsDraftInput
|
||||
id={`repo-display-name-${repo.id}`}
|
||||
@@ -287,8 +285,8 @@ export function RepositoryPane({
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title={translate('auto.components.settings.RepositoryPane.26fef02bf3', 'Project Icon')}
|
||||
description={translate(
|
||||
title={t('auto.components.settings.RepositoryPane.26fef02bf3', 'Project Icon')}
|
||||
description={t(
|
||||
'auto.components.settings.RepositoryPane.e641c359de',
|
||||
'Project icon and color used in the sidebar and tabs.'
|
||||
)}
|
||||
@@ -380,7 +378,7 @@ export function RepositoryPane({
|
||||
) : null,
|
||||
hooksSection,
|
||||
forceFullPaneForRepoMatch ||
|
||||
matchesSettingsSearch(searchQuery, [getRepositoryFormatterSearchEntry()]) ? (
|
||||
matchesSettingsSearch(searchQuery, [getRepositoryFormatterSearchEntry(t)]) ? (
|
||||
<RepositoryFormatterSection
|
||||
key={`formatter-${repo.id}-${selectedHostId}`}
|
||||
repo={repo}
|
||||
|
||||
@@ -6,21 +6,25 @@ import { Button } from '../../ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/tooltip'
|
||||
import { SearchableSetting } from '../section-layout/SearchableSetting'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate, type Translate } from '@/i18n/use-translate'
|
||||
|
||||
function getRemoveProjectDescription(scope: SettingsProjectRemovalScope): string {
|
||||
function getRemoveProjectDescription(
|
||||
scope: SettingsProjectRemovalScope,
|
||||
t: Translate = translate
|
||||
): string {
|
||||
if (scope === 'checkout') {
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.RepositoryPane.removeProjectCheckout',
|
||||
'Remove this checkout from Orca. Other checkouts of this project stay.'
|
||||
)
|
||||
}
|
||||
if (scope === 'split-project') {
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.RepositoryPane.removeProjectKeepCheckouts',
|
||||
'Remove this project from Orca. Checkouts that have their own settings stay.'
|
||||
)
|
||||
}
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.RepositoryPane.removeProjectAllHosts',
|
||||
'Remove this project from Orca on all configured hosts.'
|
||||
)
|
||||
@@ -37,6 +41,7 @@ export function RepositoryRemoveProjectButton({
|
||||
forceVisible: boolean
|
||||
removeProject: (repoId: string) => void
|
||||
}) {
|
||||
const t = useTranslate()
|
||||
const [confirmingRemove, setConfirmingRemove] = useState<string | null>(null)
|
||||
const removeProjectLabel =
|
||||
confirmingRemove === repo.id ? 'Confirm Remove Project' : 'Remove Project'
|
||||
@@ -53,8 +58,8 @@ export function RepositoryRemoveProjectButton({
|
||||
|
||||
return (
|
||||
<SearchableSetting
|
||||
title={translate('auto.components.settings.RepositoryPane.0909e5d650', 'Remove Project')}
|
||||
description={getRemoveProjectDescription(removalScope)}
|
||||
title={t('auto.components.settings.RepositoryPane.0909e5d650', 'Remove Project')}
|
||||
description={getRemoveProjectDescription(removalScope, t)}
|
||||
keywords={[repo.displayName, 'delete', 'project', 'repository']}
|
||||
className="w-auto"
|
||||
forceVisible={forceVisible}
|
||||
|
||||
+39
-35
@@ -12,6 +12,7 @@ import { Button } from '../../../ui/button'
|
||||
import { SettingsRow, SettingsSegmentedControl } from '../../form-controls/SettingsFormControls'
|
||||
import type { ProjectRuntimeSessionSummary } from './repository-runtime-session-summary'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate, type Translate } from '@/i18n/use-translate'
|
||||
import { projectRuntimeRepairSentence } from '../../../../../../shared/project-runtime-repair-message'
|
||||
|
||||
type ProjectRuntimeSegment = LocalWindowsRuntimePreference['kind']
|
||||
@@ -40,6 +41,7 @@ export function ProjectWindowsRuntimeSetting({
|
||||
runtimeSessionSummary,
|
||||
updateProject
|
||||
}: ProjectWindowsRuntimeSettingProps): React.JSX.Element | null {
|
||||
const t = useTranslate()
|
||||
const [pendingPreference, setPendingPreference] = useState<LocalWindowsRuntimePreference | null>(
|
||||
null
|
||||
)
|
||||
@@ -61,10 +63,10 @@ export function ProjectWindowsRuntimeSetting({
|
||||
})
|
||||
const isWslSelected = selectedPreference.kind === 'wsl'
|
||||
const distroOptions = getVisibleDistroOptions(selectedPreference, wslDistros)
|
||||
const runtimeSessionWarning = getRuntimeSessionWarning(runtimeSessionSummary)
|
||||
const runtimeSessionWarning = getRuntimeSessionWarning(runtimeSessionSummary, t)
|
||||
const hasRuntimeSessions = hasActiveRuntimeSessions(runtimeSessionSummary)
|
||||
const hasPendingPreference = pendingPreference !== null
|
||||
const defaultRuntimeLabel = getDefaultRuntimeLabel(settings)
|
||||
const defaultRuntimeLabel = getDefaultRuntimeLabel(settings, t)
|
||||
const commitRuntimePreference = (nextPreference: LocalWindowsRuntimePreference): void => {
|
||||
setPendingPreference(null)
|
||||
if (nextPreference.kind === 'inherit-global') {
|
||||
@@ -115,16 +117,16 @@ export function ProjectWindowsRuntimeSetting({
|
||||
return (
|
||||
<section className="space-y-3">
|
||||
<SettingsRow
|
||||
label={translate(
|
||||
label={t(
|
||||
'auto.components.settings.ProjectWindowsRuntimeSetting.projectRuntime',
|
||||
'Project runtime'
|
||||
)}
|
||||
alignTop
|
||||
description={getProjectRuntimeDescription(resolution)}
|
||||
description={getProjectRuntimeDescription(resolution, t)}
|
||||
control={
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<SettingsSegmentedControl<ProjectRuntimeSegment>
|
||||
ariaLabel={translate(
|
||||
ariaLabel={t(
|
||||
'auto.components.settings.ProjectWindowsRuntimeSetting.projectRuntime',
|
||||
'Project runtime'
|
||||
)}
|
||||
@@ -137,17 +139,14 @@ export function ProjectWindowsRuntimeSetting({
|
||||
},
|
||||
{
|
||||
value: 'windows-host',
|
||||
label: translate(
|
||||
label: t(
|
||||
'auto.components.settings.ProjectWindowsRuntimeSetting.windows',
|
||||
'Windows'
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'wsl',
|
||||
label: translate(
|
||||
'auto.components.settings.ProjectWindowsRuntimeSetting.wsl',
|
||||
'WSL'
|
||||
),
|
||||
label: t('auto.components.settings.ProjectWindowsRuntimeSetting.wsl', 'WSL'),
|
||||
disabled: wslCapabilitiesLoading || !wslAvailable || !nextWslDistro
|
||||
}
|
||||
]}
|
||||
@@ -160,7 +159,7 @@ export function ProjectWindowsRuntimeSetting({
|
||||
>
|
||||
<SelectTrigger size="sm" className="w-full min-w-52">
|
||||
<SelectValue
|
||||
placeholder={translate(
|
||||
placeholder={t(
|
||||
'auto.components.settings.ProjectWindowsRuntimeSetting.selectDistro',
|
||||
'Select distro'
|
||||
)}
|
||||
@@ -179,7 +178,7 @@ export function ProjectWindowsRuntimeSetting({
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.ProjectWindowsRuntimeSetting.runtimeChangeHelp',
|
||||
'Runtime changes apply to new terminals, agent checks, and skill discovery for this project. Existing terminals keep their current runtime.'
|
||||
)}
|
||||
@@ -190,7 +189,7 @@ export function ProjectWindowsRuntimeSetting({
|
||||
{hasPendingPreference ? (
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
<p className="mr-auto text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.ProjectWindowsRuntimeSetting.pendingRuntimeChange',
|
||||
'Runtime change pending. New project work will use the selected runtime after you apply.'
|
||||
)}
|
||||
@@ -201,7 +200,7 @@ export function ProjectWindowsRuntimeSetting({
|
||||
size="sm"
|
||||
onClick={() => setPendingPreference(null)}
|
||||
>
|
||||
{translate('auto.components.settings.ProjectWindowsRuntimeSetting.cancel', 'Cancel')}
|
||||
{t('auto.components.settings.ProjectWindowsRuntimeSetting.cancel', 'Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -213,7 +212,7 @@ export function ProjectWindowsRuntimeSetting({
|
||||
}
|
||||
}}
|
||||
>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.ProjectWindowsRuntimeSetting.applyRuntimeChange',
|
||||
'Apply runtime change'
|
||||
)}
|
||||
@@ -238,19 +237,19 @@ function sameRuntimePreference(
|
||||
return left.kind !== 'wsl' || left.distro === (right.kind === 'wsl' ? right.distro : null)
|
||||
}
|
||||
|
||||
function joinRuntimeSessionParts(parts: string[]): string {
|
||||
function joinRuntimeSessionParts(parts: string[], t: Translate = translate): string {
|
||||
if (parts.length <= 1) {
|
||||
return parts[0] ?? ''
|
||||
}
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.ProjectWindowsRuntimeSetting.runtimeSessionJoin',
|
||||
'{{value0}} and {{value1}}',
|
||||
{ value0: parts.slice(0, -1).join(', '), value1: parts.at(-1) }
|
||||
)
|
||||
}
|
||||
|
||||
function getLiveTerminalCountLabel(count: number): string {
|
||||
return translate(
|
||||
function getLiveTerminalCountLabel(count: number, t: Translate = translate): string {
|
||||
return t(
|
||||
count === 1
|
||||
? 'auto.components.settings.ProjectWindowsRuntimeSetting.liveTerminalSingular'
|
||||
: 'auto.components.settings.ProjectWindowsRuntimeSetting.liveTerminalPlural',
|
||||
@@ -259,8 +258,8 @@ function getLiveTerminalCountLabel(count: number): string {
|
||||
)
|
||||
}
|
||||
|
||||
function getActiveTaskCountLabel(count: number): string {
|
||||
return translate(
|
||||
function getActiveTaskCountLabel(count: number, t: Translate = translate): string {
|
||||
return t(
|
||||
count === 1
|
||||
? 'auto.components.settings.ProjectWindowsRuntimeSetting.activeTaskSingular'
|
||||
: 'auto.components.settings.ProjectWindowsRuntimeSetting.activeTaskPlural',
|
||||
@@ -269,7 +268,10 @@ function getActiveTaskCountLabel(count: number): string {
|
||||
)
|
||||
}
|
||||
|
||||
function getRuntimeSessionWarning(summary?: ProjectRuntimeSessionSummary): string | null {
|
||||
function getRuntimeSessionWarning(
|
||||
summary?: ProjectRuntimeSessionSummary,
|
||||
t: Translate = translate
|
||||
): string | null {
|
||||
const liveTerminalCount = summary?.liveTerminalCount ?? 0
|
||||
const activeTaskCount = summary?.activeTaskCount ?? 0
|
||||
if (liveTerminalCount === 0 && activeTaskCount === 0) {
|
||||
@@ -277,14 +279,14 @@ function getRuntimeSessionWarning(summary?: ProjectRuntimeSessionSummary): strin
|
||||
}
|
||||
|
||||
const parts = [
|
||||
liveTerminalCount > 0 ? getLiveTerminalCountLabel(liveTerminalCount) : '',
|
||||
activeTaskCount > 0 ? getActiveTaskCountLabel(activeTaskCount) : ''
|
||||
liveTerminalCount > 0 ? getLiveTerminalCountLabel(liveTerminalCount, t) : '',
|
||||
activeTaskCount > 0 ? getActiveTaskCountLabel(activeTaskCount, t) : ''
|
||||
].filter((part) => part.length > 0)
|
||||
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.ProjectWindowsRuntimeSetting.runtimeSessionWarning',
|
||||
'{{value0}} will keep running in the current runtime. Let tasks finish or restart terminals before continuing.',
|
||||
{ value0: joinRuntimeSessionParts(parts) }
|
||||
{ value0: joinRuntimeSessionParts(parts, t) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -318,14 +320,15 @@ function getVisibleDistroOptions(
|
||||
}
|
||||
|
||||
function getDefaultRuntimeLabel(
|
||||
settings: Pick<GlobalSettings, 'localWindowsRuntimeDefault'>
|
||||
settings: Pick<GlobalSettings, 'localWindowsRuntimeDefault'>,
|
||||
t: Translate = translate
|
||||
): string {
|
||||
const runtimeLabel =
|
||||
settings.localWindowsRuntimeDefault.kind === 'wsl'
|
||||
? translate('auto.components.settings.ProjectWindowsRuntimeSetting.wsl', 'WSL')
|
||||
: translate('auto.components.settings.ProjectWindowsRuntimeSetting.windows', 'Windows')
|
||||
? t('auto.components.settings.ProjectWindowsRuntimeSetting.wsl', 'WSL')
|
||||
: t('auto.components.settings.ProjectWindowsRuntimeSetting.windows', 'Windows')
|
||||
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.ProjectWindowsRuntimeSetting.defaultRuntime',
|
||||
'Default ({{value0}})',
|
||||
{ value0: runtimeLabel }
|
||||
@@ -333,7 +336,8 @@ function getDefaultRuntimeLabel(
|
||||
}
|
||||
|
||||
function getProjectRuntimeDescription(
|
||||
resolution: ReturnType<typeof resolveProjectExecutionRuntime>
|
||||
resolution: ReturnType<typeof resolveProjectExecutionRuntime>,
|
||||
t: Translate = translate
|
||||
): string {
|
||||
if (resolution.status === 'repair-required') {
|
||||
return projectRuntimeRepairSentence(resolution.repair, translate)
|
||||
@@ -341,12 +345,12 @@ function getProjectRuntimeDescription(
|
||||
|
||||
if (resolution.runtime.kind === 'wsl') {
|
||||
return resolution.runtime.reason === 'global-default'
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.ProjectWindowsRuntimeSetting.inheritedWsl',
|
||||
'No project override. General settings select {{value0}} via WSL.',
|
||||
{ value0: resolution.runtime.distro }
|
||||
)
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.ProjectWindowsRuntimeSetting.projectWsl',
|
||||
'This project runs in {{value0}} via WSL.',
|
||||
{ value0: resolution.runtime.distro }
|
||||
@@ -354,11 +358,11 @@ function getProjectRuntimeDescription(
|
||||
}
|
||||
|
||||
return resolution.runtime.reason === 'global-default'
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.ProjectWindowsRuntimeSetting.inheritedWindows',
|
||||
'No project override. General settings select Windows.'
|
||||
)
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.ProjectWindowsRuntimeSetting.projectWindows',
|
||||
'This project runs on Windows.'
|
||||
)
|
||||
|
||||
+42
-46
@@ -15,6 +15,7 @@ import { ipcOrRuntimeTargetForHostId } from '@/runtime/rpc/targets/runtime-clien
|
||||
import { UNRESOLVED_GIT_HOST_ERROR } from '@/store/slices/editor/git/git-operation-target'
|
||||
import { getRepoExecutionHostId } from '../../../../../../shared/execution-host'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { searchKeywords } from '../../search/settings-search-keywords'
|
||||
|
||||
type RepositoryForkSyncSectionProps = {
|
||||
@@ -103,6 +104,7 @@ export function RepositoryForkSyncSection({
|
||||
updateRepo,
|
||||
forceVisible
|
||||
}: RepositoryForkSyncSectionProps): React.JSX.Element | null {
|
||||
const t = useTranslate()
|
||||
const upstream = repo.upstream
|
||||
const [syncing, setSyncing] = useState(false)
|
||||
const syncInFlightRef = useRef(false)
|
||||
@@ -149,7 +151,7 @@ export function RepositoryForkSyncSection({
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
translate('auto.components.settings.RepositoryForkSyncSection.failed', 'Fork sync failed'),
|
||||
t('auto.components.settings.RepositoryForkSyncSection.failed', 'Fork sync failed'),
|
||||
{ description: error instanceof Error ? error.message : String(error) }
|
||||
)
|
||||
} finally {
|
||||
@@ -160,58 +162,55 @@ export function RepositoryForkSyncSection({
|
||||
|
||||
return (
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.RepositoryForkSyncSection.title',
|
||||
'Keep Fork Up to Date'
|
||||
)}
|
||||
description={translate(
|
||||
title={t('auto.components.settings.RepositoryForkSyncSection.title', 'Keep Fork Up to Date')}
|
||||
description={t(
|
||||
'auto.components.settings.RepositoryForkSyncSection.description',
|
||||
'Safely fast-forward this fork from upstream.'
|
||||
)}
|
||||
keywords={searchKeywords([
|
||||
repo.displayName,
|
||||
upstream.owner,
|
||||
upstream.repo,
|
||||
{ key: 'auto.components.settings.repository.search.fork', fallback: 'fork' },
|
||||
{ key: 'auto.components.settings.repository.search.upstream', fallback: 'upstream' },
|
||||
{ key: 'auto.components.settings.repository.search.syncFork', fallback: 'sync fork' },
|
||||
{
|
||||
key: 'auto.components.settings.repository.search.keepForkUpToDate',
|
||||
fallback: 'keep fork up to date'
|
||||
},
|
||||
{
|
||||
key: 'auto.components.settings.repository.search.fastForward',
|
||||
fallback: 'fast-forward'
|
||||
},
|
||||
{
|
||||
key: 'auto.components.settings.repository.search.behindUpstream',
|
||||
fallback: 'behind upstream'
|
||||
},
|
||||
{ key: 'auto.components.settings.repository.search.origin', fallback: 'origin' },
|
||||
{
|
||||
key: 'auto.components.settings.repository.search.defaultBranch',
|
||||
fallback: 'default branch'
|
||||
}
|
||||
])}
|
||||
keywords={searchKeywords(
|
||||
[
|
||||
repo.displayName,
|
||||
upstream.owner,
|
||||
upstream.repo,
|
||||
{ key: 'auto.components.settings.repository.search.fork', fallback: 'fork' },
|
||||
{ key: 'auto.components.settings.repository.search.upstream', fallback: 'upstream' },
|
||||
{ key: 'auto.components.settings.repository.search.syncFork', fallback: 'sync fork' },
|
||||
{
|
||||
key: 'auto.components.settings.repository.search.keepForkUpToDate',
|
||||
fallback: 'keep fork up to date'
|
||||
},
|
||||
{
|
||||
key: 'auto.components.settings.repository.search.fastForward',
|
||||
fallback: 'fast-forward'
|
||||
},
|
||||
{
|
||||
key: 'auto.components.settings.repository.search.behindUpstream',
|
||||
fallback: 'behind upstream'
|
||||
},
|
||||
{ key: 'auto.components.settings.repository.search.origin', fallback: 'origin' },
|
||||
{
|
||||
key: 'auto.components.settings.repository.search.defaultBranch',
|
||||
fallback: 'default branch'
|
||||
}
|
||||
],
|
||||
t
|
||||
)}
|
||||
className="space-y-3 py-3"
|
||||
forceVisible={forceVisible}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<div className="text-sm font-semibold">
|
||||
{translate(
|
||||
'auto.components.settings.RepositoryForkSyncSection.title',
|
||||
'Keep Fork Up to Date'
|
||||
)}
|
||||
{t('auto.components.settings.RepositoryForkSyncSection.title', 'Keep Fork Up to Date')}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryForkSyncSection.longDescription',
|
||||
'When this fork is behind upstream, Orca can safely fast-forward its default branch. Orca skips the update if the branch has local-only commits or conflicts.'
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryForkSyncSection.forkOf',
|
||||
'Fork of {{owner}}/{{repo}}',
|
||||
{ owner: upstream.owner, repo: upstream.repo }
|
||||
@@ -228,14 +227,14 @@ export function RepositoryForkSyncSection({
|
||||
>
|
||||
<RefreshCw className={syncing ? 'size-3.5 animate-spin' : 'size-3.5'} />
|
||||
{syncing
|
||||
? translate('auto.components.settings.RepositoryForkSyncSection.syncing', 'Syncing')
|
||||
: translate('auto.components.settings.RepositoryForkSyncSection.syncNow', 'Sync Now')}
|
||||
? t('auto.components.settings.RepositoryForkSyncSection.syncing', 'Syncing')
|
||||
: t('auto.components.settings.RepositoryForkSyncSection.syncNow', 'Sync Now')}
|
||||
</Button>
|
||||
</div>
|
||||
<SettingsSegmentedControl<ForkSyncMode>
|
||||
value={mode}
|
||||
onChange={updateMode}
|
||||
ariaLabel={translate(
|
||||
ariaLabel={t(
|
||||
'auto.components.settings.RepositoryForkSyncSection.modeLabel',
|
||||
'Fork sync mode'
|
||||
)}
|
||||
@@ -243,20 +242,17 @@ export function RepositoryForkSyncSection({
|
||||
options={[
|
||||
{
|
||||
value: 'ask',
|
||||
label: translate('auto.components.settings.RepositoryForkSyncSection.ask', 'Ask'),
|
||||
label: t('auto.components.settings.RepositoryForkSyncSection.ask', 'Ask'),
|
||||
disabled: syncing
|
||||
},
|
||||
{
|
||||
value: 'safe-auto',
|
||||
label: translate(
|
||||
'auto.components.settings.RepositoryForkSyncSection.safeAuto',
|
||||
'Safe Auto'
|
||||
),
|
||||
label: t('auto.components.settings.RepositoryForkSyncSection.safeAuto', 'Safe Auto'),
|
||||
disabled: syncing
|
||||
},
|
||||
{
|
||||
value: 'off',
|
||||
label: translate('auto.components.settings.RepositoryForkSyncSection.off', 'Off'),
|
||||
label: t('auto.components.settings.RepositoryForkSyncSection.off', 'Off'),
|
||||
disabled: syncing
|
||||
}
|
||||
]}
|
||||
|
||||
+43
-53
@@ -7,6 +7,7 @@ import type {
|
||||
} from '../../../../../../shared/orca-yaml-hook-types'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { i18n, translate } from '@/i18n/i18n'
|
||||
import { useTranslate, type Translate } from '@/i18n/use-translate'
|
||||
import { matchesSettingsSearch } from '../../search/settings-search'
|
||||
import { SettingsSwitch } from '../../form-controls/SettingsFormControls'
|
||||
import { RepositoryHooksYamlStatus } from './RepositoryHooksYamlStatus'
|
||||
@@ -39,37 +40,28 @@ function SegmentedPolicyToggle<Policy extends string>({
|
||||
)
|
||||
}
|
||||
|
||||
function getSetupRunPolicyOptions(): PolicyOption<SetupRunPolicy>[] {
|
||||
function getSetupRunPolicyOptions(t: Translate = translate): PolicyOption<SetupRunPolicy>[] {
|
||||
return [
|
||||
{
|
||||
policy: 'ask',
|
||||
label: translate(
|
||||
'auto.components.settings.RepositoryHooksSection.e03d9a8f38',
|
||||
'Ask every time'
|
||||
),
|
||||
description: translate(
|
||||
label: t('auto.components.settings.RepositoryHooksSection.e03d9a8f38', 'Ask every time'),
|
||||
description: t(
|
||||
'auto.components.settings.RepositoryHooksSection.90b1f50137',
|
||||
'Prompt before running setup.'
|
||||
)
|
||||
},
|
||||
{
|
||||
policy: 'run-by-default',
|
||||
label: translate(
|
||||
'auto.components.settings.RepositoryHooksSection.d3ef1ab247',
|
||||
'Run by default'
|
||||
),
|
||||
description: translate(
|
||||
label: t('auto.components.settings.RepositoryHooksSection.d3ef1ab247', 'Run by default'),
|
||||
description: t(
|
||||
'auto.components.settings.RepositoryHooksSection.022ba10cf2',
|
||||
'Run setup automatically.'
|
||||
)
|
||||
},
|
||||
{
|
||||
policy: 'skip-by-default',
|
||||
label: translate(
|
||||
'auto.components.settings.RepositoryHooksSection.15debc1fd9',
|
||||
'Skip by default'
|
||||
),
|
||||
description: translate(
|
||||
label: t('auto.components.settings.RepositoryHooksSection.15debc1fd9', 'Skip by default'),
|
||||
description: t(
|
||||
'auto.components.settings.RepositoryHooksSection.99e3264a49',
|
||||
'Only run setup when chosen.'
|
||||
)
|
||||
@@ -77,31 +69,30 @@ function getSetupRunPolicyOptions(): PolicyOption<SetupRunPolicy>[] {
|
||||
]
|
||||
}
|
||||
|
||||
function getCommandSourcePolicyOptions(): PolicyOption<HookCommandSourcePolicy>[] {
|
||||
function getCommandSourcePolicyOptions(
|
||||
t: Translate = translate
|
||||
): PolicyOption<HookCommandSourcePolicy>[] {
|
||||
return [
|
||||
{
|
||||
policy: 'shared-only',
|
||||
label: translate(
|
||||
'auto.components.settings.RepositoryHooksSection.d88b6ff88f',
|
||||
'orca.yaml only'
|
||||
),
|
||||
description: translate(
|
||||
label: t('auto.components.settings.RepositoryHooksSection.d88b6ff88f', 'orca.yaml only'),
|
||||
description: t(
|
||||
'auto.components.settings.RepositoryHooksSection.29397e8bbc',
|
||||
'Run only committed repo commands; ignore local commands.'
|
||||
)
|
||||
},
|
||||
{
|
||||
policy: 'local-only',
|
||||
label: translate('auto.components.settings.RepositoryHooksSection.83dc78202a', 'Local only'),
|
||||
description: translate(
|
||||
label: t('auto.components.settings.RepositoryHooksSection.83dc78202a', 'Local only'),
|
||||
description: t(
|
||||
'auto.components.settings.RepositoryHooksSection.0e8b2a520d',
|
||||
'Ignore orca.yaml; run only your local commands.'
|
||||
)
|
||||
},
|
||||
{
|
||||
policy: 'run-both',
|
||||
label: translate('auto.components.settings.RepositoryHooksSection.8d6c56bff8', 'Run both'),
|
||||
description: translate(
|
||||
label: t('auto.components.settings.RepositoryHooksSection.8d6c56bff8', 'Run both'),
|
||||
description: t(
|
||||
'auto.components.settings.RepositoryHooksSection.8561b0665f',
|
||||
'orca.yaml first, then your local commands.'
|
||||
)
|
||||
@@ -120,16 +111,17 @@ export function RepositorySetupPolicySetting({
|
||||
onRunPolicyChange: (policy: SetupRunPolicy) => void
|
||||
onStartupPolicyChange: (policy: SetupAgentStartupPolicy) => void
|
||||
}): React.JSX.Element {
|
||||
const options = getSetupRunPolicyOptions()
|
||||
const t = useTranslate()
|
||||
const options = getSetupRunPolicyOptions(t)
|
||||
return (
|
||||
<div className="space-y-4 rounded-2xl border border-border/50 bg-background/80 p-4 shadow-sm">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h5 className="text-sm font-semibold">
|
||||
{translate('auto.components.settings.RepositoryHooksSection.793dcee97d', 'When to run')}
|
||||
{t('auto.components.settings.RepositoryHooksSection.793dcee97d', 'When to run')}
|
||||
</h5>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryHooksSection.21fb607a87',
|
||||
'Default behavior when a new worktree is created.'
|
||||
)}
|
||||
@@ -144,7 +136,7 @@ export function RepositorySetupPolicySetting({
|
||||
<div className="flex items-start justify-between gap-4 border-t border-border/60 pt-4">
|
||||
<div className="min-w-0">
|
||||
<h5 className="text-sm font-semibold">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryHooksSection.waitForSetupBeforeAgent',
|
||||
'Wait for setup to complete before starting agent'
|
||||
)}
|
||||
@@ -157,7 +149,7 @@ export function RepositorySetupPolicySetting({
|
||||
setupAgentStartupPolicy === 'wait-for-setup' ? 'start-immediately' : 'wait-for-setup'
|
||||
)
|
||||
}
|
||||
ariaLabel={translate(
|
||||
ariaLabel={t(
|
||||
'auto.components.settings.RepositoryHooksSection.waitForSetupBeforeAgent',
|
||||
'Wait for setup to complete before starting agent'
|
||||
)}
|
||||
@@ -167,14 +159,14 @@ export function RepositorySetupPolicySetting({
|
||||
)
|
||||
}
|
||||
|
||||
function getCommandSourceLabel(policy: HookCommandSourcePolicy): string {
|
||||
function getCommandSourceLabel(policy: HookCommandSourcePolicy, t: Translate = translate): string {
|
||||
if (policy === 'shared-only') {
|
||||
return translate('auto.components.settings.RepositoryHooksSection.d88b6ff88f', 'orca.yaml only')
|
||||
return t('auto.components.settings.RepositoryHooksSection.d88b6ff88f', 'orca.yaml only')
|
||||
}
|
||||
if (policy === 'local-only') {
|
||||
return translate('auto.components.settings.RepositoryHooksSection.83dc78202a', 'Local only')
|
||||
return t('auto.components.settings.RepositoryHooksSection.83dc78202a', 'Local only')
|
||||
}
|
||||
return translate('auto.components.settings.RepositoryHooksSection.8d6c56bff8', 'Run both')
|
||||
return t('auto.components.settings.RepositoryHooksSection.8d6c56bff8', 'Run both')
|
||||
}
|
||||
|
||||
export function RepositoryHookCommandSourceSetting({
|
||||
@@ -198,25 +190,26 @@ export function RepositoryHookCommandSourceSetting({
|
||||
onCopyTemplate: () => void
|
||||
onAdvancedOpenChange: (open: boolean) => void
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const advancedMatchesSearch =
|
||||
searchQuery.trim() !== '' &&
|
||||
matchesSettingsSearch(searchQuery, {
|
||||
title: translate('auto.components.settings.RepositoryHooksSection.c9bc1bfd8f', 'Advanced'),
|
||||
description: translate(
|
||||
title: t('auto.components.settings.RepositoryHooksSection.c9bc1bfd8f', 'Advanced'),
|
||||
description: t(
|
||||
'auto.components.settings.RepositoryHooksSection.610d90fdbd',
|
||||
'Command source and orca.yaml details.'
|
||||
),
|
||||
keywords: [
|
||||
translate('auto.components.settings.RepositoryHooksSection.c5a55a2d2e', 'advanced'),
|
||||
translate('auto.components.settings.RepositoryHooksSection.4611b78617', 'command source'),
|
||||
translate('auto.components.settings.RepositoryHooksSection.39da2ae12f', 'orca.yaml'),
|
||||
translate('auto.components.settings.RepositoryHooksSection.d2b3016c20', 'shared'),
|
||||
translate('auto.components.settings.RepositoryHooksSection.2d03a514db', 'local'),
|
||||
translate('auto.components.settings.RepositoryHooksSection.0518758f38', 'both'),
|
||||
translate('auto.components.settings.RepositoryHooksSection.fac13f8c1e', 'authoritative')
|
||||
t('auto.components.settings.RepositoryHooksSection.c5a55a2d2e', 'advanced'),
|
||||
t('auto.components.settings.RepositoryHooksSection.4611b78617', 'command source'),
|
||||
t('auto.components.settings.RepositoryHooksSection.39da2ae12f', 'orca.yaml'),
|
||||
t('auto.components.settings.RepositoryHooksSection.d2b3016c20', 'shared'),
|
||||
t('auto.components.settings.RepositoryHooksSection.2d03a514db', 'local'),
|
||||
t('auto.components.settings.RepositoryHooksSection.0518758f38', 'both'),
|
||||
t('auto.components.settings.RepositoryHooksSection.fac13f8c1e', 'authoritative')
|
||||
]
|
||||
})
|
||||
const options = getCommandSourcePolicyOptions()
|
||||
const options = getCommandSourcePolicyOptions(t)
|
||||
return (
|
||||
<details
|
||||
className="group rounded-2xl border border-border/50 bg-background/80 shadow-sm"
|
||||
@@ -240,32 +233,29 @@ export function RepositoryHookCommandSourceSetting({
|
||||
<div className="flex items-center gap-2">
|
||||
<ChevronRight className="size-3.5 text-muted-foreground transition-transform group-open:rotate-90" />
|
||||
<h5 className="text-sm font-semibold">
|
||||
{translate('auto.components.settings.RepositoryHooksSection.c9bc1bfd8f', 'Advanced')}
|
||||
{t('auto.components.settings.RepositoryHooksSection.c9bc1bfd8f', 'Advanced')}
|
||||
</h5>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryHooksSection.bbbd6e0bc4',
|
||||
'Command source & orca.yaml'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="rounded-full border border-border bg-muted px-2 py-0.5 text-[11px] font-medium text-foreground">
|
||||
{getCommandSourceLabel(selectedPolicy)}
|
||||
{getCommandSourceLabel(selectedPolicy, t)}
|
||||
</span>
|
||||
</summary>
|
||||
<div className="space-y-5 border-t border-border/50 px-4 py-4">
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium">
|
||||
{translate(
|
||||
'auto.components.settings.RepositoryHooksSection.32fec28f5b',
|
||||
'Command Source'
|
||||
)}
|
||||
{t('auto.components.settings.RepositoryHooksSection.32fec28f5b', 'Command Source')}
|
||||
</p>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
<Trans
|
||||
i18n={i18n}
|
||||
defaults={translate(
|
||||
defaults={t(
|
||||
'auto.components.settings.RepositoryHooksSection.commandSourceDescription',
|
||||
'When both <yaml>orca.yaml</yaml> and local commands exist, choose which run.'
|
||||
)}
|
||||
|
||||
+26
-30
@@ -3,31 +3,32 @@ import { AlertTriangle, Plus } from 'lucide-react'
|
||||
import { Button } from '../../../ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../../ui/tooltip'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate, type Translate } from '@/i18n/use-translate'
|
||||
import { getRepositoryHookScriptTextareaRows } from '@/lib/script-textarea-rows'
|
||||
import type {
|
||||
LocalCommandSourcePolicyNotice,
|
||||
LocalHookField
|
||||
} from './repository-hook-settings-draft'
|
||||
|
||||
function getEnvVars(): { name: string; description: string }[] {
|
||||
function getEnvVars(t: Translate = translate): { name: string; description: string }[] {
|
||||
return [
|
||||
{
|
||||
name: '$ORCA_ROOT_PATH',
|
||||
description: translate(
|
||||
description: t(
|
||||
'auto.components.settings.RepositoryHooksSection.30952c4aa4',
|
||||
'Path to the main repo checkout. Useful for copying shared files, like .env, into a worktree.'
|
||||
)
|
||||
},
|
||||
{
|
||||
name: '$ORCA_WORKTREE_PATH',
|
||||
description: translate(
|
||||
description: t(
|
||||
'auto.components.settings.RepositoryHooksSection.54c73d88d0',
|
||||
'Path to the worktree being created. Setup commands run from this directory.'
|
||||
)
|
||||
},
|
||||
{
|
||||
name: '$ORCA_WORKSPACE_NAME',
|
||||
description: translate(
|
||||
description: t(
|
||||
'auto.components.settings.RepositoryHooksSection.0fa21e19ec',
|
||||
'Name of the workspace, usually based on the branch name.'
|
||||
)
|
||||
@@ -36,11 +37,12 @@ function getEnvVars(): { name: string; description: string }[] {
|
||||
}
|
||||
|
||||
function EnvVarChips(): React.JSX.Element {
|
||||
const envVars = getEnvVars()
|
||||
const t = useTranslate()
|
||||
const envVars = getEnvVars(t)
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryHooksSection.b2b06c7ce8',
|
||||
'Available environment variables (hover for details):'
|
||||
)}
|
||||
@@ -69,6 +71,7 @@ function EnvVarChips(): React.JSX.Element {
|
||||
}
|
||||
|
||||
function SaveIndicator({ status }: { status: 'idle' | 'saving' | 'saved' }) {
|
||||
const t = useTranslate()
|
||||
if (status === 'idle') {
|
||||
return null
|
||||
}
|
||||
@@ -82,8 +85,8 @@ function SaveIndicator({ status }: { status: 'idle' | 'saving' | 'saved' }) {
|
||||
className={`size-1.5 rounded-full ${isSaving ? 'animate-pulse bg-amber-500' : 'bg-emerald-500'}`}
|
||||
/>
|
||||
{isSaving
|
||||
? translate('auto.components.settings.RepositoryHooksSection.81057d5f71', 'Saving...')
|
||||
: translate('auto.components.settings.RepositoryHooksSection.2b6356e744', 'Saved')}
|
||||
? t('auto.components.settings.RepositoryHooksSection.81057d5f71', 'Saving...')
|
||||
: t('auto.components.settings.RepositoryHooksSection.2b6356e744', 'Saved')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -105,6 +108,7 @@ export function RepositoryHookScriptSetting({
|
||||
onCommit: () => void
|
||||
sectionId?: string
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const [showLocal, setShowLocal] = useState(value.length > 0)
|
||||
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved'>('idle')
|
||||
const lastValueRef = useRef(value)
|
||||
@@ -148,26 +152,20 @@ export function RepositoryHookScriptSetting({
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full border border-emerald-500/25 bg-emerald-500/10 px-2 py-0.5 text-[11px] font-medium text-emerald-700 dark:text-emerald-300">
|
||||
{translate('auto.components.settings.RepositoryHooksSection.39da2ae12f', 'orca.yaml')}
|
||||
{t('auto.components.settings.RepositoryHooksSection.39da2ae12f', 'orca.yaml')}
|
||||
<span className="font-normal text-emerald-700/80 dark:text-emerald-300/80">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryHooksSection.f828e1de19',
|
||||
'- shared with your team'
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{translate('auto.components.settings.RepositoryHooksSection.b113344b6a', 'Edit')}{' '}
|
||||
{t('auto.components.settings.RepositoryHooksSection.b113344b6a', 'Edit')}{' '}
|
||||
<code className="rounded bg-muted px-1 py-0.5">
|
||||
{translate(
|
||||
'auto.components.settings.RepositoryHooksSection.39da2ae12f',
|
||||
'orca.yaml'
|
||||
)}
|
||||
{t('auto.components.settings.RepositoryHooksSection.39da2ae12f', 'orca.yaml')}
|
||||
</code>{' '}
|
||||
{translate(
|
||||
'auto.components.settings.RepositoryHooksSection.7e4427b4a2',
|
||||
'to change.'
|
||||
)}
|
||||
{t('auto.components.settings.RepositoryHooksSection.7e4427b4a2', 'to change.')}
|
||||
</span>
|
||||
</div>
|
||||
<pre className="overflow-x-auto whitespace-pre-wrap break-words rounded-lg border border-border/50 bg-muted/30 p-3 font-mono text-[11.5px] leading-5 text-foreground">
|
||||
@@ -180,9 +178,9 @@ export function RepositoryHookScriptSetting({
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{hasShared ? (
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full border border-border bg-muted/30 px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
|
||||
{translate('auto.components.settings.RepositoryHooksSection.2d03a514db', 'local')}
|
||||
{t('auto.components.settings.RepositoryHooksSection.2d03a514db', 'local')}
|
||||
<span className="font-normal">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryHooksSection.40a446ae16',
|
||||
'- just for you, on this machine'
|
||||
)}
|
||||
@@ -204,7 +202,7 @@ export function RepositoryHookScriptSetting({
|
||||
className="w-full min-w-0 resize-y rounded-lg border border-input bg-muted/20 px-3 py-2 font-mono text-[12px] leading-[1.55] shadow-xs transition-[color,box-shadow] outline-none placeholder:italic placeholder:text-muted-foreground focus-visible:border-ring focus-visible:bg-background focus-visible:ring-[3px] focus-visible:ring-ring/40"
|
||||
/>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryHooksSection.8c2893fae0',
|
||||
'Runs as a single shell script. Saved on this machine.'
|
||||
)}
|
||||
@@ -219,10 +217,7 @@ export function RepositoryHookScriptSetting({
|
||||
className="gap-1.5"
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
{translate(
|
||||
'auto.components.settings.RepositoryHooksSection.5d940bde5c',
|
||||
'Add local script'
|
||||
)}
|
||||
{t('auto.components.settings.RepositoryHooksSection.5d940bde5c', 'Add local script')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -236,24 +231,25 @@ export function LocalCommandSourceNotice({
|
||||
notice: LocalCommandSourcePolicyNotice
|
||||
onSelectPolicy: (policy: 'local-only' | 'run-both') => void
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<div className="flex flex-wrap items-start justify-between gap-3 rounded-xl border border-amber-500/20 bg-amber-500/5 p-3">
|
||||
<div className="flex min-w-0 flex-1 items-start gap-3">
|
||||
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-amber-600 dark:text-amber-300" />
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-amber-700 dark:text-amber-300">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryHooksSection.5426ecbdcb',
|
||||
'Local scripts will not run'
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
{notice.kind === 'checking'
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.RepositoryHooksSection.7f78e5eea6',
|
||||
'Local scripts are saved. Orca is still checking orca.yaml before it can recommend which script source to use.'
|
||||
)
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.RepositoryHooksSection.0ce113fd7b',
|
||||
'Local scripts are saved, but Script Source is set to orca.yaml only.'
|
||||
)}
|
||||
@@ -272,7 +268,7 @@ export function LocalCommandSourceNotice({
|
||||
</Button>
|
||||
) : (
|
||||
<span className="shrink-0 rounded-full border border-border/60 bg-muted/30 px-2 py-1 text-[11px] text-muted-foreground">
|
||||
{translate('auto.components.settings.RepositoryHooksSection.673a7fd10e', 'Checking...')}
|
||||
{t('auto.components.settings.RepositoryHooksSection.673a7fd10e', 'Checking...')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+25
-33
@@ -11,7 +11,7 @@ import type { Repo } from '../../../../../../shared/repo-types'
|
||||
import { getRepoExecutionHostId } from '../../../../../../shared/execution-host'
|
||||
import { resolveHookCommandSourcePolicy } from '../../../../../../shared/hook-command-source-policy'
|
||||
import { useAppStore } from '@/store/index'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { SearchableSetting } from '../../section-layout/SearchableSetting'
|
||||
import { SettingsGroup } from '../../section-layout/SettingsGroup'
|
||||
import { getRepositoryLocalCommandsSectionId } from '../repository-settings-targets'
|
||||
@@ -54,6 +54,7 @@ export function RepositoryHooksSection({
|
||||
onCopyTemplate,
|
||||
onUpdateHookSettings
|
||||
}: RepositoryHooksSectionProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
useTranslation()
|
||||
const settingsSearchQuery = useAppStore((state) => state.settingsSearchQuery)
|
||||
const selectedHostId = getRepoExecutionHostId(repo)
|
||||
@@ -77,7 +78,7 @@ export function RepositoryHooksSection({
|
||||
repoHostIdentity,
|
||||
selectedHostId
|
||||
})
|
||||
const localHookFields = getLocalHookFields()
|
||||
const localHookFields = getLocalHookFields(t)
|
||||
const selectedSetupRunPolicy: SetupRunPolicy =
|
||||
hookSettingsDraft.setupRunPolicy ?? 'run-by-default'
|
||||
const selectedSetupAgentStartupPolicy: SetupAgentStartupPolicy =
|
||||
@@ -94,33 +95,30 @@ export function RepositoryHooksSection({
|
||||
hookSettingsDraft.commandSourcePolicy,
|
||||
{ hasLocalScript }
|
||||
)
|
||||
const localCommandSourceNotice = getLocalCommandSourcePolicyNotice({
|
||||
hooksInspectionReady,
|
||||
currentPolicy: selectedCommandSourcePolicy,
|
||||
setupScript: hookSettingsDraft.scripts.setup,
|
||||
archiveScript: hookSettingsDraft.scripts.archive,
|
||||
hasSharedScript
|
||||
})
|
||||
const localCommandSourceNotice = getLocalCommandSourcePolicyNotice(
|
||||
{
|
||||
hooksInspectionReady,
|
||||
currentPolicy: selectedCommandSourcePolicy,
|
||||
setupScript: hookSettingsDraft.scripts.setup,
|
||||
archiveScript: hookSettingsDraft.scripts.archive,
|
||||
hasSharedScript
|
||||
},
|
||||
t
|
||||
)
|
||||
const [isAdvancedOpen, setIsAdvancedOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div ref={flushScriptDraftOnUnmount}>
|
||||
<SettingsGroup
|
||||
title={translate(
|
||||
'auto.components.settings.RepositoryHooksSection.ff082fe7c6',
|
||||
'Worktree Hooks'
|
||||
)}
|
||||
description={translate(
|
||||
title={t('auto.components.settings.RepositoryHooksSection.ff082fe7c6', 'Worktree Hooks')}
|
||||
description={t(
|
||||
'auto.components.settings.RepositoryHooksSection.8567127a40',
|
||||
'Scripts that run when worktrees are created or archived. Local scripts are stored on this machine; `orca.yaml` scripts are shared with your team.'
|
||||
)}
|
||||
>
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.RepositoryHooksSection.52b31baf02',
|
||||
'Setup Script'
|
||||
)}
|
||||
description={translate(
|
||||
title={t('auto.components.settings.RepositoryHooksSection.52b31baf02', 'Setup Script')}
|
||||
description={t(
|
||||
'auto.components.settings.RepositoryHooksSection.30d555acd2',
|
||||
'Local and shared scripts that run after a new worktree is created.'
|
||||
)}
|
||||
@@ -149,11 +147,11 @@ export function RepositoryHooksSection({
|
||||
/>
|
||||
</SearchableSetting>
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
title={t(
|
||||
'auto.components.settings.RepositoryHooksSection.fb6bebcf7e',
|
||||
'When to Run Setup'
|
||||
)}
|
||||
description={translate(
|
||||
description={t(
|
||||
'auto.components.settings.RepositoryHooksSection.63e1783173',
|
||||
'Choose the default behavior when a setup script is available.'
|
||||
)}
|
||||
@@ -173,11 +171,8 @@ export function RepositoryHooksSection({
|
||||
/>
|
||||
</SearchableSetting>
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.RepositoryHooksSection.9a100323ff',
|
||||
'Archive Script'
|
||||
)}
|
||||
description={translate(
|
||||
title={t('auto.components.settings.RepositoryHooksSection.9a100323ff', 'Archive Script')}
|
||||
description={t(
|
||||
'auto.components.settings.RepositoryHooksSection.b91a0f297d',
|
||||
'Local and shared scripts that run before a worktree is archived.'
|
||||
)}
|
||||
@@ -215,11 +210,11 @@ export function RepositoryHooksSection({
|
||||
</div>
|
||||
) : null}
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
title={t(
|
||||
'auto.components.settings.RepositoryHooksSection.13394103bd',
|
||||
'Custom GitHub Issue Command'
|
||||
)}
|
||||
description={translate(
|
||||
description={t(
|
||||
'auto.components.settings.RepositoryHooksSection.2cc27dc12b',
|
||||
'Optional per-user override for the linked-issue command.'
|
||||
)}
|
||||
@@ -230,11 +225,8 @@ export function RepositoryHooksSection({
|
||||
<RepositoryIssueCommandSetting {...issueCommand} />
|
||||
</SearchableSetting>
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.RepositoryHooksSection.c9bc1bfd8f',
|
||||
'Advanced'
|
||||
)}
|
||||
description={translate(
|
||||
title={t('auto.components.settings.RepositoryHooksSection.c9bc1bfd8f', 'Advanced')}
|
||||
description={t(
|
||||
'auto.components.settings.RepositoryHooksSection.610d90fdbd',
|
||||
'Command source and orca.yaml details.'
|
||||
)}
|
||||
|
||||
+27
-21
@@ -2,6 +2,7 @@ import { AlertTriangle } from 'lucide-react'
|
||||
import type { OrcaHooks } from '../../../../../../shared/orca-yaml-hook-types'
|
||||
import { Button } from '../../../ui/button'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate, type Translate } from '@/i18n/use-translate'
|
||||
import { renderYamlScriptPreview } from './repository-hook-settings-draft'
|
||||
|
||||
const EXAMPLE_TEMPLATE = `scripts:
|
||||
@@ -28,48 +29,51 @@ const YAML_STATE_STYLES: Record<string, { card: string; titleClassName: string }
|
||||
missing: { card: 'border-border/50 bg-muted/20', titleClassName: 'text-foreground' }
|
||||
}
|
||||
|
||||
function getYamlStateCopy(yamlState: string): { heading: string; description: string } {
|
||||
function getYamlStateCopy(
|
||||
yamlState: string,
|
||||
t: Translate = translate
|
||||
): { heading: string; description: string } {
|
||||
switch (yamlState) {
|
||||
case 'loaded':
|
||||
return {
|
||||
heading: translate(
|
||||
heading: t(
|
||||
'auto.components.settings.RepositoryHooksSection.56f9a4a1d0',
|
||||
'Using `orca.yaml`'
|
||||
),
|
||||
description: translate(
|
||||
description: t(
|
||||
'auto.components.settings.RepositoryHooksSection.ca424ff135',
|
||||
'Shared hook and issue-automation defaults are defined in the repo and available to everyone who uses it.'
|
||||
)
|
||||
}
|
||||
case 'update-available':
|
||||
return {
|
||||
heading: translate(
|
||||
heading: t(
|
||||
'auto.components.settings.RepositoryHooksSection.623e0c9f31',
|
||||
'`orca.yaml` could not be parsed'
|
||||
),
|
||||
description: translate(
|
||||
description: t(
|
||||
'auto.components.settings.RepositoryHooksSection.aba825233f',
|
||||
'The file contains configuration keys that this version of Orca does not recognize. You may need to update Orca, or check the file for typos.'
|
||||
)
|
||||
}
|
||||
case 'invalid':
|
||||
return {
|
||||
heading: translate(
|
||||
heading: t(
|
||||
'auto.components.settings.RepositoryHooksSection.623e0c9f31',
|
||||
'`orca.yaml` could not be parsed'
|
||||
),
|
||||
description: translate(
|
||||
description: t(
|
||||
'auto.components.settings.RepositoryHooksSection.0cc712b823',
|
||||
'The core configuration file exists in the repo root, but Orca could not parse the supported hook definitions yet.'
|
||||
)
|
||||
}
|
||||
default:
|
||||
return {
|
||||
heading: translate(
|
||||
heading: t(
|
||||
'auto.components.settings.RepositoryHooksSection.5a67e4793d',
|
||||
'No `orca.yaml` detected'
|
||||
),
|
||||
description: translate(
|
||||
description: t(
|
||||
'auto.components.settings.RepositoryHooksSection.b20c5df6ca',
|
||||
'Add an `orca.yaml` file to enable shared setup, archive, or issue-automation defaults for this repo. Example template:'
|
||||
)
|
||||
@@ -77,17 +81,17 @@ function getYamlStateCopy(yamlState: string): { heading: string; description: st
|
||||
}
|
||||
}
|
||||
|
||||
function getParseErrorFixes(): string[] {
|
||||
function getParseErrorFixes(t: Translate = translate): string[] {
|
||||
return [
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.RepositoryHooksSection.07ba35bc68',
|
||||
'Check the indentation under `scripts:`. Hook keys should use two spaces, and command lines should use four.'
|
||||
),
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.RepositoryHooksSection.787ca433ef',
|
||||
'Define only the supported keys: `scripts`, `setup`, `archive`, and `issueCommand`.'
|
||||
),
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.RepositoryHooksSection.ecc73d9125',
|
||||
'Compare your file against the working template below and copy that shape if needed.'
|
||||
)
|
||||
@@ -101,14 +105,15 @@ function ExampleTemplateCard({
|
||||
copiedTemplate: boolean
|
||||
onCopyTemplate: () => void
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<p className="text-[10px] tracking-[0.18em] text-muted-foreground">
|
||||
{translate('auto.components.settings.RepositoryHooksSection.175daba180', 'Example')}{' '}
|
||||
{t('auto.components.settings.RepositoryHooksSection.175daba180', 'Example')}{' '}
|
||||
<code className="rounded bg-muted px-1 py-0.5">
|
||||
{translate('auto.components.settings.RepositoryHooksSection.39da2ae12f', 'orca.yaml')}
|
||||
{t('auto.components.settings.RepositoryHooksSection.39da2ae12f', 'orca.yaml')}
|
||||
</code>{' '}
|
||||
{translate('auto.components.settings.RepositoryHooksSection.95a0411b3e', 'template')}
|
||||
{t('auto.components.settings.RepositoryHooksSection.95a0411b3e', 'template')}
|
||||
</p>
|
||||
<div className="relative rounded-lg border border-border/50 bg-background/70">
|
||||
<Button
|
||||
@@ -119,8 +124,8 @@ function ExampleTemplateCard({
|
||||
onClick={onCopyTemplate}
|
||||
>
|
||||
{copiedTemplate
|
||||
? translate('auto.components.settings.RepositoryHooksSection.3149964b66', 'Copied')
|
||||
: translate('auto.components.settings.RepositoryHooksSection.da37d6f10e', 'Copy')}
|
||||
? t('auto.components.settings.RepositoryHooksSection.3149964b66', 'Copied')
|
||||
: t('auto.components.settings.RepositoryHooksSection.da37d6f10e', 'Copy')}
|
||||
</Button>
|
||||
<pre className="overflow-x-auto whitespace-pre-wrap break-words p-3 pr-16 font-mono text-[11px] leading-5 text-muted-foreground">
|
||||
{EXAMPLE_TEMPLATE}
|
||||
@@ -141,8 +146,9 @@ export function RepositoryHooksYamlStatus({
|
||||
copiedTemplate: boolean
|
||||
onCopyTemplate: () => void
|
||||
}): React.JSX.Element {
|
||||
const copy = getYamlStateCopy(yamlState)
|
||||
const parseErrorFixes = getParseErrorFixes()
|
||||
const t = useTranslate()
|
||||
const copy = getYamlStateCopy(yamlState, t)
|
||||
const parseErrorFixes = getParseErrorFixes(t)
|
||||
return (
|
||||
<div className={`space-y-3 rounded-xl border p-3 ${YAML_STATE_STYLES[yamlState].card}`}>
|
||||
<div className="space-y-1">
|
||||
@@ -161,7 +167,7 @@ export function RepositoryHooksYamlStatus({
|
||||
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-amber-600 dark:text-amber-300" />
|
||||
<div className="space-y-2 text-xs text-muted-foreground">
|
||||
<p>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryHooksSection.af49e2a19e',
|
||||
'The file is present, but Orca could not find valid `scripts` or `issueCommand` definitions.'
|
||||
)}
|
||||
|
||||
+9
-17
@@ -6,7 +6,7 @@ import type {
|
||||
ProjectHostSetupCreateResult,
|
||||
ProjectHostSetupResult
|
||||
} from '../../../../../../shared/project-types'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { Button } from '../../../ui/button'
|
||||
import { Label } from '../../../ui/label'
|
||||
import { HostCombobox } from '../../../host-picker/HostCombobox'
|
||||
@@ -57,6 +57,7 @@ export function RepositoryHostSetupActions({
|
||||
createProjectHostSetup,
|
||||
onSetupReady
|
||||
}: RepositoryHostSetupActionsProps): React.JSX.Element | null {
|
||||
const t = useTranslate()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [step, setStep] = useState<SetupStep>('choose')
|
||||
const [selectedSetupHostId, setSelectedSetupHostId] = useState<ExecutionHostId | null>(null)
|
||||
@@ -162,13 +163,10 @@ export function RepositoryHostSetupActions({
|
||||
<div className="flex items-center justify-between gap-3 rounded-md border border-border bg-muted/20 p-3">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<Label className="text-sm font-semibold">
|
||||
{translate(
|
||||
'auto.components.settings.RepositoryPane.hostAvailability',
|
||||
'Host availability'
|
||||
)}
|
||||
{t('auto.components.settings.RepositoryPane.hostAvailability', 'Host availability')}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryPane.hostAvailabilityHelp',
|
||||
'Add this same project on another connected host.'
|
||||
)}
|
||||
@@ -176,10 +174,7 @@ export function RepositoryHostSetupActions({
|
||||
</div>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => setIsOpen(true)}>
|
||||
<Plus className="size-4" />
|
||||
{translate(
|
||||
'auto.components.settings.RepositoryPane.addToAnotherHost',
|
||||
'Add to another host'
|
||||
)}
|
||||
{t('auto.components.settings.RepositoryPane.addToAnotherHost', 'Add to another host')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
@@ -190,13 +185,10 @@ export function RepositoryHostSetupActions({
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<Label className="text-sm font-semibold">
|
||||
{translate(
|
||||
'auto.components.settings.RepositoryPane.addProjectHost',
|
||||
'Add project to host'
|
||||
)}
|
||||
{t('auto.components.settings.RepositoryPane.addProjectHost', 'Add project to host')}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryPane.addProjectHostHelp',
|
||||
'Choose where this project should also be available.'
|
||||
)}
|
||||
@@ -206,7 +198,7 @@ export function RepositoryHostSetupActions({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label={translate('auto.components.settings.RepositoryPane.closeHostSetup', 'Close')}
|
||||
aria-label={t('auto.components.settings.RepositoryPane.closeHostSetup', 'Close')}
|
||||
onClick={resetFlow}
|
||||
>
|
||||
<X className="size-4" />
|
||||
@@ -215,7 +207,7 @@ export function RepositoryHostSetupActions({
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium text-muted-foreground">
|
||||
{translate('auto.components.settings.RepositoryPane.setupHostLabel', 'Host')}
|
||||
{t('auto.components.settings.RepositoryPane.setupHostLabel', 'Host')}
|
||||
</Label>
|
||||
<HostCombobox
|
||||
value={setupTargetHostId ?? ''}
|
||||
|
||||
+26
-28
@@ -19,7 +19,7 @@ import { SearchableSetting } from '../../section-layout/SearchableSetting'
|
||||
import { SettingsBadge } from '../../form-controls/SettingsFormControls'
|
||||
import { matchesSettingsSearch, type SettingsSearchEntry } from '../../search/settings-search'
|
||||
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { buildSetupHostOptions, getSetupStateLabel } from './repository-host-setup-options'
|
||||
import { RepositoryHostSetupActions } from './RepositoryHostSetupActions'
|
||||
import {
|
||||
@@ -66,6 +66,7 @@ export function RepositoryHostSetupsSection({
|
||||
searchQuery,
|
||||
searchEntries
|
||||
}: RepositoryHostSetupsSectionProps): React.JSX.Element | null {
|
||||
const t = useTranslate()
|
||||
const setSettingsProjectHostSelection = useAppStore(
|
||||
(state) => state.setSettingsProjectHostSelection
|
||||
)
|
||||
@@ -136,10 +137,13 @@ export function RepositoryHostSetupsSection({
|
||||
openableProjectHostSetups,
|
||||
selectedProjectHostSetup?.id ?? ''
|
||||
)
|
||||
const setupHostOptions = buildSetupHostOptions({
|
||||
projectHostSetups: allProjectHostSetups,
|
||||
hostOptions
|
||||
})
|
||||
const setupHostOptions = buildSetupHostOptions(
|
||||
{
|
||||
projectHostSetups: allProjectHostSetups,
|
||||
hostOptions
|
||||
},
|
||||
t
|
||||
)
|
||||
const hostOptionById = new Map(hostOptions.map((option) => [option.id, option]))
|
||||
const [deletingSetupId, setDeletingSetupId] = useState<string | null>(null)
|
||||
// Split clones share project IDs but need independent selections.
|
||||
@@ -164,8 +168,8 @@ export function RepositoryHostSetupsSection({
|
||||
|
||||
return (
|
||||
<SearchableSetting
|
||||
title={translate('auto.components.settings.RepositoryPane.availableHosts', 'Available Hosts')}
|
||||
description={translate(
|
||||
title={t('auto.components.settings.RepositoryPane.availableHosts', 'Available Hosts')}
|
||||
description={t(
|
||||
'auto.components.settings.RepositoryPane.availableHostsDescription',
|
||||
'Hosts where this project is set up.'
|
||||
)}
|
||||
@@ -176,12 +180,12 @@ export function RepositoryHostSetupsSection({
|
||||
<div className="space-y-1">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<Label className="text-sm font-semibold">
|
||||
{translate('auto.components.settings.RepositoryPane.availableHosts', 'Available Hosts')}
|
||||
{t('auto.components.settings.RepositoryPane.availableHosts', 'Available Hosts')}
|
||||
</Label>
|
||||
{switchableProjectHostSetups.length > 1 ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{translate('auto.components.settings.RepositoryPane.viewingHost', 'Viewing host')}
|
||||
{t('auto.components.settings.RepositoryPane.viewingHost', 'Viewing host')}
|
||||
</span>
|
||||
<Select
|
||||
value={selectedProjectHostSetup?.id}
|
||||
@@ -215,7 +219,7 @@ export function RepositoryHostSetupsSection({
|
||||
) : null}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryPane.availableHostsHelp',
|
||||
'Project paths and worktree settings are host-specific; creating a workspace can target any ready setup.'
|
||||
)}
|
||||
@@ -261,28 +265,25 @@ export function RepositoryHostSetupsSection({
|
||||
!runtimeOwnerRuntimeUnavailable &&
|
||||
(nestedSshStatus === undefined || nestedSshStatus === 'connected')
|
||||
const setupStateLabel = !runtimeOwnerReachable
|
||||
? translate(
|
||||
'auto.components.settings.RepositoryPane.hostStateDisconnected',
|
||||
'Disconnected'
|
||||
)
|
||||
? t('auto.components.settings.RepositoryPane.hostStateDisconnected', 'Disconnected')
|
||||
: runtimeOwnerWorkspaceWindowClosed
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.RepositoryPane.hostStateWorkspaceWindowClosed',
|
||||
'Workspace window closed'
|
||||
)
|
||||
: runtimeOwnerRuntimeUnavailable
|
||||
? translate('auto.components.settings.RepositoryPane.hostStateUnknown', 'Unknown')
|
||||
? t('auto.components.settings.RepositoryPane.hostStateUnknown', 'Unknown')
|
||||
: nestedSshStatus === null
|
||||
? translate('auto.components.settings.RepositoryPane.hostStateUnknown', 'Unknown')
|
||||
? t('auto.components.settings.RepositoryPane.hostStateUnknown', 'Unknown')
|
||||
: nestedSshStatus !== undefined && nestedSshStatus !== 'connected'
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.RepositoryPane.hostStateDisconnected',
|
||||
'Disconnected'
|
||||
)
|
||||
: getSetupStateLabel(setup.setupState)
|
||||
: getSetupStateLabel(setup.setupState, t)
|
||||
const setupHostLabel =
|
||||
runtimeOwnerEnvironmentId && executionHost?.kind === 'ssh'
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.RepositoryPane.nestedHostLabel',
|
||||
'{{value0}} via {{value1}}',
|
||||
{
|
||||
@@ -324,15 +325,12 @@ export function RepositoryHostSetupsSection({
|
||||
</div>
|
||||
<p className="mt-0.5 truncate font-mono text-[11px] text-muted-foreground">
|
||||
{setup.path ||
|
||||
translate(
|
||||
'auto.components.settings.RepositoryPane.setupPathPending',
|
||||
'Path pending'
|
||||
)}
|
||||
t('auto.components.settings.RepositoryPane.setupPathPending', 'Path pending')}
|
||||
</p>
|
||||
{runtimeOwnerWorkspaceWindowClosed ? (
|
||||
// RPC cannot open a remote desktop window; direct users to the owning host.
|
||||
<p className="mt-1 text-[11px] text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryPane.hostWorkspaceWindowClosedHelp',
|
||||
'The server is reachable but its Orca window is closed. Open Orca on {{value0}} to use this setup.',
|
||||
{ value0: runtimeOwnerHostLabel }
|
||||
@@ -342,7 +340,7 @@ export function RepositoryHostSetupsSection({
|
||||
</div>
|
||||
{isCurrentSetup ? (
|
||||
<SettingsBadge>
|
||||
{translate('auto.components.settings.RepositoryPane.currentSetup', 'Current')}
|
||||
{t('auto.components.settings.RepositoryPane.currentSetup', 'Current')}
|
||||
</SettingsBadge>
|
||||
) : null}
|
||||
{!isCurrentSetup && canOpenSetup ? (
|
||||
@@ -354,7 +352,7 @@ export function RepositoryHostSetupsSection({
|
||||
selectSetup(setup)
|
||||
}}
|
||||
>
|
||||
{translate('auto.components.settings.RepositoryPane.openSetup', 'Open')}
|
||||
{t('auto.components.settings.RepositoryPane.openSetup', 'Open')}
|
||||
</Button>
|
||||
) : null}
|
||||
{canRemoveSetup ? (
|
||||
@@ -368,7 +366,7 @@ export function RepositoryHostSetupsSection({
|
||||
setDeletingSetupId(null)
|
||||
}}
|
||||
>
|
||||
{translate('auto.components.settings.RepositoryPane.removeSetup', 'Remove')}
|
||||
{t('auto.components.settings.RepositoryPane.removeSetup', 'Remove')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
+10
-8
@@ -1,5 +1,6 @@
|
||||
import { Trans } from 'react-i18next'
|
||||
import { i18n, translate } from '@/i18n/i18n'
|
||||
import { i18n } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
const ARTIFACT_URL_TEMPLATE_TOKEN = '{{artifact_url}}'
|
||||
|
||||
@@ -20,11 +21,12 @@ export function RepositoryIssueCommandSetting({
|
||||
issueCommandStoppedDistro: string | null
|
||||
commitIssueCommand: () => Promise<void>
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<div className="space-y-3 rounded-2xl border border-border/50 bg-background/80 p-4 shadow-sm">
|
||||
<div className="space-y-1">
|
||||
<h5 className="text-sm font-semibold">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryHooksSection.13394103bd',
|
||||
'Custom GitHub Issue Command'
|
||||
)}
|
||||
@@ -32,7 +34,7 @@ export function RepositoryIssueCommandSetting({
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<Trans
|
||||
i18n={i18n}
|
||||
defaults={translate(
|
||||
defaults={t(
|
||||
'auto.components.settings.RepositoryHooksSection.issueCommandDescription',
|
||||
'Optional override. Use <artifactUrl>{{artifact_url}}</artifactUrl> for the linked issue or PR URL.',
|
||||
{ artifact_url: ARTIFACT_URL_TEMPLATE_TOKEN }
|
||||
@@ -44,14 +46,14 @@ export function RepositoryIssueCommandSetting({
|
||||
</div>
|
||||
<textarea
|
||||
value={issueCommandDraft}
|
||||
aria-label={translate(
|
||||
aria-label={t(
|
||||
'auto.components.settings.RepositoryHooksSection.13394103bd',
|
||||
'Custom GitHub Issue Command'
|
||||
)}
|
||||
readOnly={issueCommandReadOnly}
|
||||
onChange={(event) => setIssueCommandDraft(event.target.value)}
|
||||
onBlur={() => void commitIssueCommand()}
|
||||
placeholder={translate(
|
||||
placeholder={t(
|
||||
'auto.components.settings.RepositoryHooksSection.4084720f47',
|
||||
'Complete {{artifact_url}}',
|
||||
{ artifact_url: ARTIFACT_URL_TEMPLATE_TOKEN }
|
||||
@@ -65,11 +67,11 @@ export function RepositoryIssueCommandSetting({
|
||||
i18n={i18n}
|
||||
defaults={
|
||||
hasSharedIssueCommand
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.RepositoryHooksSection.issueCommandSharedDefault',
|
||||
'Leave blank to use the repo default from <yaml>orca.yaml</yaml>.'
|
||||
)
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.RepositoryHooksSection.issueCommandOptionalDefault',
|
||||
'Leave blank to use the repo default from <yaml>orca.yaml</yaml> when one exists.'
|
||||
)
|
||||
@@ -79,7 +81,7 @@ export function RepositoryIssueCommandSetting({
|
||||
</p>
|
||||
{issueCommandStoppedDistro ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryHooksSection.issueCommandWslStopped',
|
||||
'WSL distro {{distro}} is stopped. Open a terminal in it to see or edit this command.',
|
||||
{ distro: issueCommandStoppedDistro }
|
||||
|
||||
+4
-3
@@ -6,7 +6,7 @@ import { SearchableSetting } from '../../section-layout/SearchableSetting'
|
||||
import { type SettingsSearchEntry, matchesSettingsSearch } from '../../search/settings-search'
|
||||
import { ProjectWindowsRuntimeSetting } from './ProjectWindowsRuntimeSetting'
|
||||
import type { ProjectRuntimeSessionSummary } from './repository-runtime-session-summary'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { useWslMoveTargetForRepo } from '../../../wsl-move/wsl-move-entry'
|
||||
import { WslMoveRuntimeRow } from '../../../wsl-move/WslMoveRuntimeRow'
|
||||
import { WslDriveProjectGitRow } from '../../../wsl-drive-project/WslDriveProjectGitRow'
|
||||
@@ -49,6 +49,7 @@ export function RepositoryWindowsRuntimeSection({
|
||||
searchQuery,
|
||||
searchEntries
|
||||
}: RepositoryWindowsRuntimeSectionProps): React.JSX.Element | null {
|
||||
const t = useTranslate()
|
||||
// Owner decision: for a WSL project with a host, the runtime toggle becomes "Move project".
|
||||
const wslMoveDistro = useWslMoveTargetForRepo(repo)
|
||||
// D1c ships with WSL hosts; serve-only, a live block or the setting off hide its offers, as main
|
||||
@@ -60,8 +61,8 @@ export function RepositoryWindowsRuntimeSection({
|
||||
|
||||
return (
|
||||
<SearchableSetting
|
||||
title={translate('auto.components.settings.RepositoryPane.projectRuntime', 'Project Runtime')}
|
||||
description={translate(
|
||||
title={t('auto.components.settings.RepositoryPane.projectRuntime', 'Project Runtime')}
|
||||
description={t(
|
||||
'auto.components.settings.RepositoryPane.projectRuntimeDescription',
|
||||
'Choose whether this project runs on Windows or WSL.'
|
||||
)}
|
||||
|
||||
+22
-30
@@ -6,7 +6,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '.
|
||||
import { BaseRefPicker } from '../../../source-control/BaseRefPicker'
|
||||
import { RepoSettingsDraftInput } from '../RepositorySettingsDraftInput'
|
||||
import { SearchableSetting } from '../../section-layout/SearchableSetting'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { getRepoExecutionHostId } from '../../../../../../shared/execution-host'
|
||||
import {
|
||||
effectiveExternalWorktreeVisibility,
|
||||
@@ -35,6 +35,7 @@ export function RepositoryWorktreeDefaultsSection({
|
||||
refreshRepo,
|
||||
forceVisible
|
||||
}: RepositoryWorktreeDefaultsSectionProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const isLegacyRepo = isLegacyRepoForExternalWorktreeVisibility(repo)
|
||||
const globalVisibility =
|
||||
settings?.worktreeVisibilityDefaults?.external ?? (isLegacyRepo ? 'show' : 'hide')
|
||||
@@ -56,11 +57,8 @@ export function RepositoryWorktreeDefaultsSection({
|
||||
return (
|
||||
<>
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.RepositoryPane.externalWorktrees',
|
||||
'External worktrees'
|
||||
)}
|
||||
description={translate(
|
||||
title={t('auto.components.settings.RepositoryPane.externalWorktrees', 'External worktrees')}
|
||||
description={t(
|
||||
'auto.components.settings.RepositoryPane.externalWorktreesDescription',
|
||||
'Override whether worktrees created outside Orca appear for this project.'
|
||||
)}
|
||||
@@ -71,31 +69,28 @@ export function RepositoryWorktreeDefaultsSection({
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<Label className="text-sm font-semibold">
|
||||
{translate(
|
||||
'auto.components.settings.RepositoryPane.externalWorktrees',
|
||||
'External worktrees'
|
||||
)}
|
||||
{t('auto.components.settings.RepositoryPane.externalWorktrees', 'External worktrees')}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{repo.externalWorktreeVisibility === undefined
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.RepositoryPane.externalWorktreesInherited',
|
||||
'Using global: {{value0}}',
|
||||
{
|
||||
value0:
|
||||
effectiveVisibility === 'show'
|
||||
? translate('auto.components.settings.RepositoryPane.show', 'Show')
|
||||
: translate('auto.components.settings.RepositoryPane.hide', 'Hide')
|
||||
? t('auto.components.settings.RepositoryPane.show', 'Show')
|
||||
: t('auto.components.settings.RepositoryPane.hide', 'Hide')
|
||||
}
|
||||
)
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.RepositoryPane.externalWorktreesGlobal',
|
||||
'Global default: {{value0}}',
|
||||
{
|
||||
value0:
|
||||
globalVisibility === 'show'
|
||||
? translate('auto.components.settings.RepositoryPane.show', 'Show')
|
||||
: translate('auto.components.settings.RepositoryPane.hide', 'Hide')
|
||||
? t('auto.components.settings.RepositoryPane.show', 'Show')
|
||||
: t('auto.components.settings.RepositoryPane.hide', 'Hide')
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
@@ -109,13 +104,13 @@ export function RepositoryWorktreeDefaultsSection({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="global">
|
||||
{translate('auto.components.settings.RepositoryPane.useGlobal', 'Use global')}
|
||||
{t('auto.components.settings.RepositoryPane.useGlobal', 'Use global')}
|
||||
</SelectItem>
|
||||
<SelectItem value="show">
|
||||
{translate('auto.components.settings.RepositoryPane.show', 'Show')}
|
||||
{t('auto.components.settings.RepositoryPane.show', 'Show')}
|
||||
</SelectItem>
|
||||
<SelectItem value="hide">
|
||||
{translate('auto.components.settings.RepositoryPane.hide', 'Hide')}
|
||||
{t('auto.components.settings.RepositoryPane.hide', 'Hide')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -123,11 +118,8 @@ export function RepositoryWorktreeDefaultsSection({
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.RepositoryPane.f88db4fece',
|
||||
'Default Worktree Base'
|
||||
)}
|
||||
description={translate(
|
||||
title={t('auto.components.settings.RepositoryPane.f88db4fece', 'Default Worktree Base')}
|
||||
description={t(
|
||||
'auto.components.settings.RepositoryPane.8984d06520',
|
||||
'Default base branch or ref when creating worktrees.'
|
||||
)}
|
||||
@@ -136,7 +128,7 @@ export function RepositoryWorktreeDefaultsSection({
|
||||
forceVisible={forceVisible}
|
||||
>
|
||||
<Label className="text-sm font-semibold">
|
||||
{translate('auto.components.settings.RepositoryPane.f88db4fece', 'Default Worktree Base')}
|
||||
{t('auto.components.settings.RepositoryPane.f88db4fece', 'Default Worktree Base')}
|
||||
</Label>
|
||||
<BaseRefPicker
|
||||
repoId={repo.id}
|
||||
@@ -148,8 +140,8 @@ export function RepositoryWorktreeDefaultsSection({
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title={translate('auto.components.settings.RepositoryPane.e9bd57a336', 'Worktree Location')}
|
||||
description={translate(
|
||||
title={t('auto.components.settings.RepositoryPane.e9bd57a336', 'Worktree Location')}
|
||||
description={t(
|
||||
'auto.components.settings.RepositoryPane.e63bb96a9b',
|
||||
'Project-specific directory for new worktrees.'
|
||||
)}
|
||||
@@ -166,7 +158,7 @@ export function RepositoryWorktreeDefaultsSection({
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<Label className="text-sm font-semibold">
|
||||
{translate('auto.components.settings.RepositoryPane.e9bd57a336', 'Worktree Location')}
|
||||
{t('auto.components.settings.RepositoryPane.e9bd57a336', 'Worktree Location')}
|
||||
</Label>
|
||||
{repo.worktreeBasePath ? (
|
||||
<Button
|
||||
@@ -175,7 +167,7 @@ export function RepositoryWorktreeDefaultsSection({
|
||||
size="sm"
|
||||
onClick={() => updateRepo(repo.id, { worktreeBasePath: undefined })}
|
||||
>
|
||||
{translate('auto.components.settings.RepositoryPane.8ccacbeb5a', 'Use Global')}
|
||||
{t('auto.components.settings.RepositoryPane.8ccacbeb5a', 'Use Global')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -195,7 +187,7 @@ export function RepositoryWorktreeDefaultsSection({
|
||||
className="h-9 text-sm"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryPane.15a99d9b9f',
|
||||
'Relative paths resolve from this project root.'
|
||||
)}
|
||||
|
||||
+11
-13
@@ -9,7 +9,7 @@ import { getSparsePresetOperationErrorMessage } from './sparse-preset-operation-
|
||||
import { SparsePresetDraftEditor } from './sparse-preset-draft-editor'
|
||||
import type { SparsePresetDraft } from '../../../sparse/SparseCheckoutPresetDraftForm'
|
||||
import { SparsePresetSettingsRow } from './sparse-preset-settings-row'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { SettingsGroup } from '../../section-layout/SettingsGroup'
|
||||
|
||||
type SparsePresetSettingsSectionProps = {
|
||||
@@ -19,6 +19,7 @@ type SparsePresetSettingsSectionProps = {
|
||||
export function SparsePresetSettingsSection({
|
||||
repoId
|
||||
}: SparsePresetSettingsSectionProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const repo = useAppStore((s) => s.repos.find((entry) => entry.id === repoId))
|
||||
const presets = useAppStore((s) => s.sparsePresetsByRepo[repoId])
|
||||
const loadStatus = useAppStore((s) => s.sparsePresetsLoadStatusByRepo[repoId] ?? 'idle')
|
||||
@@ -72,10 +73,10 @@ export function SparsePresetSettingsSection({
|
||||
}, [fetchSparsePresets, loadStatus, mountedRef, presets, repoId])
|
||||
|
||||
const sortedPresets = presets ?? []
|
||||
const parsedDirectories = draft ? parseSparsePresetDirectories(draft.directoriesText) : null
|
||||
const parsedDirectories = draft ? parseSparsePresetDirectories(draft.directoriesText, t) : null
|
||||
const trimmedName = draft?.name.trim() ?? ''
|
||||
const nameError = draft
|
||||
? validateSparsePresetName(draft.name, sortedPresets, draft.presetId)
|
||||
? validateSparsePresetName(draft.name, sortedPresets, draft.presetId, t)
|
||||
: null
|
||||
const canSaveDraft =
|
||||
!!draft &&
|
||||
@@ -205,11 +206,11 @@ export function SparsePresetSettingsSection({
|
||||
return (
|
||||
<section ref={sectionRef}>
|
||||
<SettingsGroup
|
||||
title={translate(
|
||||
title={t(
|
||||
'auto.components.settings.SparsePresetSettingsSection.388513be2d',
|
||||
'Sparse Checkout Presets'
|
||||
)}
|
||||
description={translate(
|
||||
description={t(
|
||||
'auto.components.settings.SparsePresetSettingsSection.17f8c4ce10',
|
||||
'Manage saved directory sets for sparse worktree creation.'
|
||||
)}
|
||||
@@ -222,10 +223,7 @@ export function SparsePresetSettingsSection({
|
||||
disabled={!!draft || presets === undefined || deletingPresetId !== null}
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
{translate(
|
||||
'auto.components.settings.SparsePresetSettingsSection.d7565029a9',
|
||||
'New Preset'
|
||||
)}
|
||||
{t('auto.components.settings.SparsePresetSettingsSection.d7565029a9', 'New Preset')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
@@ -243,11 +241,11 @@ export function SparsePresetSettingsSection({
|
||||
{presets === undefined ? (
|
||||
<div className="py-4 text-sm text-muted-foreground">
|
||||
{loadError
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.SparsePresetSettingsSection.92c08ccae3',
|
||||
'Sparse presets could not be loaded.'
|
||||
)
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.SparsePresetSettingsSection.8deb7024ab',
|
||||
'Loading sparse presets...'
|
||||
)}
|
||||
@@ -273,14 +271,14 @@ export function SparsePresetSettingsSection({
|
||||
}}
|
||||
>
|
||||
<RefreshCcw />
|
||||
{translate('sparsePreset.retryLoad', 'Retry loading presets')}
|
||||
{t('sparsePreset.retryLoad', 'Retry loading presets')}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : sortedPresets.length === 0 && !draft ? (
|
||||
<div className="py-4 text-sm text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.SparsePresetSettingsSection.88bfbf1a9c',
|
||||
'No sparse presets saved for this repository.'
|
||||
)}
|
||||
|
||||
+16
-24
@@ -11,7 +11,7 @@ import {
|
||||
getWorktreeSymlinkPathFilterState,
|
||||
type WorktreeSymlinkPathSuggestion
|
||||
} from '../../../../lib/directory-path-suggestion-filter'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import {
|
||||
getRepoExecutionHostId,
|
||||
LOCAL_EXECUTION_HOST_ID
|
||||
@@ -34,6 +34,7 @@ export function WorktreeSymlinksSection({
|
||||
repo,
|
||||
updateRepo
|
||||
}: WorktreeSymlinksSectionProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
@@ -101,11 +102,11 @@ export function WorktreeSymlinksSection({
|
||||
|
||||
return (
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
title={t(
|
||||
'auto.components.settings.WorktreeSymlinksSection.4755f120b6',
|
||||
'Worktree Shared Paths'
|
||||
)}
|
||||
description={translate(
|
||||
description={t(
|
||||
'auto.components.settings.WorktreeSymlinksSection.b07ef5a8b6',
|
||||
'Paths to materialize from the primary checkout into newly created worktrees.'
|
||||
)}
|
||||
@@ -124,11 +125,11 @@ export function WorktreeSymlinksSection({
|
||||
]}
|
||||
>
|
||||
<SettingsGroup
|
||||
title={translate(
|
||||
title={t(
|
||||
'auto.components.settings.WorktreeSymlinksSection.4755f120b6',
|
||||
'Worktree Shared Paths'
|
||||
)}
|
||||
description={translate(
|
||||
description={t(
|
||||
'auto.components.settings.WorktreeSymlinksSection.7ff265071d',
|
||||
'When a new worktree is created, each path listed here is APFS clone-copied on macOS when possible, otherwise symlinked from the primary checkout.'
|
||||
)}
|
||||
@@ -137,16 +138,13 @@ export function WorktreeSymlinksSection({
|
||||
<PopoverTrigger asChild>
|
||||
<Button type="button" variant="outline" size="sm">
|
||||
<Plus className="size-3.5" />
|
||||
{translate(
|
||||
'auto.components.settings.WorktreeSymlinksSection.241325302c',
|
||||
'Add Path'
|
||||
)}
|
||||
{t('auto.components.settings.WorktreeSymlinksSection.241325302c', 'Add Path')}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="w-72 p-0">
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder={translate(
|
||||
placeholder={t(
|
||||
'auto.components.settings.WorktreeSymlinksSection.4cd2a4c077',
|
||||
'Type a path (e.g. .env or node_modules)…'
|
||||
)}
|
||||
@@ -155,7 +153,7 @@ export function WorktreeSymlinksSection({
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.WorktreeSymlinksSection.ab40b8a5f1',
|
||||
'No matches. Keep typing to add a custom path.'
|
||||
)}
|
||||
@@ -168,10 +166,7 @@ export function WorktreeSymlinksSection({
|
||||
>
|
||||
<Plus className="size-3.5 text-muted-foreground" />
|
||||
<span className="text-xs">
|
||||
{translate(
|
||||
'auto.components.settings.WorktreeSymlinksSection.b2429aeb31',
|
||||
'Add'
|
||||
)}{' '}
|
||||
{t('auto.components.settings.WorktreeSymlinksSection.b2429aeb31', 'Add')}{' '}
|
||||
<code className="rounded bg-muted px-1 py-0.5 text-[11px]">
|
||||
{queryTrimmed}
|
||||
</code>
|
||||
@@ -203,7 +198,7 @@ export function WorktreeSymlinksSection({
|
||||
<span className="truncate text-xs">{entry.name}</span>
|
||||
{alreadyAdded ? (
|
||||
<span className="ml-auto text-[10px] uppercase tracking-wide text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.WorktreeSymlinksSection.ea06227efa',
|
||||
'added'
|
||||
)}
|
||||
@@ -220,7 +215,7 @@ export function WorktreeSymlinksSection({
|
||||
>
|
||||
{paths.length === 0 ? (
|
||||
<div className="py-4 text-sm text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.WorktreeSymlinksSection.31ebab5403',
|
||||
'No shared paths configured for this repository.'
|
||||
)}
|
||||
@@ -234,18 +229,15 @@ export function WorktreeSymlinksSection({
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<h4 className="text-sm font-medium">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.WorktreeSymlinksSection.b814c618e2',
|
||||
'Linked paths'
|
||||
)}
|
||||
</h4>
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{paths.length === 1
|
||||
? translate(
|
||||
'auto.components.settings.WorktreeSymlinksSection.9ea912d811',
|
||||
'1 path'
|
||||
)
|
||||
: translate(
|
||||
? t('auto.components.settings.WorktreeSymlinksSection.9ea912d811', '1 path')
|
||||
: t(
|
||||
'auto.components.settings.WorktreeSymlinksSection.d72ba8dc68',
|
||||
'{{value0}} paths',
|
||||
{ value0: paths.length }
|
||||
@@ -264,7 +256,7 @@ export function WorktreeSymlinksSection({
|
||||
size="icon-xs"
|
||||
variant="ghost"
|
||||
onClick={() => handleRemove(path)}
|
||||
aria-label={translate(
|
||||
aria-label={t(
|
||||
'auto.components.settings.WorktreeSymlinksSection.1c1e35b219',
|
||||
'Remove {{value0}}',
|
||||
{ value0: path }
|
||||
|
||||
+28
-31
@@ -6,6 +6,7 @@ import type {
|
||||
import type { Repo } from '../../../../../../shared/repo-types'
|
||||
import { DEFAULT_REPO_HOOK_SETTINGS } from './repository-hook-defaults'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { Translate } from '@/i18n/use-translate'
|
||||
|
||||
export type LocalHookName = 'setup' | 'archive'
|
||||
export type LocalHookField = {
|
||||
@@ -18,34 +19,30 @@ export type HookSettingsPolicyDraft = Partial<
|
||||
Pick<RepoHookSettings, 'setupRunPolicy' | 'setupAgentStartupPolicy' | 'commandSourcePolicy'>
|
||||
>
|
||||
|
||||
export function getLocalHookFields(): readonly [LocalHookField, LocalHookField] {
|
||||
export function getLocalHookFields(
|
||||
t: Translate = translate
|
||||
): readonly [LocalHookField, LocalHookField] {
|
||||
return [
|
||||
{
|
||||
name: 'setup',
|
||||
label: translate(
|
||||
'auto.components.settings.RepositoryHooksSection.52b31baf02',
|
||||
'Setup Script'
|
||||
),
|
||||
description: translate(
|
||||
label: t('auto.components.settings.RepositoryHooksSection.52b31baf02', 'Setup Script'),
|
||||
description: t(
|
||||
'auto.components.settings.RepositoryHooksSection.f0710e1c83',
|
||||
'Runs after a new worktree is created; install deps, copy env files, run migrations.'
|
||||
),
|
||||
placeholder: translate(
|
||||
placeholder: t(
|
||||
'auto.components.settings.RepositoryHooksSection.a3fc966677',
|
||||
'# e.g. pnpm install cp "$ORCA_ROOT_PATH/.env" "$ORCA_WORKTREE_PATH/.env"'
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'archive',
|
||||
label: translate(
|
||||
'auto.components.settings.RepositoryHooksSection.9a100323ff',
|
||||
'Archive Script'
|
||||
),
|
||||
description: translate(
|
||||
label: t('auto.components.settings.RepositoryHooksSection.9a100323ff', 'Archive Script'),
|
||||
description: t(
|
||||
'auto.components.settings.RepositoryHooksSection.6f90ebe3fd',
|
||||
'Runs before a worktree is archived or removed.'
|
||||
),
|
||||
placeholder: translate(
|
||||
placeholder: t(
|
||||
'auto.components.settings.RepositoryHooksSection.9b821fa19d',
|
||||
'# e.g. echo "Cleaning up $ORCA_WORKSPACE_NAME"'
|
||||
)
|
||||
@@ -79,19 +76,22 @@ export type LocalCommandSourcePolicyNotice =
|
||||
| { kind: 'checking' }
|
||||
| { kind: 'action'; policy: 'local-only' | 'run-both'; label: string }
|
||||
|
||||
export function getLocalCommandSourcePolicyNotice({
|
||||
hooksInspectionReady,
|
||||
currentPolicy,
|
||||
setupScript,
|
||||
archiveScript,
|
||||
hasSharedScript
|
||||
}: {
|
||||
hooksInspectionReady: boolean
|
||||
currentPolicy: HookCommandSourcePolicy
|
||||
setupScript: string | undefined
|
||||
archiveScript: string | undefined
|
||||
hasSharedScript: boolean
|
||||
}): LocalCommandSourcePolicyNotice | null {
|
||||
export function getLocalCommandSourcePolicyNotice(
|
||||
{
|
||||
hooksInspectionReady,
|
||||
currentPolicy,
|
||||
setupScript,
|
||||
archiveScript,
|
||||
hasSharedScript
|
||||
}: {
|
||||
hooksInspectionReady: boolean
|
||||
currentPolicy: HookCommandSourcePolicy
|
||||
setupScript: string | undefined
|
||||
archiveScript: string | undefined
|
||||
hasSharedScript: boolean
|
||||
},
|
||||
t: Translate = translate
|
||||
): LocalCommandSourcePolicyNotice | null {
|
||||
if ((!setupScript?.trim() && !archiveScript?.trim()) || currentPolicy !== 'shared-only') {
|
||||
return null
|
||||
}
|
||||
@@ -102,15 +102,12 @@ export function getLocalCommandSourcePolicyNotice({
|
||||
? {
|
||||
kind: 'action',
|
||||
policy: 'run-both',
|
||||
label: translate('auto.components.settings.RepositoryHooksSection.8d6c56bff8', 'Run both')
|
||||
label: t('auto.components.settings.RepositoryHooksSection.8d6c56bff8', 'Run both')
|
||||
}
|
||||
: {
|
||||
kind: 'action',
|
||||
policy: 'local-only',
|
||||
label: translate(
|
||||
'auto.components.settings.RepositoryHooksSection.8bfe65fc60',
|
||||
'Use local commands'
|
||||
)
|
||||
label: t('auto.components.settings.RepositoryHooksSection.8bfe65fc60', 'Use local commands')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+27
-32
@@ -1,6 +1,6 @@
|
||||
import type { ComponentType } from 'react'
|
||||
import { ArrowLeft, Download, FolderOpen, Plus } from 'lucide-react'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '../../../ui/button'
|
||||
import { Input } from '../../../ui/input'
|
||||
@@ -19,12 +19,13 @@ export function HostSetupStartActions({
|
||||
onClone: () => void
|
||||
onPlan: () => void
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<div className="space-y-3 pt-1">
|
||||
<HostSetupActionButton
|
||||
icon={FolderOpen}
|
||||
title={translate('auto.components.settings.RepositoryPane.browseFolder', 'Browse folder')}
|
||||
description={translate(
|
||||
title={t('auto.components.settings.RepositoryPane.browseFolder', 'Browse folder')}
|
||||
description={t(
|
||||
'auto.components.settings.RepositoryPane.browseFolderHelp',
|
||||
'Use an existing checkout or folder on this host.'
|
||||
)}
|
||||
@@ -34,16 +35,13 @@ export function HostSetupStartActions({
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
{translate('auto.components.settings.RepositoryPane.otherWaysToAdd', 'Other ways to add')}
|
||||
{t('auto.components.settings.RepositoryPane.otherWaysToAdd', 'Other ways to add')}
|
||||
</p>
|
||||
<div className="overflow-hidden rounded-md border border-input bg-background">
|
||||
<HostSetupActionButton
|
||||
icon={Download}
|
||||
title={translate(
|
||||
'auto.components.settings.RepositoryPane.cloneFromUrl',
|
||||
'Clone from URL'
|
||||
)}
|
||||
description={translate(
|
||||
title={t('auto.components.settings.RepositoryPane.cloneFromUrl', 'Clone from URL')}
|
||||
description={t(
|
||||
'auto.components.settings.RepositoryPane.cloneFromUrlHelp',
|
||||
'Clone this repository onto the selected host.'
|
||||
)}
|
||||
@@ -53,11 +51,11 @@ export function HostSetupStartActions({
|
||||
/>
|
||||
<HostSetupActionButton
|
||||
icon={Plus}
|
||||
title={translate(
|
||||
title={t(
|
||||
'auto.components.settings.RepositoryPane.addPlannedHost',
|
||||
'Add host placeholder'
|
||||
)}
|
||||
description={translate(
|
||||
description={t(
|
||||
'auto.components.settings.RepositoryPane.addPlannedHostHelp',
|
||||
'Remember this host and finish adding the project later.'
|
||||
)}
|
||||
@@ -90,20 +88,18 @@ export function HostSetupExistingFolderStep({
|
||||
onKindChange: (value: 'git' | 'folder') => void
|
||||
onSubmit: () => void
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<div className="space-y-3 rounded-md border border-border bg-muted/20 p-3">
|
||||
<StepBackButton
|
||||
onBack={onBack}
|
||||
label={translate(
|
||||
'auto.components.settings.RepositoryPane.existingFolder',
|
||||
'Existing folder'
|
||||
)}
|
||||
label={t('auto.components.settings.RepositoryPane.existingFolder', 'Existing folder')}
|
||||
/>
|
||||
<div className="grid gap-2 sm:grid-cols-[minmax(0,1fr)_8rem]">
|
||||
<Input
|
||||
value={setupPath}
|
||||
onChange={(event) => onPathChange(event.target.value)}
|
||||
placeholder={translate(
|
||||
placeholder={t(
|
||||
'auto.components.settings.RepositoryPane.setupExistingFolderPathPlaceholder',
|
||||
'/path/to/project/on/host'
|
||||
)}
|
||||
@@ -118,10 +114,10 @@ export function HostSetupExistingFolderStep({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="git">
|
||||
{translate('auto.components.settings.RepositoryPane.setupKindGit', 'Git repo')}
|
||||
{t('auto.components.settings.RepositoryPane.setupKindGit', 'Git repo')}
|
||||
</SelectItem>
|
||||
<SelectItem value="folder">
|
||||
{translate('auto.components.settings.RepositoryPane.setupKindFolder', 'Folder')}
|
||||
{t('auto.components.settings.RepositoryPane.setupKindFolder', 'Folder')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -134,8 +130,8 @@ export function HostSetupExistingFolderStep({
|
||||
onClick={onSubmit}
|
||||
>
|
||||
{isSettingUp
|
||||
? translate('auto.components.settings.RepositoryPane.addingProject', 'Adding...')
|
||||
: translate('auto.components.settings.RepositoryPane.setupHost', 'Add project')}
|
||||
? t('auto.components.settings.RepositoryPane.addingProject', 'Adding...')
|
||||
: t('auto.components.settings.RepositoryPane.setupHost', 'Add project')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -161,17 +157,18 @@ export function HostSetupCloneStep({
|
||||
onCloneDestinationChange: (value: string) => void
|
||||
onSubmit: () => void
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<div className="space-y-3 rounded-md border border-border bg-muted/20 p-3">
|
||||
<StepBackButton
|
||||
onBack={onBack}
|
||||
label={translate('auto.components.settings.RepositoryPane.cloneFromUrl', 'Clone from URL')}
|
||||
label={t('auto.components.settings.RepositoryPane.cloneFromUrl', 'Clone from URL')}
|
||||
/>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<Input
|
||||
value={cloneUrl}
|
||||
onChange={(event) => onCloneUrlChange(event.target.value)}
|
||||
placeholder={translate(
|
||||
placeholder={t(
|
||||
'auto.components.settings.RepositoryPane.cloneUrlPlaceholder',
|
||||
'Repository URL'
|
||||
)}
|
||||
@@ -180,7 +177,7 @@ export function HostSetupCloneStep({
|
||||
<Input
|
||||
value={cloneDestination}
|
||||
onChange={(event) => onCloneDestinationChange(event.target.value)}
|
||||
placeholder={translate(
|
||||
placeholder={t(
|
||||
'auto.components.settings.RepositoryPane.cloneDestinationPlaceholder',
|
||||
'/destination/on/host'
|
||||
)}
|
||||
@@ -195,8 +192,8 @@ export function HostSetupCloneStep({
|
||||
onClick={onSubmit}
|
||||
>
|
||||
{isCloning
|
||||
? translate('auto.components.settings.RepositoryPane.cloningHost', 'Cloning...')
|
||||
: translate('auto.components.settings.RepositoryPane.cloneHost', 'Clone')}
|
||||
? t('auto.components.settings.RepositoryPane.cloningHost', 'Cloning...')
|
||||
: t('auto.components.settings.RepositoryPane.cloneHost', 'Clone')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -216,7 +213,8 @@ export function HostSetupPlannedStep({
|
||||
onBack: () => void
|
||||
onSubmit: () => void
|
||||
}): React.JSX.Element {
|
||||
const addHostLabel = translate(
|
||||
const t = useTranslate()
|
||||
const addHostLabel = t(
|
||||
'auto.components.settings.RepositoryPane.addPlannedHostToHost',
|
||||
'Add {{host}}',
|
||||
{ host: hostLabel }
|
||||
@@ -226,13 +224,10 @@ export function HostSetupPlannedStep({
|
||||
<div className="space-y-3 rounded-md border border-border bg-muted/20 p-3">
|
||||
<StepBackButton
|
||||
onBack={onBack}
|
||||
label={translate(
|
||||
'auto.components.settings.RepositoryPane.addPlannedHost',
|
||||
'Add host placeholder'
|
||||
)}
|
||||
label={t('auto.components.settings.RepositoryPane.addPlannedHost', 'Add host placeholder')}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryPane.addPlannedHostConfirm',
|
||||
'This only records that the project should be available on this host. You can add the folder or clone later.'
|
||||
)}
|
||||
@@ -245,7 +240,7 @@ export function HostSetupPlannedStep({
|
||||
onClick={onSubmit}
|
||||
>
|
||||
{isCreatingPendingSetup
|
||||
? translate('auto.components.settings.RepositoryPane.creatingPendingSetup', 'Adding...')
|
||||
? t('auto.components.settings.RepositoryPane.creatingPendingSetup', 'Adding...')
|
||||
: addHostLabel}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
+22
-24
@@ -8,6 +8,7 @@ import type {
|
||||
ProjectHostSetupState
|
||||
} from '../../../../../../shared/project-types'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { Translate } from '@/i18n/use-translate'
|
||||
import { getHostSetupUnavailableDetail } from '@/lib/project-host-setup-options'
|
||||
import { pickerExecutionHosts } from '../../../../../../shared/managed-orcad-execution-host'
|
||||
|
||||
@@ -19,37 +20,34 @@ export type SetupHostOption = {
|
||||
canUsePathActions: boolean
|
||||
}
|
||||
|
||||
export function getSetupStateLabel(setupState: ProjectHostSetupState): string {
|
||||
export function getSetupStateLabel(
|
||||
setupState: ProjectHostSetupState,
|
||||
t: Translate = translate
|
||||
): string {
|
||||
switch (setupState) {
|
||||
case 'ready':
|
||||
return translate('auto.components.settings.RepositoryPane.hostSetupStateReady', 'Ready')
|
||||
return t('auto.components.settings.RepositoryPane.hostSetupStateReady', 'Ready')
|
||||
case 'not-set-up':
|
||||
return translate(
|
||||
'auto.components.settings.RepositoryPane.hostSetupStateNotSetUp',
|
||||
'Not set up'
|
||||
)
|
||||
return t('auto.components.settings.RepositoryPane.hostSetupStateNotSetUp', 'Not set up')
|
||||
case 'setting-up':
|
||||
return translate(
|
||||
'auto.components.settings.RepositoryPane.hostSetupStateSettingUp',
|
||||
'Setting up'
|
||||
)
|
||||
return t('auto.components.settings.RepositoryPane.hostSetupStateSettingUp', 'Setting up')
|
||||
case 'error':
|
||||
return translate('auto.components.settings.RepositoryPane.hostSetupStateError', 'Error')
|
||||
return t('auto.components.settings.RepositoryPane.hostSetupStateError', 'Error')
|
||||
case 'unsupported':
|
||||
return translate(
|
||||
'auto.components.settings.RepositoryPane.hostSetupStateUnsupported',
|
||||
'Unsupported'
|
||||
)
|
||||
return t('auto.components.settings.RepositoryPane.hostSetupStateUnsupported', 'Unsupported')
|
||||
}
|
||||
}
|
||||
|
||||
export function buildSetupHostOptions({
|
||||
projectHostSetups,
|
||||
hostOptions
|
||||
}: {
|
||||
projectHostSetups: ProjectHostSetup[]
|
||||
hostOptions: readonly ExecutionHostRegistryEntry[]
|
||||
}): SetupHostOption[] {
|
||||
export function buildSetupHostOptions(
|
||||
{
|
||||
projectHostSetups,
|
||||
hostOptions
|
||||
}: {
|
||||
projectHostSetups: ProjectHostSetup[]
|
||||
hostOptions: readonly ExecutionHostRegistryEntry[]
|
||||
},
|
||||
t: Translate = translate
|
||||
): SetupHostOption[] {
|
||||
const setupHostIds = new Set(projectHostSetups.map((setup) => setup.hostId))
|
||||
return pickerExecutionHosts(hostOptions)
|
||||
.filter(
|
||||
@@ -59,7 +57,7 @@ export function buildSetupHostOptions({
|
||||
!host.aliasHostIds?.some((aliasHostId) => setupHostIds.has(aliasHostId))
|
||||
)
|
||||
.map((host) => {
|
||||
const unavailableDetail = getHostSetupUnavailableDetail(host)
|
||||
const unavailableDetail = getHostSetupUnavailableDetail(host, t)
|
||||
// Offline hosts can be placeholders; clone/import require live providers.
|
||||
const canUsePathActions = host.health === 'local' || host.health === 'available'
|
||||
return {
|
||||
@@ -69,7 +67,7 @@ export function buildSetupHostOptions({
|
||||
unavailableDetail ??
|
||||
(canUsePathActions
|
||||
? host.detail
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.RepositoryPane.hostSetupConnectionRequired',
|
||||
'Connect this host before importing or cloning the project'
|
||||
)),
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { getIntlLocale } from '@/i18n/i18n'
|
||||
export function formatSparsePresetUpdatedAt(timestamp: number): string | null {
|
||||
export function formatSparsePresetUpdatedAt(
|
||||
timestamp: number,
|
||||
intlLocale: string = getIntlLocale()
|
||||
): string | null {
|
||||
if (!Number.isFinite(timestamp)) {
|
||||
return null
|
||||
}
|
||||
@@ -9,7 +12,7 @@ export function formatSparsePresetUpdatedAt(timestamp: number): string | null {
|
||||
return null
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat(getIntlLocale(), {
|
||||
return new Intl.DateTimeFormat(intlLocale, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
|
||||
+3
-2
@@ -1,10 +1,11 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
export function SparsePresetDirectoryPreview({
|
||||
directories
|
||||
}: {
|
||||
directories: string[]
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const visibleDirectories = directories.slice(0, 6)
|
||||
const hiddenCount = directories.length - visibleDirectories.length
|
||||
|
||||
@@ -21,7 +22,7 @@ export function SparsePresetDirectoryPreview({
|
||||
))}
|
||||
{hiddenCount > 0 ? (
|
||||
<span className="rounded-md border border-border/50 bg-muted/35 px-2 py-1 text-[11px] text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.SparsePresetSettingsSection.8b64731aaf',
|
||||
'+{{value0}} more',
|
||||
{ value0: hiddenCount }
|
||||
|
||||
+13
-20
@@ -4,7 +4,7 @@ import { cn } from '@/lib/utils'
|
||||
import { Button } from '../../../ui/button'
|
||||
import { formatSparsePresetUpdatedAt } from './sparse-preset-date'
|
||||
import { SparsePresetDirectoryPreview } from './sparse-preset-directory-preview'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useIntlLocale, useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
type SparsePresetSettingsRowProps = {
|
||||
preset: SparsePreset
|
||||
@@ -25,8 +25,10 @@ export function SparsePresetSettingsRow({
|
||||
onDelete,
|
||||
onClearDeleteConfirm
|
||||
}: SparsePresetSettingsRowProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const intlLocale = useIntlLocale()
|
||||
// Older or hand-edited presets may have bad timestamps; keep Settings usable.
|
||||
const updatedLabel = formatSparsePresetUpdatedAt(preset.updatedAt)
|
||||
const updatedLabel = formatSparsePresetUpdatedAt(preset.updatedAt, intlLocale)
|
||||
const isDeleting = deletingPresetId === preset.id
|
||||
|
||||
return (
|
||||
@@ -40,11 +42,11 @@ export function SparsePresetSettingsRow({
|
||||
<h4 className="min-w-0 truncate text-sm font-medium">{preset.name}</h4>
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{preset.directories.length === 1
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.SparsePresetSettingsSection.9d3c087fc0',
|
||||
'1 directory'
|
||||
)
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.SparsePresetSettingsSection.d7b3f0bdc3',
|
||||
'{{value0}} directories',
|
||||
{ value0: preset.directories.length }
|
||||
@@ -52,12 +54,12 @@ export function SparsePresetSettingsRow({
|
||||
</span>
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{updatedLabel
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.SparsePresetSettingsSection.568d7e1e49',
|
||||
'Updated {{value0}}',
|
||||
{ value0: updatedLabel }
|
||||
)
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.SparsePresetSettingsSection.ba9ad2d4cd',
|
||||
'Updated date unknown'
|
||||
)}
|
||||
@@ -70,7 +72,7 @@ export function SparsePresetSettingsRow({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label={translate(
|
||||
aria-label={t(
|
||||
'auto.components.settings.SparsePresetSettingsSection.fe1f2c6572',
|
||||
'Edit {{value0}}',
|
||||
{ value0: preset.name }
|
||||
@@ -85,7 +87,7 @@ export function SparsePresetSettingsRow({
|
||||
type="button"
|
||||
variant={confirmingDeleteId === preset.id ? 'destructive' : 'ghost'}
|
||||
size="sm"
|
||||
aria-label={translate(
|
||||
aria-label={t(
|
||||
'auto.components.settings.SparsePresetSettingsSection.2ef2b2674b',
|
||||
'Delete {{value0}}',
|
||||
{ value0: preset.name }
|
||||
@@ -104,19 +106,10 @@ export function SparsePresetSettingsRow({
|
||||
<Trash2 className="size-3.5" />
|
||||
)}
|
||||
{isDeleting
|
||||
? translate(
|
||||
'auto.components.settings.SparsePresetSettingsSection.a7bcf206b1',
|
||||
'Deleting'
|
||||
)
|
||||
? t('auto.components.settings.SparsePresetSettingsSection.a7bcf206b1', 'Deleting')
|
||||
: confirmingDeleteId === preset.id
|
||||
? translate(
|
||||
'auto.components.settings.SparsePresetSettingsSection.755c6a1a0d',
|
||||
'Confirm'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.settings.SparsePresetSettingsSection.6fa754d20f',
|
||||
'Delete'
|
||||
)}
|
||||
? t('auto.components.settings.SparsePresetSettingsSection.755c6a1a0d', 'Confirm')
|
||||
: t('auto.components.settings.SparsePresetSettingsSection.6fa754d20f', 'Delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+6
-5
@@ -6,7 +6,7 @@ import {
|
||||
import { Label } from '../../../ui/label'
|
||||
import { ColorPicker } from '../../../ui/color-picker'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
type RepositoryIconColorSectionProps = {
|
||||
badgeColor: string | null | undefined
|
||||
@@ -17,13 +17,14 @@ export function RepositoryIconColorSection({
|
||||
badgeColor,
|
||||
onBadgeColorChange
|
||||
}: RepositoryIconColorSectionProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const selectedBadgeColor = normalizeRepoBadgeColor(badgeColor) ?? DEFAULT_REPO_BADGE_COLOR
|
||||
const isPresetBadgeColor = REPO_COLORS.some((color) => color === selectedBadgeColor)
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-semibold">
|
||||
{translate('auto.components.settings.RepositoryIconPicker.642dc29c6d', 'Color')}
|
||||
{t('auto.components.settings.RepositoryIconPicker.642dc29c6d', 'Color')}
|
||||
</Label>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{REPO_COLORS.map((color) => (
|
||||
@@ -31,7 +32,7 @@ export function RepositoryIconColorSection({
|
||||
key={color}
|
||||
type="button"
|
||||
onClick={() => onBadgeColorChange(color)}
|
||||
aria-label={translate(
|
||||
aria-label={t(
|
||||
'auto.components.settings.RepositoryIconPicker.2b7d27b93c',
|
||||
'Use {{value0}} repo color',
|
||||
{ value0: color }
|
||||
@@ -51,11 +52,11 @@ export function RepositoryIconColorSection({
|
||||
onChange={onBadgeColorChange}
|
||||
label={
|
||||
isPresetBadgeColor
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.RepositoryIconPicker.0e5f0693c1',
|
||||
'Choose custom repo color'
|
||||
)
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.RepositoryIconPicker.913c55833d',
|
||||
'Custom repo color {{value0}}',
|
||||
{ value0: selectedBadgeColor }
|
||||
|
||||
+5
-4
@@ -3,7 +3,7 @@ import EmojiPicker, { EmojiStyle, Theme, type EmojiClickData } from 'emoji-picke
|
||||
import type { RepoIcon } from '../../../../../../shared/repo-icon'
|
||||
import { sanitizeRepoIcon } from '../../../../../../shared/repo-icon'
|
||||
import { useDocumentDarkTheme } from '@/hooks/use-document-dark-theme'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
type RepositoryIconEmojiPickerProps = {
|
||||
selectedEmoji: string
|
||||
@@ -15,6 +15,7 @@ export function RepositoryIconEmojiPicker({
|
||||
selectedEmoji,
|
||||
onSetIcon
|
||||
}: RepositoryIconEmojiPickerProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
// Theme.AUTO only follows the OS setting, which can disagree with Orca's painted theme.
|
||||
const isDarkTheme = useDocumentDarkTheme()
|
||||
|
||||
@@ -23,7 +24,7 @@ export function RepositoryIconEmojiPicker({
|
||||
const repoIcon = sanitizeRepoIcon({ type: 'emoji', emoji: emojiData.emoji })
|
||||
if (!repoIcon) {
|
||||
toast.error(
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.RepositoryIconPicker.emojiTooLongForRepoIcon',
|
||||
"This emoji can't be used as a repo icon."
|
||||
)
|
||||
@@ -45,7 +46,7 @@ export function RepositoryIconEmojiPicker({
|
||||
lazyLoadEmojis
|
||||
onEmojiClick={handleEmojiClick}
|
||||
previewConfig={{ showPreview: true }}
|
||||
searchPlaceholder={translate(
|
||||
searchPlaceholder={t(
|
||||
'auto.components.settings.RepositoryIconPicker.searchEmojiPlaceholder',
|
||||
'Search emoji'
|
||||
)}
|
||||
@@ -54,7 +55,7 @@ export function RepositoryIconEmojiPicker({
|
||||
</div>
|
||||
{selectedEmoji ? (
|
||||
<p className="mt-2 text-[11px] text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryIconPicker.currentEmojiSelection',
|
||||
'Current: {{value0}}',
|
||||
{ value0: selectedEmoji }
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
resolveRepositoryGitHubAvatar,
|
||||
resolveRepositoryUpstreamLive
|
||||
} from '../../../../runtime/repository-github/repository-icon-github'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
export function RepositoryIconPicker({
|
||||
repo,
|
||||
@@ -32,6 +32,7 @@ export function RepositoryIconPicker({
|
||||
repo: Repo
|
||||
updateRepo: (repoId: string, updates: Partial<Repo>) => void
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const [loadingGitHub, setLoadingGitHub] = useState(false)
|
||||
const [resetting, setResetting] = useState(false)
|
||||
const mountedRef = useMountedRef()
|
||||
@@ -90,7 +91,7 @@ export function RepositoryIconPicker({
|
||||
}
|
||||
if (!resolution.repoIcon) {
|
||||
toast.error(
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.RepositoryIconPicker.f79972271a',
|
||||
'No GitHub remote found for this repo.'
|
||||
)
|
||||
@@ -104,7 +105,7 @@ export function RepositoryIconPicker({
|
||||
} catch {
|
||||
if (mountedRef.current) {
|
||||
toast.error(
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.RepositoryIconPicker.d71df44587',
|
||||
'Failed to resolve the GitHub repo.'
|
||||
)
|
||||
@@ -181,7 +182,7 @@ export function RepositoryIconPicker({
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<Label className="text-sm font-semibold">
|
||||
{translate('auto.components.settings.RepositoryIconPicker.4e2a14f967', 'Repo Icon')}
|
||||
{t('auto.components.settings.RepositoryIconPicker.4e2a14f967', 'Repo Icon')}
|
||||
</Label>
|
||||
<div className="mt-1 truncate text-xs text-muted-foreground">{currentIconLabel}</div>
|
||||
</div>
|
||||
@@ -194,7 +195,7 @@ export function RepositoryIconPicker({
|
||||
onClick={() => void handleResetToDefault()}
|
||||
>
|
||||
<RotateCcw className="size-3.5" />
|
||||
{translate('auto.components.settings.RepositoryIconPicker.549d126081', 'Reset')}
|
||||
{t('auto.components.settings.RepositoryIconPicker.549d126081', 'Reset')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../../ui/tabs'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../../../ui/tooltip'
|
||||
import { getRepoLucideIconOptions } from '../../../repo/repo-icon'
|
||||
import { useMountedRef } from '@/hooks/useMountedRef'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { lazyWithRetry } from '@/lib/lazy-with-retry'
|
||||
import { repoIconPickFailureText } from './repo-icon-pick-failure-text'
|
||||
|
||||
@@ -38,6 +38,7 @@ export function RepositoryIconTabs({
|
||||
onSetIcon,
|
||||
onUseGitHubAvatar
|
||||
}: RepositoryIconTabsProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const [website, setWebsite] = useState('')
|
||||
const mountedRef = useMountedRef()
|
||||
|
||||
@@ -61,7 +62,7 @@ export function RepositoryIconTabs({
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.RepositoryIconPicker.868c5c9b56',
|
||||
'Failed to import repo icon'
|
||||
)
|
||||
@@ -73,10 +74,7 @@ export function RepositoryIconTabs({
|
||||
const src = faviconUrlFromWebsite(website)
|
||||
if (!src) {
|
||||
toast.error(
|
||||
translate(
|
||||
'auto.components.settings.RepositoryIconPicker.acf31559a0',
|
||||
'Enter a valid website URL.'
|
||||
)
|
||||
t('auto.components.settings.RepositoryIconPicker.acf31559a0', 'Enter a valid website URL.')
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -84,10 +82,7 @@ export function RepositoryIconTabs({
|
||||
type: 'image',
|
||||
src,
|
||||
source: 'favicon',
|
||||
label: translate(
|
||||
'auto.components.settings.RepositoryIconPicker.4d039317f4',
|
||||
'Website favicon'
|
||||
)
|
||||
label: t('auto.components.settings.RepositoryIconPicker.4d039317f4', 'Website favicon')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -95,13 +90,13 @@ export function RepositoryIconTabs({
|
||||
<Tabs defaultValue={initialTab} className="gap-3">
|
||||
<TabsList variant="line" className="h-8">
|
||||
<TabsTrigger value="avatar" className="h-7 text-xs">
|
||||
{translate('auto.components.settings.RepositoryIconPicker.2d8bd302fa', 'Avatar')}
|
||||
{t('auto.components.settings.RepositoryIconPicker.2d8bd302fa', 'Avatar')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="icon" className="h-7 text-xs">
|
||||
{translate('auto.components.settings.RepositoryIconPicker.b2d7fd2116', 'Icon')}
|
||||
{t('auto.components.settings.RepositoryIconPicker.b2d7fd2116', 'Icon')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="emoji" className="h-7 text-xs">
|
||||
{translate('auto.components.settings.RepositoryIconPicker.c490787d24', 'Emoji')}
|
||||
{t('auto.components.settings.RepositoryIconPicker.c490787d24', 'Emoji')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -114,13 +109,10 @@ export function RepositoryIconTabs({
|
||||
onClick={() => void onUseGitHubAvatar()}
|
||||
>
|
||||
<Github className="size-3.5" />
|
||||
{translate(
|
||||
'auto.components.settings.RepositoryIconPicker.39da8a10bf',
|
||||
'Use GitHub Avatar'
|
||||
)}
|
||||
{t('auto.components.settings.RepositoryIconPicker.39da8a10bf', 'Use GitHub Avatar')}
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryIconPicker.7da623abcc',
|
||||
"Used by default — GitHub always provides one, even when the owner hasn't set a custom image."
|
||||
)}
|
||||
@@ -133,13 +125,13 @@ export function RepositoryIconTabs({
|
||||
onClick={() => void handleUploadImage()}
|
||||
>
|
||||
<Image className="size-3.5" />
|
||||
{translate('auto.components.settings.RepositoryIconPicker.381b4844fd', 'Upload PNG')}
|
||||
{t('auto.components.settings.RepositoryIconPicker.381b4844fd', 'Upload PNG')}
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={website}
|
||||
onChange={(event) => setWebsite(event.target.value)}
|
||||
placeholder={translate(
|
||||
placeholder={t(
|
||||
'auto.components.settings.RepositoryIconPicker.03ca1a4e9b',
|
||||
'example.com'
|
||||
)}
|
||||
@@ -153,11 +145,11 @@ export function RepositoryIconTabs({
|
||||
onClick={handleUseWebsiteFavicon}
|
||||
>
|
||||
<Link2 className="size-3.5" />
|
||||
{translate('auto.components.settings.RepositoryIconPicker.cc1286e263', 'Favicon')}
|
||||
{t('auto.components.settings.RepositoryIconPicker.cc1286e263', 'Favicon')}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositoryIconPicker.fde066a63b',
|
||||
'PNG uploads must be 256KB or smaller.'
|
||||
)}
|
||||
@@ -175,7 +167,7 @@ export function RepositoryIconTabs({
|
||||
size="icon-xs"
|
||||
className="size-8"
|
||||
onClick={() => onSetIcon({ type: 'lucide', name: option.name })}
|
||||
aria-label={translate(
|
||||
aria-label={t(
|
||||
'auto.components.settings.RepositoryIconPicker.useRepoIcon',
|
||||
'Use {{value0}} repo icon',
|
||||
{ value0: option.label }
|
||||
|
||||
@@ -3,26 +3,27 @@ import { AlertCircle, CheckCircle2 } from 'lucide-react'
|
||||
|
||||
import { Button } from '../../../ui/button'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate, type Translate } from '@/i18n/use-translate'
|
||||
|
||||
type McpConfigFileRowProps = {
|
||||
config: LoadedMcpConfigInspection
|
||||
onOpen: (config: LoadedMcpConfigInspection) => void
|
||||
}
|
||||
|
||||
function statusLabel(config: LoadedMcpConfigInspection): string {
|
||||
function statusLabel(config: LoadedMcpConfigInspection, t: Translate = translate): string {
|
||||
if (config.readError) {
|
||||
return translate('settings.mcp.unreadable', 'Unreadable')
|
||||
return t('settings.mcp.unreadable', 'Unreadable')
|
||||
}
|
||||
if (config.status === 'missing') {
|
||||
return translate('settings.mcp.notFound', 'Not found')
|
||||
return t('settings.mcp.notFound', 'Not found')
|
||||
}
|
||||
if (config.status === 'invalid') {
|
||||
return translate('settings.mcp.invalidJson', 'Invalid JSON')
|
||||
return t('settings.mcp.invalidJson', 'Invalid JSON')
|
||||
}
|
||||
if (config.servers.length === 0) {
|
||||
return translate('settings.mcp.noServers', 'No servers')
|
||||
return t('settings.mcp.noServers', 'No servers')
|
||||
}
|
||||
return translate('settings.mcp.serverCount', '{{count}} servers', {
|
||||
return t('settings.mcp.serverCount', '{{count}} servers', {
|
||||
count: config.servers.length,
|
||||
defaultValue_one: '{{count}} server'
|
||||
})
|
||||
@@ -38,17 +39,21 @@ function statusClassName(config: LoadedMcpConfigInspection): string {
|
||||
return 'border-border/60 bg-muted/60 text-muted-foreground'
|
||||
}
|
||||
|
||||
function serverDetailLabel(server: LoadedMcpConfigInspection['servers'][number]): string {
|
||||
function serverDetailLabel(
|
||||
server: LoadedMcpConfigInspection['servers'][number],
|
||||
t: Translate = translate
|
||||
): string {
|
||||
if (server.transport === 'http') {
|
||||
return server.url ?? translate('settings.mcp.httpServer', 'HTTP server')
|
||||
return server.url ?? t('settings.mcp.httpServer', 'HTTP server')
|
||||
}
|
||||
if (server.transport === 'stdio') {
|
||||
return server.command ?? translate('settings.mcp.stdioServer', 'stdio server')
|
||||
return server.command ?? t('settings.mcp.stdioServer', 'stdio server')
|
||||
}
|
||||
return server.issue ?? translate('settings.mcp.invalidServer', 'Invalid server')
|
||||
return server.issue ?? t('settings.mcp.invalidServer', 'Invalid server')
|
||||
}
|
||||
|
||||
export function McpConfigFileRow({ config, onOpen }: McpConfigFileRowProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<div data-settings-row="" className="space-y-2 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -61,9 +66,9 @@ export function McpConfigFileRow({ config, onOpen }: McpConfigFileRowProps): Rea
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<p className="truncate text-sm font-medium">
|
||||
{config.candidate.relativePath === '.mcp.json'
|
||||
? translate('settings.mcp.workspace', 'Workspace')
|
||||
? t('settings.mcp.workspace', 'Workspace')
|
||||
: config.candidate.relativePath === '.claude/mcp.json'
|
||||
? translate('settings.mcp.claudeWorkspace', 'Claude workspace')
|
||||
? t('settings.mcp.claudeWorkspace', 'Claude workspace')
|
||||
: config.candidate.label}
|
||||
</p>
|
||||
<p className="truncate font-mono text-[11px] text-muted-foreground">
|
||||
@@ -74,11 +79,11 @@ export function McpConfigFileRow({ config, onOpen }: McpConfigFileRowProps): Rea
|
||||
<span
|
||||
className={`shrink-0 rounded-full border px-1.5 py-0.5 text-[10px] font-medium ${statusClassName(config)}`}
|
||||
>
|
||||
{statusLabel(config)}
|
||||
{statusLabel(config, t)}
|
||||
</span>
|
||||
{config.exists ? (
|
||||
<Button variant="outline" size="xs" onClick={() => onOpen(config)}>
|
||||
{translate('auto.components.settings.McpConfigFileRow.e720c139cd', 'Open')}
|
||||
{t('auto.components.settings.McpConfigFileRow.e720c139cd', 'Open')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -102,11 +107,11 @@ export function McpConfigFileRow({ config, onOpen }: McpConfigFileRowProps): Rea
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-0.5 truncate font-mono text-[11px] text-muted-foreground">
|
||||
{serverDetailLabel(server)}
|
||||
{serverDetailLabel(server, t)}
|
||||
</p>
|
||||
{server.env && Object.keys(server.env).length > 0 ? (
|
||||
<p className="mt-0.5 truncate font-mono text-[11px] text-muted-foreground">
|
||||
{translate('auto.components.settings.McpConfigFileRow.b145eb6009', 'env:')}{' '}
|
||||
{t('auto.components.settings.McpConfigFileRow.b145eb6009', 'env:')}{' '}
|
||||
{Object.entries(server.env)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join(', ')}
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
loadMcpConfigInspections
|
||||
} from '../../../../runtime/mcp/configuration/mcp-config-inspection'
|
||||
import { formatMcpConfigSummary } from './mcp-config-summary'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { captureDirectSshMutationExpectation } from '@/lib/ssh-mutation-expectation'
|
||||
import { writeRuntimeFile } from '@/runtime/files/mutations/runtime-file-mutation-client'
|
||||
import {
|
||||
@@ -53,6 +53,7 @@ type McpConfigSectionProps = {
|
||||
const EMPTY_WORKTREES: Worktree[] = []
|
||||
|
||||
export function McpConfigSection({ repo }: McpConfigSectionProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const openFile = useAppStore((state) => state.openFile)
|
||||
const setActiveView = useAppStore((state) => state.setActiveView)
|
||||
const setActiveWorktree = useAppStore((state) => state.setActiveWorktree)
|
||||
@@ -284,12 +285,9 @@ export function McpConfigSection({ repo }: McpConfigSectionProps): React.JSX.Ele
|
||||
)
|
||||
setActiveView('terminal')
|
||||
toast.success(
|
||||
translate('auto.components.settings.McpConfigSection.1f3665e35a', 'MCP config created'),
|
||||
t('auto.components.settings.McpConfigSection.1f3665e35a', 'MCP config created'),
|
||||
{
|
||||
description: translate(
|
||||
'auto.components.settings.McpConfigSection.9ee215caf6',
|
||||
'.mcp.json'
|
||||
)
|
||||
description: t('auto.components.settings.McpConfigSection.9ee215caf6', '.mcp.json')
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
@@ -299,16 +297,16 @@ export function McpConfigSection({ repo }: McpConfigSectionProps): React.JSX.Ele
|
||||
|
||||
return (
|
||||
<SettingsGroup
|
||||
title={translate('auto.components.settings.McpConfigSection.55eea3ef47', 'MCP Configs')}
|
||||
title={t('auto.components.settings.McpConfigSection.55eea3ef47', 'MCP Configs')}
|
||||
description={
|
||||
<>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.McpConfigSection.96f5609b04',
|
||||
'Inspect MCP server definitions that agents can use while working in this repo.'
|
||||
)}
|
||||
{connectionId ? (
|
||||
<span className="block">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.McpConfigSection.6bac9ddfc6',
|
||||
'SSH repos are read through the remote filesystem. Starter creation is limited to the workspace root config.'
|
||||
)}
|
||||
@@ -322,7 +320,7 @@ export function McpConfigSection({ repo }: McpConfigSectionProps): React.JSX.Ele
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => void loadConfigs()}
|
||||
aria-label={translate(
|
||||
aria-label={t(
|
||||
'auto.components.settings.McpConfigSection.f34c152dc0',
|
||||
'Refresh MCP configs'
|
||||
)}
|
||||
@@ -342,14 +340,8 @@ export function McpConfigSection({ repo }: McpConfigSectionProps): React.JSX.Ele
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
{createConfirm
|
||||
? translate(
|
||||
'auto.components.settings.McpConfigSection.0a5c1ead54',
|
||||
'Create empty config'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.settings.McpConfigSection.82436439eb',
|
||||
'Add MCP config'
|
||||
)}
|
||||
? t('auto.components.settings.McpConfigSection.0a5c1ead54', 'Create empty config')
|
||||
: t('auto.components.settings.McpConfigSection.82436439eb', 'Add MCP config')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -359,7 +351,7 @@ export function McpConfigSection({ repo }: McpConfigSectionProps): React.JSX.Ele
|
||||
data-settings-row=""
|
||||
className="flex items-center justify-between py-3 text-xs text-muted-foreground"
|
||||
>
|
||||
<span>{formatMcpConfigSummary(configs)}</span>
|
||||
<span>{formatMcpConfigSummary(configs, t)}</span>
|
||||
{loading ? <LoaderCircle className="size-3.5 animate-spin" /> : null}
|
||||
</div>
|
||||
{visibleConfigs.length === 0 ? (
|
||||
@@ -376,7 +368,7 @@ export function McpConfigSection({ repo }: McpConfigSectionProps): React.JSX.Ele
|
||||
<span>{inspectionUnavailableMessage}</span>
|
||||
) : (
|
||||
<span>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.McpConfigSection.b900cd6282',
|
||||
'No MCP config found. Add an empty workspace config when you want this repo to define its own MCP servers.'
|
||||
)}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { LoadedMcpConfigInspection } from '../../../../runtime/mcp/configuration/mcp-config-inspection'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
export function McpMissingConfigList({
|
||||
missingConfigs
|
||||
}: {
|
||||
missingConfigs: LoadedMcpConfigInspection[]
|
||||
}): React.JSX.Element | null {
|
||||
const t = useTranslate()
|
||||
if (missingConfigs.length === 0) {
|
||||
return null
|
||||
}
|
||||
@@ -13,7 +14,7 @@ export function McpMissingConfigList({
|
||||
return (
|
||||
<div data-settings-row="" className="space-y-1.5 py-3">
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
{translate('auto.components.settings.McpConfigSection.4d16a0d9ac', 'Checked')}
|
||||
{t('auto.components.settings.McpConfigSection.4d16a0d9ac', 'Checked')}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{missingConfigs.map((config) => (
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { Translate } from '@/i18n/use-translate'
|
||||
import type { LoadedMcpConfigInspection } from '../../../../runtime/mcp/configuration/mcp-config-inspection'
|
||||
|
||||
export function formatMcpConfigSummary(configs: LoadedMcpConfigInspection[]): string {
|
||||
export function formatMcpConfigSummary(
|
||||
configs: LoadedMcpConfigInspection[],
|
||||
t: Translate = translate
|
||||
): string {
|
||||
const detected = configs.filter((config) => config.exists).length
|
||||
const count = configs.reduce((sum, config) => sum + config.servers.length, 0)
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.McpConfigSection.detectedServers',
|
||||
'{{detected}} detected · {{count}} servers',
|
||||
{ detected, count, defaultValue_one: '{{detected}} detected · {{count}} server' }
|
||||
|
||||
+14
-12
@@ -49,7 +49,7 @@ import {
|
||||
resolveAgentArgsPlaceholderAgent
|
||||
} from './repository-source-control-ai-labels'
|
||||
import { getRepositorySourceControlAiActionRecipeSectionId } from '../repository-settings-targets'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { SourceControlActionOptionField } from '../../../agent/launch-settings/AgentChoiceOptionField'
|
||||
import { pickSourceControlLaunchAgent } from '../../../../../../shared/source-control/ai/source-control-launch-agent-selection'
|
||||
import type { TuiAgent } from '../../../../../../shared/agent-launch/catalog/tui-agent'
|
||||
@@ -95,6 +95,7 @@ const SelectedRepositoryActionRows = memo(
|
||||
agentDefaultSurface: NativeChatDefaultSettings['agentDefaultSurface']
|
||||
nativeChatProviders: NativeChatDefaultSettings['nativeChatProviders']
|
||||
}): JSX.Element {
|
||||
const t = useTranslate()
|
||||
useUiLanguageRevision()
|
||||
const selections = useMemo(
|
||||
() =>
|
||||
@@ -120,7 +121,7 @@ const SelectedRepositoryActionRows = memo(
|
||||
return (
|
||||
<div data-settings-row="" className="space-y-3 py-3">
|
||||
<Label className="text-xs font-medium">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositorySourceControlAiActionRows.f0aa2cfaea',
|
||||
'Action recipes'
|
||||
)}
|
||||
@@ -179,8 +180,12 @@ const SelectedRepositoryActionRows = memo(
|
||||
defaultTuiAgent
|
||||
)
|
||||
))
|
||||
const agentWarningText = getSourceControlActionAgentWarningText(actionId, effectiveAgent)
|
||||
const agentSupportText = getSourceControlActionAgentSupportText(actionId)
|
||||
const agentWarningText = getSourceControlActionAgentWarningText(
|
||||
actionId,
|
||||
effectiveAgent,
|
||||
t
|
||||
)
|
||||
const agentSupportText = getSourceControlActionAgentSupportText(actionId, t)
|
||||
const actionDirty = actionDirtyById[actionId]
|
||||
const isSavingAction = savingActionIds[actionId] === true
|
||||
const sectionId = getRepositorySourceControlAiActionRecipeSectionId(repoId, actionId)
|
||||
@@ -222,13 +227,13 @@ const SelectedRepositoryActionRows = memo(
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ACTION_MODE_INHERIT}>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositorySourceControlAiActionRows.403876bb48',
|
||||
'Use global'
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem value={ACTION_MODE_OVERRIDE}>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositorySourceControlAiActionRows.1cd88d470a',
|
||||
'Customize'
|
||||
)}
|
||||
@@ -239,7 +244,7 @@ const SelectedRepositoryActionRows = memo(
|
||||
<div className="grid min-w-0 grid-cols-1 items-start gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[11px] text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositorySourceControlAiActionRows.f4310cf63f',
|
||||
'Agent'
|
||||
)}
|
||||
@@ -302,10 +307,7 @@ const SelectedRepositoryActionRows = memo(
|
||||
<div className="flex min-w-0 flex-wrap items-start gap-3">
|
||||
{!isLaunchAction ? (
|
||||
<SourceControlActionOptionField
|
||||
label={translate(
|
||||
'auto.components.settings.AgentsPane.cfb3f35775',
|
||||
'Arguments'
|
||||
)}
|
||||
label={t('auto.components.settings.AgentsPane.cfb3f35775', 'Arguments')}
|
||||
>
|
||||
<Input
|
||||
value={agentArgsValue}
|
||||
@@ -320,7 +322,7 @@ const SelectedRepositoryActionRows = memo(
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[11px] text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositorySourceControlAiActionRows.548a6e1281',
|
||||
'Command template'
|
||||
)}
|
||||
|
||||
+7
-6
@@ -8,7 +8,7 @@ import {
|
||||
CUSTOM_COMMAND_MODE_INHERIT,
|
||||
CUSTOM_COMMAND_MODE_REPO
|
||||
} from './repository-source-control-ai-labels'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
type RepositorySourceControlAiCustomCommandProps = {
|
||||
value: string | undefined
|
||||
@@ -28,6 +28,7 @@ export function RepositorySourceControlAiCustomCommand({
|
||||
onChange,
|
||||
onCommit
|
||||
}: RepositorySourceControlAiCustomCommandProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
// Retain empty Repository command intent so selecting that mode cannot snap back before typing.
|
||||
const [forceRepoMode, setForceRepoMode] = useState(false)
|
||||
const hasRepoCommand = isRepoCommand(value)
|
||||
@@ -38,13 +39,13 @@ export function RepositorySourceControlAiCustomCommand({
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="min-w-0 space-y-0.5">
|
||||
<Label className="text-xs font-medium">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositorySourceControlAiCustomCommand.ebffc5a28c',
|
||||
'Custom command'
|
||||
)}
|
||||
</Label>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositorySourceControlAiCustomCommand.fbb77e122a',
|
||||
'Repo fallback for text actions that select Custom command.'
|
||||
)}
|
||||
@@ -76,13 +77,13 @@ export function RepositorySourceControlAiCustomCommand({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={CUSTOM_COMMAND_MODE_INHERIT}>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositorySourceControlAiCustomCommand.e56668c291',
|
||||
'Use global'
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem value={CUSTOM_COMMAND_MODE_REPO}>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositorySourceControlAiCustomCommand.0704dd55cd',
|
||||
'Repository command'
|
||||
)}
|
||||
@@ -107,7 +108,7 @@ export function RepositorySourceControlAiCustomCommand({
|
||||
}}
|
||||
placeholder={
|
||||
source.customAgentCommand ||
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.RepositorySourceControlAiCustomCommand.f9941f0caf',
|
||||
'e.g. ollama run llama3.1 {prompt}'
|
||||
)
|
||||
|
||||
+11
-9
@@ -3,6 +3,7 @@ import type { SourceControlAiSettings } from '../../../../../../shared/source-co
|
||||
import { Label } from '../../../ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../ui/select'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate, type Translate } from '@/i18n/use-translate'
|
||||
|
||||
type RepositorySourceControlAiEnablementProps = {
|
||||
value: boolean | undefined
|
||||
@@ -20,10 +21,10 @@ function enablementValue(value: boolean | undefined): 'inherit' | 'on' | 'off' {
|
||||
return 'inherit'
|
||||
}
|
||||
|
||||
function visibilityLabel(value: boolean): string {
|
||||
function visibilityLabel(value: boolean, t: Translate = translate): string {
|
||||
return value
|
||||
? translate('auto.components.settings.RepositorySourceControlAiEnablement.show', 'Show')
|
||||
: translate('auto.components.settings.RepositorySourceControlAiEnablement.hide', 'Hide')
|
||||
? t('auto.components.settings.RepositorySourceControlAiEnablement.show', 'Show')
|
||||
: t('auto.components.settings.RepositorySourceControlAiEnablement.hide', 'Hide')
|
||||
}
|
||||
|
||||
export function RepositorySourceControlAiEnablement({
|
||||
@@ -31,6 +32,7 @@ export function RepositorySourceControlAiEnablement({
|
||||
source,
|
||||
onChange
|
||||
}: RepositorySourceControlAiEnablementProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<div
|
||||
data-settings-row=""
|
||||
@@ -38,16 +40,16 @@ export function RepositorySourceControlAiEnablement({
|
||||
>
|
||||
<div className="min-w-0 space-y-0.5">
|
||||
<Label className="text-xs font-medium">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositorySourceControlAiEnablement.showActionsLabel',
|
||||
'Show Source Control AI actions'
|
||||
)}
|
||||
</Label>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositorySourceControlAiEnablement.visibilityHelper',
|
||||
"Controls whether Source Control AI buttons are shown for this repository. Generation used by separate features follows those features' settings. Global default is {{value0}}.",
|
||||
{ value0: visibilityLabel(source.enabled) }
|
||||
{ value0: visibilityLabel(source.enabled, t) }
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -62,16 +64,16 @@ export function RepositorySourceControlAiEnablement({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="inherit">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositorySourceControlAiEnablement.62511a575d',
|
||||
'Use global'
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem value="on">
|
||||
{translate('auto.components.settings.RepositorySourceControlAiEnablement.show', 'Show')}
|
||||
{t('auto.components.settings.RepositorySourceControlAiEnablement.show', 'Show')}
|
||||
</SelectItem>
|
||||
<SelectItem value="off">
|
||||
{translate('auto.components.settings.RepositorySourceControlAiEnablement.hide', 'Hide')}
|
||||
{t('auto.components.settings.RepositorySourceControlAiEnablement.hide', 'Hide')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
+7
-5
@@ -7,6 +7,7 @@ import { Label } from '../../../ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../ui/select'
|
||||
import { triStateValue } from './repository-source-control-ai-draft'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
type HostedReviewDefaultKey = keyof NonNullable<RepoSourceControlAiOverrides['prCreationDefaults']>
|
||||
|
||||
@@ -60,10 +61,11 @@ export function RepositorySourceControlAiHostedReviewDefaults({
|
||||
source,
|
||||
onChange
|
||||
}: RepositorySourceControlAiHostedReviewDefaultsProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<div data-settings-row="" className="space-y-2 py-3">
|
||||
<Label className="text-xs font-medium">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositorySourceControlAiHostedReviewDefaults.aa6ee4b7d6',
|
||||
'Hosted-review creation defaults'
|
||||
)}
|
||||
@@ -79,7 +81,7 @@ export function RepositorySourceControlAiHostedReviewDefaults({
|
||||
<span className="min-w-0 space-y-0.5">
|
||||
<span className="block text-xs text-foreground">{row.label}</span>
|
||||
<span className="block text-[11px] text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositorySourceControlAiHostedReviewDefaults.a68849a859',
|
||||
'Global default is'
|
||||
)}{' '}
|
||||
@@ -95,19 +97,19 @@ export function RepositorySourceControlAiHostedReviewDefaults({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="inherit">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositorySourceControlAiHostedReviewDefaults.ffc3b26b26',
|
||||
'Use global'
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem value="on">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositorySourceControlAiHostedReviewDefaults.777443bf89',
|
||||
'On'
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem value="off">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RepositorySourceControlAiHostedReviewDefaults.053ccfbf52',
|
||||
'Off'
|
||||
)}
|
||||
|
||||
+4
-3
@@ -14,7 +14,7 @@ import { RepositorySourceControlAiHostedReviewDefaults } from './RepositorySourc
|
||||
import { normalizeRepoAiDraft } from './repository-source-control-ai-draft'
|
||||
import { useRepositorySourceControlAiGlobalUx } from './repository-source-control-ai-global-ux'
|
||||
import { getSettingOwnershipSummary } from '../../page/setting-ownership'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { SettingsGroup } from '../../section-layout/SettingsGroup'
|
||||
|
||||
type RepositorySourceControlAiSectionProps = {
|
||||
@@ -30,6 +30,7 @@ export function RepositorySourceControlAiSection({
|
||||
updateRepo,
|
||||
writeSourceControlAiSettings
|
||||
}: RepositorySourceControlAiSectionProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const settings = useAppStore(
|
||||
useShallow(
|
||||
(state) =>
|
||||
@@ -40,7 +41,7 @@ export function RepositorySourceControlAiSection({
|
||||
}
|
||||
)
|
||||
)
|
||||
const ownership = getSettingOwnershipSummary('repositorySourceControlAi')
|
||||
const ownership = getSettingOwnershipSummary('repositorySourceControlAi', t)
|
||||
const source = useMemo(
|
||||
() => normalizeSourceControlAiSettings(settings?.sourceControlAi, settings?.commitMessageAi),
|
||||
[settings?.sourceControlAi, settings?.commitMessageAi]
|
||||
@@ -82,7 +83,7 @@ export function RepositorySourceControlAiSection({
|
||||
data-settings-section={getRepositorySourceControlAiSectionId(repo.id)}
|
||||
>
|
||||
<SettingsGroup
|
||||
title={translate(
|
||||
title={t(
|
||||
'auto.components.settings.RepositorySourceControlAiSection.71b003b62b',
|
||||
'Source Control AI'
|
||||
)}
|
||||
|
||||
+5
-7
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'
|
||||
import type { GlobalSettings } from '../../../../../../shared/global-settings-types'
|
||||
import type { PublicKnownRuntimeEnvironment } from '../../../../../../shared/remote-runtime/host-catalog/runtime-environments'
|
||||
import { useAppStore } from '@/store/index'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { SearchableSetting } from '../../section-layout/SearchableSetting'
|
||||
import { EphemeralVmRuntimesSection } from '../../virtual-machines/runtimes/EphemeralVmRuntimesSection'
|
||||
import {
|
||||
@@ -41,6 +41,7 @@ export function RuntimeEnvironmentsPane({
|
||||
allowLocalRuntime = true,
|
||||
addServerIntentSignal
|
||||
}: RuntimeEnvironmentsPaneProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const { dialog, setDialog } = useRuntimeServersPage()
|
||||
const [pendingSwitchValue, setPendingSwitchValue] = useState<string | null>(null)
|
||||
const [pendingRemove, setPendingRemove] = useState<PublicKnownRuntimeEnvironment | null>(null)
|
||||
@@ -63,13 +64,10 @@ export function RuntimeEnvironmentsPane({
|
||||
|
||||
const getEnvironmentLabel = (value: string): string => {
|
||||
if (value === LOCAL_RUNTIME_VALUE) {
|
||||
return translate('auto.components.settings.serverList.thisComputer', 'This computer')
|
||||
return t('auto.components.settings.serverList.thisComputer', 'This computer')
|
||||
}
|
||||
if (value === NO_RUNTIME_VALUE) {
|
||||
return translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.b07070ed3c',
|
||||
'No server connected'
|
||||
)
|
||||
return t('auto.components.settings.RuntimeEnvironmentsPane.b07070ed3c', 'No server connected')
|
||||
}
|
||||
// The default may be WSL, which this listed-server subset omits.
|
||||
return (
|
||||
@@ -232,7 +230,7 @@ export function RuntimeEnvironmentsPane({
|
||||
|
||||
{managedCatalog.loadFailed ? (
|
||||
<p className="text-xs text-destructive">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.managedServers.loadFailed',
|
||||
'Orca couldn’t load SSH hosts and unfinished moves. Reopen Settings to try again.'
|
||||
)}
|
||||
|
||||
+7
-9
@@ -1,6 +1,6 @@
|
||||
import { Monitor, Server } from 'lucide-react'
|
||||
import type { PublicKnownRuntimeEnvironment } from '../../../../../../shared/remote-runtime/host-catalog/runtime-environments'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { useAppStore } from '@/store/index'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../ui/select'
|
||||
import { getActiveServerModeDescription } from './runtime-environment-host-details'
|
||||
@@ -21,6 +21,7 @@ export function RuntimeDefaultHostRow({
|
||||
isBusy,
|
||||
onValueChange
|
||||
}: RuntimeDefaultHostRowProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const savedEnvironments = useAppStore((state) => state.runtimeEnvironments)
|
||||
// Retain unlisted active defaults such as WSL distros so users can change them.
|
||||
const unlistedActive =
|
||||
@@ -37,7 +38,7 @@ export function RuntimeDefaultHostRow({
|
||||
data-settings-section="default-runtime"
|
||||
className="scroll-mt-8 text-xs text-muted-foreground"
|
||||
>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.serverList.defaultHostEmptyLine',
|
||||
'New projects open on this computer until you add a server.'
|
||||
)}
|
||||
@@ -51,13 +52,10 @@ export function RuntimeDefaultHostRow({
|
||||
>
|
||||
<div className="min-w-0 space-y-0.5">
|
||||
<div id="runtime-active-server-label" className="text-sm font-medium">
|
||||
{translate(
|
||||
'auto.components.settings.serverList.defaultHostLabel',
|
||||
'New projects open on'
|
||||
)}
|
||||
{t('auto.components.settings.serverList.defaultHostLabel', 'New projects open on')}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{getActiveServerModeDescription(allowLocalRuntime)}
|
||||
{getActiveServerModeDescription(allowLocalRuntime, t)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
@@ -77,11 +75,11 @@ export function RuntimeDefaultHostRow({
|
||||
{allowLocalRuntime ? (
|
||||
<SelectItem value={LOCAL_RUNTIME_VALUE}>
|
||||
<Monitor />
|
||||
{translate('auto.components.settings.serverList.thisComputer', 'This computer')}
|
||||
{t('auto.components.settings.serverList.thisComputer', 'This computer')}
|
||||
</SelectItem>
|
||||
) : options.length === 0 ? (
|
||||
<SelectItem value={NO_RUNTIME_VALUE} disabled>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.b07070ed3c',
|
||||
'No server connected'
|
||||
)}
|
||||
|
||||
+14
-24
@@ -1,6 +1,6 @@
|
||||
import { Loader2, Trash2 } from 'lucide-react'
|
||||
import type { PublicKnownRuntimeEnvironment } from '../../../../../../shared/remote-runtime/host-catalog/runtime-environments'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { Button } from '../../../ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
@@ -28,18 +28,16 @@ export function RuntimeEnvironmentSwitchDialog({
|
||||
onCancel: () => void
|
||||
onConfirm: () => void
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<Dialog open={pendingSwitchValue !== null} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-sm sm:max-w-sm" showCloseButton={false}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-sm">
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.d570c35a99',
|
||||
'Switch Server'
|
||||
)}
|
||||
{t('auto.components.settings.RuntimeEnvironmentsPane.d570c35a99', 'Switch Server')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.b2290ed203',
|
||||
'Orca will focus this host and load its projects. Existing terminals and browser tabs on other hosts stay alive.'
|
||||
)}
|
||||
@@ -48,10 +46,7 @@ export function RuntimeEnvironmentSwitchDialog({
|
||||
{pendingSwitchValue ? (
|
||||
<div className="rounded-md border border-border/70 bg-muted/35 px-3 py-2 text-xs">
|
||||
<div className="text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.05e0fc3ebf',
|
||||
'Switch to'
|
||||
)}
|
||||
{t('auto.components.settings.RuntimeEnvironmentsPane.05e0fc3ebf', 'Switch to')}
|
||||
</div>
|
||||
<div className="mt-0.5 truncate font-medium">
|
||||
{getEnvironmentLabel(pendingSwitchValue)}
|
||||
@@ -61,11 +56,11 @@ export function RuntimeEnvironmentSwitchDialog({
|
||||
{switchError ? <p className="text-sm text-destructive">{switchError}</p> : null}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onCancel} disabled={switchingValue !== null}>
|
||||
{translate('auto.components.settings.RuntimeEnvironmentsPane.af53761f31', 'Cancel')}
|
||||
{t('auto.components.settings.RuntimeEnvironmentsPane.af53761f31', 'Cancel')}
|
||||
</Button>
|
||||
<Button onClick={onConfirm} disabled={switchingValue !== null}>
|
||||
{switchingValue !== null ? <Loader2 className="animate-spin" /> : null}
|
||||
{translate('auto.components.settings.RuntimeEnvironmentsPane.d2e00809e4', 'Switch')}
|
||||
{t('auto.components.settings.RuntimeEnvironmentsPane.d2e00809e4', 'Switch')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -90,23 +85,21 @@ export function RuntimeEnvironmentRemoveDialog({
|
||||
onCancel: () => void
|
||||
onConfirm: () => void
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<Dialog open={pendingRemove !== null} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-sm sm:max-w-sm" showCloseButton={false}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-sm">
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.bb90dd6487',
|
||||
'Remove Server'
|
||||
)}
|
||||
{t('auto.components.settings.RuntimeEnvironmentsPane.bb90dd6487', 'Remove Server')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{removingActiveServer
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.removeDefaultServerDescription',
|
||||
'This server is the default host for new projects. Choose another one under New projects open on before removing it. Existing host sessions are left alone.'
|
||||
)
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.ed3e3f069d',
|
||||
'This removes the saved server from Orca. It does not change the default host for new projects.'
|
||||
)}
|
||||
@@ -117,21 +110,18 @@ export function RuntimeEnvironmentRemoveDialog({
|
||||
<div className="truncate font-medium">{pendingRemove.name}</div>
|
||||
<div className="mt-0.5 truncate font-mono text-muted-foreground">
|
||||
{pendingRemove.endpoints[0]?.endpoint ??
|
||||
translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.6ef71985da',
|
||||
'No endpoint'
|
||||
)}
|
||||
t('auto.components.settings.RuntimeEnvironmentsPane.6ef71985da', 'No endpoint')}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{removeError ? <p className="text-sm text-destructive">{removeError}</p> : null}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onCancel} disabled={removingId !== null}>
|
||||
{translate('auto.components.settings.RuntimeEnvironmentsPane.af53761f31', 'Cancel')}
|
||||
{t('auto.components.settings.RuntimeEnvironmentsPane.af53761f31', 'Cancel')}
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={onConfirm} disabled={removingId !== null}>
|
||||
{removingId !== null ? <Loader2 className="animate-spin" /> : <Trash2 />}
|
||||
{translate('auto.components.settings.RuntimeEnvironmentsPane.d25f0688b1', 'Remove')}
|
||||
{t('auto.components.settings.RuntimeEnvironmentsPane.d25f0688b1', 'Remove')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
+33
-25
@@ -1,4 +1,5 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { Translate } from '@/i18n/use-translate'
|
||||
import {
|
||||
evaluateRuntimeCompat,
|
||||
type RuntimeCompatVerdict
|
||||
@@ -68,7 +69,10 @@ export function getHostDetailsSummary(details: RuntimeHostDetails | undefined):
|
||||
return translate('auto.components.settings.RuntimeEnvironmentsPane.9a91c4a0eb', 'Compatible')
|
||||
}
|
||||
|
||||
export function getHostDetailsDescription(details: RuntimeHostDetails | undefined): string | null {
|
||||
export function getHostDetailsDescription(
|
||||
details: RuntimeHostDetails | undefined,
|
||||
t: Translate = translate
|
||||
): string | null {
|
||||
if (!details || details.status === 'loading') {
|
||||
return null
|
||||
}
|
||||
@@ -76,10 +80,10 @@ export function getHostDetailsDescription(details: RuntimeHostDetails | undefine
|
||||
return details.error
|
||||
}
|
||||
if (details.compatibility?.kind === 'blocked') {
|
||||
return describeRuntimeCompatBlock(details.compatibility)
|
||||
return describeRuntimeCompatBlock(details.compatibility, t)
|
||||
}
|
||||
if (details.runtimeStatus === null && details.compatibility === null) {
|
||||
const unavailableDescription = translate(
|
||||
const unavailableDescription = t(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.serverRuntimeUnavailableDescription',
|
||||
'SSH transport is connected, but the Orca runtime did not answer. The host may still be running.'
|
||||
)
|
||||
@@ -88,10 +92,13 @@ export function getHostDetailsDescription(details: RuntimeHostDetails | undefine
|
||||
return null
|
||||
}
|
||||
|
||||
export function getRuntimeCapabilitiesSummary(status: RuntimeStatus | null | undefined): string {
|
||||
export function getRuntimeCapabilitiesSummary(
|
||||
status: RuntimeStatus | null | undefined,
|
||||
t: Translate = translate
|
||||
): string {
|
||||
const capabilities = status?.capabilities ?? []
|
||||
if (capabilities.length === 0) {
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.4b5c6d7e8f',
|
||||
'No capabilities reported'
|
||||
)
|
||||
@@ -102,14 +109,15 @@ export function getRuntimeCapabilitiesSummary(status: RuntimeStatus | null | und
|
||||
}
|
||||
|
||||
export function getHostModelCapabilitySummary(
|
||||
status: RuntimeStatus | null | undefined
|
||||
status: RuntimeStatus | null | undefined,
|
||||
t: Translate = translate
|
||||
): string | null {
|
||||
if (!status) {
|
||||
return null
|
||||
}
|
||||
const capabilities = status.capabilities
|
||||
if (!capabilities) {
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.hostModelCapabilityUnknown',
|
||||
'Host model support: checking server capabilities'
|
||||
)
|
||||
@@ -120,13 +128,13 @@ export function getHostModelCapabilitySummary(
|
||||
WORKSPACE_RUN_CONTEXT_RUNTIME_CAPABILITY
|
||||
].filter((capability) => !capabilities.includes(capability))
|
||||
if (missing.length === 0) {
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.hostModelCapabilitySupported',
|
||||
'Host model support: ready'
|
||||
)
|
||||
}
|
||||
const missingLabels = missing.map(getHostModelCapabilityLabel)
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.hostModelCapabilityMissing',
|
||||
'Host model support: update server for {{value0}}',
|
||||
{ value0: missingLabels.join(', ') }
|
||||
@@ -155,13 +163,16 @@ function getHostModelCapabilityLabel(capability: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
export function getActiveServerModeDescription(allowLocalRuntime: boolean): string {
|
||||
export function getActiveServerModeDescription(
|
||||
allowLocalRuntime: boolean,
|
||||
t: Translate = translate
|
||||
): string {
|
||||
return allowLocalRuntime
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.serverList.defaultHostHelp',
|
||||
'Existing projects stay where they are.'
|
||||
)
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.serverList.defaultHostWebHelp',
|
||||
'This browser opens new projects on the chosen server.'
|
||||
)
|
||||
@@ -204,35 +215,32 @@ export function isRuntimeServerTransportConnected(state: RuntimeServerConnection
|
||||
return isConnectedRuntimeHostState(state)
|
||||
}
|
||||
|
||||
export function getRuntimeServerConnectionLabel(state: RuntimeServerConnectionState): string {
|
||||
export function getRuntimeServerConnectionLabel(
|
||||
state: RuntimeServerConnectionState,
|
||||
t: Translate = translate
|
||||
): string {
|
||||
switch (state) {
|
||||
case 'connected':
|
||||
return translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.serverConnected',
|
||||
'Connected'
|
||||
)
|
||||
return t('auto.components.settings.RuntimeEnvironmentsPane.serverConnected', 'Connected')
|
||||
case 'runtime-unavailable':
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.serverRuntimeUnavailable',
|
||||
'Orca unavailable'
|
||||
)
|
||||
case 'workspace-window-closed':
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.serverWorkspaceWindowClosed',
|
||||
'Workspace window closed'
|
||||
)
|
||||
case 'reconnecting':
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.serverReconnecting',
|
||||
'Reconnecting'
|
||||
)
|
||||
case 'checking':
|
||||
return translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.serverChecking',
|
||||
'Checking…'
|
||||
)
|
||||
return t('auto.components.settings.RuntimeEnvironmentsPane.serverChecking', 'Checking…')
|
||||
case 'disconnected':
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.serverDisconnected',
|
||||
'Disconnected'
|
||||
)
|
||||
|
||||
+5
-4
@@ -1,6 +1,6 @@
|
||||
import { useState, type Dispatch, type MutableRefObject, type SetStateAction } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { unwrapRuntimeRpcResult } from '@/runtime/rpc/calls/runtime-rpc-result'
|
||||
import { extractRuntimeTransportDiagnostics } from '@/runtime/hosts/connections/runtime-status-probe-diagnostics'
|
||||
import { useAppStore } from '@/store/index'
|
||||
@@ -27,6 +27,7 @@ export function useRuntimeEnvironmentConnectionActions({
|
||||
setActiveRuntimeEnvironmentPreference,
|
||||
getEnvironmentLabel
|
||||
}: RuntimeEnvironmentConnectionActionParams) {
|
||||
const t = useTranslate()
|
||||
const [connectingId, setConnectingId] = useState<string | null>(null)
|
||||
const [switchingValue, setSwitchingValue] = useState<string | null>(null)
|
||||
const [disconnectingId, setDisconnectingId] = useState<string | null>(null)
|
||||
@@ -53,7 +54,7 @@ export function useRuntimeEnvironmentConnectionActions({
|
||||
}
|
||||
}))
|
||||
toast.success(
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.disconnectedServer',
|
||||
'Disconnected from {{value0}}.',
|
||||
{ value0: environment.name }
|
||||
@@ -120,7 +121,7 @@ export function useRuntimeEnvironmentConnectionActions({
|
||||
)
|
||||
if (mountedRef.current) {
|
||||
toast.success(
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.runtimeReachable',
|
||||
'{{value0}} is reachable.',
|
||||
{ value0: environment.name }
|
||||
@@ -167,7 +168,7 @@ export function useRuntimeEnvironmentConnectionActions({
|
||||
if (switched) {
|
||||
if (mountedRef.current) {
|
||||
toast.success(
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.99ac81fb43',
|
||||
'Switched to {{value0}}.',
|
||||
{ value0: getEnvironmentLabel(value) }
|
||||
|
||||
+10
-11
@@ -1,6 +1,6 @@
|
||||
import { useState, type MutableRefObject } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { useAppStore } from '@/store/index'
|
||||
import type { GlobalSettings } from '../../../../../../shared/global-settings-types'
|
||||
import type { PublicKnownRuntimeEnvironment } from '../../../../../../shared/remote-runtime/host-catalog/runtime-environments'
|
||||
@@ -31,6 +31,7 @@ export function useRuntimeEnvironmentMutationActions({
|
||||
loadEnvironments,
|
||||
connectEnvironment
|
||||
}: RuntimeEnvironmentMutationActionParams) {
|
||||
const t = useTranslate()
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [removingId, setRemovingId] = useState<string | null>(null)
|
||||
const [removeError, setRemoveError] = useState<string | null>(null)
|
||||
@@ -55,7 +56,7 @@ export function useRuntimeEnvironmentMutationActions({
|
||||
defaultServerNameForAccessLink(trimmedPairingCode, useAppStore.getState().runtimeEnvironments)
|
||||
if (!trimmedName || !trimmedPairingCode) {
|
||||
toast.error(
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.0c55a47480',
|
||||
'Name and pairing code are required.'
|
||||
)
|
||||
@@ -87,7 +88,7 @@ export function useRuntimeEnvironmentMutationActions({
|
||||
})
|
||||
if (result.updatedExisting && mountedRef.current) {
|
||||
toast.success(
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.serverAlreadyPaired',
|
||||
'This server is already paired as {{value0}}; updated its connection.',
|
||||
{ value0: result.environment.name }
|
||||
@@ -105,7 +106,7 @@ export function useRuntimeEnvironmentMutationActions({
|
||||
} else if (!result.updatedExisting) {
|
||||
if (mountedRef.current) {
|
||||
toast.success(
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.connectedNewProjectsHint',
|
||||
'Connected to {{value0}}. To create new projects there by default, choose it under New projects open on.',
|
||||
{ value0: result.environment.name }
|
||||
@@ -121,7 +122,7 @@ export function useRuntimeEnvironmentMutationActions({
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.6cb6eae14f',
|
||||
'Failed to save runtime environment.'
|
||||
)
|
||||
@@ -143,7 +144,7 @@ export function useRuntimeEnvironmentMutationActions({
|
||||
if (isRuntimeEnvironmentRemovalBlocked(settings.activeRuntimeEnvironmentId, environment.id)) {
|
||||
if (mountedRef.current) {
|
||||
setRemoveError(
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.removeDefaultServerBlocked',
|
||||
'This server is the default host for new projects. Choose another one under New projects open on before removing it.'
|
||||
)
|
||||
@@ -157,11 +158,9 @@ export function useRuntimeEnvironmentMutationActions({
|
||||
// A WSL host whose Remove couldn't prove its server stopped says so instead of "Removed".
|
||||
if (mountedRef.current && !warnWslHostServerStop(removal?.serverStop)) {
|
||||
toast.success(
|
||||
translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.b5b5114cb0',
|
||||
'Removed {{value0}}.',
|
||||
{ value0: environment.name }
|
||||
)
|
||||
t('auto.components.settings.RuntimeEnvironmentsPane.b5b5114cb0', 'Removed {{value0}}.', {
|
||||
value0: environment.name
|
||||
})
|
||||
)
|
||||
}
|
||||
return true
|
||||
|
||||
+37
-26
@@ -4,38 +4,43 @@ import type {
|
||||
} from '../../../../../../shared/orcad-managed-runtime'
|
||||
import type { OrcadMigrationBlocker } from '../../../../../../shared/orcad-migration/orcad-migration-preflight'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { Translate } from '@/i18n/use-translate'
|
||||
import { ORCAD_UPDATE_WSL_MOVE_RUNNING_CODE } from '../../../../../../shared/wsl-host-types'
|
||||
import { dependencyKindLabel } from './managed-server-dependency-kinds'
|
||||
|
||||
export function migrationPhaseLabel(phase: OrcadManagedPendingMigrationRow['phase']): string {
|
||||
export function migrationPhaseLabel(
|
||||
phase: OrcadManagedPendingMigrationRow['phase'],
|
||||
t: Translate = translate
|
||||
): string {
|
||||
switch (phase) {
|
||||
case 'source-fenced':
|
||||
return translate('auto.components.settings.managedServers.phase.fenced', 'Host locked')
|
||||
return t('auto.components.settings.managedServers.phase.fenced', 'Host locked')
|
||||
case 'destination-staged':
|
||||
return translate('auto.components.settings.managedServers.phase.staged', 'Copy staged')
|
||||
return t('auto.components.settings.managedServers.phase.staged', 'Copy staged')
|
||||
case 'destination-committed':
|
||||
return translate('auto.components.settings.managedServers.phase.committed', 'Copy committed')
|
||||
return t('auto.components.settings.managedServers.phase.committed', 'Copy committed')
|
||||
case 'source-retired':
|
||||
return translate('auto.components.settings.managedServers.phase.retired', 'Finished')
|
||||
return t('auto.components.settings.managedServers.phase.retired', 'Finished')
|
||||
}
|
||||
}
|
||||
|
||||
export function recoveryLabel(
|
||||
recovery: NonNullable<OrcadManagedRuntimeStatus['recovery']>
|
||||
recovery: NonNullable<OrcadManagedRuntimeStatus['recovery']>,
|
||||
t: Translate = translate
|
||||
): string {
|
||||
switch (recovery.operation) {
|
||||
case 'activate':
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.managedServers.recovery.activate',
|
||||
'An update was interrupted'
|
||||
)
|
||||
case 'rollback':
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.managedServers.recovery.rollback',
|
||||
'A rollback was interrupted'
|
||||
)
|
||||
case 'decommission':
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.managedServers.recovery.decommission',
|
||||
'A stop was interrupted'
|
||||
)
|
||||
@@ -43,14 +48,17 @@ export function recoveryLabel(
|
||||
}
|
||||
|
||||
/** `null` counts are unknown, never zero: the server did not answer. */
|
||||
export function terminalCensusLabel(terminals: OrcadManagedRuntimeStatus['terminals']): string {
|
||||
export function terminalCensusLabel(
|
||||
terminals: OrcadManagedRuntimeStatus['terminals'],
|
||||
t: Translate = translate
|
||||
): string {
|
||||
if (terminals.liveSessions === null) {
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.managedServers.terminals.unknown',
|
||||
'Running terminals: unknown'
|
||||
)
|
||||
}
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.managedServers.terminals.count',
|
||||
'Running terminals: {{count}}',
|
||||
{ count: terminals.liveSessions }
|
||||
@@ -58,8 +66,8 @@ export function terminalCensusLabel(terminals: OrcadManagedRuntimeStatus['termin
|
||||
}
|
||||
|
||||
/** Copy for Update anyway; it states only what the forced update was verified to keep. */
|
||||
export function forceUpdateMessage(): string {
|
||||
return translate(
|
||||
export function forceUpdateMessage(t: Translate = translate): string {
|
||||
return t(
|
||||
'auto.components.settings.managedServers.row.forceUpdate',
|
||||
'Terminals are running on this host. They keep running through the update, and their panes reconnect when Orca’s server restarts.'
|
||||
)
|
||||
@@ -142,49 +150,52 @@ const MIGRATION_CODES = new Set([
|
||||
'orcad_rollback_crosses_migration'
|
||||
])
|
||||
|
||||
export function managedServerOutcomeLabel(result: {
|
||||
outcome: string
|
||||
code?: string
|
||||
verdict?: string
|
||||
}): string {
|
||||
export function managedServerOutcomeLabel(
|
||||
result: {
|
||||
outcome: string
|
||||
code?: string
|
||||
verdict?: string
|
||||
},
|
||||
t: Translate = translate
|
||||
): string {
|
||||
const code = result.code ?? ''
|
||||
if (code === 'orcad_stop_active_environment') {
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.managedServers.outcome.defaultServer',
|
||||
'Not done: this server is the default host for new projects. Choose another one under New projects open on before stopping it.'
|
||||
)
|
||||
}
|
||||
if (result.verdict === 'live' || LIVE_TERMINAL_CODES.has(code)) {
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.managedServers.outcome.live',
|
||||
'Not done: terminals on this server are still running. Close them and try again.'
|
||||
)
|
||||
}
|
||||
if (result.verdict === 'unverifiable' || UNVERIFIABLE_CODES.has(code)) {
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.managedServers.outcome.unverifiable',
|
||||
'Not done: Orca couldn’t confirm what is running on this server. Try again when the host is reachable.'
|
||||
)
|
||||
}
|
||||
if (code === ORCAD_UPDATE_WSL_MOVE_RUNNING_CODE) {
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.managedServers.outcome.wslMoveRunning',
|
||||
'Not done: a project is moving onto this server. Orca updates it once the move finishes.'
|
||||
)
|
||||
}
|
||||
if (MIGRATION_CODES.has(code)) {
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.managedServers.outcome.migration',
|
||||
'Not done: a move onto this server is unfinished. Resume it first.'
|
||||
)
|
||||
}
|
||||
if (result.outcome === 'deferred' || result.outcome === 'pending') {
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.managedServers.outcome.deferred',
|
||||
'Not done yet: the server isn’t ready for this. Try again later.'
|
||||
)
|
||||
}
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.managedServers.outcome.failed',
|
||||
'Orca couldn’t finish this on the server. Try again.'
|
||||
)
|
||||
|
||||
+49
-49
@@ -1,6 +1,7 @@
|
||||
import { ArrowUpCircle, RefreshCw, RotateCcw, ServerOff, Trash2, Wrench } from 'lucide-react'
|
||||
import type { OrcadManagedRuntimeStatus } from '../../../../../../shared/orcad-managed-runtime'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate, type Translate } from '@/i18n/use-translate'
|
||||
import { LoadingSpinner } from '../../../LoadingSpinner'
|
||||
import { Button } from '../../../ui/button'
|
||||
import { DropdownMenuItem, DropdownMenuSeparator } from '../../../ui/dropdown-menu'
|
||||
@@ -16,36 +17,36 @@ import type { ManagedServerLifecycle } from './use-managed-server-lifecycle'
|
||||
// Unloaded status means unknown; only loaded evidence can claim Not running.
|
||||
export function managedServerVersionLabel(
|
||||
status: OrcadManagedRuntimeStatus | null,
|
||||
statusError: string | null
|
||||
statusError: string | null,
|
||||
t: Translate = translate
|
||||
): string {
|
||||
if (status) {
|
||||
return (
|
||||
status.activeAppVersion ??
|
||||
status.activeVersion ??
|
||||
translate('auto.components.settings.managedServers.row.notRunning', 'Not running')
|
||||
t('auto.components.settings.managedServers.row.notRunning', 'Not running')
|
||||
)
|
||||
}
|
||||
return statusError
|
||||
? translate('auto.components.settings.managedServers.row.statusUnknown', 'Status unknown')
|
||||
: translate('auto.components.settings.managedServers.row.checking', 'Checking…')
|
||||
? t('auto.components.settings.managedServers.row.statusUnknown', 'Status unknown')
|
||||
: t('auto.components.settings.managedServers.row.checking', 'Checking…')
|
||||
}
|
||||
|
||||
export function getManagedServerSummary(lifecycle: ManagedServerLifecycle): string | null {
|
||||
export function getManagedServerSummary(
|
||||
lifecycle: ManagedServerLifecycle,
|
||||
t: Translate = translate
|
||||
): string | null {
|
||||
const { status, recovery } = lifecycle
|
||||
if (recovery) {
|
||||
return recoveryLabel(recovery)
|
||||
return recoveryLabel(recovery, t)
|
||||
}
|
||||
if (status?.migration) {
|
||||
return translate(
|
||||
'auto.components.settings.managedServers.row.migration',
|
||||
'Migration: {{phase}}',
|
||||
{
|
||||
phase: migrationPhaseLabel(status.migration.phase)
|
||||
}
|
||||
)
|
||||
return t('auto.components.settings.managedServers.row.migration', 'Migration: {{phase}}', {
|
||||
phase: migrationPhaseLabel(status.migration.phase, t)
|
||||
})
|
||||
}
|
||||
if (status?.deferredUpdate) {
|
||||
return translate(
|
||||
return t(
|
||||
'auto.components.settings.managedServers.row.deferredVersion',
|
||||
'Update to {{version}} deferred.',
|
||||
{ version: status.deferredUpdate.candidateVersion }
|
||||
@@ -59,40 +60,39 @@ export function ManagedServerDetailLines({
|
||||
}: {
|
||||
lifecycle: ManagedServerLifecycle
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const { status, statusError, recovery } = lifecycle
|
||||
return (
|
||||
<div className="space-y-0.5 text-xs text-muted-foreground">
|
||||
<p>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.managedServers.row.orcaVersion',
|
||||
'Orca on this host: {{version}}',
|
||||
{
|
||||
version: managedServerVersionLabel(status, statusError)
|
||||
version: managedServerVersionLabel(status, statusError, t)
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
{statusError ? <p className="text-destructive">{statusError}</p> : null}
|
||||
{status ? (
|
||||
<>
|
||||
<p>{terminalCensusLabel(status.terminals)}</p>
|
||||
{recovery ? <p>{recoveryLabel(recovery)}</p> : null}
|
||||
<p>{terminalCensusLabel(status.terminals, t)}</p>
|
||||
{recovery ? <p>{recoveryLabel(recovery, t)}</p> : null}
|
||||
{status.migration ? (
|
||||
<p>
|
||||
{translate(
|
||||
'auto.components.settings.managedServers.row.migration',
|
||||
'Migration: {{phase}}',
|
||||
{ phase: migrationPhaseLabel(status.migration.phase) }
|
||||
)}
|
||||
{t('auto.components.settings.managedServers.row.migration', 'Migration: {{phase}}', {
|
||||
phase: migrationPhaseLabel(status.migration.phase, t)
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
{status.deferredUpdate ? (
|
||||
<p>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.managedServers.row.deferredVersion',
|
||||
'Update to {{version}} deferred.',
|
||||
{ version: status.deferredUpdate.candidateVersion }
|
||||
)}{' '}
|
||||
{managedServerOutcomeLabel(status.deferredUpdate)}
|
||||
{managedServerOutcomeLabel(status.deferredUpdate, t)}
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
@@ -126,6 +126,7 @@ export function ManagedServerPrimaryAction({
|
||||
}: {
|
||||
lifecycle: ManagedServerLifecycle
|
||||
}): React.JSX.Element | null {
|
||||
const t = useTranslate()
|
||||
const { recovery, busy, run } = lifecycle
|
||||
if (!recovery && managedServerUpdateOffered(lifecycle)) {
|
||||
return (
|
||||
@@ -137,7 +138,7 @@ export function ManagedServerPrimaryAction({
|
||||
onClick={() => void run('update')}
|
||||
>
|
||||
{busy === 'update' || busy === 'forceUpdate' ? <LoadingSpinner className="size-3" /> : null}
|
||||
{translate('auto.components.settings.managedServers.row.update', 'Update')}
|
||||
{t('auto.components.settings.managedServers.row.update', 'Update')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -155,8 +156,8 @@ export function ManagedServerPrimaryAction({
|
||||
>
|
||||
{busy === 'recover' || busy === 'cancelStop' ? <LoadingSpinner className="size-3" /> : null}
|
||||
{cancelStop
|
||||
? translate('auto.components.settings.managedServers.row.cancelStop', 'Cancel stop')
|
||||
: translate('auto.components.settings.managedServers.row.recover', 'Recover')}
|
||||
? t('auto.components.settings.managedServers.row.cancelStop', 'Cancel stop')
|
||||
: t('auto.components.settings.managedServers.row.recover', 'Recover')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -223,18 +224,19 @@ export function ManagedServerConfirmations({
|
||||
lifecycle: ManagedServerLifecycle
|
||||
forgettable: boolean
|
||||
}): React.JSX.Element | null {
|
||||
const t = useTranslate()
|
||||
const { recovery, busy, run } = lifecycle
|
||||
const disabled = busy !== null
|
||||
const forceUpdate = lifecycle.confirmingForceUpdate
|
||||
if (forceUpdate) {
|
||||
return (
|
||||
<ConfirmStrip
|
||||
message={forceUpdateMessage()}
|
||||
confirmLabel={translate(
|
||||
message={forceUpdateMessage(t)}
|
||||
confirmLabel={t(
|
||||
'auto.components.settings.managedServers.row.updateAnyway',
|
||||
'Update anyway'
|
||||
)}
|
||||
keepLabel={translate('auto.components.settings.managedServers.row.notNow', 'Not now')}
|
||||
keepLabel={t('auto.components.settings.managedServers.row.notNow', 'Not now')}
|
||||
disabled={disabled}
|
||||
tone="default"
|
||||
onConfirm={() => void run('forceUpdate')}
|
||||
@@ -245,15 +247,15 @@ export function ManagedServerConfirmations({
|
||||
if (lifecycle.confirmingStop) {
|
||||
return (
|
||||
<ConfirmStrip
|
||||
message={translate(
|
||||
message={t(
|
||||
'auto.components.settings.managedServers.row.stopOrcaDescription',
|
||||
'Stops Orca on the SSH host and removes it from this list.'
|
||||
)}
|
||||
confirmLabel={translate(
|
||||
confirmLabel={t(
|
||||
'auto.components.settings.managedServers.row.confirmStop',
|
||||
'Stop and remove server'
|
||||
)}
|
||||
keepLabel={translate('auto.components.settings.managedServers.row.keep', 'Keep running')}
|
||||
keepLabel={t('auto.components.settings.managedServers.row.keep', 'Keep running')}
|
||||
disabled={disabled}
|
||||
onConfirm={() => {
|
||||
lifecycle.setConfirmingStop(false)
|
||||
@@ -267,18 +269,15 @@ export function ManagedServerConfirmations({
|
||||
if (forgettable && lifecycle.confirmingForget) {
|
||||
return (
|
||||
<ConfirmStrip
|
||||
message={translate(
|
||||
message={t(
|
||||
'auto.components.settings.managedServers.row.forgetDescription',
|
||||
'Unlinks this server here without contacting its host. If the host comes back, the server may still be running there.'
|
||||
)}
|
||||
confirmLabel={translate(
|
||||
confirmLabel={t(
|
||||
'auto.components.settings.managedServers.row.confirmForget',
|
||||
'Forget on this desktop'
|
||||
)}
|
||||
keepLabel={translate(
|
||||
'auto.components.settings.managedServers.row.keepLinked',
|
||||
'Keep linked'
|
||||
)}
|
||||
keepLabel={t('auto.components.settings.managedServers.row.keepLinked', 'Keep linked')}
|
||||
disabled={disabled}
|
||||
onConfirm={() => {
|
||||
lifecycle.setConfirmingForget(false)
|
||||
@@ -291,12 +290,12 @@ export function ManagedServerConfirmations({
|
||||
if (recovery && lifecycle.confirmingRestore) {
|
||||
return (
|
||||
<ConfirmStrip
|
||||
message={recoveryLabel(recovery)}
|
||||
confirmLabel={translate(
|
||||
message={recoveryLabel(recovery, t)}
|
||||
confirmLabel={t(
|
||||
'auto.components.settings.managedServers.row.confirmRestore',
|
||||
'Restore snapshot and restart'
|
||||
)}
|
||||
keepLabel={translate('auto.components.settings.managedServers.row.keepState', 'Keep state')}
|
||||
keepLabel={t('auto.components.settings.managedServers.row.keepState', 'Keep state')}
|
||||
disabled={disabled}
|
||||
onConfirm={() => {
|
||||
lifecycle.setConfirmingRestore(false)
|
||||
@@ -316,17 +315,18 @@ export function ManagedServerMenuItems({
|
||||
lifecycle: ManagedServerLifecycle
|
||||
forgettable: boolean
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const { status, recovery, busy, run } = lifecycle
|
||||
const disabled = busy !== null
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuItem disabled={disabled} onSelect={() => void lifecycle.loadStatus()}>
|
||||
<RefreshCw />
|
||||
{translate('auto.components.settings.managedServers.row.refresh', 'Refresh status')}
|
||||
{t('auto.components.settings.managedServers.row.refresh', 'Refresh status')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem disabled={disabled} onSelect={() => void run('update')}>
|
||||
<ArrowUpCircle />
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.managedServers.row.updateOrcaOnHost',
|
||||
'Update Orca on this host'
|
||||
)}
|
||||
@@ -334,14 +334,14 @@ export function ManagedServerMenuItems({
|
||||
{status?.rollbackAvailable ? (
|
||||
<DropdownMenuItem disabled={disabled} onSelect={() => void run('rollback')}>
|
||||
<RotateCcw />
|
||||
{translate('auto.components.settings.managedServers.row.rollback', 'Roll back')}
|
||||
{t('auto.components.settings.managedServers.row.rollback', 'Roll back')}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{/* Cancel stop owns the primary slot during decommission; keep Recover in the menu. */}
|
||||
{recovery?.operation === 'decommission' ? (
|
||||
<DropdownMenuItem disabled={disabled} onSelect={() => void run('recover')}>
|
||||
<Wrench />
|
||||
{translate('auto.components.settings.managedServers.row.recover', 'Recover')}
|
||||
{t('auto.components.settings.managedServers.row.recover', 'Recover')}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
<DropdownMenuSeparator />
|
||||
@@ -354,7 +354,7 @@ export function ManagedServerMenuItems({
|
||||
}}
|
||||
>
|
||||
<ServerOff />
|
||||
{translate('auto.components.settings.managedServers.row.stop', 'Stop…')}
|
||||
{t('auto.components.settings.managedServers.row.stop', 'Stop…')}
|
||||
</DropdownMenuItem>
|
||||
{forgettable ? (
|
||||
<DropdownMenuItem
|
||||
@@ -366,7 +366,7 @@ export function ManagedServerMenuItems({
|
||||
}}
|
||||
>
|
||||
<Trash2 />
|
||||
{translate('auto.components.settings.managedServers.row.forget', 'Forget…')}
|
||||
{t('auto.components.settings.managedServers.row.forget', 'Forget…')}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
+22
-25
@@ -3,7 +3,7 @@ import { toast } from 'sonner'
|
||||
import type { ManagedOrcadPreloadApi } from '../../../../../../preload/api/managed-orcad-api'
|
||||
import type { SshTarget } from '../../../../../../shared/ssh-types'
|
||||
import { useMountedRef } from '@/hooks/useMountedRef'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { useAppStore } from '@/store/index'
|
||||
import { LoadingSpinner } from '../../../LoadingSpinner'
|
||||
import { Button } from '../../../ui/button'
|
||||
@@ -37,6 +37,7 @@ export function RuntimeInstallServerDialog({
|
||||
onOpenChange,
|
||||
onDeployed
|
||||
}: RuntimeInstallServerDialogProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const mountedRef = useMountedRef()
|
||||
const openSettingsTarget = useAppStore((state) => state.openSettingsTarget)
|
||||
const [name, setName] = useState('')
|
||||
@@ -66,13 +67,9 @@ export function RuntimeInstallServerDialog({
|
||||
toast.message(managedServerOutcomeLabel(result))
|
||||
} else {
|
||||
toast.success(
|
||||
translate(
|
||||
'auto.components.settings.serverList.setUpDone',
|
||||
'Orca is set up on {{name}}.',
|
||||
{
|
||||
name: result.environment.name
|
||||
}
|
||||
)
|
||||
t('auto.components.settings.serverList.setUpDone', 'Orca is set up on {{name}}.', {
|
||||
name: result.environment.name
|
||||
})
|
||||
)
|
||||
const sameSession = session === sessionRef.current
|
||||
if (mountedRef.current && (!openRef.current || sameSession)) {
|
||||
@@ -96,7 +93,7 @@ export function RuntimeInstallServerDialog({
|
||||
const handleOpenChange = (next: boolean): void => {
|
||||
if (!next && deploying) {
|
||||
toast.message(
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.serverList.settingUpInBackground',
|
||||
'Setting up Orca on {{name}} in the background. It appears in the list when it’s ready.',
|
||||
{ name: serverName }
|
||||
@@ -121,7 +118,7 @@ export function RuntimeInstallServerDialog({
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.serverList.setUpOnSshHost',
|
||||
'Set up Orca on an SSH host'
|
||||
)}
|
||||
@@ -129,14 +126,14 @@ export function RuntimeInstallServerDialog({
|
||||
<DialogDescription asChild>
|
||||
<div className="space-y-2">
|
||||
<p>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.managedServers.description',
|
||||
'Orca installs itself on an empty SSH host and keeps itself up to date.'
|
||||
)}
|
||||
</p>
|
||||
{}
|
||||
<p>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.managedServers.reassurance',
|
||||
'Runs on each host, reached over your own SSH connection. Your code and files stay where they are. Only Orca’s list of projects, tabs and terminals moves to the host.'
|
||||
)}
|
||||
@@ -146,25 +143,25 @@ export function RuntimeInstallServerDialog({
|
||||
</DialogHeader>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={hostFieldId}>
|
||||
{translate('auto.components.settings.managedServers.deploy.host', 'SSH host')}
|
||||
{t('auto.components.settings.managedServers.deploy.host', 'SSH host')}
|
||||
</Label>
|
||||
<SshTargetSelect
|
||||
id={hostFieldId}
|
||||
targets={emptyTargets}
|
||||
value={targetId}
|
||||
onChange={setTargetId}
|
||||
placeholder={translate(
|
||||
placeholder={t(
|
||||
'auto.components.settings.serverList.installHostPlaceholder',
|
||||
'Choose an SSH host without Orca'
|
||||
)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{emptyTargets.length === 0
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.serverList.installNoHosts',
|
||||
'No SSH hosts without an Orca server.'
|
||||
)
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.serverList.installHostHelp',
|
||||
'Only hosts without an Orca server.'
|
||||
)}{' '}
|
||||
@@ -176,12 +173,12 @@ export function RuntimeInstallServerDialog({
|
||||
openSettingsTarget({ pane: 'ssh', repoId: null })
|
||||
}}
|
||||
>
|
||||
{translate('auto.components.settings.serverList.addSshHost', 'Add an SSH host')}
|
||||
{t('auto.components.settings.serverList.addSshHost', 'Add an SSH host')}
|
||||
</button>
|
||||
</p>
|
||||
{loadFailed ? (
|
||||
<p className="text-xs text-destructive">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.managedServers.loadFailed',
|
||||
'Orca couldn’t load SSH hosts and unfinished moves. Reopen Settings to try again.'
|
||||
)}
|
||||
@@ -190,9 +187,9 @@ export function RuntimeInstallServerDialog({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={nameFieldId}>
|
||||
{translate('auto.components.settings.managedServers.deploy.name', 'Server name')}{' '}
|
||||
{t('auto.components.settings.managedServers.deploy.name', 'Server name')}{' '}
|
||||
<span className="font-normal text-muted-foreground">
|
||||
{translate('auto.components.settings.RuntimeHostAccessForm.optional', '(optional)')}
|
||||
{t('auto.components.settings.RuntimeHostAccessForm.optional', '(optional)')}
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
@@ -200,7 +197,7 @@ export function RuntimeInstallServerDialog({
|
||||
value={name}
|
||||
placeholder={
|
||||
targetLabel ||
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.serverList.installNamePlaceholder',
|
||||
'Same as the SSH host'
|
||||
)
|
||||
@@ -211,20 +208,20 @@ export function RuntimeInstallServerDialog({
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="ghost" size="sm" onClick={() => handleOpenChange(false)}>
|
||||
{deploying
|
||||
? translate('auto.components.settings.serverList.close', 'Close')
|
||||
: translate('auto.components.settings.serverList.cancel', 'Cancel')}
|
||||
? t('auto.components.settings.serverList.close', 'Close')
|
||||
: t('auto.components.settings.serverList.cancel', 'Cancel')}
|
||||
</Button>
|
||||
<Button type="submit" size="sm" disabled={!canSubmit}>
|
||||
{deploying ? <LoadingSpinner className="size-3.5" /> : null}
|
||||
{deploying
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.sshHostServer.deploying',
|
||||
'Setting up Orca on {{host}}…',
|
||||
{
|
||||
host: targetLabel
|
||||
}
|
||||
)
|
||||
: translate('auto.components.settings.serverList.setUp', 'Set up')}
|
||||
: t('auto.components.settings.serverList.setUp', 'Set up')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
+29
-23
@@ -2,10 +2,11 @@ import { Loader2 } from 'lucide-react'
|
||||
import type { RuntimeAccessGrant } from '../../../../../../shared/runtime-access-grants'
|
||||
import { Button } from '../../../ui/button'
|
||||
import { getIntlLocale, translate } from '@/i18n/i18n'
|
||||
import { useIntlLocale, useTranslate, type Translate } from '@/i18n/use-translate'
|
||||
import { LegacyEncryptionNotice } from '../../legacy-encryption-notice'
|
||||
|
||||
function formatAccessTimestamp(timestamp: number): string {
|
||||
return new Intl.DateTimeFormat(getIntlLocale(), {
|
||||
function formatAccessTimestamp(timestamp: number, intlLocale: string = getIntlLocale()): string {
|
||||
return new Intl.DateTimeFormat(intlLocale, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
@@ -14,10 +15,14 @@ function formatAccessTimestamp(timestamp: number): string {
|
||||
}
|
||||
|
||||
// Why: main names each link "Runtime <date>"; show it as a link, not a runtime.
|
||||
function grantDisplayName(grant: RuntimeAccessGrant): string {
|
||||
function grantDisplayName(
|
||||
grant: RuntimeAccessGrant,
|
||||
t: Translate = translate,
|
||||
intlLocale: string = getIntlLocale()
|
||||
): string {
|
||||
return /^Runtime \d/.test(grant.name)
|
||||
? translate('auto.components.settings.RuntimeAccessGrantList.linkName', 'Link · {{value0}}', {
|
||||
value0: new Intl.DateTimeFormat(getIntlLocale(), { month: 'short', day: 'numeric' }).format(
|
||||
? t('auto.components.settings.RuntimeAccessGrantList.linkName', 'Link · {{value0}}', {
|
||||
value0: new Intl.DateTimeFormat(intlLocale, { month: 'short', day: 'numeric' }).format(
|
||||
new Date(grant.createdAt)
|
||||
)
|
||||
})
|
||||
@@ -41,20 +46,19 @@ export function RuntimeAccessGrantList({
|
||||
revokingGrantId,
|
||||
onRevoke
|
||||
}: RuntimeAccessGrantListProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const intlLocale = useIntlLocale()
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<h3 className="text-sm font-medium">
|
||||
{translate('auto.components.settings.RuntimeAccessGrantList.activeLinks', 'Active links')}
|
||||
{t('auto.components.settings.RuntimeAccessGrantList.activeLinks', 'Active links')}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{grants.length === 0 && isLoading ? null : grants.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeAccessGrantList.noActiveLinks',
|
||||
'No active links.'
|
||||
)}
|
||||
{t('auto.components.settings.RuntimeAccessGrantList.noActiveLinks', 'No active links.')}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
@@ -68,10 +72,12 @@ export function RuntimeAccessGrantList({
|
||||
>
|
||||
<div className="min-w-0 space-y-0.5">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate text-sm font-medium">{grantDisplayName(grant)}</span>
|
||||
<span className="truncate text-sm font-medium">
|
||||
{grantDisplayName(grant, t, intlLocale)}
|
||||
</span>
|
||||
{isCurrent ? (
|
||||
<span className="text-muted-foreground shrink-0 text-xs">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RuntimeAccessGrantList.434e4a6af6',
|
||||
'Current link'
|
||||
)}
|
||||
@@ -79,33 +85,33 @@ export function RuntimeAccessGrantList({
|
||||
) : null}
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RuntimeAccessGrantList.createdAt',
|
||||
'Created {{value0}}',
|
||||
{ value0: formatAccessTimestamp(grant.createdAt) }
|
||||
{ value0: formatAccessTimestamp(grant.createdAt, intlLocale) }
|
||||
)}
|
||||
{' · '}
|
||||
{grant.lastSeenAt
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.RuntimeAccessGrantList.b18d1764ef',
|
||||
'Last used {{value0}}',
|
||||
{ value0: formatAccessTimestamp(grant.lastSeenAt) }
|
||||
{ value0: formatAccessTimestamp(grant.lastSeenAt, intlLocale) }
|
||||
)
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.RuntimeAccessGrantList.df142657a5',
|
||||
'Not used yet'
|
||||
)}
|
||||
</div>
|
||||
{grant.usesLegacyEncryption ? (
|
||||
<LegacyEncryptionNotice
|
||||
message={translate(
|
||||
message={t(
|
||||
'auto.components.settings.RuntimeAccessGrantList.legacyEncryption',
|
||||
'Last used with older encryption. Update Orca where this link is used.'
|
||||
)}
|
||||
/>
|
||||
) : grant.olderPairingMethod ? (
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RuntimeAccessGrantList.olderPairingMethod',
|
||||
'Uses an older pairing method. Update Orca on the device that uses this link.'
|
||||
)}
|
||||
@@ -118,14 +124,14 @@ export function RuntimeAccessGrantList({
|
||||
size="xs"
|
||||
onClick={() => onRevoke(grant)}
|
||||
disabled={isRevoking}
|
||||
aria-label={translate(
|
||||
aria-label={t(
|
||||
'auto.components.settings.RuntimeAccessGrantList.6f6d5188ed',
|
||||
'Revoke {{value0}}',
|
||||
{ value0: grantDisplayName(grant) }
|
||||
{ value0: grantDisplayName(grant, t, intlLocale) }
|
||||
)}
|
||||
>
|
||||
{isRevoking ? <Loader2 className="animate-spin" /> : null}
|
||||
{translate('auto.components.settings.RuntimeAccessGrantList.revoke', 'Revoke')}
|
||||
{t('auto.components.settings.RuntimeAccessGrantList.revoke', 'Revoke')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
@@ -134,7 +140,7 @@ export function RuntimeAccessGrantList({
|
||||
)}
|
||||
{grants.length > 0 ? (
|
||||
<p className="text-muted-foreground mt-3 text-xs">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RuntimeAccessGrantList.sshAccessCaveat',
|
||||
"Revoking here doesn't stop someone with SSH access to this computer from pairing again. To cut them off, remove their SSH access."
|
||||
)}
|
||||
|
||||
+3
-2
@@ -1,7 +1,7 @@
|
||||
import { Check, Copy } from 'lucide-react'
|
||||
import { Button } from '../../../ui/button'
|
||||
import { Label } from '../../../ui/label'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
export function GeneratedUrlRow({
|
||||
label,
|
||||
@@ -16,6 +16,7 @@ export function GeneratedUrlRow({
|
||||
copied: boolean
|
||||
onCopy: () => void
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<Label>{label}</Label>
|
||||
@@ -29,7 +30,7 @@ export function GeneratedUrlRow({
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={onCopy}
|
||||
aria-label={translate(
|
||||
aria-label={t(
|
||||
'auto.components.settings.RuntimePairingGeneratedUrlRows.0495f68959',
|
||||
'Copy {{value0}}',
|
||||
{ value0: label }
|
||||
|
||||
+42
-52
@@ -10,7 +10,7 @@ import { parseServerShareAddress } from '../../../../../../shared/network/server
|
||||
import { GeneratedUrlRow, UnavailableUrlRow } from './RuntimePairingGeneratedUrlRows'
|
||||
import type { RuntimePairingIntent } from './runtime-pairing-link-state'
|
||||
import type { RuntimePairingLinkExpiry } from './use-runtime-pairing-link-expiry'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useIntlLocale, useTranslate } from '@/i18n/use-translate'
|
||||
import { uiDateTimeFormat } from '@/i18n/ui-intl-formatters'
|
||||
|
||||
type RuntimePairingGeneratorFormProps = {
|
||||
@@ -50,40 +50,39 @@ export function RuntimePairingGeneratorForm({
|
||||
onGenerate,
|
||||
onCopy
|
||||
}: RuntimePairingGeneratorFormProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const intlLocale = useIntlLocale()
|
||||
const options: AddressOption[] = networkInterfaces.map((networkInterface) => ({
|
||||
value: networkInterface.address,
|
||||
label: describeNetworkAddress(networkInterface.name, networkInterface.address)
|
||||
label: describeNetworkAddress(networkInterface.name, networkInterface.address, t)
|
||||
}))
|
||||
const intentOptions: { value: RuntimePairingIntent; label: string; description: string }[] = [
|
||||
{
|
||||
value: 'another',
|
||||
label: translate(
|
||||
label: t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.anotherDevice',
|
||||
'Another device'
|
||||
),
|
||||
description: translate(
|
||||
description: t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.anotherDeviceHelp',
|
||||
'Tailscale, LAN, or another reachable address'
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'local',
|
||||
label: translate(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.thisComputer',
|
||||
'This computer'
|
||||
),
|
||||
description: translate(
|
||||
label: t('auto.components.settings.RuntimePairingUrlGenerator.thisComputer', 'This computer'),
|
||||
description: t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.localOnlyHelp',
|
||||
'A browser or Orca client on this computer'
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'custom',
|
||||
label: translate(
|
||||
label: t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.customAddress',
|
||||
'Custom address'
|
||||
),
|
||||
description: translate(
|
||||
description: t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.customAddressHelp',
|
||||
'SSH tunnel, reverse proxy, or custom hostname'
|
||||
)
|
||||
@@ -103,7 +102,7 @@ export function RuntimePairingGeneratorForm({
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<div id="runtime-pairing-intent-label" className="text-sm font-medium">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.whoOpens',
|
||||
'Who will open the link'
|
||||
)}
|
||||
@@ -137,13 +136,13 @@ export function RuntimePairingGeneratorForm({
|
||||
{intent === 'local' ? (
|
||||
<div className="rounded-md border border-border/60 bg-muted/30 p-3 text-xs">
|
||||
<div className="font-medium">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.localLink',
|
||||
'Local-only link'
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.localLinkHelp',
|
||||
'This link only works in a browser or Orca client running on this computer.'
|
||||
)}
|
||||
@@ -153,7 +152,7 @@ export function RuntimePairingGeneratorForm({
|
||||
) : intent === 'custom' ? (
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="runtime-pairing-custom-address">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.custom-title',
|
||||
'Custom connection address'
|
||||
)}
|
||||
@@ -162,7 +161,7 @@ export function RuntimePairingGeneratorForm({
|
||||
id="runtime-pairing-custom-address"
|
||||
value={selectedAddress}
|
||||
onChange={(event) => onSelectedAddressChange(event.target.value)}
|
||||
placeholder={translate(
|
||||
placeholder={t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.45cf476df3',
|
||||
'host, host:port, or wss://host/path'
|
||||
)}
|
||||
@@ -178,11 +177,11 @@ export function RuntimePairingGeneratorForm({
|
||||
}
|
||||
>
|
||||
{customAddressInvalid
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.customInvalid',
|
||||
'Enter a valid host, host:port, IPv6 address, or ws(s):// URL.'
|
||||
)
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.custom-hint',
|
||||
'Enter a host, host:port, or a ws(s):// URL.'
|
||||
)}
|
||||
@@ -191,17 +190,14 @@ export function RuntimePairingGeneratorForm({
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
<Label id="runtime-pairing-address-label" htmlFor="runtime-pairing-address">
|
||||
{translate(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.addressLabel',
|
||||
'Address'
|
||||
)}
|
||||
{t('auto.components.settings.RuntimePairingUrlGenerator.addressLabel', 'Address')}
|
||||
</Label>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<AddressPicker
|
||||
id="runtime-pairing-address"
|
||||
// Bound short labels without stretching the card; let longer values grow before truncating.
|
||||
className="min-w-[240px] max-w-full"
|
||||
triggerAriaLabel={translate(
|
||||
triggerAriaLabel={t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.de77eb1b65',
|
||||
'Connection address'
|
||||
)}
|
||||
@@ -211,43 +207,43 @@ export function RuntimePairingGeneratorForm({
|
||||
placeholder=""
|
||||
customInputId="runtime-pairing-custom-address"
|
||||
formatCustomLabel={(address) =>
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.custom-option',
|
||||
'{{address}} (custom)',
|
||||
{ address }
|
||||
)
|
||||
}
|
||||
addCustomLabel={translate(
|
||||
addCustomLabel={t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.add-custom',
|
||||
'Use custom address…'
|
||||
)}
|
||||
validateCustom={parseServerShareAddress}
|
||||
customDialogCopy={{
|
||||
title: translate(
|
||||
title: t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.custom-title',
|
||||
'Custom connection address'
|
||||
),
|
||||
description: translate(
|
||||
description: t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.custom-description',
|
||||
'Advertise an address another device can reach — a LAN or Tailscale host, or a full ws(s):// URL.'
|
||||
),
|
||||
inputLabel: translate(
|
||||
inputLabel: t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.4531ea3158',
|
||||
'Custom address'
|
||||
),
|
||||
placeholder: translate(
|
||||
placeholder: t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.45cf476df3',
|
||||
'host, host:port, or wss://host/path'
|
||||
),
|
||||
hint: translate(
|
||||
hint: t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.custom-hint',
|
||||
'Enter a host, host:port, or a ws(s):// URL.'
|
||||
),
|
||||
cancel: translate(
|
||||
cancel: t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.custom-cancel',
|
||||
'Cancel'
|
||||
),
|
||||
confirm: translate(
|
||||
confirm: t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.custom-use',
|
||||
'Use address'
|
||||
)
|
||||
@@ -262,7 +258,7 @@ export function RuntimePairingGeneratorForm({
|
||||
size="icon-sm"
|
||||
onClick={onRefreshNetworkInterfaces}
|
||||
disabled={refreshingNetworkInterfaces}
|
||||
aria-label={translate(
|
||||
aria-label={t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.360c548cf3',
|
||||
'Refresh connection addresses'
|
||||
)}
|
||||
@@ -272,7 +268,7 @@ export function RuntimePairingGeneratorForm({
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" sideOffset={6}>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.360c548cf3',
|
||||
'Refresh connection addresses'
|
||||
)}
|
||||
@@ -283,7 +279,7 @@ export function RuntimePairingGeneratorForm({
|
||||
networkInterfaces.length === 0 &&
|
||||
!refreshingNetworkInterfaces ? (
|
||||
<p role="alert" className="text-xs text-destructive">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.noExternalAddress',
|
||||
'No address for another device was found. Connect this computer to a LAN or Tailscale, refresh, or choose Custom address.'
|
||||
)}
|
||||
@@ -293,7 +289,7 @@ export function RuntimePairingGeneratorForm({
|
||||
)}
|
||||
{staleGeneratedLink && selectedAddress !== '' ? (
|
||||
<div className="rounded-md border border-border bg-muted/30 px-3 py-2 text-xs">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.staleAddressCreate',
|
||||
'The connection address changed. Create a new link for {{address}}.',
|
||||
{ address: selectedAddress }
|
||||
@@ -309,21 +305,18 @@ export function RuntimePairingGeneratorForm({
|
||||
disabled={isGeneratingPairing || !canGenerate}
|
||||
>
|
||||
{isGeneratingPairing ? <Loader2 className="animate-spin" /> : <Link2 />}
|
||||
{translate(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.createLink',
|
||||
'Create link'
|
||||
)}
|
||||
{t('auto.components.settings.RuntimePairingUrlGenerator.createLink', 'Create link')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showLink && webClientUrl ? (
|
||||
<GeneratedUrlRow
|
||||
label={translate(
|
||||
label={t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.browserLinkLabel',
|
||||
'Browser link'
|
||||
)}
|
||||
description={translate(
|
||||
description={t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.1ca2e5194d',
|
||||
'Use this URL from a browser that can reach the selected address.'
|
||||
)}
|
||||
@@ -333,11 +326,11 @@ export function RuntimePairingGeneratorForm({
|
||||
/>
|
||||
) : showLink && runtimePairingUrl ? (
|
||||
<UnavailableUrlRow
|
||||
label={translate(
|
||||
label={t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.browserLinkLabel',
|
||||
'Browser link'
|
||||
)}
|
||||
description={translate(
|
||||
description={t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.f7cafdc9f3',
|
||||
'Browser link unavailable in this build. The pairing URL still works for Orca clients.'
|
||||
)}
|
||||
@@ -346,11 +339,11 @@ export function RuntimePairingGeneratorForm({
|
||||
|
||||
{showLink && runtimePairingUrl ? (
|
||||
<GeneratedUrlRow
|
||||
label={translate(
|
||||
label={t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.accessLinkLabel',
|
||||
'Access link'
|
||||
)}
|
||||
description={translate(
|
||||
description={t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.accessLinkHelp',
|
||||
'Paste this into Add server on the other Orca.'
|
||||
)}
|
||||
@@ -363,15 +356,12 @@ export function RuntimePairingGeneratorForm({
|
||||
{generatedIsCurrent && runtimePairingUrl && linkExpiry ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{linkExpiry.kind === 'expired'
|
||||
? translate(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.linkExpired',
|
||||
'Link expired'
|
||||
)
|
||||
: translate(
|
||||
? t('auto.components.settings.RuntimePairingUrlGenerator.linkExpired', 'Link expired')
|
||||
: t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.unusedLinkExpires',
|
||||
'Unused link expires at {{time}}',
|
||||
{
|
||||
time: uiDateTimeFormat({ hour: 'numeric', minute: '2-digit' }).format(
|
||||
time: uiDateTimeFormat({ hour: 'numeric', minute: '2-digit' }, intlLocale).format(
|
||||
linkExpiry.at
|
||||
)
|
||||
}
|
||||
|
||||
+11
-9
@@ -5,6 +5,7 @@ import type { RuntimeAccessGrant } from '../../../../../../shared/runtime-access
|
||||
import { Label } from '../../../ui/label'
|
||||
import { RuntimeAccessGrantList } from './RuntimeAccessGrantList'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { RuntimePairingGeneratorForm } from './RuntimePairingGeneratorForm'
|
||||
import {
|
||||
RUNTIME_PAIRING_LOOPBACK_ADDRESS,
|
||||
@@ -24,6 +25,7 @@ export function RuntimePairingUrlGenerator({
|
||||
showHeader = true,
|
||||
showGeneratorForm = true
|
||||
}: RuntimePairingUrlGeneratorProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const [networkInterfaces, setNetworkInterfaces] = useState<{ name: string; address: string }[]>(
|
||||
[]
|
||||
)
|
||||
@@ -182,7 +184,7 @@ export function RuntimePairingUrlGenerator({
|
||||
// Prefer specific exposure guidance after widening failure, with generic fallback for other unavailable cases (STA-2370).
|
||||
toast.error(
|
||||
result.guidance ??
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.2752126f3e',
|
||||
'Runtime pairing is unavailable.'
|
||||
)
|
||||
@@ -208,11 +210,11 @@ export function RuntimePairingUrlGenerator({
|
||||
if (mountedRef.current) {
|
||||
toast.success(
|
||||
result.webClientUrl
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.6dd594a507',
|
||||
'Generated web client URL.'
|
||||
)
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.11d5248e62',
|
||||
'Generated pairing URL.'
|
||||
)
|
||||
@@ -223,7 +225,7 @@ export function RuntimePairingUrlGenerator({
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.2ed55c841a',
|
||||
'Failed to generate pairing URL.'
|
||||
)
|
||||
@@ -243,7 +245,7 @@ export function RuntimePairingUrlGenerator({
|
||||
if (!result.revoked) {
|
||||
if (mountedRef.current) {
|
||||
toast.error(
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.d797f516b1',
|
||||
'Shared access was already revoked.'
|
||||
)
|
||||
@@ -262,7 +264,7 @@ export function RuntimePairingUrlGenerator({
|
||||
}
|
||||
if (mountedRef.current) {
|
||||
toast.success(
|
||||
translate(
|
||||
t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.9f8e037c4a',
|
||||
'Shared access revoked.'
|
||||
)
|
||||
@@ -273,7 +275,7 @@ export function RuntimePairingUrlGenerator({
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.e8d83f2b0f',
|
||||
'Failed to revoke shared access.'
|
||||
)
|
||||
@@ -329,13 +331,13 @@ export function RuntimePairingUrlGenerator({
|
||||
{showHeader ? (
|
||||
<div className="space-y-1">
|
||||
<Label id="runtime-share-server-label">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.f8500e134a',
|
||||
'Share this Orca server'
|
||||
)}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.ff80904fc4',
|
||||
'Create a revocable access grant for browser or desktop clients.'
|
||||
)}
|
||||
|
||||
+15
-22
@@ -1,33 +1,26 @@
|
||||
import { classifyRemotePairingHostname } from '../../../../../../shared/remote-runtime/pairing/remote-pairing-address'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { Translate } from '@/i18n/use-translate'
|
||||
|
||||
/** "Tailscale · 100.x.y.z" instead of the interface name ("utun6"), which means nothing to most people. */
|
||||
export function describeNetworkAddress(interfaceName: string, address: string): string {
|
||||
export function describeNetworkAddress(
|
||||
interfaceName: string,
|
||||
address: string,
|
||||
t: Translate = translate
|
||||
): string {
|
||||
switch (classifyRemotePairingHostname(address)) {
|
||||
case 'tailscale':
|
||||
return translate(
|
||||
'auto.components.settings.networkAddress.tailscale',
|
||||
'Tailscale · {{address}}',
|
||||
{
|
||||
address
|
||||
}
|
||||
)
|
||||
return t('auto.components.settings.networkAddress.tailscale', 'Tailscale · {{address}}', {
|
||||
address
|
||||
})
|
||||
case 'lan':
|
||||
return translate(
|
||||
'auto.components.settings.networkAddress.lan',
|
||||
'Local network · {{address}}',
|
||||
{
|
||||
address
|
||||
}
|
||||
)
|
||||
return t('auto.components.settings.networkAddress.lan', 'Local network · {{address}}', {
|
||||
address
|
||||
})
|
||||
case 'loopback':
|
||||
return translate(
|
||||
'auto.components.settings.networkAddress.loopback',
|
||||
'This computer · {{address}}',
|
||||
{
|
||||
address
|
||||
}
|
||||
)
|
||||
return t('auto.components.settings.networkAddress.loopback', 'This computer · {{address}}', {
|
||||
address
|
||||
})
|
||||
case 'public':
|
||||
case 'custom':
|
||||
return `${interfaceName} · ${address}`
|
||||
|
||||
+4
-3
@@ -1,4 +1,4 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { useAppStore } from '@/store/index'
|
||||
import {
|
||||
Dialog,
|
||||
@@ -33,6 +33,7 @@ export function RuntimeAddServerDialog({
|
||||
onClose,
|
||||
onSubmit
|
||||
}: RuntimeAddServerDialogProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const takenNames = useAppStore((state) => state.runtimeEnvironments)
|
||||
return (
|
||||
<Dialog
|
||||
@@ -46,10 +47,10 @@ export function RuntimeAddServerDialog({
|
||||
<DialogContent scrollable>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{translate('auto.components.settings.serverList.addTitle', 'Add server')}
|
||||
{t('auto.components.settings.serverList.addTitle', 'Add server')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.serverList.addDescriptionShort',
|
||||
'Paste the access link from Share this computer on the other machine.'
|
||||
)}
|
||||
|
||||
+16
-21
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Share2 } from 'lucide-react'
|
||||
import { useMountedRef } from '@/hooks/useMountedRef'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { useAppStore } from '@/store/index'
|
||||
import { Button } from '../../../ui/button'
|
||||
import {
|
||||
@@ -18,6 +18,7 @@ import { usePublishedMachineName } from '../../../machine-identity/use-published
|
||||
|
||||
/** Desktop only: lets other devices pair with this computer. */
|
||||
export function RuntimeShareComputerButton(): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [editingName, setEditingName] = useState(false)
|
||||
const [activeLinkCount, setActiveLinkCount] = useState<number | null>(null)
|
||||
@@ -51,16 +52,16 @@ export function RuntimeShareComputerButton(): React.JSX.Element {
|
||||
<>
|
||||
<Button type="button" size="sm" onClick={() => setOpen(true)}>
|
||||
<Share2 />
|
||||
{translate('auto.components.settings.serverList.shareTitle', 'Share this computer')}
|
||||
{t('auto.components.settings.serverList.shareTitle', 'Share this computer')}
|
||||
</Button>
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="sm:max-w-xl" scrollable>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{translate('auto.components.settings.serverList.shareTitle', 'Share this computer')}
|
||||
{t('auto.components.settings.serverList.shareTitle', 'Share this computer')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.serverList.shareDialogDescriptionRuns',
|
||||
'Anyone with the link can run commands as you on this computer until you revoke it.'
|
||||
)}
|
||||
@@ -69,12 +70,10 @@ export function RuntimeShareComputerButton(): React.JSX.Element {
|
||||
{activeLinkCount ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{activeLinkCount === 1
|
||||
? translate('auto.components.settings.serverList.oneActiveLink', '1 active link')
|
||||
: translate(
|
||||
'auto.components.settings.serverList.activeLinks',
|
||||
'{{value0}} active links',
|
||||
{ value0: activeLinkCount }
|
||||
)}
|
||||
? t('auto.components.settings.serverList.oneActiveLink', '1 active link')
|
||||
: t('auto.components.settings.serverList.activeLinks', '{{value0}} active links', {
|
||||
value0: activeLinkCount
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
<RuntimePairingUrlGenerator framed={false} showHeader={false} />
|
||||
@@ -83,15 +82,11 @@ export function RuntimeShareComputerButton(): React.JSX.Element {
|
||||
) : (
|
||||
<div className="flex items-center gap-2 border-t border-border pt-3 text-xs text-muted-foreground">
|
||||
<span className="min-w-0 truncate">
|
||||
{translate(
|
||||
'auto.components.settings.serverList.shownAs',
|
||||
'Shown to others as {{name}}',
|
||||
{
|
||||
name:
|
||||
publishedMachineName ??
|
||||
translate('auto.components.settings.serverList.thisComputer', 'This computer')
|
||||
}
|
||||
)}
|
||||
{t('auto.components.settings.serverList.shownAs', 'Shown to others as {{name}}', {
|
||||
name:
|
||||
publishedMachineName ??
|
||||
t('auto.components.settings.serverList.thisComputer', 'This computer')
|
||||
})}
|
||||
</span>
|
||||
<span aria-hidden>·</span>
|
||||
<button
|
||||
@@ -99,7 +94,7 @@ export function RuntimeShareComputerButton(): React.JSX.Element {
|
||||
className="shrink-0 cursor-pointer rounded-sm text-foreground underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
onClick={() => setEditingName(true)}
|
||||
>
|
||||
{translate('auto.components.settings.serverList.editName', 'Edit')}
|
||||
{t('auto.components.settings.serverList.editName', 'Edit')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -110,7 +105,7 @@ export function RuntimeShareComputerButton(): React.JSX.Element {
|
||||
size="sm"
|
||||
onClick={() => handleOpenChange(false)}
|
||||
>
|
||||
{translate('auto.components.settings.serverList.done', 'Done')}
|
||||
{t('auto.components.settings.serverList.done', 'Done')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
+5
-4
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
type CopyTarget = 'web' | 'pairing'
|
||||
|
||||
@@ -10,6 +10,7 @@ export function useRuntimePairingUrlCopy(mountedRef: { current: boolean }): {
|
||||
copyGeneratedUrl: (target: CopyTarget, value: string) => Promise<void>
|
||||
setContainerNode: (node: HTMLDivElement | null) => void
|
||||
} {
|
||||
const t = useTranslate()
|
||||
const [copiedTarget, setCopiedTarget] = useState<CopyTarget | null>(null)
|
||||
const copiedTargetResetTimerRef = useRef<number | null>(null)
|
||||
|
||||
@@ -45,11 +46,11 @@ export function useRuntimePairingUrlCopy(mountedRef: { current: boolean }): {
|
||||
}, 1400)
|
||||
toast.success(
|
||||
target === 'web'
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.13704d635e',
|
||||
'Copied web client URL.'
|
||||
)
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.df0aa45a86',
|
||||
'Copied pairing URL.'
|
||||
)
|
||||
@@ -60,7 +61,7 @@ export function useRuntimePairingUrlCopy(mountedRef: { current: boolean }): {
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: translate(
|
||||
: t(
|
||||
'auto.components.settings.RuntimePairingUrlGenerator.d6c081adf4',
|
||||
'Failed to copy URL.'
|
||||
)
|
||||
|
||||
+12
-14
@@ -1,5 +1,5 @@
|
||||
import { ArrowUpRight, ChevronDown, Cloud, Link2, Plus, TerminalSquare } from 'lucide-react'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { useAppStore } from '@/store/index'
|
||||
import { LoadingSpinner } from '../../LoadingSpinner'
|
||||
import { Button } from '../../ui/button'
|
||||
@@ -46,6 +46,7 @@ export function RuntimeServersHeaderActions({
|
||||
}: {
|
||||
canGeneratePairingUrl: boolean
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const { setDialog } = useRuntimeServersPage()
|
||||
const remoteServerUpdates = useAppStore((state) => state.remoteServerUpdates)
|
||||
const remoteServerUpdatesChecking = useAppStore((state) => state.remoteServerUpdatesChecking)
|
||||
@@ -75,11 +76,11 @@ export function RuntimeServersHeaderActions({
|
||||
>
|
||||
{remoteServerUpdatesRunning ? <LoadingSpinner className="size-3.5" /> : null}
|
||||
{remoteServerUpdatesRunning
|
||||
? translate(
|
||||
? t(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.updatingServers',
|
||||
'Updating servers…'
|
||||
)
|
||||
: translate('auto.components.settings.serverList.updateAll', 'Update all ({{count}})', {
|
||||
: t('auto.components.settings.serverList.updateAll', 'Update all ({{count}})', {
|
||||
count: availableCount
|
||||
})}
|
||||
</Button>
|
||||
@@ -87,7 +88,7 @@ export function RuntimeServersHeaderActions({
|
||||
<ButtonGroup>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => setDialog('add')}>
|
||||
<Plus />
|
||||
{translate('auto.components.settings.serverList.addServer', 'Add server')}
|
||||
{t('auto.components.settings.serverList.addServer', 'Add server')}
|
||||
</Button>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -95,7 +96,7 @@ export function RuntimeServersHeaderActions({
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
aria-label={translate(
|
||||
aria-label={t(
|
||||
'auto.components.settings.serverList.moreWaysToAdd',
|
||||
'More ways to add a server'
|
||||
)}
|
||||
@@ -106,11 +107,8 @@ export function RuntimeServersHeaderActions({
|
||||
<DropdownMenuContent align="end" className="w-80">
|
||||
<AddServerMenuItem
|
||||
icon={<Link2 />}
|
||||
label={translate(
|
||||
'auto.components.settings.serverList.pasteAccessLink',
|
||||
'Paste access link'
|
||||
)}
|
||||
description={translate(
|
||||
label={t('auto.components.settings.serverList.pasteAccessLink', 'Paste access link')}
|
||||
description={t(
|
||||
'auto.components.settings.serverList.pasteAccessLinkHelp',
|
||||
'Pair with Orca already running on another machine.'
|
||||
)}
|
||||
@@ -119,11 +117,11 @@ export function RuntimeServersHeaderActions({
|
||||
{canInstall ? (
|
||||
<AddServerMenuItem
|
||||
icon={<TerminalSquare />}
|
||||
label={translate(
|
||||
label={t(
|
||||
'auto.components.settings.serverList.setUpOnSshHost',
|
||||
'Set up Orca on an SSH host'
|
||||
)}
|
||||
description={translate(
|
||||
description={t(
|
||||
'auto.components.settings.managedServers.description',
|
||||
'Orca installs itself on an empty SSH host and keeps itself up to date.'
|
||||
)}
|
||||
@@ -132,9 +130,9 @@ export function RuntimeServersHeaderActions({
|
||||
) : null}
|
||||
<AddServerMenuItem
|
||||
icon={<Cloud />}
|
||||
label={translate('auto.components.settings.serverList.cloudVm', 'Cloud VM')}
|
||||
label={t('auto.components.settings.serverList.cloudVm', 'Cloud VM')}
|
||||
trailing={<ArrowUpRight className="ml-auto" />}
|
||||
description={translate(
|
||||
description={t(
|
||||
'auto.components.settings.serverList.cloudVmHelp',
|
||||
'Created per workspace from an environment recipe.'
|
||||
)}
|
||||
|
||||
+7
-8
@@ -1,6 +1,6 @@
|
||||
import type React from 'react'
|
||||
import { MoreHorizontal, Server } from 'lucide-react'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { LoadingSpinner } from '../../../LoadingSpinner'
|
||||
import { Button } from '../../../ui/button'
|
||||
@@ -79,6 +79,7 @@ export function ServerListRow({
|
||||
menuDisabled = false,
|
||||
footer
|
||||
}: ServerListRowProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<div data-settings-section={sectionId} className="scroll-mt-8">
|
||||
<div className="flex min-h-14 items-center gap-3 px-4 py-2.5">
|
||||
@@ -114,7 +115,7 @@ export function ServerListRow({
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
disabled={menuDisabled}
|
||||
aria-label={translate(
|
||||
aria-label={t(
|
||||
'auto.components.settings.serverList.moreActions',
|
||||
'More actions for {{name}}',
|
||||
{ name }
|
||||
@@ -125,10 +126,7 @@ export function ServerListRow({
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={4}>
|
||||
{translate(
|
||||
'auto.components.settings.serverList.moreActionsTooltip',
|
||||
'More actions'
|
||||
)}
|
||||
{t('auto.components.settings.serverList.moreActionsTooltip', 'More actions')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent align="end" className="min-w-52">
|
||||
@@ -150,6 +148,7 @@ export function ServerRowDetailsToggle({
|
||||
expanded: boolean
|
||||
onToggle: () => void
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
@@ -158,8 +157,8 @@ export function ServerRowDetailsToggle({
|
||||
onClick={onToggle}
|
||||
>
|
||||
{expanded
|
||||
? translate('auto.components.settings.serverList.hideDetails', 'Hide details')
|
||||
: translate('auto.components.settings.serverList.details', 'Details')}
|
||||
? t('auto.components.settings.serverList.hideDetails', 'Hide details')
|
||||
: t('auto.components.settings.serverList.details', 'Details')}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
+7
-6
@@ -7,7 +7,7 @@ import {
|
||||
import type { ManagedOrcadPreloadApi } from '../../../../../../preload/api/managed-orcad-api'
|
||||
import type { RemoteServerUpdateEntry } from '@/runtime/hosts/updates/remote-server-update-coordinator'
|
||||
import type { UpdateCheckOptions } from '../../../../../../shared/update-status-types'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { duplicatePairedServerNames } from '@/lib/duplicate-paired-servers'
|
||||
import { LoadingSpinner } from '../../../LoadingSpinner'
|
||||
import { Button } from '../../../ui/button'
|
||||
@@ -77,6 +77,7 @@ export function RuntimeServerList({
|
||||
rechecking,
|
||||
onChanged
|
||||
}: RuntimeServerListProps): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const { api, pending, targets, resumingId, resume, reload } = managedCatalog
|
||||
|
||||
if (environments.length === 0 && pending.length === 0) {
|
||||
@@ -87,10 +88,10 @@ export function RuntimeServerList({
|
||||
</span>
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm font-medium">
|
||||
{translate('auto.components.settings.serverList.noSavedServers', 'No saved servers')}
|
||||
{t('auto.components.settings.serverList.noSavedServers', 'No saved servers')}
|
||||
</div>
|
||||
<p className="max-w-sm text-xs text-muted-foreground">
|
||||
{translate(
|
||||
{t(
|
||||
'auto.components.settings.serverList.emptyDescription',
|
||||
'Pair a dev box, a cloud machine, or a desktop at home. Its sessions keep running there when this computer sleeps.'
|
||||
)}
|
||||
@@ -150,9 +151,9 @@ export function RuntimeServerList({
|
||||
<ServerListRow
|
||||
key={row.migrationId}
|
||||
name={targets.find((target) => target.id === row.sshTargetId)?.label ?? row.name}
|
||||
meta={<span className="truncate">{migrationPhaseLabel(row.phase)}</span>}
|
||||
meta={<span className="truncate">{migrationPhaseLabel(row.phase, t)}</span>}
|
||||
status={{
|
||||
label: translate('auto.components.settings.serverList.movePaused', 'Move paused'),
|
||||
label: t('auto.components.settings.serverList.movePaused', 'Move paused'),
|
||||
tone: 'muted'
|
||||
}}
|
||||
primaryAction={
|
||||
@@ -164,7 +165,7 @@ export function RuntimeServerList({
|
||||
onClick={() => void resume(row)}
|
||||
>
|
||||
{resumingId === row.migrationId ? <LoadingSpinner className="size-3" /> : null}
|
||||
{translate('auto.components.settings.managedServers.pending.resume', 'Resume')}
|
||||
{t('auto.components.settings.managedServers.pending.resume', 'Resume')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user