diff --git a/config/localization-coverage-allowlist.json b/config/localization-coverage-allowlist.json index 2a3b4603f36..b37d3374bf5 100644 --- a/config/localization-coverage-allowlist.json +++ b/config/localization-coverage-allowlist.json @@ -6,6 +6,13 @@ "dynamic": false, "count": 1 }, + { + "filePath": "src/renderer/src/components/sidebar/WorktreeCard.tsx", + "kind": "jsx-attribute:label", + "text": "sidebar", + "dynamic": false, + "count": 1 + }, { "filePath": "src/renderer/src/components/settings/appearance-search.ts", "kind": "object-property:keywords", diff --git a/config/scripts/audit-localization-coverage.mjs b/config/scripts/audit-localization-coverage.mjs index 8b978e38ed4..fd750a9150d 100644 --- a/config/scripts/audit-localization-coverage.mjs +++ b/config/scripts/audit-localization-coverage.mjs @@ -60,6 +60,14 @@ const USER_VISIBLE_OBJECT_METHODS = new Set([ 'warning' ]) const USER_VISIBLE_OBJECT_NAMES = new Set(['toast']) +// Why: only comparison operands are code, not copy. Bailing on every non-`+` +// operator hid whole subtrees behind `cond && ` guards and `?? 'fallback'`. +const COPY_PRESERVING_BINARY_OPERATORS = new Set([ + ts.SyntaxKind.PlusToken, + ts.SyntaxKind.QuestionQuestionToken, + ts.SyntaxKind.BarBarToken, + ts.SyntaxKind.AmpersandAmpersandToken +]) function normalizePath(root, filePath) { return path.relative(root, filePath).split(path.sep).join('/') @@ -226,7 +234,7 @@ function isRenderedJsxExpression(node) { continue } if (ts.isBinaryExpression(current)) { - if (current.operatorToken.kind !== ts.SyntaxKind.PlusToken) { + if (!COPY_PRESERVING_BINARY_OPERATORS.has(current.operatorToken.kind)) { return false } current = current.parent @@ -318,7 +326,8 @@ function classifyStringNode(node) { findAncestor( node, (ancestor) => - ts.isBinaryExpression(ancestor) && ancestor.operatorToken.kind !== ts.SyntaxKind.PlusToken + ts.isBinaryExpression(ancestor) && + !COPY_PRESERVING_BINARY_OPERATORS.has(ancestor.operatorToken.kind) ) ) { return undefined diff --git a/config/scripts/audit-localization-coverage.test.mjs b/config/scripts/audit-localization-coverage.test.mjs new file mode 100644 index 00000000000..291e6e29595 --- /dev/null +++ b/config/scripts/audit-localization-coverage.test.mjs @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest' + +import { collectLocalizationCandidates } from './audit-localization-coverage.mjs' + +const ROOT = process.cwd() + +function candidates(fileName, source) { + return collectLocalizationCandidates(`${ROOT}/src/renderer/src/${fileName}`, source, ROOT) +} + +describe('localization coverage candidates', () => { + it('sees copy guarded by a nullish or logical fallback', () => { + const reports = candidates( + 'Sample.tsx', + `export function Sample({ label, connecting }) { + return {label ?? (connecting ? 'Connecting…' : 'Idle')} + }` + ) + + expect(reports.map((report) => report.text)).toEqual(['Connecting…', 'Idle']) + }) + + it('sees copy nested inside a conditional JSX guard', () => { + const reports = candidates( + 'Sample.tsx', + `export function Sample({ show }) { + return
{show &&
+ }` + ) + + expect(reports.map((report) => report.text)).toEqual(['Retry the sync']) + }) + + it('ignores literals used as comparison operands', () => { + const reports = candidates( + 'Sample.tsx', + `export function Sample({ phase }) { + return {phase === 'workspace conflict' ? phase : null} + }` + ) + + expect(reports).toEqual([]) + }) +}) diff --git a/src/renderer/src/components/settings/CliSkillRuntimeSetup.tsx b/src/renderer/src/components/settings/CliSkillRuntimeSetup.tsx index 039c2035d59..1e8ac06c414 100644 --- a/src/renderer/src/components/settings/CliSkillRuntimeSetup.tsx +++ b/src/renderer/src/components/settings/CliSkillRuntimeSetup.tsx @@ -280,7 +280,14 @@ export async function ensureWslCliAvailableForAgentSkillTerminal( 'auto.components.settings.CliSkillRuntimeSetup.windowsPathUnknown', 'WSL shell command PATH could not be checked' ), - { description: status.detail ?? 'Refresh CLI registration status and try again.' } + { + description: + status.detail ?? + translate( + 'auto.components.settings.CliSkillRuntimeSetup.refreshCliRegistration', + 'Refresh CLI registration status and try again.' + ) + } ) return status } diff --git a/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx b/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx index fe0f5e1a297..c4690a1c79a 100644 --- a/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx +++ b/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx @@ -1184,12 +1184,9 @@ export function ResourceUsageStatusSegment({
- {resourceManagerTooltipLines.map((line, index) => ( -
- {line} + {resourceManagerTooltipLines.map((line) => ( +
+ {line.text}
))}
diff --git a/src/renderer/src/components/status-bar/SshStatusSegment.tsx b/src/renderer/src/components/status-bar/SshStatusSegment.tsx index 70e500ad09a..fc417d679f1 100644 --- a/src/renderer/src/components/status-bar/SshStatusSegment.tsx +++ b/src/renderer/src/components/status-bar/SshStatusSegment.tsx @@ -18,6 +18,11 @@ import { } from '../../../../shared/execution-host' import { isUserManagedRuntimeEnvironment } from '../../../../shared/runtime-environments' import { RuntimeHostStatusRow, type RuntimeHostConnectionState } from './RuntimeHostStatusRow' +import { + connectedHostCountLabel, + connectingHostsLabel, + workspaceSyncProblemLabel +} from './ssh-status-segment-copy' import { SshTargetStatusRow } from './SshTargetStatusRow' import type { RemoteRuntimeSharedConnectionDiagnostics } from '../../../../shared/remote-runtime-shared-control-types' import { connectRuntimeEnvironmentAndRecordStatus } from './runtime-environment-explicit-connect' @@ -59,10 +64,6 @@ function overallDotColor( } } -function connectedHostCountLabel(count: number): string { - return `${count} ${count === 1 ? 'host' : 'hosts'}` -} - function sshStatusForOverall(status: SshConnectionStatus): HostStatus { if (status === 'connected') { return 'connected' @@ -286,9 +287,7 @@ export function SshStatusSegment({ (t) => t.syncStatus?.phase === 'conflict' || t.syncStatus?.phase === 'error' ) const syncProblemLabel = syncProblem - ? syncProblem.syncStatus?.phase === 'conflict' - ? 'Workspace conflict' - : 'Workspace sync error' + ? workspaceSyncProblemLabel(syncProblem.syncStatus?.phase) : null return ( {syncProblemLabel ?? - (anyConnecting ? 'Connecting…' : connectedHostCountLabel(connectedHostCount))} + (anyConnecting + ? connectingHostsLabel() + : connectedHostCountLabel(connectedHostCount))} )} diff --git a/src/renderer/src/components/status-bar/StatusBar.tsx b/src/renderer/src/components/status-bar/StatusBar.tsx index 9d7534e8c3b..1b3075e305b 100644 --- a/src/renderer/src/components/status-bar/StatusBar.tsx +++ b/src/renderer/src/components/status-bar/StatusBar.tsx @@ -2369,7 +2369,11 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele className="relative inline-flex size-5 cursor-pointer items-center justify-center rounded border border-border bg-secondary text-secondary-foreground shadow-xs transition-colors hover:bg-accent hover:text-accent-foreground" aria-label={ showFloatingWorkspaceAttentionDot - ? `${floatingTerminalActionLabel}, new activity` + ? translate( + 'auto.components.status.bar.StatusBar.floatingTerminalNewActivity', + '{{label}}, new activity', + { label: floatingTerminalActionLabel } + ) : floatingTerminalActionLabel } onClick={() => { diff --git a/src/renderer/src/components/status-bar/resource-manager-terminal-copy.test.ts b/src/renderer/src/components/status-bar/resource-manager-terminal-copy.test.ts index 42b42d492b6..7f6d1eefa3d 100644 --- a/src/renderer/src/components/status-bar/resource-manager-terminal-copy.test.ts +++ b/src/renderer/src/components/status-bar/resource-manager-terminal-copy.test.ts @@ -1,4 +1,12 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' + +// Mirrors resource-memory-metric-copy.test.ts: assert the English source copy +// through the catalog fallback, with placeholders filled the way i18next would. +vi.mock('@/i18n/i18n', () => ({ + translate: (_key: string, fallback: string, options?: Record) => + fallback.replace(/\{\{(\w+)\}\}/g, (match, name: string) => String(options?.[name] ?? match)) +})) + import { formatTerminalSessionCount, getResourceManagerAriaLabel, @@ -19,8 +27,12 @@ describe('resource manager terminal copy', () => { spaceScanReady: false }) ).toEqual([ - 'Resource Manager - 512 MB · Σ RSS - 2 terminal sessions', - 'Terminal sessions are grouped by workspace.' + { + id: 'summary', + text: 'Resource Manager - 512 MB · Σ RSS - 2 terminal sessions', + emphasized: false + }, + { id: 'sessions-hint', text: 'Terminal sessions are grouped by workspace.', emphasized: false } ]) }) @@ -32,12 +44,45 @@ describe('resource manager terminal copy', () => { spaceScanReady: true }) ).toEqual([ - 'Resource Manager - memory unavailable - 0 terminal sessions', - 'Space scan ready', - 'No terminal sessions yet.' + { + id: 'summary', + text: 'Resource Manager - memory unavailable - 0 terminal sessions', + emphasized: false + }, + { id: 'space-scan', text: 'Space scan ready', emphasized: true }, + { id: 'sessions-hint', text: 'No terminal sessions yet.', emphasized: false } ]) }) + // Why: the tooltip used to tint this row by matching its English text, so any + // translated build lost the tint. The flag is what the segment reads now. + it('flags the space-scan row instead of leaving callers to match its wording', () => { + const lines = getResourceManagerTooltipLines({ + memoryLabel: '512 MB', + sessionCount: 1, + spaceScanReady: true + }) + + expect(lines.filter((line) => line.emphasized).map((line) => line.text)).toEqual([ + 'Space scan ready' + ]) + }) + + // Why: the tooltip keys rows by id, so a repeated id would silently drop a row. + it('gives every tooltip row a unique id to key on', () => { + for (const spaceScanReady of [false, true]) { + for (const sessionCount of [0, 1, 4]) { + const ids = getResourceManagerTooltipLines({ + memoryLabel: '512 MB', + sessionCount, + spaceScanReady + }).map((line) => line.id) + + expect(new Set(ids).size).toBe(ids.length) + } + } + }) + it('keeps the trigger label descriptive for screen readers', () => { expect( getResourceManagerAriaLabel({ diff --git a/src/renderer/src/components/status-bar/resource-manager-terminal-copy.ts b/src/renderer/src/components/status-bar/resource-manager-terminal-copy.ts index 69b5b7b6193..0d082654199 100644 --- a/src/renderer/src/components/status-bar/resource-manager-terminal-copy.ts +++ b/src/renderer/src/components/status-bar/resource-manager-terminal-copy.ts @@ -1,30 +1,86 @@ +import { translate } from '@/i18n/i18n' + export function formatTerminalSessionCount(count: number): string { - return `${count} terminal session${count === 1 ? '' : 's'}` + return count === 1 + ? translate( + 'auto.components.status.bar.resource.manager.terminal.copy.terminalSessionCount_one', + '{{count}} terminal session', + { count } + ) + : translate( + 'auto.components.status.bar.resource.manager.terminal.copy.terminalSessionCount_other', + '{{count}} terminal sessions', + { count } + ) +} + +function spaceScanReadyLabel(): string { + return translate( + 'auto.components.status.bar.resource.manager.terminal.copy.spaceScanReady', + 'Space scan ready' + ) +} + +/** + * `emphasized` marks the space-scan row the tooltip tints. The segment used to + * recognize that row by comparing it against the English text, which stops + * matching the moment the copy comes from the catalog. + * + * `id` names the row's role. Each role appears at most once per tooltip, so it is + * a unique React key that survives locale switches and memory/count updates — + * unlike the translated text, which locales are free to duplicate across rows. + */ +export type ResourceManagerTooltipLine = { + id: 'summary' | 'space-scan' | 'sessions-hint' + text: string + emphasized: boolean } export function getResourceManagerTooltipLines(args: { memoryLabel: string sessionCount: number spaceScanReady: boolean -}): string[] { +}): ResourceManagerTooltipLine[] { const rawMemoryLabel = args.memoryLabel.trim() const memoryLabel = rawMemoryLabel === '' || rawMemoryLabel === '-' || rawMemoryLabel === '—' - ? 'memory unavailable' + ? translate( + 'auto.components.status.bar.resource.manager.terminal.copy.memoryUnavailable', + 'memory unavailable' + ) : rawMemoryLabel - const lines = [ - `Resource Manager - ${memoryLabel} - ${formatTerminalSessionCount(args.sessionCount)}` + // Why: whole lines are single keys — locales reorder the summary and repunctuate + // its separators, so it can't be concatenated from translated fragments here. + const lines: ResourceManagerTooltipLine[] = [ + { + id: 'summary', + text: translate( + 'auto.components.status.bar.resource.manager.terminal.copy.tooltipSummary', + 'Resource Manager - {{memory}} - {{sessions}}', + { memory: memoryLabel, sessions: formatTerminalSessionCount(args.sessionCount) } + ), + emphasized: false + } ] if (args.spaceScanReady) { - lines.push('Space scan ready') + lines.push({ id: 'space-scan', text: spaceScanReadyLabel(), emphasized: true }) } - if (args.sessionCount > 0) { - lines.push('Terminal sessions are grouped by workspace.') - } else { - lines.push('No terminal sessions yet.') - } + lines.push({ + id: 'sessions-hint', + text: + args.sessionCount > 0 + ? translate( + 'auto.components.status.bar.resource.manager.terminal.copy.sessionsGroupedByWorkspace', + 'Terminal sessions are grouped by workspace.' + ) + : translate( + 'auto.components.status.bar.resource.manager.terminal.copy.noTerminalSessions', + 'No terminal sessions yet.' + ), + emphasized: false + }) return lines } @@ -33,11 +89,19 @@ export function getResourceManagerAriaLabel(args: { sessionCount: number spaceScanReady: boolean }): string { - const parts = ['Resource Manager', formatTerminalSessionCount(args.sessionCount)] + const sessions = formatTerminalSessionCount(args.sessionCount) if (args.spaceScanReady) { - parts.push('Space scan ready') + return translate( + 'auto.components.status.bar.resource.manager.terminal.copy.ariaLabelWithSpaceScan', + 'Resource Manager, {{sessions}}, {{spaceScan}}', + { sessions, spaceScan: spaceScanReadyLabel() } + ) } - return parts.join(', ') + return translate( + 'auto.components.status.bar.resource.manager.terminal.copy.ariaLabel', + 'Resource Manager, {{sessions}}', + { sessions } + ) } diff --git a/src/renderer/src/components/status-bar/ssh-status-segment-copy.test.ts b/src/renderer/src/components/status-bar/ssh-status-segment-copy.test.ts new file mode 100644 index 00000000000..3cd3957872f --- /dev/null +++ b/src/renderer/src/components/status-bar/ssh-status-segment-copy.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it, vi } from 'vitest' + +vi.mock('@/i18n/i18n', () => ({ + translate: (_key: string, fallback: string, options?: Record) => + fallback.replace(/\{\{(\w+)\}\}/g, (match, name: string) => String(options?.[name] ?? match)) +})) + +import { + connectedHostCountLabel, + connectingHostsLabel, + workspaceSyncProblemLabel +} from './ssh-status-segment-copy' + +describe('ssh status segment copy', () => { + it('keeps the host noun singular for a lone connected host', () => { + expect(connectedHostCountLabel(0)).toBe('0 hosts') + expect(connectedHostCountLabel(1)).toBe('1 host') + expect(connectedHostCountLabel(4)).toBe('4 hosts') + }) + + it('distinguishes a sync conflict from a sync failure', () => { + expect(connectingHostsLabel()).toBe('Connecting…') + expect(workspaceSyncProblemLabel('conflict')).toBe('Workspace conflict') + expect(workspaceSyncProblemLabel('error')).toBe('Workspace sync error') + }) +}) diff --git a/src/renderer/src/components/status-bar/ssh-status-segment-copy.ts b/src/renderer/src/components/status-bar/ssh-status-segment-copy.ts new file mode 100644 index 00000000000..cf4cd7e3552 --- /dev/null +++ b/src/renderer/src/components/status-bar/ssh-status-segment-copy.ts @@ -0,0 +1,31 @@ +import { translate } from '@/i18n/i18n' + +export function connectedHostCountLabel(count: number): string { + return count === 1 + ? translate( + 'auto.components.status.bar.SshStatusSegment.connectedHostCount_one', + '{{count}} host', + { count } + ) + : translate( + 'auto.components.status.bar.SshStatusSegment.connectedHostCount_other', + '{{count}} hosts', + { count } + ) +} + +export function connectingHostsLabel(): string { + return translate('auto.components.status.bar.SshStatusSegment.connecting', 'Connecting…') +} + +export function workspaceSyncProblemLabel(phase: string | undefined): string { + return phase === 'conflict' + ? translate( + 'auto.components.status.bar.SshStatusSegment.workspaceConflict', + 'Workspace conflict' + ) + : translate( + 'auto.components.status.bar.SshStatusSegment.workspaceSyncError', + 'Workspace sync error' + ) +} diff --git a/src/renderer/src/components/status-bar/status-bar-copy-localization.test.tsx b/src/renderer/src/components/status-bar/status-bar-copy-localization.test.tsx new file mode 100644 index 00000000000..96da506d9e9 --- /dev/null +++ b/src/renderer/src/components/status-bar/status-bar-copy-localization.test.tsx @@ -0,0 +1,183 @@ +// @vitest-environment happy-dom +/** + * The Resource Manager tooltip and the SSH segment's host count were built from + * bare English literals inside helper functions, so they stayed English while + * every label around them translated. The coverage audit cannot see values + * returned from helpers, so only a runtime assertion against the real catalog + * keeps them honest — same reasoning as + * `src/renderer/src/i18n/settings-status-label-localization.test.ts`. + */ +import { cleanup, render, screen } from '@testing-library/react' +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' +import type { SshConnectionStatus } from '../../../../shared/ssh-types' +import { i18n } from '@/i18n/i18n' +import { + formatTerminalSessionCount, + getResourceManagerAriaLabel, + getResourceManagerTooltipLines +} from './resource-manager-terminal-copy' +import { SshStatusSegment } from './SshStatusSegment' + +type StoreState = { + sshConnectionStates: Map + sshTargetLabels: Map + remoteWorkspaceSyncStatusByTargetId: Record +} + +let storeState: StoreState = { + sshConnectionStates: new Map(), + sshTargetLabels: new Map(), + remoteWorkspaceSyncStatusByTargetId: {} +} + +vi.mock('../../store', () => { + const state = (): Record => ({ + ...storeState, + settings: null, + runtimeEnvironments: [], + runtimeStatusByEnvironmentId: new Map(), + setRuntimeEnvironmentStatus: vi.fn(), + hydrateRuntimeEnvironmentStatuses: vi.fn(), + setActiveView: vi.fn(), + openSettingsTarget: vi.fn(), + recordFeatureInteraction: vi.fn(), + fetchRuntimeEnvironmentRepos: vi.fn(), + fetchWorktrees: vi.fn(), + fetchWorktreeLineage: vi.fn() + }) + const useAppStore = (selector: (value: Record) => unknown): unknown => + selector(state()) + useAppStore.getState = state + return { useAppStore } +}) + +function setSshTargets( + entries: { id: string; label: string; status: SshConnectionStatus; syncPhase?: string }[] +): void { + storeState = { + sshConnectionStates: new Map(entries.map((entry) => [entry.id, { status: entry.status }])), + sshTargetLabels: new Map(entries.map((entry) => [entry.id, entry.label])), + remoteWorkspaceSyncStatusByTargetId: Object.fromEntries( + entries.flatMap((entry) => (entry.syncPhase ? [[entry.id, { phase: entry.syncPhase }]] : [])) + ) + } +} + +function triggerText(): string { + return screen.getByRole('button').textContent ?? '' +} + +describe('status-bar copy under a non-English UI language', () => { + beforeAll(async () => { + await i18n.changeLanguage('ja') + }) + + afterEach(() => { + cleanup() + }) + + afterAll(async () => { + await i18n.changeLanguage('en') + }) + + it('translates both plural forms of the terminal session count', () => { + expect(formatTerminalSessionCount(1)).toBe('1 件のターミナルセッション') + expect(formatTerminalSessionCount(5)).toBe('5 件のターミナルセッション') + }) + + it('translates every Resource Manager tooltip line', () => { + expect( + getResourceManagerTooltipLines({ + memoryLabel: '512 MB', + sessionCount: 2, + spaceScanReady: true + }) + ).toEqual([ + { + id: 'summary', + text: 'リソースマネージャー - 512 MB - 2 件のターミナルセッション', + emphasized: false + }, + { id: 'space-scan', text: '容量スキャンの準備完了', emphasized: true }, + { + id: 'sessions-hint', + text: 'ターミナルセッションはワークスペースごとにグループ化されます。', + emphasized: false + } + ]) + }) + + // Why: the tint used to be selected by `line === 'Space scan ready'`, so it + // silently vanished for every non-English locale once the copy translated. + it('keeps the space-scan row flagged when its copy is no longer English', () => { + const lines = getResourceManagerTooltipLines({ + memoryLabel: '512 MB', + sessionCount: 2, + spaceScanReady: true + }) + + const emphasized = lines.filter((line) => line.emphasized) + expect(emphasized).toHaveLength(1) + expect(emphasized[0]?.text).toBe('容量スキャンの準備完了') + expect(emphasized[0]?.text).not.toBe('Space scan ready') + }) + + it('translates the memory-unavailable and empty-session tooltip lines', () => { + expect( + getResourceManagerTooltipLines({ memoryLabel: '—', sessionCount: 0, spaceScanReady: false }) + ).toEqual([ + { + id: 'summary', + text: 'リソースマネージャー - メモリ情報を取得できません - 0 件のターミナルセッション', + emphasized: false + }, + { id: 'sessions-hint', text: 'ターミナルセッションはまだありません。', emphasized: false } + ]) + }) + + it('translates the Resource Manager trigger label read by screen readers', () => { + expect(getResourceManagerAriaLabel({ sessionCount: 1, spaceScanReady: true })).toBe( + 'リソースマネージャー、1 件のターミナルセッション、容量スキャンの準備完了' + ) + expect(getResourceManagerAriaLabel({ sessionCount: 3, spaceScanReady: false })).toBe( + 'リソースマネージャー、3 件のターミナルセッション' + ) + }) + + it('translates the connected host count next to the translated aria label', () => { + setSshTargets([ + { id: 'ssh-1', label: 'builder', status: 'connected' }, + { id: 'ssh-2', label: 'openclaw', status: 'connected' } + ]) + render() + + expect(screen.getByRole('button').getAttribute('aria-label')).toBe('リモートホスト接続状態') + expect(triggerText()).toContain('2 台のホスト') + }) + + it('translates the singular host count', () => { + setSshTargets([{ id: 'ssh-1', label: 'builder', status: 'connected' }]) + render() + + expect(triggerText()).toContain('1 台のホスト') + }) + + it('translates the connecting and workspace-sync states', () => { + setSshTargets([{ id: 'ssh-1', label: 'builder', status: 'connecting' }]) + render() + expect(triggerText()).toContain('接続中…') + cleanup() + + setSshTargets([ + { id: 'ssh-1', label: 'builder', status: 'connected', syncPhase: 'conflict' }, + { id: 'ssh-2', label: 'openclaw', status: 'connected', syncPhase: 'error' } + ]) + render() + expect(triggerText()).toContain('ワークスペースの競合') + cleanup() + + setSshTargets([{ id: 'ssh-1', label: 'builder', status: 'connected', syncPhase: 'error' }]) + render() + expect(triggerText()).toContain('ワークスペースの同期エラー') + }) +}) diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index b25a212f7a2..467ca68d5aa 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -415,7 +415,8 @@ "8d6eedf97e": "Failed to register the Orca CLI in PATH.", "0f116999f1": "Restart your shell or add the Orca CLI directory to PATH before setup.", "15cbedc3e3": "Install the Orca CLI before running agent skill setup.", - "windowsPathUnknown": "Orca could not check your Windows user PATH" + "windowsPathUnknown": "Orca could not check your Windows user PATH", + "refreshCliRegistration": "Refresh CLI registration status and try again." } } } @@ -631,6 +632,9 @@ "0b01ff98d2": "Port", "7d732521ec": "Comment" } + }, + "activation": { + "cannotOpenFolderWorkspace": "Cannot open folder workspace" } }, "folderWorkspacePathStatus": { @@ -3262,7 +3266,12 @@ "runtime_disconnect_failed": "Disconnect failed", "runtime_reconnecting": "Reconnecting", "runtime_last_close_reason": "Closed: {{value0}}", - "runtime_reconnect_attempt": "Attempt {{value0}}" + "runtime_reconnect_attempt": "Attempt {{value0}}", + "connectedHostCount_one": "{{count}} host", + "connectedHostCount_other": "{{count}} hosts", + "connecting": "Connecting…", + "workspaceConflict": "Workspace conflict", + "workspaceSyncError": "Workspace sync error" }, "StatusBar": { "9659e38343": "Ports", @@ -3322,7 +3331,8 @@ "antigravityUsage": "Antigravity Usage", "antigravityUsageDetails": "Open Antigravity usage details", "grokUsageAria": "Open Grok usage details", - "grokUsageMenu": "Grok Usage" + "grokUsageMenu": "Grok Usage", + "floatingTerminalNewActivity": "{{label}}, new activity" }, "StatusBarUsageEmptyCta": { "828c764a79": "Connect an account", @@ -3548,6 +3558,21 @@ "workingSetDescription": "Summed working set (WS). Shared pages can appear in more than one process.", "rssDescription": "Summed resident set size (RSS). Shared or aliased pages can appear in more than one process." } + }, + "manager": { + "terminal": { + "copy": { + "terminalSessionCount_one": "{{count}} terminal session", + "terminalSessionCount_other": "{{count}} terminal sessions", + "memoryUnavailable": "memory unavailable", + "tooltipSummary": "Resource Manager - {{memory}} - {{sessions}}", + "spaceScanReady": "Space scan ready", + "sessionsGroupedByWorkspace": "Terminal sessions are grouped by workspace.", + "noTerminalSessions": "No terminal sessions yet.", + "ariaLabel": "Resource Manager, {{sessions}}", + "ariaLabelWithSpaceScan": "Resource Manager, {{sessions}}, {{spaceScan}}" + } + } } } } @@ -5792,7 +5817,8 @@ "f00d6aa9b5": "WSL is not available on this machine.", "7c776ff9d8": "wsl", "fc0fcf72fd": "Register the WSL shell command before skill setup.", - "windowsPathUnknown": "WSL shell command PATH could not be checked" + "windowsPathUnknown": "WSL shell command PATH could not be checked", + "refreshCliRegistration": "Refresh CLI registration status and try again." }, "CommitMessageAiPane": { "841ed9884a": "Used by repositories that have not customized Source Control AI.", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index ac0f5c012bf..e60f92ba08d 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -349,7 +349,8 @@ "8d6eedf97e": "No se pudo registrar Orca CLI en PATH.", "0f116999f1": "Reinicia tu shell o agrega el directorio de Orca CLI a PATH antes de la configuración.", "15cbedc3e3": "Instala la CLI de Orca antes de configurar skills para agentes.", - "windowsPathUnknown": "Orca could not check your Windows user PATH" + "windowsPathUnknown": "Orca could not check your Windows user PATH", + "refreshCliRegistration": "Actualiza el estado de registro de la CLI e inténtalo de nuevo." } } } @@ -560,6 +561,9 @@ "0b01ff98d2": "Puerto", "7d732521ec": "Comentario" } + }, + "activation": { + "cannotOpenFolderWorkspace": "No se puede abrir el espacio de trabajo de carpeta" } }, "folderWorkspacePathStatus": { @@ -3174,7 +3178,12 @@ "runtime_disconnect_failed": "No se pudo desconectar", "runtime_reconnecting": "Reconectando", "runtime_last_close_reason": "Cerrado: {{value0}}", - "runtime_reconnect_attempt": "Intento {{value0}}" + "runtime_reconnect_attempt": "Intento {{value0}}", + "connectedHostCount_one": "{{count}} host", + "connectedHostCount_other": "{{count}} hosts", + "connecting": "Conectando…", + "workspaceConflict": "Conflicto del espacio de trabajo", + "workspaceSyncError": "Error de sincronización del espacio de trabajo" }, "StatusBar": { "9659e38343": "Puertos", @@ -3234,7 +3243,8 @@ "antigravityUsage": "Uso de Antigravity", "antigravityUsageDetails": "Abrir detalles de uso de Antigravity", "grokUsageAria": "Abrir detalles de uso de Grok", - "grokUsageMenu": "Uso de Grok" + "grokUsageMenu": "Uso de Grok", + "floatingTerminalNewActivity": "{{label}}, nueva actividad" }, "StatusBarUsageEmptyCta": { "828c764a79": "Conectar una cuenta", @@ -3444,6 +3454,21 @@ "workingSetDescription": "Suma del conjunto de trabajo (WS). Las páginas compartidas pueden aparecer en más de un proceso.", "rssDescription": "Suma del tamaño del conjunto residente (RSS). Las páginas compartidas o con alias pueden aparecer en más de un proceso." } + }, + "manager": { + "terminal": { + "copy": { + "terminalSessionCount_one": "{{count}} sesión de terminal", + "terminalSessionCount_other": "{{count}} sesiones de terminal", + "memoryUnavailable": "memoria no disponible", + "tooltipSummary": "Administrador de recursos - {{memory}} - {{sessions}}", + "spaceScanReady": "Escaneo de espacio listo", + "sessionsGroupedByWorkspace": "Las sesiones de terminal se agrupan por espacio de trabajo.", + "noTerminalSessions": "Aún no hay sesiones de terminal.", + "ariaLabel": "Administrador de recursos, {{sessions}}", + "ariaLabelWithSpaceScan": "Administrador de recursos, {{sessions}}, {{spaceScan}}" + } + } } }, "SkillUpdateStatusSegment": { @@ -5624,7 +5649,8 @@ "f00d6aa9b5": "WSL no está disponible en esta máquina.", "7c776ff9d8": "wsl", "fc0fcf72fd": "Registra el comando de shell de WSL antes de configurar el skill.", - "windowsPathUnknown": "WSL shell command PATH could not be checked" + "windowsPathUnknown": "WSL shell command PATH could not be checked", + "refreshCliRegistration": "Actualiza el estado de registro de la CLI e inténtalo de nuevo." }, "CommitMessageAiPane": { "841ed9884a": "Usado por repositorios que no han personalizado Source Control AI.", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index d239b240df0..f008abfa70e 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -349,7 +349,8 @@ "8d6eedf97e": "Orca CLI を PATH に登録できませんでした。", "0f116999f1": "セットアップ前にシェルを再起動するか、Orca CLI ディレクトリを PATH に追加します。", "15cbedc3e3": "agent スキル セットアップを実行する前に、Orca CLI をインストールします。", - "windowsPathUnknown": "Orca could not check your Windows user PATH" + "windowsPathUnknown": "Orca could not check your Windows user PATH", + "refreshCliRegistration": "CLI の登録状態を更新してから、もう一度お試しください。" } } } @@ -560,6 +561,9 @@ "0b01ff98d2": "ポート", "7d732521ec": "コメント" } + }, + "activation": { + "cannotOpenFolderWorkspace": "フォルダーワークスペースを開けません" } }, "folderWorkspacePathStatus": { @@ -3174,7 +3178,12 @@ "runtime_disconnect_failed": "切断に失敗しました", "runtime_reconnecting": "再接続中", "runtime_last_close_reason": "クローズ: {{value0}}", - "runtime_reconnect_attempt": "試行 {{value0}}" + "runtime_reconnect_attempt": "試行 {{value0}}", + "connectedHostCount_one": "{{count}} 台のホスト", + "connectedHostCount_other": "{{count}} 台のホスト", + "connecting": "接続中…", + "workspaceConflict": "ワークスペースの競合", + "workspaceSyncError": "ワークスペースの同期エラー" }, "StatusBar": { "9659e38343": "ポート", @@ -3234,7 +3243,8 @@ "antigravityUsage": "Antigravity の使用状況", "antigravityUsageDetails": "Antigravity の使用状況詳細を開く", "grokUsageAria": "Grok の使用状況詳細を開く", - "grokUsageMenu": "Grok の使用状況" + "grokUsageMenu": "Grok の使用状況", + "floatingTerminalNewActivity": "{{label}}、新しいアクティビティ" }, "StatusBarUsageEmptyCta": { "828c764a79": "アカウントを接続する", @@ -3444,6 +3454,21 @@ "workingSetDescription": "合計ワーキングセット(WS)。共有ページが複数のプロセスに表示されることがあります。", "rssDescription": "合計レジデントセットサイズ(RSS)。共有またはエイリアスされたページが複数のプロセスに表示されることがあります。" } + }, + "manager": { + "terminal": { + "copy": { + "terminalSessionCount_one": "{{count}} 件のターミナルセッション", + "terminalSessionCount_other": "{{count}} 件のターミナルセッション", + "memoryUnavailable": "メモリ情報を取得できません", + "tooltipSummary": "リソースマネージャー - {{memory}} - {{sessions}}", + "spaceScanReady": "容量スキャンの準備完了", + "sessionsGroupedByWorkspace": "ターミナルセッションはワークスペースごとにグループ化されます。", + "noTerminalSessions": "ターミナルセッションはまだありません。", + "ariaLabel": "リソースマネージャー、{{sessions}}", + "ariaLabelWithSpaceScan": "リソースマネージャー、{{sessions}}、{{spaceScan}}" + } + } } }, "SkillUpdateStatusSegment": { @@ -5609,7 +5634,8 @@ "f00d6aa9b5": "このマシンでは WSL を利用できません。", "7c776ff9d8": "wsl", "fc0fcf72fd": "スキルセットアップ前に WSL シェルコマンドを登録してください。", - "windowsPathUnknown": "WSL shell command PATH could not be checked" + "windowsPathUnknown": "WSL shell command PATH could not be checked", + "refreshCliRegistration": "CLI の登録状態を更新してから、もう一度お試しください。" }, "CommitMessageAiPane": { "841ed9884a": "ソース管理 AI をカスタマイズしていない repos によって使用されます。", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 77f37c36a19..1b4c5c4fba2 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -349,7 +349,8 @@ "8d6eedf97e": "PATH에 Orca CLI를 등록하지 못했습니다.", "0f116999f1": "설정하기 전에 셸을 다시 시작하거나 Orca CLI 디렉터리를 PATH에 추가하세요.", "15cbedc3e3": "agent 스킬 설정을 실행하기 전에 Orca CLI를 설치하십시오.", - "windowsPathUnknown": "Orca could not check your Windows user PATH" + "windowsPathUnknown": "Orca could not check your Windows user PATH", + "refreshCliRegistration": "CLI 등록 상태를 새로 고친 후 다시 시도하세요." } } } @@ -560,6 +561,9 @@ "0b01ff98d2": "포트", "7d732521ec": "댓글" } + }, + "activation": { + "cannotOpenFolderWorkspace": "폴더 워크스페이스를 열 수 없습니다" } }, "folderWorkspacePathStatus": { @@ -3174,7 +3178,12 @@ "runtime_disconnect_failed": "연결 해제 실패", "runtime_reconnecting": "다시 연결 중", "runtime_last_close_reason": "닫힘: {{value0}}", - "runtime_reconnect_attempt": "{{value0}}번째 시도" + "runtime_reconnect_attempt": "{{value0}}번째 시도", + "connectedHostCount_one": "호스트 {{count}}개", + "connectedHostCount_other": "호스트 {{count}}개", + "connecting": "연결 중…", + "workspaceConflict": "워크스페이스 충돌", + "workspaceSyncError": "워크스페이스 동기화 오류" }, "StatusBar": { "9659e38343": "포트", @@ -3234,7 +3243,8 @@ "antigravityUsage": "Antigravity 사용량", "antigravityUsageDetails": "Antigravity 사용량 세부 정보 열기", "grokUsageAria": "Grok 사용량 세부 정보 열기", - "grokUsageMenu": "Grok 사용량" + "grokUsageMenu": "Grok 사용량", + "floatingTerminalNewActivity": "{{label}}, 새 활동" }, "StatusBarUsageEmptyCta": { "828c764a79": "계정 연결", @@ -3444,6 +3454,21 @@ "workingSetDescription": "합산된 워킹 세트(WS). 공유 페이지가 둘 이상의 프로세스에 나타날 수 있습니다.", "rssDescription": "합산된 레지던트 세트 크기(RSS). 공유 또는 별칭된 페이지가 둘 이상의 프로세스에 나타날 수 있습니다." } + }, + "manager": { + "terminal": { + "copy": { + "terminalSessionCount_one": "터미널 세션 {{count}}개", + "terminalSessionCount_other": "터미널 세션 {{count}}개", + "memoryUnavailable": "메모리 정보 없음", + "tooltipSummary": "리소스 관리자 - {{memory}} - {{sessions}}", + "spaceScanReady": "공간 스캔 준비됨", + "sessionsGroupedByWorkspace": "터미널 세션은 워크스페이스별로 그룹화됩니다.", + "noTerminalSessions": "아직 터미널 세션이 없습니다.", + "ariaLabel": "리소스 관리자, {{sessions}}", + "ariaLabelWithSpaceScan": "리소스 관리자, {{sessions}}, {{spaceScan}}" + } + } } }, "SkillUpdateStatusSegment": { @@ -5608,8 +5633,9 @@ "0c9f3cf9da": "Orca가 글로벌 agent 스킬을 확인하고 설치하는 위치를 선택합니다.", "f00d6aa9b5": "이 컴퓨터에서는 WSL을 사용할 수 없습니다.", "7c776ff9d8": "wsl", - "fc0fcf72fd": "스킬 설정 전 WSL 셸 명령어를 등록하세요.", - "windowsPathUnknown": "WSL shell command PATH could not be checked" + "fc0fcf72fd": "스킬 설정 전 WSL 쉘 명령어를 등록하세요.", + "windowsPathUnknown": "WSL shell command PATH could not be checked", + "refreshCliRegistration": "CLI 등록 상태를 새로 고친 후 다시 시도하세요." }, "CommitMessageAiPane": { "841ed9884a": "소스 제어 AI를 사용자 정의하지 않은 리포지토리에서 사용됩니다.", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index b71ed76c0e6..90628fb006f 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -349,7 +349,8 @@ "8d6eedf97e": "无法在 PATH 中注册 Orca CLI。", "0f116999f1": "在安装之前重新启动 shell 或将 Orca CLI 目录添加到 PATH。", "15cbedc3e3": "在运行智能体技能设置之前安装 Orca CLI。", - "windowsPathUnknown": "Orca 无法检查您的 Windows 用户 PATH" + "windowsPathUnknown": "Orca 无法检查您的 Windows 用户 PATH", + "refreshCliRegistration": "请刷新 CLI 注册状态后重试。" } } } @@ -560,6 +561,9 @@ "0b01ff98d2": "端口", "7d732521ec": "评论" } + }, + "activation": { + "cannotOpenFolderWorkspace": "无法打开文件夹工作区" } }, "folderWorkspacePathStatus": { @@ -3186,7 +3190,12 @@ "runtime_disconnect_failed": "断开连接失败", "runtime_reconnecting": "重新连接中", "runtime_last_close_reason": "已关闭:{{value0}}", - "runtime_reconnect_attempt": "第 {{value0}} 次尝试" + "runtime_reconnect_attempt": "第 {{value0}} 次尝试", + "connectedHostCount_one": "{{count}} 个主机", + "connectedHostCount_other": "{{count}} 个主机", + "connecting": "正在连接…", + "workspaceConflict": "工作区冲突", + "workspaceSyncError": "工作区同步错误" }, "StatusBar": { "9659e38343": "端口", @@ -3246,7 +3255,8 @@ "antigravityUsage": "Antigravity 使用量", "antigravityUsageDetails": "打开 Antigravity 使用详情", "grokUsageAria": "打开 Grok 使用详情", - "grokUsageMenu": "Grok 使用量" + "grokUsageMenu": "Grok 使用量", + "floatingTerminalNewActivity": "{{label}},有新活动" }, "StatusBarUsageEmptyCta": { "828c764a79": "连接账户", @@ -3456,6 +3466,21 @@ "workingSetDescription": "工作集 (WS) 的总和。共享页可能会出现在多个进程中。", "rssDescription": "驻留集大小 (RSS) 的总和。共享页或别名映射页可能会出现在多个进程中。" } + }, + "manager": { + "terminal": { + "copy": { + "terminalSessionCount_one": "{{count}} 个终端会话", + "terminalSessionCount_other": "{{count}} 个终端会话", + "memoryUnavailable": "内存信息不可用", + "tooltipSummary": "资源管理器 - {{memory}} - {{sessions}}", + "spaceScanReady": "空间扫描已就绪", + "sessionsGroupedByWorkspace": "终端会话按工作区分组。", + "noTerminalSessions": "暂无终端会话。", + "ariaLabel": "资源管理器,{{sessions}}", + "ariaLabelWithSpaceScan": "资源管理器,{{sessions}},{{spaceScan}}" + } + } } }, "SkillUpdateStatusSegment": { @@ -5621,7 +5646,8 @@ "f00d6aa9b5": "WSL 在此计算机上不可用。", "7c776ff9d8": "wsl", "fc0fcf72fd": "在技​​能设置之前注册 WSL shell 命令。", - "windowsPathUnknown": "无法检查 WSL shell 命令 PATH" + "windowsPathUnknown": "无法检查 WSL shell 命令 PATH", + "refreshCliRegistration": "请刷新 CLI 注册状态后重试。" }, "CommitMessageAiPane": { "841ed9884a": "由未自定义源代码控制 AI 的存储库使用。", diff --git a/src/renderer/src/lib/agent-skill-cli-prerequisite.ts b/src/renderer/src/lib/agent-skill-cli-prerequisite.ts index 1c731bae1fe..aa02dec7460 100644 --- a/src/renderer/src/lib/agent-skill-cli-prerequisite.ts +++ b/src/renderer/src/lib/agent-skill-cli-prerequisite.ts @@ -117,7 +117,14 @@ function showCliPrerequisiteWarning(status: CliInstallStatus): void { 'auto.lib.agent.skill.cli.prerequisite.windowsPathUnknown', 'Orca could not check your Windows user PATH' ), - { description: status.detail ?? 'Refresh CLI registration status and try again.' } + { + description: + status.detail ?? + translate( + 'auto.lib.agent.skill.cli.prerequisite.refreshCliRegistration', + 'Refresh CLI registration status and try again.' + ) + } ) return } diff --git a/src/renderer/src/lib/worktree-activation.ts b/src/renderer/src/lib/worktree-activation.ts index c5b57fd1a9d..7e317024123 100644 --- a/src/renderer/src/lib/worktree-activation.ts +++ b/src/renderer/src/lib/worktree-activation.ts @@ -19,6 +19,7 @@ import { buildSetupRunnerCommand } from './setup-runner' import { createSequencedSetupAgentCommands } from '../../../shared/setup-agent-sequencing' import { getSetupRunnerCommandPlatformForPath } from '../../../shared/setup-runner-command' import { agentKindToTuiAgent } from '../../../shared/agent-kind' +import { translate } from '@/i18n/i18n' import { useAppStore } from '@/store' import type { PendingSidebarWorktreeReveal } from '@/store/slices/ui' import { tabHasLivePty } from '@/lib/tab-has-live-pty' @@ -231,7 +232,13 @@ export function activateAndRevealFolderWorkspace( { runtimeEnvironmentId } ) if (folderWorkspaceActivationBlocked(pathStatus)) { - toast.error(getFolderWorkspacePathStatusTitle(pathStatus) ?? 'Cannot open folder workspace', { + const title = + getFolderWorkspacePathStatusTitle(pathStatus) ?? + translate( + 'auto.lib.worktree.activation.cannotOpenFolderWorkspace', + 'Cannot open folder workspace' + ) + toast.error(title, { description: getFolderWorkspacePathStatusDescription(pathStatus) ?? folderWorkspace.folderPath }) return false