From 6855d63ec6757cdb64f2b0259803cdff7dd8c52b Mon Sep 17 00:00:00 2001 From: 777genius Date: Sun, 24 May 2026 15:33:51 +0300 Subject: [PATCH] feat(i18n): add localization foundation Refs https://github.com/777genius/agent-teams-ai/issues/139 --- i18next.config.ts | 29 + package.json | 7 + pnpm-lock.yaml | 930 ++- scripts/i18n/validate.ts | 145 + .../renderer/ui/GraphActivityHud.tsx | 8 +- .../renderer/ui/GraphBlockingEdgePopover.tsx | 14 +- .../renderer/ui/GraphMemberLogPreviewHud.tsx | 10 +- .../renderer/ui/GraphNodePopover.tsx | 61 +- .../renderer/ui/GraphProvisioningHud.tsx | 10 +- .../localization/contracts/appLocale.ts | 19 + src/features/localization/contracts/index.ts | 11 + .../localization/contracts/namespaces.ts | 13 + .../core/application/resolveRuntimeLocale.ts | 15 + .../validateTranslationCatalogs.ts | 7 + .../localization/core/domain/catalogPolicy.ts | 203 + .../localization/core/domain/localePolicy.ts | 43 + src/features/localization/index.ts | 11 + .../adapters/browserSystemLocaleAdapter.ts | 3 + .../composition/createI18nextInstance.ts | 35 + .../composition/localizationResources.ts | 34 + .../renderer/hooks/useAppTranslation.ts | 17 + .../renderer/hooks/useLocaleFormatters.ts | 54 + .../localization/renderer/i18next.d.ts | 10 + src/features/localization/renderer/index.ts | 4 + .../renderer/locales/en/common.json | 900 +++ .../renderer/locales/en/dashboard.json | 197 + .../renderer/locales/en/errors.json | 3 + .../renderer/locales/en/extensions.json | 684 +++ .../renderer/locales/en/report.json | 217 + .../renderer/locales/en/settings.json | 983 +++ .../renderer/locales/en/team.json | 2415 ++++++++ .../renderer/locales/ru/common.json | 900 +++ .../renderer/locales/ru/dashboard.json | 197 + .../renderer/locales/ru/errors.json | 3 + .../renderer/locales/ru/extensions.json | 684 +++ .../renderer/locales/ru/report.json | 217 + .../renderer/locales/ru/settings.json | 983 +++ .../renderer/locales/ru/team.json | 2415 ++++++++ .../localization/renderer/resources.d.ts | 5402 +++++++++++++++++ .../renderer/ui/AppLanguageSelect.tsx | 64 + .../renderer/ui/LocalizationProvider.tsx | 40 + .../adapters/MemberLogStreamSection.tsx | 14 +- .../renderer/ui/ExecutionLogStreamView.tsx | 4 +- .../ui/MemberRuntimeProcessLogsPanel.tsx | 13 +- .../renderer/ui/MemberWorkSyncDetails.tsx | 31 +- .../renderer/ui/MemberWorkSyncStatusPanel.tsx | 11 +- .../renderer/ui/RecentProjectCard.tsx | 23 +- .../renderer/ui/RecentProjectsSection.tsx | 25 +- .../renderer/ui/RunningTeamsSection.tsx | 4 +- .../ui/RuntimeProviderManagementPanelView.tsx | 215 +- .../renderer/ui/TmuxInstallerBannerView.tsx | 41 +- src/main/ipc/configValidation.ts | 8 + .../services/infrastructure/ConfigManager.ts | 4 + src/renderer/App.tsx | 20 +- src/renderer/components/chat/AIChatGroup.tsx | 4 +- src/renderer/components/chat/ChatHistory.tsx | 12 +- .../components/chat/ChatHistoryEmptyState.tsx | 11 +- .../components/chat/CompactBoundary.tsx | 19 +- src/renderer/components/chat/ContextBadge.tsx | 63 +- .../components/chat/DisplayItemList.tsx | 13 +- .../components/chat/LastOutputDisplay.tsx | 8 +- .../DirectoryTree/DirectoryTreeNode.tsx | 6 +- .../components/ClaudeMdFilesSection.tsx | 5 +- .../components/ClaudeMdSection.tsx | 6 +- .../components/CollapsibleSection.tsx | 5 +- .../components/FlatInjectionList.tsx | 4 +- .../components/MentionedFilesSection.tsx | 5 +- .../components/RankedInjectionList.tsx | 4 +- .../components/SessionContextHeader.tsx | 44 +- .../components/SessionContextHelpTooltip.tsx | 22 +- .../components/TaskCoordinationSection.tsx | 5 +- .../components/ThinkingTextSection.tsx | 5 +- .../components/ToolOutputsSection.tsx | 5 +- .../components/UserMessagesSection.tsx | 5 +- .../chat/SessionContextPanel/index.tsx | 8 +- .../items/ClaudeMdItem.tsx | 4 +- .../items/MentionedFileItem.tsx | 12 +- .../items/TaskCoordinationItem.tsx | 12 +- .../items/ThinkingTextItem.tsx | 14 +- .../items/ToolBreakdownItem.tsx | 5 +- .../items/ToolOutputItem.tsx | 12 +- .../items/UserMessageItem.tsx | 10 +- .../components/chat/SystemChatGroup.tsx | 4 +- .../components/chat/UserChatGroup.tsx | 18 +- .../components/chat/items/ExecutionTrace.tsx | 18 +- .../components/chat/items/LinkedToolItem.tsx | 14 +- .../components/chat/items/MetricsPill.tsx | 8 +- .../components/chat/items/SubagentItem.tsx | 39 +- .../chat/items/TeammateMessageItem.tsx | 12 +- .../items/linkedTool/DefaultToolViewer.tsx | 16 +- .../chat/items/linkedTool/EditToolViewer.tsx | 6 +- .../chat/items/linkedTool/ReadToolViewer.tsx | 8 +- .../chat/items/linkedTool/SkillToolViewer.tsx | 8 +- .../items/linkedTool/ToolErrorDisplay.tsx | 5 +- .../chat/items/linkedTool/WriteToolViewer.tsx | 10 +- .../chat/items/linkedTool/renderHelpers.tsx | 33 +- src/renderer/components/chat/session-panel.ts | 1 + .../chat/viewers/CodeBlockViewer.tsx | 6 +- .../components/chat/viewers/DiffViewer.tsx | 8 +- .../chat/viewers/MarkdownViewer.tsx | 45 +- .../chat/viewers/MermaidDiagram.tsx | 4 +- .../common/CliInstallWarningBanner.tsx | 4 +- .../components/common/ConfirmDialog.tsx | 4 +- .../common/ContextSwitchOverlay.tsx | 10 +- src/renderer/components/common/CopyButton.tsx | 6 +- .../components/common/ErrorBoundary.tsx | 57 +- .../components/common/ExportDropdown.tsx | 6 +- .../components/common/OngoingIndicator.tsx | 5 +- .../common/ProviderActivityStatusStrip.tsx | 15 +- .../components/common/RepositoryDropdown.tsx | 10 +- .../components/common/TokenUsageDisplay.tsx | 66 +- .../components/common/UpdateBanner.tsx | 8 +- .../components/common/UpdateDialog.tsx | 18 +- .../components/common/WorkspaceIndicator.tsx | 4 +- .../components/dashboard/CliStatusBanner.tsx | 639 +- .../dashboard/DashboardUpdateBanner.tsx | 6 +- .../components/dashboard/DashboardView.tsx | 13 +- .../components/dashboard/WebPreviewBanner.tsx | 11 +- .../dashboard/WindowsAdministratorBanner.tsx | 9 +- .../extensions/ExtensionStoreView.tsx | 170 +- .../extensions/apikeys/ApiKeyCard.tsx | 4 +- .../extensions/apikeys/ApiKeyFormDialog.tsx | 76 +- .../extensions/apikeys/ApiKeysPanel.tsx | 24 +- .../extensions/common/InstallButton.tsx | 14 +- .../extensions/mcp/CustomMcpServerDialog.tsx | 60 +- .../extensions/mcp/McpServerCard.tsx | 22 +- .../extensions/mcp/McpServerDetailDialog.tsx | 69 +- .../extensions/mcp/McpServersPanel.tsx | 77 +- .../extensions/plugins/PluginCard.tsx | 4 +- .../extensions/plugins/PluginDetailDialog.tsx | 58 +- .../extensions/plugins/PluginsPanel.tsx | 82 +- .../extensions/skills/SkillDetailDialog.tsx | 94 +- .../extensions/skills/SkillEditorDialog.tsx | 168 +- .../extensions/skills/SkillImportDialog.tsx | 88 +- .../extensions/skills/SkillReviewDialog.tsx | 45 +- .../extensions/skills/SkillsPanel.tsx | 152 +- .../components/layout/CustomTitleBar.tsx | 16 +- src/renderer/components/layout/MoreMenu.tsx | 26 +- .../components/layout/PaneContent.tsx | 23 +- src/renderer/components/layout/PaneView.tsx | 4 +- .../components/layout/SessionTabContent.tsx | 12 +- src/renderer/components/layout/Sidebar.tsx | 12 +- .../components/layout/SortableTab.tsx | 10 +- src/renderer/components/layout/TabBar.tsx | 8 +- .../components/layout/TabBarActions.tsx | 26 +- src/renderer/components/layout/TabBarRow.tsx | 6 +- .../components/layout/TabContextMenu.tsx | 29 +- .../components/layout/TeamTabSectionNav.tsx | 20 +- .../notifications/NotificationRow.tsx | 13 +- .../notifications/NotificationsView.tsx | 48 +- .../components/report/SessionReportTab.tsx | 6 +- .../report/sections/CostSection.tsx | 79 +- .../report/sections/ErrorSection.tsx | 25 +- .../report/sections/FrictionSection.tsx | 22 +- .../components/report/sections/GitSection.tsx | 17 +- .../report/sections/InsightsSection.tsx | 34 +- .../report/sections/KeyTakeawaysSection.tsx | 5 +- .../report/sections/OverviewSection.tsx | 24 +- .../report/sections/QualitySection.tsx | 56 +- .../report/sections/SubagentSection.tsx | 22 +- .../report/sections/TimelineSection.tsx | 22 +- .../report/sections/TokenSection.tsx | 30 +- .../report/sections/ToolSection.tsx | 20 +- .../runtime/CodexLoginLinkCopyButton.tsx | 19 +- .../runtime/ProviderModelBadges.tsx | 29 +- .../ProviderRuntimeBackendSelector.tsx | 85 +- .../runtime/ProviderRuntimeSettingsDialog.tsx | 641 +- .../runtime/providerConnectionUi.ts | 521 +- .../components/schedules/SchedulesView.tsx | 72 +- .../components/search/CommandPalette.tsx | 64 +- src/renderer/components/search/SearchBar.tsx | 22 +- .../components/AddTriggerForm.tsx | 14 +- .../components/ColorPaletteSelector.tsx | 8 +- .../components/DynamicConfigSection.tsx | 44 +- .../components/GeneralInfoSection.tsx | 14 +- .../components/IgnorePatternsSection.tsx | 13 +- .../components/ModeSelector.tsx | 8 +- .../components/RepositoryScopeSection.tsx | 13 +- .../components/TriggerCardHeader.tsx | 25 +- .../components/TriggerConfiguration.tsx | 61 +- .../components/TriggerPreview.tsx | 22 +- .../hooks/useTriggerForm.ts | 19 +- .../NotificationTriggerSettings/index.tsx | 16 +- .../NotificationTriggerSettings/types.ts | 1 + .../utils/constants.ts | 70 +- .../components/settings/SettingsTabs.tsx | 37 +- .../components/settings/SettingsView.tsx | 12 +- .../settings/hooks/useSettingsConfig.ts | 2 + .../settings/hooks/useSettingsHandlers.ts | 10 + .../settings/sections/AdvancedSection.tsx | 40 +- .../settings/sections/CliStatusSection.tsx | 107 +- .../settings/sections/ConfigEditorDialog.tsx | 34 +- .../settings/sections/ConnectionSection.tsx | 73 +- .../settings/sections/GeneralSection.tsx | 192 +- .../sections/NotificationsSection.tsx | 195 +- .../settings/sections/WorkspaceSection.tsx | 68 +- .../sidebar/DateGroupedSessions.tsx | 78 +- .../components/sidebar/GlobalTaskList.tsx | 72 +- .../sidebar/SessionFiltersPopover.tsx | 10 +- .../components/sidebar/SessionItem.tsx | 18 +- .../components/sidebar/SidebarTaskItem.tsx | 52 +- .../components/sidebar/TaskContextMenu.tsx | 17 +- .../components/sidebar/TaskFiltersPopover.tsx | 46 +- .../components/sidebar/taskFiltersState.ts | 16 +- .../team/ClaudeLogsFilterPopover.tsx | 24 +- .../components/team/ClaudeLogsPanel.tsx | 34 +- .../components/team/ClaudeLogsSection.tsx | 10 +- .../team/LiveRuntimeStatusBridge.tsx | 4 +- .../team/LiveRuntimeStatusSection.tsx | 39 +- .../components/team/ProcessesSection.tsx | 22 +- .../team/ProvisioningProgressBlock.tsx | 45 +- src/renderer/components/team/RoleSelect.tsx | 65 +- src/renderer/components/team/TaskTooltip.tsx | 6 +- .../components/team/TeamChangesSection.tsx | 76 +- .../components/team/TeamDetailView.tsx | 571 +- .../components/team/TeamEmptyState.tsx | 14 +- .../components/team/TeamListFilterPopover.tsx | 16 +- src/renderer/components/team/TeamListView.tsx | 136 +- .../components/team/TeamSessionsSection.tsx | 32 +- .../components/team/TeamTaskStatusSummary.tsx | 13 +- .../team/ToolApprovalDiffPreview.tsx | 12 +- .../components/team/ToolApprovalSheet.tsx | 16 +- .../team/activity/ActiveTasksBlock.tsx | 4 +- .../components/team/activity/ActivityItem.tsx | 128 +- .../team/activity/ActivityTimeline.tsx | 66 +- .../team/activity/LeadThoughtsGroup.tsx | 18 +- .../team/activity/MessageExpandDialog.tsx | 11 +- .../team/activity/PendingRepliesBlock.tsx | 20 +- .../team/activity/ReplyQuoteBlock.tsx | 6 +- .../team/activity/ThoughtBodyContent.tsx | 6 +- .../team/attachments/AttachmentDisplay.tsx | 4 +- .../team/attachments/DropZoneOverlay.tsx | 5 +- .../attachments/SourceMessageAttachments.tsx | 5 +- .../components/team/context-metric-alias.ts | 2 + .../team/dialogs/AddMemberDialog.tsx | 10 +- .../team/dialogs/AdvancedCliSection.tsx | 30 +- .../dialogs/AnthropicExtraUsageWarning.tsx | 34 +- .../dialogs/AnthropicFastModeSelector.tsx | 25 +- .../team/dialogs/CodexFastModeSelector.tsx | 18 +- .../team/dialogs/CodexReconnectPrompt.tsx | 7 +- .../team/dialogs/CreateTaskDialog.tsx | 75 +- .../team/dialogs/CreateTeamDialog.tsx | 176 +- .../team/dialogs/EditTeamDialog.tsx | 147 +- .../team/dialogs/EffortLevelSelector.tsx | 7 +- .../team/dialogs/GlobalTaskDetailDialog.tsx | 4 +- .../team/dialogs/LaunchTeamDialog.tsx | 213 +- .../team/dialogs/LimitContextCheckbox.tsx | 66 +- .../team/dialogs/MembersJsonEditor.tsx | 4 +- .../dialogs/OpenCodeContextConfigHint.tsx | 26 +- .../team/dialogs/OptionalSettingsSection.tsx | 4 +- .../team/dialogs/ProjectPathSelector.tsx | 63 +- .../ProvisioningProviderStatusList.tsx | 258 +- .../components/team/dialogs/ReviewDialog.tsx | 14 +- .../team/dialogs/SendMessageDialog.tsx | 45 +- .../team/dialogs/SkipPermissionsCheckbox.tsx | 96 +- .../team/dialogs/StatusHistoryTimeline.tsx | 79 +- .../team/dialogs/TaskAttachments.tsx | 10 +- .../team/dialogs/TaskCommentAwaitingReply.tsx | 12 +- .../team/dialogs/TaskCommentInput.tsx | 25 +- .../team/dialogs/TaskCommentsSection.tsx | 52 +- .../team/dialogs/TaskDetailDialog.tsx | 123 +- .../team/dialogs/TeamModelSelector.tsx | 348 +- .../TeammateRuntimeCompatibilityNotice.tsx | 5 +- .../dialogs/ToolApprovalSettingsPanel.tsx | 64 +- .../dialogs/WorktreeGitReadinessBanner.tsx | 19 +- .../team/editor/EditorBinaryPlaceholder.tsx | 6 +- .../team/editor/EditorEmptyState.tsx | 27 +- .../team/editor/EditorErrorBoundary.tsx | 31 +- .../team/editor/EditorErrorState.tsx | 6 +- .../components/team/editor/EditorFileTree.tsx | 27 +- .../team/editor/EditorImagePreview.tsx | 8 +- .../team/editor/EditorSearchPanel.tsx | 28 +- .../team/editor/EditorShortcutsHelp.tsx | 157 +- .../team/editor/EditorStatusBar.tsx | 22 +- .../components/team/editor/EditorTabBar.tsx | 4 +- .../components/team/editor/EditorToolbar.tsx | 22 +- .../components/team/editor/GoToLineDialog.tsx | 12 +- .../components/team/editor/NewFileDialog.tsx | 28 +- .../team/editor/ProjectEditorOverlay.tsx | 85 +- .../team/editor/QuickOpenDialog.tsx | 12 +- .../team/editor/SearchInFilesPanel.tsx | 32 +- .../components/team/kanban/KanbanBoard.tsx | 39 +- .../team/kanban/KanbanFilterPopover.tsx | 34 +- .../team/kanban/KanbanGridLayout.tsx | 8 +- .../team/kanban/KanbanSearchInput.tsx | 17 +- .../team/kanban/KanbanSortPopover.tsx | 44 +- .../components/team/kanban/KanbanTaskCard.tsx | 49 +- .../components/team/kanban/TrashDialog.tsx | 21 +- .../components/team/lead-load-guards.ts | 1 + .../team/members/CurrentTaskIndicator.tsx | 4 +- .../components/team/members/LeadModelRow.tsx | 27 +- .../components/team/members/MemberCard.tsx | 27 +- .../team/members/MemberDetailDialog.tsx | 26 +- .../team/members/MemberDetailHeader.tsx | 4 +- .../team/members/MemberDraftRow.tsx | 115 +- .../team/members/MemberExecutionLog.tsx | 20 +- .../team/members/MemberHoverCard.tsx | 4 +- .../components/team/members/MemberList.tsx | 18 +- .../components/team/members/MemberLogsTab.tsx | 33 +- .../team/members/MemberMessagesTab.tsx | 28 +- .../team/members/MemberStatsTab.tsx | 53 +- .../team/members/MemberTasksTab.tsx | 4 +- .../team/members/MembersEditorSection.tsx | 20 +- .../members/SubagentRecentMessagesPreview.tsx | 12 +- .../team/messages/ActionModeSelector.tsx | 4 +- .../team/messages/MessageComposer.tsx | 92 +- .../team/messages/MessagesFilterPopover.tsx | 24 +- .../team/messages/MessagesPanel.tsx | 109 +- .../team/messages/OpenCodeDeliveryWarning.tsx | 74 +- .../components/team/messages/StatusBlock.tsx | 4 +- .../components/team/provisioningSteps.ts | 8 +- .../team/review/ChangeReviewDialog.tsx | 23 +- .../team/review/ChangesLoadingAnimation.tsx | 34 +- .../team/review/CodeMirrorDiffView.tsx | 22 +- .../team/review/ConfidenceBadge.tsx | 21 +- .../components/team/review/ConflictDialog.tsx | 18 +- .../team/review/ContinuousScrollView.tsx | 4 +- .../team/review/DiffErrorBoundary.tsx | 45 +- .../team/review/FileEditTimeline.tsx | 5 +- .../team/review/FileSectionDiff.tsx | 6 +- .../team/review/FileSectionHeader.tsx | 108 +- .../team/review/FileSectionPlaceholder.tsx | 52 +- .../team/review/FullDiffLoadingBanner.tsx | 30 +- .../team/review/KeyboardShortcutsHelp.tsx | 37 +- .../components/team/review/ReviewFileTree.tsx | 35 +- .../components/team/review/ReviewToolbar.tsx | 40 +- .../team/review/ScopeWarningBanner.tsx | 100 +- .../team/review/ViewedProgressBar.tsx | 8 +- .../team/schedule/CronScheduleInput.tsx | 51 +- .../team/schedule/ScheduleEmptyState.tsx | 26 +- .../team/schedule/ScheduleRunLogDialog.tsx | 20 +- .../team/schedule/ScheduleSection.tsx | 27 +- .../team/schedule/ScheduleStatusBadge.tsx | 79 +- .../team/session-injection-types.ts | 1 + .../team/taskLogs/ExactTaskLogCard.tsx | 6 +- .../team/taskLogs/ExactTaskLogsSection.tsx | 21 +- .../taskLogs/ExecutionSessionsSection.tsx | 10 +- .../team/taskLogs/TaskActivitySection.tsx | 20 +- .../team/taskLogs/TaskLogStreamSection.tsx | 4 +- .../components/team/tasks/TaskList.tsx | 35 +- .../team/useTeamProvisioningPresentation.ts | 5 +- .../components/terminal/TerminalModal.tsx | 35 +- .../components/ui/ChipInteractionLayer.tsx | 13 +- .../components/ui/ExpandableContent.tsx | 6 +- src/renderer/components/ui/MemberSelect.tsx | 12 +- .../components/ui/MentionSuggestionList.tsx | 18 +- src/renderer/components/ui/dialog.tsx | 53 +- .../components/ui/tiptap/TiptapBubbleMenu.tsx | 10 +- src/renderer/hooks/useOptionalTabId.ts | 1 + .../utils/teamProvisioningPresentation.ts | 551 +- src/shared/types/notifications.ts | 2 + .../localization/core/catalogPolicy.test.ts | 51 + .../localization/core/localePolicy.test.ts | 30 + test/main/ipc/configValidation.test.ts | 16 + test/renderer/store/extensionsSlice.test.ts | 5 +- 355 files changed, 26205 insertions(+), 4964 deletions(-) create mode 100644 i18next.config.ts create mode 100644 scripts/i18n/validate.ts create mode 100644 src/features/localization/contracts/appLocale.ts create mode 100644 src/features/localization/contracts/index.ts create mode 100644 src/features/localization/contracts/namespaces.ts create mode 100644 src/features/localization/core/application/resolveRuntimeLocale.ts create mode 100644 src/features/localization/core/application/validateTranslationCatalogs.ts create mode 100644 src/features/localization/core/domain/catalogPolicy.ts create mode 100644 src/features/localization/core/domain/localePolicy.ts create mode 100644 src/features/localization/index.ts create mode 100644 src/features/localization/renderer/adapters/browserSystemLocaleAdapter.ts create mode 100644 src/features/localization/renderer/composition/createI18nextInstance.ts create mode 100644 src/features/localization/renderer/composition/localizationResources.ts create mode 100644 src/features/localization/renderer/hooks/useAppTranslation.ts create mode 100644 src/features/localization/renderer/hooks/useLocaleFormatters.ts create mode 100644 src/features/localization/renderer/i18next.d.ts create mode 100644 src/features/localization/renderer/index.ts create mode 100644 src/features/localization/renderer/locales/en/common.json create mode 100644 src/features/localization/renderer/locales/en/dashboard.json create mode 100644 src/features/localization/renderer/locales/en/errors.json create mode 100644 src/features/localization/renderer/locales/en/extensions.json create mode 100644 src/features/localization/renderer/locales/en/report.json create mode 100644 src/features/localization/renderer/locales/en/settings.json create mode 100644 src/features/localization/renderer/locales/en/team.json create mode 100644 src/features/localization/renderer/locales/ru/common.json create mode 100644 src/features/localization/renderer/locales/ru/dashboard.json create mode 100644 src/features/localization/renderer/locales/ru/errors.json create mode 100644 src/features/localization/renderer/locales/ru/extensions.json create mode 100644 src/features/localization/renderer/locales/ru/report.json create mode 100644 src/features/localization/renderer/locales/ru/settings.json create mode 100644 src/features/localization/renderer/locales/ru/team.json create mode 100644 src/features/localization/renderer/resources.d.ts create mode 100644 src/features/localization/renderer/ui/AppLanguageSelect.tsx create mode 100644 src/features/localization/renderer/ui/LocalizationProvider.tsx create mode 100644 src/renderer/components/chat/session-panel.ts create mode 100644 src/renderer/components/team/context-metric-alias.ts create mode 100644 src/renderer/components/team/lead-load-guards.ts create mode 100644 src/renderer/components/team/session-injection-types.ts create mode 100644 src/renderer/hooks/useOptionalTabId.ts create mode 100644 test/features/localization/core/catalogPolicy.test.ts create mode 100644 test/features/localization/core/localePolicy.test.ts diff --git a/i18next.config.ts b/i18next.config.ts new file mode 100644 index 00000000..a8c56bbb --- /dev/null +++ b/i18next.config.ts @@ -0,0 +1,29 @@ +import { defineConfig } from 'i18next-cli'; + +import { + DEFAULT_TRANSLATION_NAMESPACE, + FALLBACK_APP_LOCALE, + RESOLVED_APP_LOCALES, +} from './src/features/localization/contracts'; + +export default defineConfig({ + locales: [...RESOLVED_APP_LOCALES], + extract: { + defaultNS: DEFAULT_TRANSLATION_NAMESPACE, + input: ['src/**/*.{ts,tsx}'], + ignore: ['src/**/*.test.{ts,tsx}', 'src/**/__tests__/**'], + output: 'src/features/localization/renderer/locales/{{language}}/{{namespace}}.json', + primaryLanguage: FALLBACK_APP_LOCALE, + sort: true, + useTranslationNames: ['useTranslation', { name: 'useAppTranslation', nsArg: 0 }], + }, + lint: { + ignore: ['src/**/*.test.{ts,tsx}', 'src/**/__tests__/**'], + }, + types: { + basePath: `src/features/localization/renderer/locales/${FALLBACK_APP_LOCALE}`, + input: [`src/features/localization/renderer/locales/${FALLBACK_APP_LOCALE}/*.json`], + output: 'src/features/localization/renderer/i18next.d.ts', + resourcesFile: 'src/features/localization/renderer/resources.d.ts', + }, +}); diff --git a/package.json b/package.json index ac084a39..f10e6bf4 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,10 @@ "test:semantic": "tsx test/test-semantic-steps.ts", "test:noise": "tsx test/test-noise-filtering.ts", "test:task-filtering": "tsx test/test-task-filtering.ts", + "i18n:extract": "i18next-cli extract --with-types", + "i18n:status": "i18next-cli status", + "i18n:validate": "tsx scripts/i18n/validate.ts", + "i18n:types": "i18next-cli types --quiet", "test": "vitest run", "test:ci": "vitest run --maxWorkers 1 --minWorkers 1", "test:task-change-ledger": "vitest run test/main/services/team/TaskChangeLedgerReader.test.ts test/main/services/team/taskChangeLedgerFixtures.integration.test.ts test/main/services/team/ReviewApplierService.test.ts test/main/services/team/FileContentResolver.test.ts test/main/services/team/ChangeExtractorService.test.ts test/renderer/store/changeReviewSlice.test.ts test/renderer/utils/reviewKey.test.ts test/main/services/team/TeamLogSourceTracker.test.ts test/main/services/team/stallMonitor/TeamTaskLogFreshnessReader.test.ts", @@ -164,6 +168,7 @@ "fast-json-stringify": "^6.4.0", "fastify": "^5.8.5", "highlight.js": "^11.11.1", + "i18next": "26.2.0", "idb-keyval": "^6.2.2", "isbinaryfile": "^6.0.0", "json-schema-ref-resolver": "^3.0.0", @@ -178,6 +183,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-grid-layout": "^2.2.2", + "react-i18next": "17.0.8", "react-markdown": "^10.1.0", "react-modal-sheet": "5.6.0", "react-resizable": "^3.1.3", @@ -232,6 +238,7 @@ "globals": "^17.2.0", "happy-dom": "^20.9.0", "husky": "^9.1.7", + "i18next-cli": "1.58.0", "knip": "^5.82.1", "lint-staged": "^16.2.7", "postcss": "^8.5.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ba38e32b..2793d188 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -275,6 +275,9 @@ importers: highlight.js: specifier: ^11.11.1 version: 11.11.1 + i18next: + specifier: 26.2.0 + version: 26.2.0(typescript@5.9.3) idb-keyval: specifier: ^6.2.2 version: 6.2.2 @@ -317,6 +320,9 @@ importers: react-grid-layout: specifier: ^2.2.2 version: 2.2.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-i18next: + specifier: 17.0.8 + version: 17.0.8(i18next@26.2.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) react-markdown: specifier: ^10.1.0 version: 10.1.0(@types/react@19.2.14)(react@19.2.4) @@ -377,7 +383,7 @@ importers: version: 4.0.4 '@eslint-community/eslint-plugin-eslint-comments': specifier: ^4.6.0 - version: 4.6.0(eslint@9.39.4(jiti@2.7.0)) + version: 4.6.0(eslint@9.39.4(jiti@1.21.7)) '@eslint/js': specifier: ^9.39.2 version: 9.39.2 @@ -410,10 +416,10 @@ importers: version: 1.15.5 '@vitejs/plugin-react': specifier: ^4.3.1 - version: 4.7.0(vite@6.4.2(@types/node@25.0.7)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.7.0(vite@6.4.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) '@vitest/coverage-v8': specifier: ^3.1.4 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.9.0)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.9.0)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) autoprefixer: specifier: ^10.4.17 version: 10.4.23(postcss@8.5.10) @@ -425,43 +431,43 @@ importers: version: 26.8.1(electron-builder-squirrel-windows@26.8.1) electron-vite: specifier: ^5.0.0 - version: 5.0.0(vite@6.4.2(@types/node@25.0.7)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 5.0.0(@swc/core@1.15.33)(vite@6.4.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) eslint: specifier: ^9.39.4 - version: 9.39.4(jiti@2.7.0) + version: 9.39.4(jiti@1.21.7) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.4(jiti@2.7.0)) + version: 10.1.8(eslint@9.39.4(jiti@1.21.7)) eslint-import-resolver-typescript: specifier: ^4.4.4 - version: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) + version: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@1.21.7)))(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)) eslint-plugin-boundaries: specifier: ^5.3.1 - version: 5.3.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)) + version: 5.3.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@1.21.7)) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)) + version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@1.21.7)) eslint-plugin-jsx-a11y: specifier: ^6.10.2 - version: 6.10.2(eslint@9.39.4(jiti@2.7.0)) + version: 6.10.2(eslint@9.39.4(jiti@1.21.7)) eslint-plugin-react: specifier: ^7.37.5 - version: 7.37.5(eslint@9.39.4(jiti@2.7.0)) + version: 7.37.5(eslint@9.39.4(jiti@1.21.7)) eslint-plugin-react-hooks: specifier: ^7.0.1 - version: 7.0.1(eslint@9.39.4(jiti@2.7.0)) + version: 7.0.1(eslint@9.39.4(jiti@1.21.7)) eslint-plugin-react-refresh: specifier: ^0.4.26 - version: 0.4.26(eslint@9.39.4(jiti@2.7.0)) + version: 0.4.26(eslint@9.39.4(jiti@1.21.7)) eslint-plugin-security: specifier: ^3.0.1 version: 3.0.1 eslint-plugin-simple-import-sort: specifier: ^12.1.1 - version: 12.1.1(eslint@9.39.4(jiti@2.7.0)) + version: 12.1.1(eslint@9.39.4(jiti@1.21.7)) eslint-plugin-sonarjs: specifier: ^3.0.6 - version: 3.0.6(eslint@9.39.4(jiti@2.7.0)) + version: 3.0.6(eslint@9.39.4(jiti@1.21.7)) eslint-plugin-tailwindcss: specifier: ^3.18.2 version: 3.18.2(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.9.0)) @@ -474,6 +480,9 @@ importers: husky: specifier: ^9.1.7 version: 9.1.7 + i18next-cli: + specifier: 1.58.0 + version: 1.58.0(@types/node@25.0.7)(i18next@26.2.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(typescript@5.9.3) knip: specifier: ^5.82.1 version: 5.82.1(@types/node@25.0.7)(typescript@5.9.3) @@ -500,13 +509,13 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.54.0 - version: 8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + version: 8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) vite: specifier: ^6.4.2 - version: 6.4.2(@types/node@25.0.7)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + version: 6.4.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) vitest: specifier: ^3.1.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.9.0)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.9.0)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) agent-teams-controller: {} @@ -612,7 +621,7 @@ importers: version: 22.19.15 tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@2.7.0)(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) + version: 8.5.1(@swc/core@1.15.33)(jiti@2.7.0)(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) tsx: specifier: ^4.21.0 version: 4.21.0 @@ -809,6 +818,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} @@ -978,6 +991,12 @@ packages: '@colordx/core@5.4.3': resolution: {integrity: sha512-kIxYSfA5T8HXjav55UaaH/o/cKivF6jCCGIb8eqtcsfI46wsvlSiT8jMDyrl779qLec3c2c2oHBZo4oAhvbjrQ==} + '@croct/json5-parser@0.2.2': + resolution: {integrity: sha512-0NJMLrbeLbQ0eCVj3UoH/kG2QckUgOASfwmfDTjyW1xAYPyTNJXcWVT/dssJdTJd0pRchW+qF0VFWQHcxs1OVw==} + + '@croct/json@2.1.0': + resolution: {integrity: sha512-UrWfjNQVlBxN+OVcFwHmkjARMW55MBN04E9KfGac8ac8z1QnFVuiOOFtMWXCk3UwsyRqhsNaFoYLZC+xxqsVjQ==} + '@develar/schema-utils@2.6.5': resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==} engines: {node: '>= 8.9.0'} @@ -1877,6 +1896,140 @@ packages: peerDependencies: vue: '>=3' + '@inquirer/ansi@2.0.5': + resolution: {integrity: sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + + '@inquirer/checkbox@5.1.5': + resolution: {integrity: sha512-Jmf9tgBHIEK5SAOB7swYfStqmtkZb00xOTpSQmkoGEpdxOTpJi9RS0A8bkfDPHTTItZRJrRdZrEMu25wyj0VfQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@6.0.13': + resolution: {integrity: sha512-wkGPC7yJ5WJk1DJ5SX7fzk+gfj4BM8cf5dDDi71B/551xHrdsZVRJOC0WyikXd0pEsb/9cLniuE4atbsMqmFkw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@11.1.10': + resolution: {integrity: sha512-a4Q5BXHQAHa9eO202sTaFCHFYVB3x5fauDuThEAdZ9gfn76pSxiKU7wWcEH0N1O0XmQvNfQNU6QXpiRxmYQx+A==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@5.1.2': + resolution: {integrity: sha512-Y3Nor7S/DhIPo+8Ym/dSY4efwKI4BsflKDwXh0jNeXJsSF3dteS/3Yf+z4wkibVZDvYMyCgknSTQlNahfunGHg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@5.0.14': + resolution: {integrity: sha512-qyY9zcIX2eKYwaAUiQo9zORd61Lc3sXeM72fVbeHkYnDkqfr8/armcRbmVAIrExeJhI2puk+uomeKtWrpUVUmQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@3.0.0': + resolution: {integrity: sha512-lDSwMgg+M5rq6JKBYaJwSX6T9e/HK2qqZ1oxmOwn4AQoJE5D+7TumsxLGC02PWS//rkIVqbZv3XA3ejsc9FYvg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@2.0.5': + resolution: {integrity: sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + + '@inquirer/input@5.0.13': + resolution: {integrity: sha512-0l0jCHlJnXIV8CTxwQC0C+5Ziq8WP22edWgmciW2xYvoeoSck4v5FvCS1ctKdqLLR0dUo93uAHgWHywgBSoRyw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@4.0.13': + resolution: {integrity: sha512-WHmkYnnJAou5gx7RgcvAfUggnHNM1zWfoh0dFPl3dxVssuqt+dK5rIbaOYQXNyOegvFnopbKupjnhw2O8gANNg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@5.0.13': + resolution: {integrity: sha512-XDGu64ROHZjOOXLAANvJN7iIxWKhOSCG5VakrZ5kaScVR+snVJCFglD/hL3/677awtWcu4pXoWa280CDIYcBeg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@8.4.3': + resolution: {integrity: sha512-ai5LseTw9HhegupIgmo4cn7RpnCGznjjXu4OI+7jMR8vu7T1ZCCNMzFFAovUCjL1fl0cceksIN1++yQE59SmZw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@5.2.9': + resolution: {integrity: sha512-a1ErXEfgjfPYpyQ89dp+7n2IISjH9oQg3ygvF5adz8B7aHn4n2PjEgu1wpVTp69K3bj3lVLxP0qJ2b1clk1Whw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@4.1.9': + resolution: {integrity: sha512-ZlbM28Q9lmLkFPNAIv+ZuY530n5Km8U1WW48oYEvDhe9yc2uL3m3t+JSdRUkQlk5fuIuskgiIVjcb7czFzQpuA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@5.1.5': + resolution: {integrity: sha512-6SRg6kHfK/sjLXOsuqNebuir+sjwrf/iWuRUnXgB2slzEewppI1WfzeS16XxDcOQmXBruMmmB9Cgrz7wsAxqMg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@4.0.5': + resolution: {integrity: sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@intlify/bundle-utils@10.0.1': resolution: {integrity: sha512-WkaXfSevtpgtUR4t8K2M6lbR7g03mtOxFeh+vXp5KExvPqS12ppaRj1QxzwRuRI5VUto54A22BjKoBMLyHILWQ==} engines: {node: '>= 18'} @@ -4102,6 +4255,99 @@ packages: peerDependencies: eslint: ^9.0.0 || ^10.0.0 + '@swc/core-darwin-arm64@1.15.33': + resolution: {integrity: sha512-N+L0uXhuO7FIfzqwgxmzv0zIpV0qEp8wPX3QQs2p4atjMoywup2JTeDlXPw+z9pWJGCae3JjM+tZ6myclI+2gA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.33': + resolution: {integrity: sha512-/Il4QHSOhV4FekbsDtkrNmKbsX26oSysvgrRswa/RYOHXAkwXDbB4jaeKq6PsJLSPkzJ2KzQ061gtBnk0vNHfA==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.33': + resolution: {integrity: sha512-C64hBnBxq4viOPQ8hlx+2lJ23bzZBGnjw7ryALmS+0Q3zHmwO8lw1/DArLENw4Q18/0w5wdEO1k3m1wWNtKGqQ==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.33': + resolution: {integrity: sha512-TRJfnJbX3jqpxRDRoieMzRiCBS5jOmXNb3iQXmcgjFEHKLnAgK1RZRU8Cq1MsPqO4jAJp/ld1G4O3fXuxv85uw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-arm64-musl@1.15.33': + resolution: {integrity: sha512-il7tYM+CpUNzieQbwAjFT1P8zqAhmGWNAGhQZBnxurXZ0aNn+5nqYFTEUKNZl7QibtT0uQXzTZrNGHCIj6Y1Og==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@swc/core-linux-ppc64-gnu@1.15.33': + resolution: {integrity: sha512-ZtNBwN0Z7CFj9Il0FcPaKdjgP7URyKu/3RfH46vq+0paOBqLj4NYldD6Qo//Duif/7IOtAraUfDOmp0PLAufog==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-s390x-gnu@1.15.33': + resolution: {integrity: sha512-De1IyajoOmhOYYjw/lx66bKlyDpHZTueqwpDrWgf5O7T6d1ODeJJO9/OqMBmrBQc5C+dNnlmIufHsp4QVCWufA==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@swc/core-linux-x64-gnu@1.15.33': + resolution: {integrity: sha512-mGTH0YxmUN+x6vRN/I6NOk5X0ogNktkwPnJ94IMvR7QjhRDwL0O8RXEDhyUM0YtwWrryBOqaJQBX4zruxEPRGw==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-x64-musl@1.15.33': + resolution: {integrity: sha512-hj628ZkSEJf6zMf5VMbYrG2O6QqyTIp2qwY6VlCjvIa9lAEZ5c2lfPblCLVGYubTeLJDxadLB/CxqQYOQABeEQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@swc/core-win32-arm64-msvc@1.15.33': + resolution: {integrity: sha512-GV2oohtN2/5+KSccl86VULu3aT+LrISC8uzgSq0FRnikpD+Zwc+sBlXmoKQ+Db6jI57ITUOIB8jRkdGMABC29g==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.33': + resolution: {integrity: sha512-gtyvzSNR8DHKfFEA2uqb8Ld1myqi6uEg2jyeUq3ikn5ytYs7H8RpZYC8mdy4NXr8hfcdJfCLXPlYaqqfBXpoEQ==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.33': + resolution: {integrity: sha512-d6fRqQSkJI+kmMEBWaDQ7TMl8+YjLYbwRUPZQ9DY0ORBJeTzOrG0twvfvlZ2xgw6jA0ScQKgfBm4vHLSLl5Hqg==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.33': + resolution: {integrity: sha512-jOlwnFV2xhuuZeAUILGFULeR6vDPfijEJ57evfocwznQldLU3w2cZ9bSDryY9ip+AsM3r1NJKzf47V2NXebkeQ==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/types@0.1.26': + resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==} + '@szmarczak/http-timer@4.0.6': resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} @@ -5556,6 +5802,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + change-case@5.4.4: resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} @@ -5571,6 +5821,9 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + check-error@2.1.3: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} @@ -5622,6 +5875,10 @@ packages: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} + cli-spinners@3.4.0: + resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==} + engines: {node: '>=18.20'} + cli-truncate@2.1.0: resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} engines: {node: '>=8'} @@ -5630,6 +5887,10 @@ packages: resolution: {integrity: sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==} engines: {node: '>=20'} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -7285,6 +7546,9 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} @@ -7349,6 +7613,23 @@ packages: engines: {node: '>=18'} hasBin: true + i18next-cli@1.58.0: + resolution: {integrity: sha512-S5Nm6fE/HZzbuC5g8i2JQGq2CCVw7uSpDomY+PpqIglslEuQpje1rQ5r97ZDBdJebUZ4E5v7wf9FRLfGXDGjtQ==} + engines: {node: '>=22'} + hasBin: true + + i18next-resources-for-ts@2.1.0: + resolution: {integrity: sha512-n5UexwEVt0OoIAhG2MWpSnAVJW1U8mQrQTmXyxc5DMAx+NLhcLZhSMJo/FnUsA5JQ3obTYqTgB7YIuZKWpDgow==} + hasBin: true + + i18next@26.2.0: + resolution: {integrity: sha512-zwBHldHdTmwN7r6UNc7lC6GWNN+YYg3DrRSeHR5PRRBf5QnJZcYHrQc0uaU26qZeYxR7iFZD+Y315dPnKP47wA==} + peerDependencies: + typescript: ^5 || ^6 + peerDependenciesMeta: + typescript: + optional: true + iconv-corefoundation@1.1.7: resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==} engines: {node: ^8.11.2 || >=10} @@ -7414,6 +7695,15 @@ packages: inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + inquirer@13.4.3: + resolution: {integrity: sha512-EPd3IqieHSavSOXh+LZhrIkdQcOELWeRblLT6kslQr+cF9XTh/HxZdSt1YkHH1iq4dvqBnV42uwg2YlorgOy6g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -7545,6 +7835,10 @@ packages: resolution: {integrity: sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==} engines: {node: '>=18'} + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -7774,6 +8068,9 @@ packages: resolution: {integrity: sha512-1e4qoRgnn448pRuMvKGsFFymUCquZV0mpGgOyIKNgD3JVDTsVJyRBGH/Fm0tBb8WsWGgmB1mDe6/yJMQM37DUA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -7944,6 +8241,10 @@ packages: lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + log-symbols@7.0.1: + resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} + engines: {node: '>=18'} + log-update@6.1.0: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} @@ -8348,6 +8649,10 @@ packages: multimath@2.0.0: resolution: {integrity: sha512-toRx66cAMJ+Ccz7pMIg38xSIrtnbozk0dchXezwQDMgQmbGpfxjtv68H+L00iFL8hxDaVjrmwAFSb3I6bg8Q2g==} + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} + mux-embed@5.18.1: resolution: {integrity: sha512-ePsHjiEKY+FgrSBiMmaF+LOtTQSSBWv/1zqpREQFN96JE93xlsArT/MEi30yKOE06MgjOlL70YI750molu3y7g==} @@ -8599,6 +8904,10 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + ora@9.4.0: + resolution: {integrity: sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ==} + engines: {node: '>=20'} + orderedmap@2.1.1: resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} @@ -9324,6 +9633,22 @@ packages: react: '>= 16.3.0' react-dom: '>= 16.3.0' + react-i18next@17.0.8: + resolution: {integrity: sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==} + peerDependencies: + i18next: '>= 26.2.0' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + typescript: ^5 || ^6 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -9393,6 +9718,10 @@ packages: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} + react@19.2.6: + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} + engines: {node: '>=0.10.0'} + read-binary-file-arch@1.0.6: resolution: {integrity: sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==} hasBin: true @@ -9609,12 +9938,19 @@ packages: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} + run-async@4.0.6: + resolution: {integrity: sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==} + engines: {node: '>=0.12.0'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -9905,6 +10241,10 @@ packages: std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + stdin-discarder@0.3.2: + resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==} + engines: {node: '>=18'} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -10890,6 +11230,10 @@ packages: jsdom: optional: true + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} @@ -11445,6 +11789,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/runtime@7.29.2': {} + '@babel/template@7.28.6': dependencies: '@babel/code-frame': 7.29.0 @@ -11494,10 +11840,10 @@ snapshots: '@borewit/text-codec@0.2.1': {} - '@boundaries/elements@1.1.2(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0))': + '@boundaries/elements@1.1.2(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@1.21.7))': dependencies: eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@1.21.7)) handlebars: 4.7.9 is-core-module: 2.16.1 micromatch: 4.0.8 @@ -11800,6 +12146,12 @@ snapshots: '@colordx/core@5.4.3': {} + '@croct/json5-parser@0.2.2': + dependencies: + '@croct/json': 2.1.0 + + '@croct/json@2.1.0': {} + '@develar/schema-utils@2.6.5': dependencies: ajv: 6.14.0 @@ -12297,12 +12649,17 @@ snapshots: '@esbuild/win32-x64@0.28.0': optional: true - '@eslint-community/eslint-plugin-eslint-comments@4.6.0(eslint@9.39.4(jiti@2.7.0))': + '@eslint-community/eslint-plugin-eslint-comments@4.6.0(eslint@9.39.4(jiti@1.21.7))': dependencies: escape-string-regexp: 4.0.0 - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4(jiti@1.21.7) ignore: 7.0.5 + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@1.21.7))': + dependencies: + eslint: 9.39.4(jiti@1.21.7) + eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.7.0))': dependencies: eslint: 9.39.4(jiti@2.7.0) @@ -12492,6 +12849,125 @@ snapshots: '@iconify/types': 2.0.0 vue: 3.5.30(typescript@5.9.3) + '@inquirer/ansi@2.0.5': {} + + '@inquirer/checkbox@5.1.5(@types/node@25.0.7)': + dependencies: + '@inquirer/ansi': 2.0.5 + '@inquirer/core': 11.1.10(@types/node@25.0.7) + '@inquirer/figures': 2.0.5 + '@inquirer/type': 4.0.5(@types/node@25.0.7) + optionalDependencies: + '@types/node': 25.0.7 + + '@inquirer/confirm@6.0.13(@types/node@25.0.7)': + dependencies: + '@inquirer/core': 11.1.10(@types/node@25.0.7) + '@inquirer/type': 4.0.5(@types/node@25.0.7) + optionalDependencies: + '@types/node': 25.0.7 + + '@inquirer/core@11.1.10(@types/node@25.0.7)': + dependencies: + '@inquirer/ansi': 2.0.5 + '@inquirer/figures': 2.0.5 + '@inquirer/type': 4.0.5(@types/node@25.0.7) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.2 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 25.0.7 + + '@inquirer/editor@5.1.2(@types/node@25.0.7)': + dependencies: + '@inquirer/core': 11.1.10(@types/node@25.0.7) + '@inquirer/external-editor': 3.0.0(@types/node@25.0.7) + '@inquirer/type': 4.0.5(@types/node@25.0.7) + optionalDependencies: + '@types/node': 25.0.7 + + '@inquirer/expand@5.0.14(@types/node@25.0.7)': + dependencies: + '@inquirer/core': 11.1.10(@types/node@25.0.7) + '@inquirer/type': 4.0.5(@types/node@25.0.7) + optionalDependencies: + '@types/node': 25.0.7 + + '@inquirer/external-editor@3.0.0(@types/node@25.0.7)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 25.0.7 + + '@inquirer/figures@2.0.5': {} + + '@inquirer/input@5.0.13(@types/node@25.0.7)': + dependencies: + '@inquirer/core': 11.1.10(@types/node@25.0.7) + '@inquirer/type': 4.0.5(@types/node@25.0.7) + optionalDependencies: + '@types/node': 25.0.7 + + '@inquirer/number@4.0.13(@types/node@25.0.7)': + dependencies: + '@inquirer/core': 11.1.10(@types/node@25.0.7) + '@inquirer/type': 4.0.5(@types/node@25.0.7) + optionalDependencies: + '@types/node': 25.0.7 + + '@inquirer/password@5.0.13(@types/node@25.0.7)': + dependencies: + '@inquirer/ansi': 2.0.5 + '@inquirer/core': 11.1.10(@types/node@25.0.7) + '@inquirer/type': 4.0.5(@types/node@25.0.7) + optionalDependencies: + '@types/node': 25.0.7 + + '@inquirer/prompts@8.4.3(@types/node@25.0.7)': + dependencies: + '@inquirer/checkbox': 5.1.5(@types/node@25.0.7) + '@inquirer/confirm': 6.0.13(@types/node@25.0.7) + '@inquirer/editor': 5.1.2(@types/node@25.0.7) + '@inquirer/expand': 5.0.14(@types/node@25.0.7) + '@inquirer/input': 5.0.13(@types/node@25.0.7) + '@inquirer/number': 4.0.13(@types/node@25.0.7) + '@inquirer/password': 5.0.13(@types/node@25.0.7) + '@inquirer/rawlist': 5.2.9(@types/node@25.0.7) + '@inquirer/search': 4.1.9(@types/node@25.0.7) + '@inquirer/select': 5.1.5(@types/node@25.0.7) + optionalDependencies: + '@types/node': 25.0.7 + + '@inquirer/rawlist@5.2.9(@types/node@25.0.7)': + dependencies: + '@inquirer/core': 11.1.10(@types/node@25.0.7) + '@inquirer/type': 4.0.5(@types/node@25.0.7) + optionalDependencies: + '@types/node': 25.0.7 + + '@inquirer/search@4.1.9(@types/node@25.0.7)': + dependencies: + '@inquirer/core': 11.1.10(@types/node@25.0.7) + '@inquirer/figures': 2.0.5 + '@inquirer/type': 4.0.5(@types/node@25.0.7) + optionalDependencies: + '@types/node': 25.0.7 + + '@inquirer/select@5.1.5(@types/node@25.0.7)': + dependencies: + '@inquirer/ansi': 2.0.5 + '@inquirer/core': 11.1.10(@types/node@25.0.7) + '@inquirer/figures': 2.0.5 + '@inquirer/type': 4.0.5(@types/node@25.0.7) + optionalDependencies: + '@types/node': 25.0.7 + + '@inquirer/type@4.0.5(@types/node@25.0.7)': + optionalDependencies: + '@types/node': 25.0.7 + '@intlify/bundle-utils@10.0.1(vue-i18n@10.0.8(vue@3.5.30(typescript@5.9.3)))': dependencies: '@intlify/message-compiler': 11.3.0 @@ -14879,6 +15355,66 @@ snapshots: estraverse: 5.3.0 picomatch: 4.0.4 + '@swc/core-darwin-arm64@1.15.33': + optional: true + + '@swc/core-darwin-x64@1.15.33': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.33': + optional: true + + '@swc/core-linux-arm64-gnu@1.15.33': + optional: true + + '@swc/core-linux-arm64-musl@1.15.33': + optional: true + + '@swc/core-linux-ppc64-gnu@1.15.33': + optional: true + + '@swc/core-linux-s390x-gnu@1.15.33': + optional: true + + '@swc/core-linux-x64-gnu@1.15.33': + optional: true + + '@swc/core-linux-x64-musl@1.15.33': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.33': + optional: true + + '@swc/core-win32-ia32-msvc@1.15.33': + optional: true + + '@swc/core-win32-x64-msvc@1.15.33': + optional: true + + '@swc/core@1.15.33': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.26 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.33 + '@swc/core-darwin-x64': 1.15.33 + '@swc/core-linux-arm-gnueabihf': 1.15.33 + '@swc/core-linux-arm64-gnu': 1.15.33 + '@swc/core-linux-arm64-musl': 1.15.33 + '@swc/core-linux-ppc64-gnu': 1.15.33 + '@swc/core-linux-s390x-gnu': 1.15.33 + '@swc/core-linux-x64-gnu': 1.15.33 + '@swc/core-linux-x64-musl': 1.15.33 + '@swc/core-win32-arm64-msvc': 1.15.33 + '@swc/core-win32-ia32-msvc': 1.15.33 + '@swc/core-win32-x64-msvc': 1.15.33 + + '@swc/counter@0.1.3': {} + + '@swc/types@0.1.26': + dependencies: + '@swc/counter': 0.1.3 + '@szmarczak/http-timer@4.0.6': dependencies: defer-to-connect: 2.0.1 @@ -15385,15 +15921,15 @@ snapshots: '@types/node': 25.0.7 optional: true - '@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.54.0 - '@typescript-eslint/type-utils': 8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/utils': 8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.54.0 - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4(jiti@1.21.7) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -15417,14 +15953,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.54.0 '@typescript-eslint/types': 8.54.0 '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.54.0 debug: 4.4.3 - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -15477,13 +16013,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.54.0 '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4(jiti@1.21.7) ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -15535,17 +16071,29 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/utils@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7)) '@typescript-eslint/scope-manager': 8.54.0 '@typescript-eslint/types': 8.54.0 '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7)) + '@typescript-eslint/scope-manager': 8.57.1 + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) + eslint: 9.39.4(jiti@1.21.7) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + optional: true + '@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) @@ -15658,7 +16206,7 @@ snapshots: - rollup - supports-color - '@vitejs/plugin-react@4.7.0(vite@6.4.2(@types/node@25.0.7)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': + '@vitejs/plugin-react@4.7.0(vite@6.4.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@babel/core': 7.28.6 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.6) @@ -15666,7 +16214,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.2(@types/node@25.0.7)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 6.4.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -15694,7 +16242,7 @@ snapshots: vite: 7.3.3(@types/node@25.0.7)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) vue: 3.5.34(typescript@5.9.3) - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.9.0)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.9.0)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -15709,7 +16257,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.9.0)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.9.0)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -15729,13 +16277,13 @@ snapshots: optionalDependencies: vite: 7.3.2(@types/node@22.19.15)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) - '@vitest/mocker@3.2.4(vite@7.3.2(@types/node@25.0.7)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': + '@vitest/mocker@3.2.4(vite@7.3.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.2(@types/node@25.0.7)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 7.3.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) '@vitest/pretty-format@3.2.4': dependencies: @@ -16566,7 +17114,7 @@ snapshots: dotenv: 17.3.1 exsolve: 1.0.8 giget: 2.0.0 - jiti: 2.6.1 + jiti: 2.7.0 ohash: 2.0.11 pathe: 2.0.3 perfect-debounce: 2.1.0 @@ -16661,6 +17209,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.6.2: {} + change-case@5.4.4: {} character-entities-html4@2.1.0: {} @@ -16671,6 +17221,8 @@ snapshots: character-reference-invalid@2.0.1: {} + chardet@2.1.1: {} + check-error@2.1.3: {} chokidar@3.6.0: @@ -16721,6 +17273,8 @@ snapshots: dependencies: restore-cursor: 5.1.0 + cli-spinners@3.4.0: {} + cli-truncate@2.1.0: dependencies: slice-ansi: 3.0.0 @@ -16732,6 +17286,8 @@ snapshots: slice-ansi: 7.1.2 string-width: 8.2.0 + cli-width@4.1.0: {} + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -17422,7 +17978,7 @@ snapshots: transitivePeerDependencies: - supports-color - electron-vite@5.0.0(vite@6.4.2(@types/node@25.0.7)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)): + electron-vite@5.0.0(@swc/core@1.15.33)(vite@6.4.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)): dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) @@ -17430,7 +17986,9 @@ snapshots: esbuild: 0.25.12 magic-string: 0.30.21 picocolors: 1.1.1 - vite: 6.4.2(@types/node@25.0.7)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 6.4.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + optionalDependencies: + '@swc/core': 1.15.33 transitivePeerDependencies: - supports-color @@ -17741,9 +18299,9 @@ snapshots: '@eslint/compat': 2.0.3(eslint@9.39.4(jiti@2.7.0)) eslint: 9.39.4(jiti@2.7.0) - eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.7.0)): + eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@1.21.7)): dependencies: - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4(jiti@1.21.7) eslint-flat-config-utils@3.0.2: dependencies: @@ -17765,10 +18323,10 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@1.21.7)))(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)): dependencies: debug: 4.4.3 - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4(jiti@1.21.7) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) get-tsconfig: 4.13.0 is-bun-module: 2.0.0 @@ -17776,8 +18334,8 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)) - eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@1.21.7)) + eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@1.21.7)) transitivePeerDependencies: - supports-color @@ -17785,24 +18343,24 @@ snapshots: dependencies: eslint: 9.39.4(jiti@2.7.0) - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@1.21.7)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) - eslint: 9.39.4(jiti@2.7.0) + '@typescript-eslint/parser': 8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.39.4(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@1.21.7)))(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)) transitivePeerDependencies: - supports-color - eslint-plugin-boundaries@5.3.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)): + eslint-plugin-boundaries@5.3.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@1.21.7)): dependencies: - '@boundaries/elements': 1.1.2(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)) + '@boundaries/elements': 1.1.2(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@1.21.7)) chalk: 4.1.2 - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@1.21.7)) micromatch: 4.0.8 transitivePeerDependencies: - '@typescript-eslint/parser' @@ -17814,6 +18372,26 @@ snapshots: dependencies: eslint: 9.39.4(jiti@2.7.0) + eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@1.21.7)): + dependencies: + '@package-json/types': 0.0.12 + '@typescript-eslint/types': 8.57.1 + comment-parser: 1.4.5 + debug: 4.4.3 + eslint: 9.39.4(jiti@1.21.7) + eslint-import-context: 0.1.9(unrs-resolver@1.11.1) + is-glob: 4.0.3 + minimatch: 9.0.7 + semver: 7.7.4 + stable-hash-x: 0.2.0 + unrs-resolver: 1.11.1 + optionalDependencies: + '@typescript-eslint/utils': 8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + optional: true + eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@2.7.0)): dependencies: '@package-json/types': 0.0.12 @@ -17833,7 +18411,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@1.21.7)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -17842,9 +18420,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@1.21.7)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -17856,7 +18434,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -17882,7 +18460,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.7.0)): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@1.21.7)): dependencies: aria-query: 5.3.2 array-includes: 3.1.9 @@ -17892,7 +18470,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4(jiti@1.21.7) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -17901,22 +18479,22 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@7.0.1(eslint@9.39.4(jiti@2.7.0)): + eslint-plugin-react-hooks@7.0.1(eslint@9.39.4(jiti@1.21.7)): dependencies: '@babel/core': 7.28.6 '@babel/parser': 7.28.6 - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4(jiti@1.21.7) hermes-parser: 0.25.1 zod: 4.3.6 zod-validation-error: 4.0.2(zod@4.3.6) transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.4.26(eslint@9.39.4(jiti@2.7.0)): + eslint-plugin-react-refresh@0.4.26(eslint@9.39.4(jiti@1.21.7)): dependencies: - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4(jiti@1.21.7) - eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.7.0)): + eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@1.21.7)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -17924,7 +18502,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.2 - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4(jiti@1.21.7) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -17953,16 +18531,16 @@ snapshots: dependencies: safe-regex: 2.1.1 - eslint-plugin-simple-import-sort@12.1.1(eslint@9.39.4(jiti@2.7.0)): + eslint-plugin-simple-import-sort@12.1.1(eslint@9.39.4(jiti@1.21.7)): dependencies: - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4(jiti@1.21.7) - eslint-plugin-sonarjs@3.0.6(eslint@9.39.4(jiti@2.7.0)): + eslint-plugin-sonarjs@3.0.6(eslint@9.39.4(jiti@1.21.7)): dependencies: '@eslint-community/regexpp': 4.12.2 builtin-modules: 3.3.0 bytes: 3.1.2 - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4(jiti@1.21.7) functional-red-black-tree: 1.0.1 jsx-ast-utils-x: 0.1.0 lodash.merge: 4.6.2 @@ -18033,6 +18611,47 @@ snapshots: eslint-visitor-keys@5.0.1: {} + eslint@9.39.4(jiti@1.21.7): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.14.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.4 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 1.21.7 + transitivePeerDependencies: + - supports-color + eslint@9.39.4(jiti@2.7.0): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) @@ -18890,6 +19509,10 @@ snapshots: html-escaper@2.0.2: {} + html-parse-stringify@3.0.1: + dependencies: + void-elements: 3.1.0 + html-url-attributes@3.0.1: {} html-void-elements@3.0.0: {} @@ -18959,6 +19582,45 @@ snapshots: husky@9.1.7: {} + i18next-cli@1.58.0(@types/node@25.0.7)(i18next@26.2.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(typescript@5.9.3): + dependencies: + '@croct/json5-parser': 0.2.2 + '@swc/core': 1.15.33 + chokidar: 5.0.0 + commander: 14.0.3 + execa: 9.6.1 + glob: 13.0.6 + i18next-resources-for-ts: 2.1.0 + inquirer: 13.4.3(@types/node@25.0.7) + jiti: 2.7.0 + jsonc-parser: 3.3.1 + magic-string: 0.30.21 + minimatch: 10.2.3 + ora: 9.4.0 + react: 19.2.6 + react-i18next: 17.0.8(i18next@26.2.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.6)(typescript@5.9.3) + yaml: 2.9.0 + transitivePeerDependencies: + - '@swc/helpers' + - '@types/node' + - i18next + - react-dom + - react-native + - typescript + + i18next-resources-for-ts@2.1.0: + dependencies: + '@babel/runtime': 7.29.2 + '@swc/core': 1.15.33 + chokidar: 5.0.0 + yaml: 2.9.0 + transitivePeerDependencies: + - '@swc/helpers' + + i18next@26.2.0(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + iconv-corefoundation@1.1.7: dependencies: cli-truncate: 2.1.0 @@ -19020,6 +19682,18 @@ snapshots: inline-style-parser@0.2.7: {} + inquirer@13.4.3(@types/node@25.0.7): + dependencies: + '@inquirer/ansi': 2.0.5 + '@inquirer/core': 11.1.10(@types/node@25.0.7) + '@inquirer/prompts': 8.4.3(@types/node@25.0.7) + '@inquirer/type': 4.0.5(@types/node@25.0.7) + mute-stream: 3.0.0 + run-async: 4.0.6 + rxjs: 7.8.2 + optionalDependencies: + '@types/node': 25.0.7 + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -19154,6 +19828,8 @@ snapshots: global-directory: 4.0.1 is-path-inside: 4.0.0 + is-interactive@2.0.0: {} + is-map@2.0.3: {} is-module@1.0.0: {} @@ -19350,6 +20026,8 @@ snapshots: espree: 9.6.1 semver: 7.7.4 + jsonc-parser@3.3.1: {} + jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -19563,6 +20241,11 @@ snapshots: lodash@4.18.1: {} + log-symbols@7.0.1: + dependencies: + is-unicode-supported: 2.1.0 + yoctocolors: 2.1.2 + log-update@6.1.0: dependencies: ansi-escapes: 7.3.0 @@ -20194,6 +20877,8 @@ snapshots: glur: 1.1.2 object-assign: 4.1.1 + mute-stream@3.0.0: {} + mux-embed@5.18.1: {} mz@2.7.0: @@ -20659,6 +21344,17 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + ora@9.4.0: + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 3.4.0 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 7.0.1 + stdin-discarder: 0.3.2 + string-width: 8.2.0 + orderedmap@2.1.1: {} own-keys@1.0.1: @@ -21441,6 +22137,28 @@ snapshots: react-resizable: 3.1.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) resize-observer-polyfill: 1.5.1 + react-i18next@17.0.8(i18next@26.2.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3): + dependencies: + '@babel/runtime': 7.29.2 + html-parse-stringify: 3.0.1 + i18next: 26.2.0(typescript@5.9.3) + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + react-dom: 19.2.4(react@19.2.4) + typescript: 5.9.3 + + react-i18next@17.0.8(i18next@26.2.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.6)(typescript@5.9.3): + dependencies: + '@babel/runtime': 7.29.2 + html-parse-stringify: 3.0.1 + i18next: 26.2.0(typescript@5.9.3) + react: 19.2.6 + use-sync-external-store: 1.6.0(react@19.2.6) + optionalDependencies: + react-dom: 19.2.4(react@19.2.4) + typescript: 5.9.3 + react-is@16.13.1: {} react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.4): @@ -21513,6 +22231,8 @@ snapshots: react@19.2.4: {} + react@19.2.6: {} + read-binary-file-arch@1.0.6: dependencies: debug: 4.4.3 @@ -21812,12 +22532,18 @@ snapshots: run-applescript@7.1.0: {} + run-async@4.0.6: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 rw@1.3.3: {} + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -22120,6 +22846,8 @@ snapshots: std-env@4.1.0: {} + stdin-discarder@0.3.2: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -22525,7 +23253,7 @@ snapshots: tsscmp@1.0.6: {} - tsup@8.5.1(jiti@2.7.0)(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0): + tsup@8.5.1(@swc/core@1.15.33)(jiti@2.7.0)(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0): dependencies: bundle-require: 5.1.0(esbuild@0.27.2) cac: 6.7.14 @@ -22545,6 +23273,7 @@ snapshots: tinyglobby: 0.2.15 tree-kill: 1.2.2 optionalDependencies: + '@swc/core': 1.15.33 postcss: 8.5.10 typescript: 5.9.3 transitivePeerDependencies: @@ -22614,13 +23343,13 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3): + typescript-eslint@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/parser': 8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) - eslint: 9.39.4(jiti@2.7.0) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.39.4(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -22948,6 +23677,10 @@ snapshots: dependencies: react: 19.2.4 + use-sync-external-store@1.6.0(react@19.2.6): + dependencies: + react: 19.2.6 + utf8-byte-length@1.0.5: {} util-deprecate@1.0.2: {} @@ -23009,13 +23742,13 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@25.0.7)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): + vite-node@3.2.4(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.2(@types/node@25.0.7)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 7.3.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -23105,7 +23838,7 @@ snapshots: transitivePeerDependencies: - supports-color - vite@6.4.2(@types/node@25.0.7)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): + vite@6.4.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -23116,7 +23849,7 @@ snapshots: optionalDependencies: '@types/node': 25.0.7 fsevents: 2.3.3 - jiti: 2.7.0 + jiti: 1.21.7 sass: 1.98.0 terser: 5.46.0 tsx: 4.21.0 @@ -23139,6 +23872,23 @@ snapshots: tsx: 4.21.0 yaml: 2.9.0 + vite@7.3.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): + dependencies: + esbuild: 0.27.4 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.10 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.0.7 + fsevents: 2.3.3 + jiti: 1.21.7 + sass: 1.98.0 + terser: 5.46.0 + tsx: 4.21.0 + yaml: 2.9.0 + vite@7.3.2(@types/node@25.0.7)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): dependencies: esbuild: 0.27.4 @@ -23288,11 +24038,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.9.0)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.9.0)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.2(@types/node@25.0.7)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) + '@vitest/mocker': 3.2.4(vite@7.3.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -23310,8 +24060,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.2(@types/node@25.0.7)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) - vite-node: 3.2.4(@types/node@25.0.7)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 7.3.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite-node: 3.2.4(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -23331,6 +24081,8 @@ snapshots: - tsx - yaml + void-elements@3.1.0: {} + vscode-uri@3.1.0: {} vue-bundle-renderer@2.2.0: diff --git a/scripts/i18n/validate.ts b/scripts/i18n/validate.ts new file mode 100644 index 00000000..98abbbbd --- /dev/null +++ b/scripts/i18n/validate.ts @@ -0,0 +1,145 @@ +import { readdir, readFile } from 'node:fs/promises'; +import path from 'node:path'; +import process from 'node:process'; + +import { + FALLBACK_APP_LOCALE, + RESOLVED_APP_LOCALES, + TRANSLATION_NAMESPACES, +} from '../../src/features/localization/contracts'; +import { validateTranslationCatalogs } from '../../src/features/localization/core/application/validateTranslationCatalogs'; + +import type { + CatalogValidationIssue, + TranslationCatalogByNamespace, + TranslationCatalogsByLocale, + TranslationCatalogNode, +} from '../../src/features/localization/core/application/validateTranslationCatalogs'; + +const repoRoot = process.cwd(); +const localesRoot = path.join(repoRoot, 'src/features/localization/renderer/locales'); + +const issues: CatalogValidationIssue[] = []; +const catalogs = await readCatalogs(localesRoot, issues); + +validateConfiguredLocales(catalogs, issues); +validateConfiguredNamespaces(catalogs, issues); +issues.push(...validateTranslationCatalogs(catalogs, FALLBACK_APP_LOCALE)); + +if (issues.length > 0) { + for (const issue of issues) { + console.error(`${issue.locale}/${issue.namespace}: ${issue.message}`); + } + process.exit(1); +} + +console.log( + `i18n catalogs valid (${RESOLVED_APP_LOCALES.length} locale set, ${TRANSLATION_NAMESPACES.length} namespaces)` +); + +async function readCatalogs( + root: string, + issuesOutput: CatalogValidationIssue[] +): Promise { + const localeEntries = await readdir(root, { withFileTypes: true }); + const result: TranslationCatalogsByLocale = {}; + + for (const localeEntry of localeEntries) { + if (!localeEntry.isDirectory()) continue; + + const locale = localeEntry.name; + const localeDir = path.join(root, locale); + const namespaceEntries = await readdir(localeDir, { withFileTypes: true }); + const localeCatalog: TranslationCatalogByNamespace = {}; + + for (const namespaceEntry of namespaceEntries) { + if (!namespaceEntry.isFile() || !namespaceEntry.name.endsWith('.json')) continue; + + const namespace = namespaceEntry.name.slice(0, -'.json'.length); + const filePath = path.join(localeDir, namespaceEntry.name); + const parsed = JSON.parse(await readFile(filePath, 'utf8')) as unknown; + + if (!isTranslationCatalogNode(parsed)) { + issuesOutput.push({ + type: 'shape-mismatch', + locale, + namespace, + message: `Catalog "${locale}/${namespace}.json" must contain a JSON object of nested strings`, + }); + continue; + } + + localeCatalog[namespace] = parsed; + } + + result[locale] = localeCatalog; + } + + return result; +} + +function validateConfiguredLocales( + catalogs: TranslationCatalogsByLocale, + issuesOutput: CatalogValidationIssue[] +): void { + for (const locale of RESOLVED_APP_LOCALES) { + if (!catalogs[locale]) { + issuesOutput.push({ + type: 'missing-namespace', + locale, + namespace: '*', + message: `Configured locale "${locale}" has no catalog directory`, + }); + } + } + + for (const locale of Object.keys(catalogs)) { + if (!RESOLVED_APP_LOCALES.includes(locale as (typeof RESOLVED_APP_LOCALES)[number])) { + issuesOutput.push({ + type: 'extra-key', + locale, + namespace: '*', + message: `Catalog directory "${locale}" is not listed in RESOLVED_APP_LOCALES`, + }); + } + } +} + +function validateConfiguredNamespaces( + catalogs: TranslationCatalogsByLocale, + issuesOutput: CatalogValidationIssue[] +): void { + for (const [locale, catalog] of Object.entries(catalogs)) { + for (const namespace of TRANSLATION_NAMESPACES) { + if (!catalog[namespace]) { + issuesOutput.push({ + type: 'missing-namespace', + locale, + namespace, + message: `Configured namespace "${namespace}" is missing for locale "${locale}"`, + }); + } + } + + for (const namespace of Object.keys(catalog)) { + if (!TRANSLATION_NAMESPACES.includes(namespace as (typeof TRANSLATION_NAMESPACES)[number])) { + issuesOutput.push({ + type: 'extra-key', + locale, + namespace, + message: `Catalog namespace "${namespace}" is not listed in TRANSLATION_NAMESPACES`, + }); + } + } + } +} + +function isTranslationCatalogNode(value: unknown): value is TranslationCatalogNode { + if (typeof value === 'string') return true; + if (!isPlainObject(value)) return false; + return Object.values(value).every(isTranslationCatalogNode); +} + +function isPlainObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/src/features/agent-graph/renderer/ui/GraphActivityHud.tsx b/src/features/agent-graph/renderer/ui/GraphActivityHud.tsx index b81c165e..a6315b25 100644 --- a/src/features/agent-graph/renderer/ui/GraphActivityHud.tsx +++ b/src/features/agent-graph/renderer/ui/GraphActivityHud.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { ACTIVITY_LANE } from '@claude-teams/agent-graph'; +import { useAppTranslation } from '@features/localization/renderer'; import { buildMessageContext } from '@renderer/components/team/activity/activityMessageContext'; import { MessageExpandDialog } from '@renderer/components/team/activity/MessageExpandDialog'; import { useStableTeamMentionMeta } from '@renderer/hooks/useStableTeamMentionMeta'; @@ -77,6 +78,7 @@ export const GraphActivityHud = ({ onOpenTaskDetail, onOpenMemberProfile, }: GraphActivityHudProps): React.JSX.Element | null => { + const { t } = useAppTranslation('team'); const worldLayerRef = useRef(null); const shellRefs = useRef(new Map()); const connectorRefs = useRef(new Map()); @@ -552,12 +554,12 @@ export const GraphActivityHud = ({ >
- Activity + {t('agentGraph.activityHud.activity')}
{lane.entries.length === 0 && lane.overflowCount === 0 ? (
- No recent activity + {t('agentGraph.activityHud.noRecentActivity')}
) : null} {lane.entries.map(renderLaneEntry)} @@ -568,7 +570,7 @@ export const GraphActivityHud = ({ className={`${INTERACTIVE_ACTIVITY_CONTROL_CLASS} h-8 min-h-8 w-full rounded-md border border-white/10 bg-[rgba(8,14,28,0.64)] px-3 py-1 text-center text-[11px] font-medium text-slate-300 transition-colors hover:border-white/20 hover:bg-[rgba(12,20,40,0.78)]`} onClick={() => handleOpenOwnerActivity(lane.node)} > - +{lane.overflowCount} more + {t('agentGraph.activityHud.more', { count: lane.overflowCount })} ) : null}
diff --git a/src/features/agent-graph/renderer/ui/GraphBlockingEdgePopover.tsx b/src/features/agent-graph/renderer/ui/GraphBlockingEdgePopover.tsx index 5b4d5c82..27db9063 100644 --- a/src/features/agent-graph/renderer/ui/GraphBlockingEdgePopover.tsx +++ b/src/features/agent-graph/renderer/ui/GraphBlockingEdgePopover.tsx @@ -1,5 +1,6 @@ import { useMemo } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { Badge } from '@renderer/components/ui/badge'; import { Button } from '@renderer/components/ui/button'; @@ -63,6 +64,7 @@ export const GraphBlockingEdgePopover = ({ onSelectNode, onOpenTaskDetail, }: GraphBlockingEdgePopoverProps): React.JSX.Element => { + const { t } = useAppTranslation('team'); const { teamData } = useGraphActivityContext(teamName); const tasksById = useMemo( () => new Map((teamData?.tasks ?? []).map((task) => [task.id, task] as const)), @@ -102,7 +104,7 @@ export const GraphBlockingEdgePopover = ({
- Blocking Dependency + {t('agentGraph.blockingEdge.title')}
{relationCount > 1 && ( {sourceLabel}
{sourceHiddenTasks.length > 0 && ( )} -
blocks
+
+ {t('agentGraph.blockingEdge.blocks')} +
{targetLabel}
{targetHiddenTasks.length > 0 && ( )}
diff --git a/src/features/agent-graph/renderer/ui/GraphMemberLogPreviewHud.tsx b/src/features/agent-graph/renderer/ui/GraphMemberLogPreviewHud.tsx index 16c4e79a..51e32def 100644 --- a/src/features/agent-graph/renderer/ui/GraphMemberLogPreviewHud.tsx +++ b/src/features/agent-graph/renderer/ui/GraphMemberLogPreviewHud.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { AlertCircle, Brain, @@ -279,6 +280,7 @@ export const GraphMemberLogPreviewHud = ({ enabled = true, onOpenMemberProfile, }: GraphMemberLogPreviewHudProps): React.JSX.Element | null => { + const { t } = useAppTranslation('team'); const worldLayerRef = useRef(null); const shellRefs = useRef(new Map()); const visibleKeyRef = useRef(''); @@ -607,7 +609,7 @@ export const GraphMemberLogPreviewHud = ({
- Logs + {t('agentGraph.logPreview.logs')}
{items.length > 0 ? ( @@ -617,10 +619,10 @@ export const GraphMemberLogPreviewHud = ({ type="button" className={`${INTERACTIVE_LOG_CONTROL_CLASS} flex min-h-0 flex-1 rounded-md text-left text-[11px] text-slate-400/60`} aria-busy="true" - aria-label="Loading logs" + aria-label={t('agentGraph.logPreview.loading')} onClick={() => openLogs(memberName)} > - Loading logs + {t('agentGraph.logPreview.loading')} {renderLoadingSkeleton()} ) : ( @@ -638,7 +640,7 @@ export const GraphMemberLogPreviewHud = ({ className={`${INTERACTIVE_LOG_CONTROL_CLASS} h-8 min-h-8 w-full rounded-md border border-white/10 bg-[rgba(8,14,28,0.64)] px-3 py-1 text-center text-[11px] font-medium text-slate-300 transition-colors hover:border-white/20 hover:bg-[rgba(12,20,40,0.78)]`} onClick={() => openLogs(memberName)} > - +{preview.overflowCount} more + {t('agentGraph.logPreview.more', { count: preview.overflowCount })} ) : null}
diff --git a/src/features/agent-graph/renderer/ui/GraphNodePopover.tsx b/src/features/agent-graph/renderer/ui/GraphNodePopover.tsx index 2c2b3a4d..6ea2258d 100644 --- a/src/features/agent-graph/renderer/ui/GraphNodePopover.tsx +++ b/src/features/agent-graph/renderer/ui/GraphNodePopover.tsx @@ -6,6 +6,7 @@ import { useMemo } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { Badge } from '@renderer/components/ui/badge'; import { Button } from '@renderer/components/ui/button'; import { @@ -115,6 +116,7 @@ export const GraphNodePopover = ({ onViewChanges, onDeleteTask, }: GraphNodePopoverProps): React.JSX.Element => { + const { t } = useAppTranslation('team'); if (node.kind === 'member' || node.kind === 'lead') { return ( {'\u{2194}'} {extTeamName}
-
External team
+
+ {t('agentGraph.popover.externalTeam')} +
); } @@ -185,11 +189,15 @@ export const GraphNodePopover = ({
{node.processRegisteredBy && (
- Started by: {node.processRegisteredBy} + {t('agentGraph.popover.process.startedBy')}{' '} + {node.processRegisteredBy}
)} {node.processRegisteredAt && ( -
At: {new Date(node.processRegisteredAt).toLocaleTimeString()}
+
+ {t('agentGraph.popover.process.at')}{' '} + {new Date(node.processRegisteredAt).toLocaleTimeString()} +
)} {node.exceptionLabel && ( - Open URL + {t('agentGraph.popover.process.openUrl')} )}
@@ -229,6 +237,7 @@ const OverflowPopoverContent = ({ onClose: () => void; onOpenTaskDetail?: (taskId: string) => void; }): React.JSX.Element => { + const { t } = useAppTranslation('team'); const { teamData } = useGraphActivityContext(teamName); const tasksById = new Map((teamData?.tasks ?? []).map((task) => [task.id, task])); const hiddenTasks = (node.overflowTaskIds ?? []) @@ -238,14 +247,18 @@ const OverflowPopoverContent = ({ return (
-
Hidden tasks
+
+ {t('agentGraph.popover.overflow.hiddenTasks')} +
{node.overflowCount ?? hiddenTasks.length}
{hiddenTasks.length === 0 ? ( -
No hidden tasks available.
+
+ {t('agentGraph.popover.overflow.empty')} +
) : ( hiddenTasks.map((task) => { const reviewer = resolveTaskReviewer(task, teamData?.kanbanState.tasks[task.id]); @@ -303,6 +316,7 @@ const MemberPopoverContent = ({ onCreateTask?: (owner: string) => void; onOpenTask?: (taskId: string) => void; }): React.JSX.Element => { + const { t } = useAppTranslation('team'); const memberName = node.domainRef.kind === 'member' || node.domainRef.kind === 'lead' ? node.domainRef.memberName @@ -342,6 +356,7 @@ const MemberPopoverContent = ({ members: teamMembers, memberSpawnStatuses, memberSpawnSnapshot, + t, }) : null; const launchPresentation = member @@ -372,11 +387,11 @@ const MemberPopoverContent = ({ const fallbackSpawnStatusLabel = node.spawnStatus && node.spawnStatus !== 'online' ? node.spawnStatus === 'waiting' - ? 'waiting to start' + ? t('agentGraph.popover.member.spawn.waitingToStart') : node.spawnStatus === 'spawning' - ? 'starting' + ? t('agentGraph.popover.member.spawn.starting') : node.spawnStatus === 'error' - ? 'failed' + ? t('agentGraph.popover.member.spawn.failed') : node.spawnStatus : null; const statusLabel = @@ -385,13 +400,13 @@ const MemberPopoverContent = ({ launchPresentation?.presenceLabel ?? fallbackSpawnStatusLabel ?? (node.state === 'active' - ? 'active' + ? t('agentGraph.popover.member.state.active') : node.state === 'idle' - ? 'idle' + ? t('agentGraph.popover.member.state.idle') : node.state === 'terminated' - ? 'offline' + ? t('agentGraph.popover.member.state.offline') : node.state === 'tool_calling' - ? 'running tool' + ? t('agentGraph.popover.member.state.runningTool') : node.state); const statusDotClass = launchPresentation?.dotClass ?? @@ -464,7 +479,7 @@ const MemberPopoverContent = ({ variant="outline" className="border-blue-500/30 px-1.5 py-0 text-[10px] text-blue-400" > - Lead + {t('agentGraph.popover.member.lead')} )} {(launchPresentation?.spawnBadgeLabel ?? fallbackSpawnStatusLabel) && @@ -499,7 +514,9 @@ const MemberPopoverContent = ({ className="size-3 shrink-0 animate-spin" style={{ color: node.color ?? '#66ccff' }} /> - working on + + {t('agentGraph.popover.member.workingOn')} +
@@ -555,7 +572,7 @@ const MemberPopoverContent = ({ {node.recentTools && node.recentTools.length > 0 && (
- Recent tools + {t('agentGraph.popover.member.recentTools')}
{node.recentTools.slice(0, 5).map((tool) => { @@ -594,7 +611,7 @@ const MemberPopoverContent = ({ onClose(); }} > - Message + {t('agentGraph.popover.member.actions.message')}
diff --git a/src/features/agent-graph/renderer/ui/GraphProvisioningHud.tsx b/src/features/agent-graph/renderer/ui/GraphProvisioningHud.tsx index fd3bad8b..0700f332 100644 --- a/src/features/agent-graph/renderer/ui/GraphProvisioningHud.tsx +++ b/src/features/agent-graph/renderer/ui/GraphProvisioningHud.tsx @@ -1,5 +1,6 @@ import { useEffect, useMemo, useRef, useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { DISPLAY_STEPS } from '@renderer/components/team/provisioningSteps'; import { StepProgressBar } from '@renderer/components/team/StepProgressBar'; import { TeamProvisioningPanel } from '@renderer/components/team/TeamProvisioningPanel'; @@ -15,7 +16,6 @@ import { import type { TeamProvisioningPresentation } from '@renderer/utils/teamProvisioningPresentation'; import type { CSSProperties } from 'react'; -const MINI_STEPS = DISPLAY_STEPS.map((step) => ({ key: step.key, label: step.label })); const HUD_STEPPER_STYLE: CSSProperties = { ['--stepper-done' as string]: '#22c55e', ['--stepper-done-glow' as string]: 'rgba(34, 197, 94, 0.24)', @@ -46,6 +46,8 @@ export const GraphProvisioningHud = ({ teamName, enabled = true, }: GraphProvisioningHudProps): React.JSX.Element | null => { + const { t } = useAppTranslation('team'); + const miniSteps = DISPLAY_STEPS.map((step) => ({ key: step.key, label: t(step.labelKey) })); const { presentation, runInstanceKey } = useTeamProvisioningPresentation(teamName); const lastActiveStepRef = useRef(-1); const [detailsOpen, setDetailsOpen] = useState(false); @@ -88,7 +90,7 @@ export const GraphProvisioningHud = ({ >
- Launch details + {t('agentGraph.provisioning.launchDetails')} - Detailed team launch progress, live output and CLI logs. + {t('agentGraph.provisioning.launchDetailsDescription')}
diff --git a/src/features/localization/contracts/appLocale.ts b/src/features/localization/contracts/appLocale.ts new file mode 100644 index 00000000..aa3d490b --- /dev/null +++ b/src/features/localization/contracts/appLocale.ts @@ -0,0 +1,19 @@ +export const APP_LOCALE_PREFERENCES = ['system', 'en', 'ru'] as const; + +export const RESOLVED_APP_LOCALES = ['en', 'ru'] as const; + +export type AppLocalePreference = (typeof APP_LOCALE_PREFERENCES)[number]; + +export type ResolvedAppLocale = (typeof RESOLVED_APP_LOCALES)[number]; + +export const DEFAULT_APP_LOCALE_PREFERENCE: AppLocalePreference = 'system'; + +export const FALLBACK_APP_LOCALE: ResolvedAppLocale = 'en'; + +export function isAppLocalePreference(value: unknown): value is AppLocalePreference { + return typeof value === 'string' && APP_LOCALE_PREFERENCES.includes(value as AppLocalePreference); +} + +export function isResolvedAppLocale(value: unknown): value is ResolvedAppLocale { + return typeof value === 'string' && RESOLVED_APP_LOCALES.includes(value as ResolvedAppLocale); +} diff --git a/src/features/localization/contracts/index.ts b/src/features/localization/contracts/index.ts new file mode 100644 index 00000000..3d29bb19 --- /dev/null +++ b/src/features/localization/contracts/index.ts @@ -0,0 +1,11 @@ +export type { AppLocalePreference, ResolvedAppLocale } from './appLocale'; +export { + APP_LOCALE_PREFERENCES, + DEFAULT_APP_LOCALE_PREFERENCE, + FALLBACK_APP_LOCALE, + isAppLocalePreference, + isResolvedAppLocale, + RESOLVED_APP_LOCALES, +} from './appLocale'; +export type { TranslationNamespace } from './namespaces'; +export { DEFAULT_TRANSLATION_NAMESPACE, TRANSLATION_NAMESPACES } from './namespaces'; diff --git a/src/features/localization/contracts/namespaces.ts b/src/features/localization/contracts/namespaces.ts new file mode 100644 index 00000000..f7b75a92 --- /dev/null +++ b/src/features/localization/contracts/namespaces.ts @@ -0,0 +1,13 @@ +export const TRANSLATION_NAMESPACES = [ + 'common', + 'settings', + 'errors', + 'report', + 'dashboard', + 'extensions', + 'team', +] as const; + +export type TranslationNamespace = (typeof TRANSLATION_NAMESPACES)[number]; + +export const DEFAULT_TRANSLATION_NAMESPACE: TranslationNamespace = 'common'; diff --git a/src/features/localization/core/application/resolveRuntimeLocale.ts b/src/features/localization/core/application/resolveRuntimeLocale.ts new file mode 100644 index 00000000..e8c8301d --- /dev/null +++ b/src/features/localization/core/application/resolveRuntimeLocale.ts @@ -0,0 +1,15 @@ +import { resolveAppLocale } from '../domain/localePolicy'; + +import type { AppLocalePreference, ResolvedAppLocale } from '../../contracts'; + +export interface ResolveRuntimeLocaleInput { + readonly preference: AppLocalePreference; + readonly systemLocale: string | null; +} + +export function resolveRuntimeLocale(input: ResolveRuntimeLocaleInput): ResolvedAppLocale { + return resolveAppLocale({ + preference: input.preference, + systemLocale: input.systemLocale, + }); +} diff --git a/src/features/localization/core/application/validateTranslationCatalogs.ts b/src/features/localization/core/application/validateTranslationCatalogs.ts new file mode 100644 index 00000000..c8b6340c --- /dev/null +++ b/src/features/localization/core/application/validateTranslationCatalogs.ts @@ -0,0 +1,7 @@ +export type { + CatalogValidationIssue, + TranslationCatalogByNamespace, + TranslationCatalogNode, + TranslationCatalogsByLocale, +} from '../domain/catalogPolicy'; +export { validateCatalogCompleteness as validateTranslationCatalogs } from '../domain/catalogPolicy'; diff --git a/src/features/localization/core/domain/catalogPolicy.ts b/src/features/localization/core/domain/catalogPolicy.ts new file mode 100644 index 00000000..9b36102e --- /dev/null +++ b/src/features/localization/core/domain/catalogPolicy.ts @@ -0,0 +1,203 @@ +export type TranslationCatalogNode = string | { readonly [key: string]: TranslationCatalogNode }; + +export interface CatalogValidationIssue { + readonly type: + | 'missing-namespace' + | 'missing-key' + | 'extra-key' + | 'shape-mismatch' + | 'empty-message' + | 'interpolation-mismatch'; + readonly locale: string; + readonly namespace: string; + readonly key?: string; + readonly message: string; +} + +export type TranslationCatalogByNamespace = Record; + +export type TranslationCatalogsByLocale = Record; + +export function validateCatalogCompleteness( + catalogsByLocale: TranslationCatalogsByLocale, + sourceLocale: string +): CatalogValidationIssue[] { + const sourceCatalog = catalogsByLocale[sourceLocale]; + if (!sourceCatalog) { + return [ + { + type: 'missing-namespace', + locale: sourceLocale, + namespace: '*', + message: `Source locale "${sourceLocale}" is missing`, + }, + ]; + } + + const issues: CatalogValidationIssue[] = []; + for (const [locale, localeCatalog] of Object.entries(catalogsByLocale)) { + compareLocaleCatalog(issues, locale, localeCatalog, sourceCatalog); + } + return issues; +} + +function compareLocaleCatalog( + issues: CatalogValidationIssue[], + locale: string, + localeCatalog: TranslationCatalogByNamespace, + sourceCatalog: TranslationCatalogByNamespace +): void { + for (const [namespace, sourceNamespaceCatalog] of Object.entries(sourceCatalog)) { + const targetNamespaceCatalog = localeCatalog[namespace]; + if (!targetNamespaceCatalog) { + issues.push({ + type: 'missing-namespace', + locale, + namespace, + message: `Locale "${locale}" is missing namespace "${namespace}"`, + }); + continue; + } + + compareCatalogNode(issues, { + locale, + namespace, + keyPath: [], + sourceNode: sourceNamespaceCatalog, + targetNode: targetNamespaceCatalog, + }); + } + + for (const namespace of Object.keys(localeCatalog)) { + if (!(namespace in sourceCatalog)) { + issues.push({ + type: 'extra-key', + locale, + namespace, + message: `Locale "${locale}" has extra namespace "${namespace}"`, + }); + } + } +} + +interface CompareCatalogNodeInput { + readonly locale: string; + readonly namespace: string; + readonly keyPath: readonly string[]; + readonly sourceNode: TranslationCatalogNode; + readonly targetNode: TranslationCatalogNode; +} + +function compareCatalogNode( + issues: CatalogValidationIssue[], + input: CompareCatalogNodeInput +): void { + const key = input.keyPath.join('.'); + + if (typeof input.sourceNode === 'string') { + validateStringNode(issues, input, key); + return; + } + + if (typeof input.targetNode === 'string') { + issues.push({ + type: 'shape-mismatch', + locale: input.locale, + namespace: input.namespace, + key, + message: `Expected object at "${input.namespace}:${key}"`, + }); + return; + } + + for (const [childKey, sourceChildNode] of Object.entries(input.sourceNode)) { + if (!(childKey in input.targetNode)) { + const missingKey = [...input.keyPath, childKey].join('.'); + issues.push({ + type: 'missing-key', + locale: input.locale, + namespace: input.namespace, + key: missingKey, + message: `Missing key "${input.namespace}:${missingKey}" for locale "${input.locale}"`, + }); + continue; + } + + compareCatalogNode(issues, { + locale: input.locale, + namespace: input.namespace, + keyPath: [...input.keyPath, childKey], + sourceNode: sourceChildNode, + targetNode: input.targetNode[childKey], + }); + } + + for (const childKey of Object.keys(input.targetNode)) { + if (!(childKey in input.sourceNode)) { + const extraKey = [...input.keyPath, childKey].join('.'); + issues.push({ + type: 'extra-key', + locale: input.locale, + namespace: input.namespace, + key: extraKey, + message: `Extra key "${input.namespace}:${extraKey}" for locale "${input.locale}"`, + }); + } + } +} + +function validateStringNode( + issues: CatalogValidationIssue[], + input: CompareCatalogNodeInput, + key: string +): void { + const sourceMessage = input.sourceNode; + if (typeof sourceMessage !== 'string') { + return; + } + + if (typeof input.targetNode !== 'string') { + issues.push({ + type: 'shape-mismatch', + locale: input.locale, + namespace: input.namespace, + key, + message: `Expected string at "${input.namespace}:${key}"`, + }); + return; + } + + if (input.targetNode.trim().length === 0) { + issues.push({ + type: 'empty-message', + locale: input.locale, + namespace: input.namespace, + key, + message: `Empty message at "${input.namespace}:${key}" for locale "${input.locale}"`, + }); + } + + const sourceVariables = extractInterpolationVariables(sourceMessage); + const targetVariables = extractInterpolationVariables(input.targetNode); + if (!hasSameItems(sourceVariables, targetVariables)) { + issues.push({ + type: 'interpolation-mismatch', + locale: input.locale, + namespace: input.namespace, + key, + message: `Interpolation variables differ at "${input.namespace}:${key}" for locale "${input.locale}"`, + }); + } +} + +export function extractInterpolationVariables(message: string): readonly string[] { + const variables = new Set(); + for (const match of message.matchAll(/\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/g)) { + variables.add(match[1]); + } + return [...variables].sort(); +} + +function hasSameItems(left: readonly string[], right: readonly string[]): boolean { + return left.length === right.length && left.every((item, index) => item === right[index]); +} diff --git a/src/features/localization/core/domain/localePolicy.ts b/src/features/localization/core/domain/localePolicy.ts new file mode 100644 index 00000000..28151706 --- /dev/null +++ b/src/features/localization/core/domain/localePolicy.ts @@ -0,0 +1,43 @@ +import { + FALLBACK_APP_LOCALE, + isAppLocalePreference, + isResolvedAppLocale, + RESOLVED_APP_LOCALES, +} from '../../contracts'; + +import type { AppLocalePreference, ResolvedAppLocale } from '../../contracts'; + +export interface LocaleResolutionInput { + readonly preference: unknown; + readonly systemLocale?: string | null; + readonly supportedLocales?: readonly ResolvedAppLocale[]; + readonly fallbackLocale?: ResolvedAppLocale; +} + +export function normalizeAppLocalePreference(value: unknown): AppLocalePreference { + return isAppLocalePreference(value) ? value : 'system'; +} + +export function extractPrimaryLocaleSubtag(locale: string | null | undefined): string | null { + const trimmed = locale?.trim(); + if (!trimmed) return null; + + const normalized = trimmed.replace('_', '-').toLowerCase(); + const primary = normalized.split('-')[0]?.trim(); + return primary || null; +} + +export function resolveAppLocale(input: LocaleResolutionInput): ResolvedAppLocale { + const supportedLocales = input.supportedLocales ?? RESOLVED_APP_LOCALES; + const fallbackLocale = input.fallbackLocale ?? FALLBACK_APP_LOCALE; + const preference = normalizeAppLocalePreference(input.preference); + + if (preference !== 'system') { + return supportedLocales.includes(preference) ? preference : fallbackLocale; + } + + const primarySystemLocale = extractPrimaryLocaleSubtag(input.systemLocale); + return isResolvedAppLocale(primarySystemLocale) && supportedLocales.includes(primarySystemLocale) + ? primarySystemLocale + : fallbackLocale; +} diff --git a/src/features/localization/index.ts b/src/features/localization/index.ts new file mode 100644 index 00000000..555be93b --- /dev/null +++ b/src/features/localization/index.ts @@ -0,0 +1,11 @@ +export type { AppLocalePreference, ResolvedAppLocale, TranslationNamespace } from './contracts'; +export { + APP_LOCALE_PREFERENCES, + DEFAULT_APP_LOCALE_PREFERENCE, + FALLBACK_APP_LOCALE, + isAppLocalePreference, + isResolvedAppLocale, + RESOLVED_APP_LOCALES, + TRANSLATION_NAMESPACES, +} from './contracts'; +export { normalizeAppLocalePreference, resolveAppLocale } from './core/domain/localePolicy'; diff --git a/src/features/localization/renderer/adapters/browserSystemLocaleAdapter.ts b/src/features/localization/renderer/adapters/browserSystemLocaleAdapter.ts new file mode 100644 index 00000000..5fa9acff --- /dev/null +++ b/src/features/localization/renderer/adapters/browserSystemLocaleAdapter.ts @@ -0,0 +1,3 @@ +export function getBrowserSystemLocale(): string | null { + return globalThis.navigator?.language ?? null; +} diff --git a/src/features/localization/renderer/composition/createI18nextInstance.ts b/src/features/localization/renderer/composition/createI18nextInstance.ts new file mode 100644 index 00000000..8a0339eb --- /dev/null +++ b/src/features/localization/renderer/composition/createI18nextInstance.ts @@ -0,0 +1,35 @@ +import { initReactI18next } from 'react-i18next'; + +import i18next from 'i18next'; + +import { + DEFAULT_TRANSLATION_NAMESPACE, + FALLBACK_APP_LOCALE, + RESOLVED_APP_LOCALES, + TRANSLATION_NAMESPACES, +} from '../../contracts'; + +import { localizationResources } from './localizationResources'; + +export function createI18nextInstance(initialLocale = FALLBACK_APP_LOCALE): typeof i18next { + const instance = i18next.createInstance(); + + void instance.use(initReactI18next).init({ + debug: false, + defaultNS: DEFAULT_TRANSLATION_NAMESPACE, + fallbackLng: FALLBACK_APP_LOCALE, + initAsync: false, + interpolation: { + escapeValue: false, + }, + lng: initialLocale, + ns: [...TRANSLATION_NAMESPACES], + resources: localizationResources, + returnEmptyString: false, + supportedLngs: [...RESOLVED_APP_LOCALES], + }); + + return instance; +} + +export const appI18n = createI18nextInstance(); diff --git a/src/features/localization/renderer/composition/localizationResources.ts b/src/features/localization/renderer/composition/localizationResources.ts new file mode 100644 index 00000000..c7ef2394 --- /dev/null +++ b/src/features/localization/renderer/composition/localizationResources.ts @@ -0,0 +1,34 @@ +import { RESOLVED_APP_LOCALES, TRANSLATION_NAMESPACES } from '../../contracts'; + +import type { ResolvedAppLocale, TranslationNamespace } from '../../contracts'; + +type TranslationResource = Record; +type TranslationResources = Record< + ResolvedAppLocale, + Record +>; + +const catalogModules = import.meta.glob('../locales/*/*.json', { + eager: true, + import: 'default', +}); + +export const localizationResources = buildLocalizationResources(); + +function buildLocalizationResources(): TranslationResources { + const resources = {} as TranslationResources; + + for (const locale of RESOLVED_APP_LOCALES) { + resources[locale] = {} as Record; + + for (const namespace of TRANSLATION_NAMESPACES) { + const resource = catalogModules[`../locales/${locale}/${namespace}.json`]; + if (!resource) { + throw new Error(`Missing i18n catalog: ${locale}/${namespace}.json`); + } + resources[locale][namespace] = resource; + } + } + + return resources; +} diff --git a/src/features/localization/renderer/hooks/useAppTranslation.ts b/src/features/localization/renderer/hooks/useAppTranslation.ts new file mode 100644 index 00000000..d3c98ef1 --- /dev/null +++ b/src/features/localization/renderer/hooks/useAppTranslation.ts @@ -0,0 +1,17 @@ +import { useTranslation } from 'react-i18next'; + +import type { TranslationNamespace } from '../../contracts'; +import type { TFunction } from 'i18next'; + +export interface AppTranslationApi { + readonly t: TFunction; + readonly resolvedLanguage: string | undefined; +} + +export function useAppTranslation(namespace: TranslationNamespace): AppTranslationApi { + const { i18n, t } = useTranslation(namespace); + return { + t, + resolvedLanguage: i18n.resolvedLanguage, + }; +} diff --git a/src/features/localization/renderer/hooks/useLocaleFormatters.ts b/src/features/localization/renderer/hooks/useLocaleFormatters.ts new file mode 100644 index 00000000..d6a0fcb5 --- /dev/null +++ b/src/features/localization/renderer/hooks/useLocaleFormatters.ts @@ -0,0 +1,54 @@ +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { FALLBACK_APP_LOCALE } from '../../contracts'; + +export interface LocaleFormatters { + readonly date: (value: Date | string | number, options?: Intl.DateTimeFormatOptions) => string; + readonly time: (value: Date | string | number, options?: Intl.DateTimeFormatOptions) => string; + readonly dateTime: ( + value: Date | string | number, + options?: Intl.DateTimeFormatOptions + ) => string; + readonly number: (value: number, options?: Intl.NumberFormatOptions) => string; + readonly currency: ( + value: number, + currency: string, + options?: Intl.NumberFormatOptions + ) => string; +} + +export function useLocaleFormatters(): LocaleFormatters { + const { i18n } = useTranslation(); + const locale = i18n.resolvedLanguage || i18n.language || FALLBACK_APP_LOCALE; + + return useMemo( + () => ({ + date: (value, options) => + new Intl.DateTimeFormat(locale, options ?? { dateStyle: 'medium' }).format( + normalizeDate(value) + ), + time: (value, options) => + new Intl.DateTimeFormat(locale, options ?? { hour: '2-digit', minute: '2-digit' }).format( + normalizeDate(value) + ), + dateTime: (value, options) => + new Intl.DateTimeFormat( + locale, + options ?? { dateStyle: 'medium', timeStyle: 'short' } + ).format(normalizeDate(value)), + number: (value, options) => new Intl.NumberFormat(locale, options).format(value), + currency: (value, currency, options) => + new Intl.NumberFormat(locale, { + currency, + style: 'currency', + ...options, + }).format(value), + }), + [locale] + ); +} + +function normalizeDate(value: Date | string | number): Date { + return value instanceof Date ? value : new Date(value); +} diff --git a/src/features/localization/renderer/i18next.d.ts b/src/features/localization/renderer/i18next.d.ts new file mode 100644 index 00000000..b1127ae9 --- /dev/null +++ b/src/features/localization/renderer/i18next.d.ts @@ -0,0 +1,10 @@ +// This file is automatically generated by i18next-cli, because it was not existing. You can edit it based on your needs: https://www.i18next.com/overview/typescript#custom-type-options +import type Resources from './resources'; + +declare module 'i18next' { + interface CustomTypeOptions { + enableSelector: false; + defaultNS: 'common'; + resources: Resources; + } +} diff --git a/src/features/localization/renderer/index.ts b/src/features/localization/renderer/index.ts new file mode 100644 index 00000000..c2ea035b --- /dev/null +++ b/src/features/localization/renderer/index.ts @@ -0,0 +1,4 @@ +export { useAppTranslation } from './hooks/useAppTranslation'; +export { useLocaleFormatters } from './hooks/useLocaleFormatters'; +export { AppLanguageSelect } from './ui/AppLanguageSelect'; +export { LocalizationProvider } from './ui/LocalizationProvider'; diff --git a/src/features/localization/renderer/locales/en/common.json b/src/features/localization/renderer/locales/en/common.json new file mode 100644 index 00000000..0b020a1c --- /dev/null +++ b/src/features/localization/renderer/locales/en/common.json @@ -0,0 +1,900 @@ +{ + "actions": { + "cancel": "Cancel", + "close": "Close", + "copied": "Copied", + "copyUrl": "Copy URL", + "open": "Open", + "reveal": "Reveal", + "retry": "Retry", + "save": "Save", + "showLess": "Show less", + "showMore": "Show more", + "refresh": "Refresh", + "reset": "Reset", + "copyToClipboard": "Copy to clipboard", + "moreActions": "More actions", + "closeDialog": "Close dialog", + "goToDashboard": "Go to Dashboard", + "or": "or", + "hide": "Hide", + "resetSelection": "Reset selection" + }, + "code": { + "line": "line {{line}}", + "lines": "lines {{from}}-{{to}}", + "moreLines": "({{count}} more lines...)", + "moreLines_few": "({{count}} more lines...)", + "moreLines_many": "({{count}} more lines...)", + "moreLines_one": "({{count}} more line...)", + "moreLines_other": "({{count}} more lines...)", + "code": "Code", + "preview": "Preview", + "markdownPreview": "Markdown Preview", + "linesParenthesized": "(lines {{from}}-{{to}})", + "mermaidSyntaxError": "Mermaid syntax error" + }, + "contextBadge": { + "badge": "Context", + "breakdown": { + "text": "Text", + "thinking": "Thinking" + }, + "detailsAria": "Context injection details", + "sectionSummary": "{{title}} ({{count}}) ~{{tokens}} tokens", + "sections": { + "claudeMdFiles": "CLAUDE.md Files", + "mentionedFiles": "Mentioned Files", + "taskCoordination": "Task Coordination", + "thinkingText": "Thinking + Text", + "toolOutputs": "Tool Outputs", + "userMessages": "User Messages" + }, + "title": "New Context Injected In This Turn", + "tokenCount": "~{{tokens}} tokens", + "totalNewTokens": "Total new tokens", + "turn": "Turn {{turn}}", + "sectionSummary_few": "{{title}} ({{count}}) ~{{tokens}} tokens", + "sectionSummary_many": "{{title}} ({{count}}) ~{{tokens}} tokens", + "sectionSummary_one": "{{title}} ({{count}}) ~{{tokens}} tokens", + "sectionSummary_other": "{{title}} ({{count}}) ~{{tokens}} tokens" + }, + "locales": { + "emptyMessage": "No language found.", + "names": { + "en": "English", + "ru": "Russian", + "system": "System" + }, + "searchPlaceholder": "Search language...", + "selectPlaceholder": "Select app language...", + "systemWithResolved": "System - {{locale}}" + }, + "members": { + "emptyMessage": "No members found.", + "searchPlaceholder": "Search members...", + "unassigned": "Unassigned", + "teammateFallback": "teammate" + }, + "providerRuntime": { + "codex": { + "install": { + "checking": "Checking", + "downloading": "Downloading", + "installCli": "Install Codex CLI", + "installing": "Installing", + "retryInstall": "Retry install" + } + } + }, + "search": { + "noMatchingSuggestions": "No matching suggestions", + "searching": "Searching...", + "searchingFiles": "Searching files...", + "findInConversation": "Find in conversation...", + "resultCount": "{{current}} of {{total}}", + "resultCountCapped": "{{current}} of {{total}}+", + "noResults": "No results", + "previousResultShortcut": "Previous result (Shift+Enter)", + "nextResultShortcut": "Next result (Enter)", + "closeShortcut": "Close (Esc)", + "nothingFound": "Nothing found", + "placeholder": "Search..." + }, + "schedules": { + "actions": { + "addSchedule": "Add Schedule", + "clearFilters": "Clear filters", + "createSchedule": "Create Schedule", + "delete": "Delete", + "edit": "Edit", + "pause": "Pause", + "resume": "Resume", + "runNow": "Run now" + }, + "empty": { + "description": "Create a schedule on any team to automate Claude task execution with cron expressions. Schedules from all teams will appear here.", + "noMatches": "No schedules match the current filters", + "title": "No scheduled tasks" + }, + "filters": { + "allTeams": "All teams" + }, + "item": { + "loadingRunHistory": "Loading run history...", + "nextRun": "Next: {{value}}", + "noRunsYet": "No runs yet" + }, + "loading": "Loading schedules...", + "searchPlaceholder": "Search schedules...", + "status": { + "active": "Active", + "all": "All", + "disabled": "Disabled", + "paused": "Paused" + }, + "title": "Schedules" + }, + "sessions": { + "actions": { + "hide": "Hide", + "pin": "Pin", + "unhide": "Unhide" + }, + "empty": { + "noMatchingSessions": "No matching sessions", + "noMatchingSessionsDescription": "This project has no matching sessions yet.", + "noMatchingSessionsFiltered": "Try another query or reset the provider filter.", + "noSessions": "No sessions found", + "noSessionsDescription": "This project has no sessions yet", + "selectProject": "Select a project to view sessions" + }, + "errors": { + "loading": "Error loading sessions" + }, + "loadedMatchingMore": "{{count}} matching sessions loaded so far - scroll down to load more.", + "loadingMore": "Loading more sessions...", + "pinned": "Pinned", + "scrollToLoadMore": "Scroll to load more", + "search": { + "clear": "Clear session search", + "placeholder": "Search sessions..." + }, + "selection": { + "cancel": "Cancel selection", + "exitMode": "Exit selection mode", + "hideSelected": "Hide selected sessions", + "pinSelected": "Pin selected sessions", + "selectSessions": "Select sessions", + "selected": "{{count}} selected", + "unhideSelected": "Unhide selected sessions", + "selected_few": "{{count}} selected", + "selected_many": "{{count}} selected", + "selected_one": "{{count}} selected", + "selected_other": "{{count}} selected" + }, + "sort": { + "byContext": "By Context", + "byContextTooltip": "Sort by context consumption", + "byRecentTooltip": "Sort by recent", + "contextLoadedOnly": "Context sorting only ranks loaded sessions." + }, + "title": "Sessions", + "visibility": { + "hideHidden": "Hide hidden sessions", + "showHidden": "Show hidden sessions" + }, + "worktree": { + "switch": "Switch Worktree" + }, + "loadedMatchingMore_few": "{{count}} matching sessions loaded so far - scroll down to load more.", + "loadedMatchingMore_many": "{{count}} matching sessions loaded so far - scroll down to load more.", + "loadedMatchingMore_one": "{{count}} matching sessions loaded so far - scroll down to load more.", + "loadedMatchingMore_other": "{{count}} matching sessions loaded so far - scroll down to load more.", + "failedToLoad": "Failed to load session", + "loading": "Loading session...", + "filter": { + "title": "Filter sessions" + }, + "count": "{{count}} sessions", + "count_one": "{{count}} session", + "count_other": "{{count}} sessions", + "count_few": "{{count}} sessions", + "count_many": "{{count}} sessions", + "inProgress": "Session is in progress..." + }, + "states": { + "loading": "Loading...", + "offline": "Offline", + "online": "Online", + "unknown": "Unknown", + "error": "Error" + }, + "markdown": { + "imageFallback": "[Image: {{label}}]", + "largeContentNotice": "Content is very large ({{count}} chars). Showing raw preview to keep the UI responsive.", + "largeContentTitle": "Large content is shown as raw to prevent UI freeze", + "raw": "Raw", + "rawPreview": "Raw preview", + "renderMarkdown": "Render markdown", + "showAll": "Show all", + "showMore": "Show more", + "showRaw": "Show raw", + "showingChars": "Showing {{shown}} / {{total}} chars", + "largeContentNotice_few": "Content is very large ({{count}} chars). Showing raw preview to keep the UI responsive.", + "largeContentNotice_many": "Content is very large ({{count}} chars). Showing raw preview to keep the UI responsive.", + "largeContentNotice_one": "Content is very large ({{count}} chars). Showing raw preview to keep the UI responsive.", + "largeContentNotice_other": "Content is very large ({{count}} chars). Showing raw preview to keep the UI responsive." + }, + "terminal": { + "checkOutputForDetails": "Check terminal output above for details", + "closingInSeconds": "Closing in {{count}}s...", + "closingInSeconds_few": "Closing in {{count}}s...", + "closingInSeconds_many": "Closing in {{count}}s...", + "closingInSeconds_one": "Closing in {{count}}s...", + "closingInSeconds_other": "Closing in {{count}}s...", + "completedSuccessfully": "Completed successfully", + "exitCode": "(exit code {{code}})", + "processFailed": "Process failed", + "title": "Terminal" + }, + "tokens": { + "accumulatedWithoutDuplication": "Accumulated across entire session without duplication", + "cacheRead": "Cache Read", + "cacheWrite": "Cache Write", + "costUsd": "Cost (USD)", + "inputTokens": "Input Tokens", + "model": "Model", + "outputTokens": "Output Tokens", + "phase": "Phase {{phase}}/{{total}}", + "promptInputShare": "{{percent}}% of prompt input", + "taskCoordination": "Task Coordination", + "thinkingText": "Thinking + Text", + "toolOutputs": "Tool Outputs", + "total": "Total", + "userMessages": "User Messages", + "visibleContext": "Visible Context", + "includesClaudeMd": "incl. CLAUDE.md ×{{count}}", + "claudeMd": "CLAUDE.md", + "mentionedFiles": "@files", + "percentValue": "({{percent}}%)", + "approxTokens": "~{{tokens}} tokens", + "approxTokensParenthesized": "(~{{tokens}})" + }, + "list": { + "actions": { + "copyTeam": "Copy team", + "createTeam": "Create Team", + "deleteForever": "Delete forever", + "deletePermanently": "Delete permanently", + "deleteTeam": "Delete team", + "launching": "Launching...", + "launchTeam": "Launch team", + "relaunchTeam": "Relaunch team", + "restore": "Restore", + "restoreTeam": "Restore team", + "retry": "Retry", + "stopTeam": "Stop team", + "stopping": "Stopping..." + }, + "status": { + "active": "Active", + "deleted": "Deleted", + "launching": "Launching...", + "offline": "Offline", + "partialFailure": "Launch failed partway", + "partialPending": "Bootstrap pending", + "partialSkipped": "Launch skipped member", + "running": "Running" + }, + "partial": { + "pending": "Last launch is still reconciling.", + "skipped": "Last launch has skipped teammates.", + "skippedWithCount": "Last launch skipped {{count}}/{{expected}} teammate.", + "skippedWithCount_few": "Last launch skipped {{count}}/{{expected}} teammates.", + "skippedWithCount_many": "Last launch skipped {{count}}/{{expected}} teammates.", + "skippedWithCount_one": "Last launch skipped {{count}}/{{expected}} teammate.", + "skippedWithCount_other": "Last launch skipped {{count}}/{{expected}} teammates.", + "stopped": "Last launch stopped before all teammates joined.", + "stoppedWithCount": "Last launch stopped before {{count}}/{{expected}} teammate joined.", + "stoppedWithCount_few": "Last launch stopped before {{count}}/{{expected}} teammates joined.", + "stoppedWithCount_many": "Last launch stopped before {{count}}/{{expected}} teammates joined.", + "stoppedWithCount_one": "Last launch stopped before {{count}}/{{expected}} teammate joined.", + "stoppedWithCount_other": "Last launch stopped before {{count}}/{{expected}} teammates joined." + }, + "noDescription": "No description", + "solo": "Solo", + "membersCount": "Members: {{count}}", + "membersCount_few": "Members: {{count}}", + "membersCount_many": "Members: {{count}}", + "membersCount_one": "Member: {{count}}", + "membersCount_other": "Members: {{count}}", + "all": "All", + "moreCount": "+{{count}} more", + "moreCount_one": "+{{count}} more", + "moreCount_other": "+{{count}} more", + "moreCount_few": "+{{count}} more", + "moreCount_many": "+{{count}} more" + }, + "runtimeProvider": { + "defaults": { + "scopeDescriptionAllProjects": "Default for every project that does not have its own OpenCode override.", + "scopeDescriptionProject": "Override only the selected project. Running teams are not changed.", + "setAllProjectsDefault": "Set all-projects default", + "setProjectDefault": "Set project default", + "validationContext": "Validation context", + "projectOverrideContext": "Project override context", + "selectProjectHint": "Select a project before testing local models or saving defaults.", + "allProjectsHint": "Tests use {{project}}. Default applies unless a project has an override.", + "projectHint": "Saving overrides only {{project}}." + } + }, + "sessionContext": { + "header": { + "title": "Context", + "closePanel": "Close panel", + "phase": "Phase:", + "current": "Current", + "view": "View:", + "category": "Category", + "bySize": "By Size" + }, + "metrics": { + "unavailable": "Unavailable", + "contextUsed": "Context Used", + "promptInput": "Prompt Input", + "visibleContext": "Visible Context", + "ofContext": "of context", + "ofPrompt": "of prompt", + "codexTelemetryUnavailable": "Codex prompt-side usage is not exposed by the current runtime telemetry yet, so Prompt Input and Context Used stay unavailable instead of showing a fake zero.", + "sessionCost": "Session Cost:", + "parentPlus": "parent +", + "subagents": "subagents", + "details": "details" + }, + "help": { + "contextUsed": { + "title": "Context Used", + "description": "Prompt input plus output tokens currently occupying the model's context window." + }, + "promptInput": { + "title": "Prompt Input", + "description": "Tokens sent to the model before generation. For Claude this includes `input_tokens + cache_creation_input_tokens + cache_read_input_tokens`." + }, + "visibleContext": { + "title": "Visible Context", + "description": "The inspectable subset of prompt input: files, CLAUDE.md, tool outputs, user messages, and similar injections that you can optimize directly." + }, + "availability": { + "title": "Availability", + "description": "If a provider runtime does not expose prompt-side usage yet, the panel shows metrics as unavailable instead of pretending they are zero." + } + }, + "items": { + "turn": "@Turn {{turn}}", + "tokensApprox": "~{{tokens}} tokens", + "toolsCount": "{{count}} tools", + "toolsCount_one": "{{count}} tool", + "toolsCount_other": "{{count}} tools", + "toolsCount_few": "{{count}} tools", + "toolsCount_many": "{{count}} tools", + "itemsCount": "{{count}} items", + "itemsCount_one": "{{count}} item", + "itemsCount_other": "{{count}} items", + "itemsCount_few": "{{count}} items", + "itemsCount_many": "{{count}} items", + "missing": "missing", + "thinking": "Thinking", + "text": "Text" + }, + "empty": "No context injections detected in this session", + "view": { + "grouped": "Grouped", + "flat": "Flat" + }, + "claudeMdFiles": "CLAUDE.md Files", + "mentionedFiles": "Mentioned Files" + }, + "chat": { + "subagent": { + "fallbackName": "Subagent", + "shutdownConfirmed": "Shutdown confirmed", + "summary": { + "tools": "{{count}} tools", + "tools_one": "{{count}} tool", + "tools_other": "{{count}} tools", + "tools_few": "{{count}} tools", + "tools_many": "{{count}} tools" + }, + "meta": { + "type": "Type", + "duration": "Duration", + "model": "Model", + "id": "ID" + }, + "metrics": { + "contextWindow": "Context Window", + "contextUsage": "Context Usage", + "mainContext": "Main Context", + "totalOutput": "Total Output", + "turns": "({{count}} turns)", + "turns_one": "({{count}} turn)", + "turns_other": "({{count}} turns)", + "subagentContext": "Subagent Context", + "phase": "Phase {{phase}}", + "turns_few": "({{count}} turns)", + "turns_many": "({{count}} turns)" + }, + "trace": { + "title": "Execution Trace" + } + }, + "user": { + "you": "You", + "showMore": "Show more", + "showLess": "Show less", + "backgroundTask": "Background task", + "exitCode": "exit {{code}}", + "imagesAttached": "{{count}} images attached", + "imagesAttached_one": "{{count}} image attached", + "imagesAttached_few": "{{count}} images attached", + "imagesAttached_many": "{{count}} images attached", + "imagesAttached_other": "{{count}} images attached" + }, + "compact": { + "toggle": "Toggle compacted content", + "contextCompacted": "Context compacted", + "freedTokens": "({{tokens}} freed)", + "phase": "Phase {{phase}}", + "conversationCompacted": "Conversation Compacted", + "summary": "Previous messages were summarized to save context. The full conversation history is preserved in the session file.", + "compacted": "Compacted" + }, + "executionTrace": { + "empty": "No execution items", + "nested": "Nested: {{name}}", + "input": "Input" + }, + "items": { + "empty": "No items to display" + }, + "tools": { + "teammateSpawned": "Teammate spawned", + "shutdownRequested": "Shutdown requested ->", + "noResultReceived": "No result received", + "duration": "Duration: {{duration}}", + "result": "Result", + "write": { + "createdFile": "Created file", + "wroteToFile": "Wrote to file" + }, + "skill": { + "instructions": "Skill Instructions", + "unknown": "Unknown Skill" + } + }, + "lastOutput": { + "requestInterrupted": "Request interrupted by user", + "planReadyForApproval": "Plan Ready for Approval" + }, + "empty": { + "icon": "💬", + "title": "No conversation history", + "description": "This session does not contain any messages yet." + }, + "context": { + "remainingPercent": "({{percent}}% left)", + "count": "Context ({{count}})", + "count_one": "Context ({{count}})", + "count_other": "Context ({{count}})", + "count_few": "Context ({{count}})", + "count_many": "Context ({{count}})" + }, + "scrollToBottom": "Scroll to bottom", + "bottom": "Bottom", + "teammateMessage": { + "message": "Message", + "resent": "Resent", + "fallback": "Teammate message" + }, + "system": { + "label": "System" + } + }, + "tmuxInstaller": { + "summaryTitle": "tmux is not installed", + "detectedOs": "Detected OS: {{os}}", + "runtimePath": "Runtime path: {{path}}", + "phase": "Phase: {{phase}}", + "actions": { + "cancel": "Cancel", + "manualGuide": "Manual guide", + "hideSetupSteps": "Hide setup steps", + "showSetupSteps": "Show setup steps ({{count}})", + "showSetupSteps_one": "Show setup step ({{count}})", + "showSetupSteps_other": "Show setup steps ({{count}})", + "recheck": "Re-check", + "showSetupSteps_few": "Show setup steps ({{count}})", + "showSetupSteps_many": "Show setup steps ({{count}})" + }, + "installerProgress": "Installer progress", + "input": { + "placeholder": "Send input to the installer", + "send": "Send input", + "passwordNotice": "Password input is sent directly to the installer terminal and is not added to the log output." + }, + "details": { + "show": "Show details", + "hide": "Hide details" + } + }, + "commandPalette": { + "noRecentActivity": "No recent activity", + "sessionsCount": "{{count}} sessions", + "sessionsCount_one": "{{count}} session", + "sessionsCount_other": "{{count}} sessions", + "mode": { + "searchProjects": "Search projects", + "searchAcrossProjects": "Search across all projects", + "searchInProject": "Search in project" + }, + "currentProject": "Current project", + "global": "Global", + "placeholders": { + "projects": "Search projects...", + "conversations": "Search conversations..." + }, + "empty": { + "noProjectsForQuery": "No projects found for \"{{query}}\"", + "noProjects": "No projects found", + "minChars": "Type at least 2 characters to search", + "noFastResults": "No fast results in recent sessions for \"{{query}}\"", + "noResults": "No results found for \"{{query}}\"" + }, + "footer": { + "projectsCount": "{{count}} projects", + "projectsCount_one": "{{count}} project", + "projectsCount_other": "{{count}} projects", + "results": "{{count}} {{speed}}results", + "results_one": "{{count}} {{speed}}result", + "results_other": "{{count}} {{speed}}results", + "resultsAcrossProjects": "{{count}} {{speed}}results across all projects", + "resultsAcrossProjects_one": "{{count}} {{speed}}result across all projects", + "resultsAcrossProjects_other": "{{count}} {{speed}}results across all projects", + "fastPrefix": "fast ", + "typeToSearch": "Type to search", + "navigate": "navigate", + "select": "select", + "open": "open", + "global": "global", + "close": "close", + "results_few": "{{count}} {{speed}}results", + "results_many": "{{count}} {{speed}}results", + "resultsAcrossProjects_few": "{{count}} {{speed}}results across all projects", + "resultsAcrossProjects_many": "{{count}} {{speed}}results across all projects", + "projectsCount_few": "{{count}} projects", + "projectsCount_many": "{{count}} projects", + "upDownKey": "↑↓", + "escapeKey": "esc" + }, + "sessionsCount_few": "{{count}} sessions", + "sessionsCount_many": "{{count}} sessions" + }, + "tasksPanel": { + "title": "Tasks", + "searchPlaceholder": "Search tasks...", + "pinned": "Pinned", + "groupByLabel": "Group by:", + "groupByAria": "Group by", + "groupModes": { + "none": "None", + "project": "Project", + "time": "Time" + }, + "showArchived": "Show archived", + "hideArchived": "Hide archived", + "empty": { + "noMatchingTasks": "No matching tasks", + "noTasks": "No tasks found" + }, + "teamLabel": "Team: {{team}}", + "showMore": "Show more", + "showLess": "Show less", + "deleteConfirm": { + "title": "Delete task", + "message": "Move task #{{taskId}} to trash?", + "confirmLabel": "Delete", + "cancelLabel": "Cancel" + }, + "deleteFailed": { + "title": "Failed to delete task", + "fallbackMessage": "An unexpected error occurred", + "confirmLabel": "OK" + }, + "sort": { + "byTime": "By time", + "byUnread": "By unread", + "byProject": "By project", + "byTeam": "By team" + } + }, + "toolViewer": { + "input": "Input", + "replaceAll": "(replace all)", + "noInputRecorded": "No input recorded for this tool call.", + "agent": { + "action": "action", + "teammate": "teammate", + "team": "team", + "runtime": "runtime", + "type": "type", + "startupInstructionsHidden": "Startup instructions are hidden in the UI." + } + }, + "taskContextMenu": { + "unpin": "Unpin", + "pin": "Pin", + "rename": "Rename", + "markUnread": "Mark as unread", + "unarchive": "Unarchive", + "archive": "Archive", + "deleteTask": "Delete task" + }, + "updateDialog": { + "closeDialog": "Close dialog", + "updateAvailable": "Update available", + "updateReady": "Update Ready", + "noReleaseNotes": "No release notes available.", + "viewOnGitHub": "View on GitHub", + "later": "Later", + "restartNow": "Restart now", + "download": "Download" + }, + "errorBoundary": { + "title": "Something went wrong", + "description": "An unexpected error occurred in the application. You can try reloading the page or resetting the error state.", + "componentStack": "Component Stack", + "tryAgain": "Try Again", + "copied": "Copied", + "copyErrorDetails": "Copy Error Details", + "reportBugOnGitHub": "Report Bug on GitHub", + "reloadApp": "Reload App", + "diagnosticsNotice": "GitHub bug reports and copied diagnostics include the error message, stack traces, app version, active tab, selected team, task context, and environment details." + }, + "runtimeBackendSelector": { + "label": "Runtime backend", + "resolved": "Resolved: {{backend}}", + "current": "Current", + "recommended": "Recommended", + "unavailable": "Unavailable", + "cannotSelectYet": "This backend cannot be selected yet.", + "auto": "Auto", + "autoCurrently": "Auto (currently: {{backend}})", + "audience": { + "internal": "Internal" + }, + "states": { + "locked": "Locked", + "disabled": "Disabled", + "authRequired": "Auth required", + "runtimeMissing": "Runtime missing", + "degraded": "Degraded", + "unavailable": "Unavailable" + } + }, + "providerModelBadges": { + "checking": "Checking", + "unavailable": "Unavailable", + "checkFailed": "Check failed", + "free": "Free", + "freeTooltip": "Reported by OpenCode metadata. Availability and limits may change." + }, + "taskFilters": { + "status": "Status", + "clearAll": "Clear all", + "selectAll": "Select all", + "team": "Team", + "allTeams": "All teams", + "searchTeams": "Search teams...", + "noTeamsFound": "No teams found", + "project": "Project", + "allProjects": "All Projects", + "searchProjects": "Search projects...", + "noProjects": "No projects", + "comments": "Comments", + "apply": "Apply", + "read": { + "all": "All", + "unread": "Unread", + "read": "Read" + }, + "statusOptions": { + "todo": "TODO", + "inProgress": "IN PROGRESS", + "needsFix": "NEEDS FIXES", + "done": "DONE", + "review": "REVIEW", + "approved": "APPROVED" + } + }, + "sessionItem": { + "totalContext": "Total Context: {{tokens}} tokens", + "context": "Context: {{tokens}}", + "phase": "Phase {{phase}}:", + "compactedTo": "(compacted to {{tokens}})" + }, + "notifications": { + "row": { + "team": "team", + "subagent": "subagent", + "markAsRead": "Mark as read", + "delete": "Delete", + "viewInSession": "View in session" + }, + "title": "Notifications", + "loading": "Loading notifications...", + "actions": { + "markFilteredAsRead": "Mark filtered as read", + "markAllAsRead": "Mark all as read", + "markFilteredRead": "Mark filtered read", + "markAllRead": "Mark all read", + "clearFilteredNotifications": "Clear filtered notifications", + "clearAllNotifications": "Clear all notifications", + "clickToConfirm": "Click to confirm", + "clearFiltered": "Clear filtered", + "clearAll": "Clear all" + }, + "counts": { + "unreadInFilter": "{{count}} unread in filter", + "unreadInFilter_one": "{{count}} unread in filter", + "unreadInFilter_few": "{{count}} unread in filter", + "unreadInFilter_many": "{{count}} unread in filter", + "unreadInFilter_other": "{{count}} unread in filter", + "inFilter": "{{count}} in filter", + "inFilter_one": "{{count}} in filter", + "inFilter_few": "{{count}} in filter", + "inFilter_many": "{{count}} in filter", + "inFilter_other": "{{count}} in filter", + "unread": "{{count}} unread", + "unread_one": "{{count}} unread", + "unread_few": "{{count}} unread", + "unread_many": "{{count}} unread", + "unread_other": "{{count}} unread", + "total": "{{count}} total", + "total_one": "{{count}} total", + "total_few": "{{count}} total", + "total_many": "{{count}} total", + "total_other": "{{count}} total" + }, + "filters": { + "other": "Other" + }, + "empty": { + "noMatching": "No matching notifications", + "noNotifications": "No notifications", + "tryDifferentFilter": "Try a different filter", + "allCaughtUp": "You're all caught up!" + } + }, + "updates": { + "restartToUpdate": "Restart to update", + "updateApp": "Update app", + "downloadedRestartTooltip": "Update downloaded, restart to apply", + "newVersionAvailable": "New version available", + "updatingApp": "Updating app", + "updateReady": "Update ready", + "restartNow": "Restart now" + }, + "layout": { + "github": "GitHub", + "discord": "Discord", + "expandSidebar": "Expand sidebar", + "collapseSidebarShortcut": "Collapse sidebar ({{shortcut}})", + "sidebarView": "Sidebar view", + "resizeSidebar": "Resize sidebar", + "closeTab": "Close tab", + "openedFromSearch": "Opened from search", + "pinnedSession": "Pinned session", + "jumpToSection": "Jump to section", + "newTab": "New tab", + "newTabDashboard": "New tab (Dashboard)", + "refreshSession": "Refresh session", + "refreshSessionWithShortcut": "Refresh Session ({{shortcut}})", + "loadingTab": "Loading tab", + "menu": { + "teams": "Teams", + "settings": "Settings", + "extensions": "Extensions", + "search": "Search", + "schedules": "Schedules", + "docs": "Docs", + "exportMarkdown": "Export as Markdown", + "exportJson": "Export as JSON", + "exportPlainText": "Export as Plain Text", + "analyzeSession": "Analyze Session" + }, + "tabMenu": { + "closeTabs": "Close {{count}} Tabs", + "closeTabs_one": "Close {{count}} Tab", + "closeTabs_few": "Close {{count}} Tabs", + "closeTabs_many": "Close {{count}} Tabs", + "closeTabs_other": "Close {{count}} Tabs", + "closeTab": "Close Tab", + "closeOtherTabs": "Close Other Tabs", + "splitRight": "Split Right", + "splitLeft": "Split Left", + "pinToSidebar": "Pin to Sidebar", + "unpinFromSidebar": "Unpin from Sidebar", + "hideFromSidebar": "Hide from Sidebar", + "unhideFromSidebar": "Unhide from Sidebar", + "closeAllTabs": "Close All Tabs" + }, + "sections": { + "team": "Team", + "sessions": "Sessions", + "kanban": "Kanban", + "claudeLogs": "Claude Logs", + "messages": "Messages" + } + }, + "editorFormatting": { + "bold": "Bold", + "italic": "Italic", + "strike": "Strike", + "code": "Code" + }, + "diff": { + "changed": "Changed", + "noChangesDetected": "No changes detected" + }, + "codexLogin": { + "copyLoginLinkAndCode": "Copy ChatGPT login link and code", + "copyLoginLink": "Copy ChatGPT login link", + "copyFailed": "Copy failed", + "copyLinkAndCode": "Copy link + code", + "copyLink": "Copy link", + "enterCodeOnLoginPage": "Enter this code on the ChatGPT login page" + }, + "window": { + "minimize": "Minimize", + "maximize": "Maximize", + "restore": "Restore" + }, + "context": { + "local": "Local", + "switchingTo": "Switching to {{workspace}}", + "loadingWorkspace": "Loading workspace", + "switchWorkspace": "Switch Workspace" + }, + "repositories": { + "noneAvailable": "No repositories available", + "remove": "Remove repository" + }, + "export": { + "session": "Export session", + "sessionTitle": "Export Session" + }, + "brand": { + "claude": "Claude" + }, + "sessionReport": { + "noSessionData": "No session data available", + "title": "Session Report" + }, + "sessionFilters": { + "project": { + "selectProject": "Select Project" + } + }, + "tasks": { + "date": { + "updatedPrefix": "upd", + "updatedYesterday": "upd yesterday", + "yesterday": "Yesterday" + }, + "reviewState": { + "needsFix": "Needs Fixes" + }, + "unassigned": "unassigned" + } +} diff --git a/src/features/localization/renderer/locales/en/dashboard.json b/src/features/localization/renderer/locales/en/dashboard.json new file mode 100644 index 00000000..7fe4d83e --- /dev/null +++ b/src/features/localization/renderer/locales/en/dashboard.json @@ -0,0 +1,197 @@ +{ + "cliStatus": { + "actions": { + "alreadyLoggedIn": "Already logged in?", + "becomeSponsor": "Become a sponsor", + "cancel": "Cancel", + "checkNow": "Check now", + "checkUpdates": "Check for Updates", + "checking": "Checking...", + "connect": "Connect", + "extensions": "Extensions", + "login": "Login", + "manage": "Manage", + "manageProviders": "Manage Providers", + "plan": "Plan", + "recheck": "Re-check", + "recheckProvider": "Re-check {{provider}}", + "retry": "Retry", + "updateTo": "Update to v{{version}}", + "useCode": "Use code" + }, + "atlas": { + "alt": "Atlas Cloud", + "description": "Atlas Cloud is a full-modal AI inference platform that gives developers a single AI API to access video generation, image generation, and LLM APIs. Instead of managing multiple vendor integrations, you connect once and get unified access to 300+ curated models across all modalities. Check out Atlas Cloud's new coding plan promotion for more budget-friendly API access.", + "openCodeProvider": "OpenCode provider", + "plan": "Atlas Cloud coding plan", + "sponsor": "Sponsor" + }, + "errors": { + "checkStatusFailed": "Failed to check CLI status", + "installationFailed": "Installation failed", + "refreshFailed": "Failed to check for updates. Check your network connection and try again.", + "runtimeUpdatedRefreshFailed": "Runtime updated, but failed to refresh provider status." + }, + "hints": { + "backgroundStatus": "{{runtime}} status will be checked in the background.", + "codexApiKeyFallback": "{{hint}} API key fallback is available if you switch auth mode.", + "codexAutoApiKey": "{{hint}} Auto will keep using the API key until ChatGPT is connected.", + "codexFinishLogin": "Finish ChatGPT login in the browser. Enter the shown code if prompted.", + "codexNoActiveLogin": "Usage limits appear only after Codex CLI sees an active ChatGPT account. Right now it reports no active ChatGPT login.", + "codexNoActiveManagedSession": "Usage limits appear only after Codex CLI sees an active ChatGPT account. Local Codex account data exists, but no active managed session is selected right now.", + "codexReconnectNeeded": "Usage limits appear only after Codex refreshes the currently selected ChatGPT session. Right now the local session needs reconnect.", + "firstCheckSlow": "First check may take up to 30 seconds", + "loginRequiredForTeams": "Browsing sessions and projects works without login. Login is only needed to run agent teams.", + "troubleshootTitle": "If you're sure you're logged in, try these steps:" + }, + "installer": { + "checkingLatest": "Checking latest version...", + "downloading": "Downloading {{runtime}}...", + "installing": "Installing {{runtime}}...", + "success": "Successfully installed {{runtime}} v{{version}}", + "verifying": "Verifying checksum..." + }, + "labels": { + "apiKeyRequired": "API key required", + "comingSoon": "Coming soon", + "collapseProviderDetails": "Collapse provider details", + "expandProviderDetails": "Expand provider details", + "generateLink": "Generate link", + "loadingRateLimits": "Rate limits loading", + "loggedOut": "Provider logged out", + "loginAuthFailed": "Authentication failed", + "loginAuthUpdated": "Authentication updated", + "loginComplete": "Login complete", + "loginFailed": "Login failed", + "loginTitle": "Login", + "logoutFailed": "Logout failed", + "logoutTitle": "Logout", + "notLoggedIn": "Not logged in", + "openLogin": "Open login", + "providerActionRequired": "Provider action required", + "resets": "resets {{time}}", + "runtimeLoginTitle": "{{runtime}} Login" + }, + "loading": { + "aiProviders": "Checking AI Providers...", + "claudeCli": "Checking Claude CLI..." + }, + "provider": { + "authenticated": "Authenticated", + "backend": "Backend: {{backend}}", + "checkingAuthentication": "Checking authentication...", + "checkingProviders": "Checking providers...", + "configuredLocalCount": "{{count}} configured local", + "configuredLocalCount_few": "{{count}} configured local", + "configuredLocalCount_many": "{{count}} configured local", + "configuredLocalCount_one": "{{count}} configured local", + "configuredLocalCount_other": "{{count}} configured local", + "configuredLocalTitle": "Local OpenCode routes imported from your OpenCode config.", + "connectedCount": "Providers: {{connected}}/{{denominator}} connected", + "freeModels": "Free models", + "freeModelsTitle": "OpenCode includes free model options such as Big Pickle when available in your setup. OpenRouter through OpenCode can also expose free models, but not every OpenCode/OpenRouter model is free. Availability and limits may change.", + "loadingModels": "Loading models...", + "modelsUnavailable": "Models unavailable for this runtime build", + "runtime": "Runtime: {{runtime}}", + "verifiedCount": "{{count}} verified", + "verifiedCount_few": "{{count}} verified", + "verifiedCount_many": "{{count}} verified", + "verifiedCount_one": "{{count}} verified", + "verifiedCount_other": "{{count}} verified", + "verifiedTitle": "OpenCode routes with a successful execution proof." + }, + "runtime": { + "configuredHealthCheckFailed": "The configured {{runtime}} failed its startup health check.", + "configuredNotFound": "The configured {{runtime}} was not found.", + "foundButFailed": "{{runtime}} was found but failed to start", + "healthCheckFailedDescription": "The app found the configured {{runtime}}, but its startup health check failed. Repair or reinstall it, then retry.", + "install": "Install {{runtime}}", + "installRequiredDescription": "{{runtime}} is required for team provisioning and session management. Install it to get started.", + "isRequired": "{{runtime}} is required", + "reinstall": "Reinstall {{runtime}}" + }, + "runtimeInstall": { + "checking": "Checking", + "codexTitle": "Install Codex CLI into app data", + "downloading": "Downloading", + "downloadingPercent": "Downloading {{percent}}%", + "install": "Install", + "installing": "Installing", + "openCodeTitle": "Install OpenCode runtime into app data", + "retryInstall": "Retry install" + }, + "troubleshoot": { + "again": "again", + "authStatusCommand": "your configured CLI auth status command", + "checkLoggedIn": "- check if it shows \"Logged in\"", + "click": "Click", + "loginCommand": "the runtime login command", + "logoutCommand": "the runtime logout command", + "openTerminal": "Open your terminal and run:", + "reloginPrefix": "If it says logged in but the app doesn't see it, try:", + "sameRuntime": "Make sure the CLI in your terminal is the same runtime the app uses", + "statusCacheHint": "- sometimes the status is cached for a few seconds", + "then": "then" + }, + "warnings": { + "multipleApiKeysMissing": "One or more providers are set to API key mode, but no API key is configured. Open Manage Providers to add keys or switch the connection mode.", + "multipleApiKeysNeedAttention": "One or more providers are set to API key mode and need attention. Open Manage Providers to review saved keys or switch the connection mode.", + "notAuthenticated": "{{runtime}} is installed but you are not authenticated. Login is required for team provisioning and AI features.", + "singleApiKeyMissing": "{{provider}} is set to API key mode, but no API key is configured. Open Manage Providers to add a key or switch the connection mode.", + "singleApiKeyNeedsAttention": "{{provider}} is set to API key mode, but it is not connected. Open Manage Providers to review the saved key or switch the connection mode." + } + }, + "recentProjects": { + "selectFolderTitle": "Select a project folder", + "selectFolder": "Select Folder", + "failedToLoad": "Failed to load projects", + "retry": "Retry", + "noProjects": "No projects found", + "noMatches": "No matches for \"{{query}}\"", + "noRecentProjects": "No recent projects found", + "emptyDescription": "Recent Claude and Codex activity will appear here.", + "loadMore": "Load more", + "card": { + "deleted": "Deleted", + "projectFolderMissing": "Project folder no longer exists", + "taskCounts": { + "active": "{{count}} active", + "active_one": "{{count}} active", + "active_other": "{{count}} active", + "active_few": "{{count}} active", + "active_many": "{{count}} active", + "pending": "{{count}} pending", + "pending_one": "{{count}} pending", + "pending_other": "{{count}} pending", + "pending_few": "{{count}} pending", + "pending_many": "{{count}} pending", + "done": "{{count}} done", + "done_one": "{{count}} done", + "done_other": "{{count}} done", + "done_few": "{{count}} done", + "done_many": "{{count}} done" + } + }, + "title": "Recent Projects", + "searchResults": "Search Results", + "searchPlaceholder": "Search projects..." + }, + "actions": { + "selectTeam": "Select Team", + "or": "or", + "clearSearch": "Clear search" + }, + "windowsAdmin": { + "title": "Windows Administrator mode recommended", + "description": "OpenCode runtime checks can time out when Agent Teams AI is not elevated. Restart the app with Run as administrator before launching OpenCode teams." + }, + "webPreview": { + "title": "Open the desktop app for full functionality", + "description": "The browser version is still in development. Project actions, integrations, and live status updates may be limited here. Use the desktop app to access all features reliably." + }, + "updateBanner": { + "newVersionAvailable": "New version available", + "restartNow": "Restart now", + "viewDetails": "View details" + } +} diff --git a/src/features/localization/renderer/locales/en/errors.json b/src/features/localization/renderer/locales/en/errors.json new file mode 100644 index 00000000..027abdd1 --- /dev/null +++ b/src/features/localization/renderer/locales/en/errors.json @@ -0,0 +1,3 @@ +{ + "fallback": "Something went wrong." +} diff --git a/src/features/localization/renderer/locales/en/extensions.json b/src/features/localization/renderer/locales/en/extensions.json new file mode 100644 index 00000000..acfcea20 --- /dev/null +++ b/src/features/localization/renderer/locales/en/extensions.json @@ -0,0 +1,684 @@ +{ + "store": { + "actions": { + "addCustom": "Add Custom", + "openDashboard": "Open Dashboard", + "refreshCatalog": "Refresh catalog" + }, + "capabilities": { + "mcp": "MCP: {{status}}", + "plugins": "Plugins: {{status}}", + "skills": "Skills: {{status}}" + }, + "desktopOnly": "Available in the desktop app only.", + "provider": { + "checkingStatus": "Checking provider status...", + "connected": "Connected", + "loading": "Loading...", + "needsSetup": "Needs setup", + "readyToConfigure": "Ready to configure", + "unsupported": "Unsupported" + }, + "runtime": { + "checkingAvailabilityDescription": "Extensions need the configured runtime to manage plugins, MCP servers, skills, and provider connections.", + "checkingAvailabilityTitle": "Checking extensions runtime availability", + "failedToStartDescription": "Extensions are disabled until the runtime passes its startup health check. Open the Dashboard to repair or reinstall it.", + "failedToStartTitle": "The configured runtime was found but failed to start", + "multimodelCapabilitiesDescription": "Provider support can differ by section. Plugins are shown only where the runtime explicitly declares support.", + "multimodelCapabilitiesTitle": "Multimodel runtime capabilities", + "needsSignInDescription": "{{runtime}} was found{{version}}, but plugin installs are disabled until you sign in from the Dashboard.", + "needsSignInTitle": "{{runtime}} needs sign-in", + "notAvailableDescription": "Extensions are disabled until the runtime is installed. Open the Dashboard to install it and retry.", + "notAvailableTitle": "The configured runtime is not available", + "readyDescription": "Plugins can be installed from this page{{versionSuffix}}.", + "readyTitle": "{{runtime}} is ready", + "requiredForMutations": "The configured runtime is required to install or uninstall extensions. Install or repair it from the Dashboard." + }, + "sessionsRestartWarning": "Running sessions won't pick up extension changes until restarted.", + "tabs": { + "apiKeys": { + "description": "Secret keys for online services. Add them here so plugins, servers, and integrations can connect and work.", + "label": "API Keys" + }, + "mcpServers": { + "description": "Connections to outside tools and apps. They let the runtime read data or do actions beyond this app.", + "label": "MCP Servers" + }, + "plugins": { + "description": "Small add-ons for the runtime. In multimodel mode they currently apply to Anthropic sessions when supported. Broader provider support is in development.", + "label": "Plugins" + }, + "skills": { + "description": "Ready-made instructions for common jobs. They help the runtime handle repeatable tasks more consistently.", + "label": "Skills" + } + }, + "title": "Extensions" + }, + "pluginsPanel": { + "activeFilters": "{{count}} active", + "browseByFit": "Browse by fit", + "capabilities": "Capabilities", + "categories": "Categories", + "clearAllFilters": "Clear all filters", + "clearFilters": "Clear filters", + "counts": { + "capabilities": "{{count}} capabilities", + "categories": "{{count}} categories", + "plugins": "{{count}} plugins", + "capabilities_few": "{{count}} capabilities", + "capabilities_many": "{{count}} capabilities", + "capabilities_one": "{{count}} capabilities", + "capabilities_other": "{{count}} capabilities", + "categories_few": "{{count}} categories", + "categories_many": "{{count}} categories", + "categories_one": "{{count}} categories", + "categories_other": "{{count}} categories", + "plugins_few": "{{count}} plugins", + "plugins_many": "{{count}} plugins", + "plugins_one": "{{count}} plugins", + "plugins_other": "{{count}} plugins" + }, + "empty": { + "description": "Check back later for new plugins", + "filteredDescription": "Try adjusting your search or filter criteria", + "filteredTitle": "No plugins match your filters", + "title": "No plugins available" + }, + "filterDescription": "Narrow the catalog by category, capability, or installed state.", + "installedOnly": "Installed only", + "providerSupportNotice": "Plugin support is currently guaranteed for Anthropic (Claude) sessions only. We're working to support plugins across all agents.", + "resultsUpdateInstantly": "Results update instantly as you refine filters.", + "searchPlaceholder": "Search plugins...", + "selectedCount": "{{count}} selected", + "showing": "Showing {{shown}} of {{total}} plugins", + "sort": { + "category": "Category", + "nameAsc": "Name A-Z", + "nameDesc": "Name Z-A", + "popular": "Popular" + }, + "activeFilters_few": "{{count}} active", + "activeFilters_many": "{{count}} active", + "activeFilters_one": "{{count}} active", + "activeFilters_other": "{{count}} active", + "selectedCount_few": "{{count}} selected", + "selectedCount_many": "{{count}} selected", + "selectedCount_one": "{{count}} selected", + "selectedCount_other": "{{count}} selected" + }, + "customMcp": { + "actions": { + "add": "Add", + "cancel": "Cancel", + "install": "Install", + "installing": "Installing..." + }, + "description": "Add a server manually without the catalog.", + "errors": { + "installFailed": "Install failed", + "invalidServerName": "Invalid server name. Use alphanumeric characters, dashes, underscores, dots.", + "npmPackageRequired": "npm package name is required", + "serverNameRequired": "Server name is required", + "serverUrlRequired": "Server URL is required" + }, + "fields": { + "environmentVariables": "Environment Variables", + "headers": "Headers", + "npmPackage": "npm Package", + "scope": "Scope", + "serverName": "Server Name", + "serverUrl": "Server URL", + "transport": "Transport", + "transportType": "Transport Type", + "versionOptional": "Version (optional)" + }, + "title": "Add Custom MCP Server", + "transport": { + "httpSse": "HTTP / SSE", + "stdio": "Stdio (npm)" + }, + "placeholders": { + "headerName": "Header-Name", + "envVarName": "ENV_VAR_NAME", + "serverName": "my-server", + "latest": "latest", + "value": "value", + "serverUrl": "https://api.example.com/mcp" + } + }, + "mcpDetail": { + "auth": { + "remoteMayNeedHeaders": "Remote MCP servers may still require custom headers or API keys even when the registry does not describe them. If connection fails after install, check the provider docs.", + "required": "This server requires authentication" + }, + "diagnostics": { + "launchTarget": "Launch Target" + }, + "form": { + "autoFilled": "Auto-filled", + "environmentVariables": "Environment Variables", + "headers": "Headers", + "scope": "Scope", + "serverName": "Server Name" + }, + "install": { + "httpTransport": "HTTP: {{transport}}", + "manualSetupDescription": "This server requires manual setup. Check the repository for installation instructions.", + "manualSetupRequired": "Manual setup required", + "npmPackage": "npm: {{package}}", + "manage": "Manage Installation", + "install": "Install Server" + }, + "links": { + "glama": "Glama", + "repository": "Repository", + "website": "Website" + }, + "metadata": { + "author": "Author", + "githubStars": "GitHub Stars", + "hosting": "Hosting", + "installType": "Install Type", + "license": "License", + "published": "Published", + "source": "Source", + "updated": "Updated", + "version": "Version" + }, + "scope": { + "local": "Local", + "project": "Project" + }, + "tools": { + "title": "Tools ({{count}})", + "title_few": "Tools ({{count}})", + "title_many": "Tools ({{count}})", + "title_one": "Tools ({{count}})", + "title_other": "Tools ({{count}})" + }, + "placeholders": { + "serverName": "my-server" + } + }, + "skillEditor": { + "actions": { + "cancel": "Cancel", + "createSkill": "Create Skill", + "preparing": "Preparing...", + "reviewAndCreate": "Review And Create", + "reviewAndSave": "Review And Save", + "saveSkill": "Save Skill" + }, + "advanced": { + "customDescription": "This skill uses a custom markdown format, so edit it directly here.", + "customTitle": "2. SKILL.md editor", + "description": "Most people can skip this. Open it only if you want direct control over the raw markdown file.", + "hide": "Hide Advanced Editor", + "resetFromStructuredFields": "Reset From Structured Fields", + "show": "Show Advanced Editor", + "title": "4. Advanced SKILL.md editor" + }, + "basics": { + "description": "Give this skill a clear name, choose who can use it, and decide where it should live.", + "title": "1. Basics" + }, + "description": { + "create": "Describe the workflow in plain language, review the files that will be created, then save it.", + "edit": "Update this skill, review the resulting file changes, then save it." + }, + "extraFiles": { + "addedFiles": "Added files:", + "assets": "Assets", + "assetsDescription": "Add screenshots or bundled media only if they help explain the workflow.", + "description": "Add supporting docs, scripts, or assets only if this skill really needs them.", + "lockedForEdits": "Root and folder are locked for edits", + "optionalDescription": "Add starter files that will be included in the review and written together with `SKILL.md`.", + "optionalTitle": "Optional files", + "references": "References", + "referencesDescription": "Add supporting docs, links, or examples the runtime can look at.", + "scripts": "Scripts", + "scriptsDescription": "Add helper commands or setup notes. Review carefully before sharing this skill.", + "title": "3. Extra files" + }, + "fields": { + "compatibility": "Compatibility", + "description": "Description", + "folderName": "Folder name", + "folderNameHint": "We suggest this automatically from the skill name so review works right away.", + "invocation": "How it should be used", + "license": "License", + "name": "Skill name", + "notes": "Extra notes or guardrails", + "root": "Where to store it", + "scope": "Who can use it", + "steps": "Main steps to follow", + "whenToUse": "When to reach for this" + }, + "instructions": { + "description": "These sections generate the skill file for you, so you do not need to edit markdown unless you want to.", + "locked": "Structured fields are locked because you switched to manual `SKILL.md` editing below.", + "title": "2. Instructions" + }, + "invocation": { + "auto": "Can be used automatically", + "manualOnly": "Only when you ask for it" + }, + "placeholders": { + "description": "What this skill helps with", + "name": "Write concise skill name", + "notes": "Example: Call out missing tests, regressions, and risky assumptions.", + "steps": "1. Inspect the relevant files.\n2. Explain the main risk first.\n3. Suggest the safest fix.", + "whenToUse": "Example: Use this when the task is a code review or bug triage request.", + "license": "MIT", + "compatibility": "claude-code, cursor" + }, + "review": { + "creating": "Creating a skill", + "hint": "Review the file changes first, then confirm save in the next step.", + "saving": "Saving this skill" + }, + "root": { + "codexOnly": " - Codex only", + "shared": " - Shared" + }, + "scope": { + "project": "Project: {{project}}", + "projectUnavailable": "Project unavailable", + "user": "User" + }, + "title": { + "create": "Create skill", + "edit": "Edit skill" + } + }, + "skillDetail": { + "actions": { + "cancel": "Cancel", + "delete": "Delete", + "deleteSkill": "Delete Skill", + "deleting": "Deleting...", + "editSkill": "Edit Skill", + "openFolder": "Open Folder", + "openSkillFile": "Open SKILL.md", + "retry": "Retry" + }, + "badges": { + "assets": "Assets", + "autoUse": "Auto use", + "hasScripts": "Has scripts", + "manualUse": "Manual use", + "references": "References", + "storedIn": "Stored in {{root}}" + }, + "deleteDialog": { + "description": "Delete this skill and move it to Trash?", + "descriptionWithName": "Delete \"{{name}}\" and move it to Trash? You can restore it later from Trash if needed.", + "title": "Delete skill?" + }, + "descriptionFallback": "Inspect discovered skill metadata and raw instructions.", + "errors": { + "deleteFailed": "Failed to delete skill", + "loadFailed": "Unable to load this skill." + }, + "files": { + "advancedDetails": "Advanced file details", + "assets": "Assets", + "references": "References", + "scripts": "Scripts", + "storedAt": "Stored at" + }, + "includes": { + "assets": "assets", + "instructionsOnly": "Just the skill instructions", + "references": "references", + "scripts": "scripts" + }, + "invocation": { + "auto": "Runs automatically when it matches the task.", + "manualOnly": "Only runs when you explicitly ask for it." + }, + "issues": { + "bundledScripts": "This skill includes bundled scripts", + "reviewCarefully": "Review this skill carefully before using it" + }, + "loading": "Loading skill details...", + "scope": { + "personal": "Your personal skills", + "projectOnly": "This project only" + }, + "summary": { + "howUsed": "How it is used", + "included": "What comes with it", + "whoCanUse": "Who can use it" + }, + "titleFallback": "Skill details" + }, + "skillsPanel": { + "actions": { + "createSkill": "Create Skill", + "import": "Import" + }, + "badges": { + "assets": "Assets", + "hasScripts": "Has scripts", + "needsAttention": "Needs attention", + "references": "References", + "storedIn": "Stored in {{root}}" + }, + "configuredRuntime": "the configured runtime", + "counts": { + "codexOnly": "{{count}} Codex only", + "personal": "{{count}} personal", + "project": "{{count}} project", + "shared": "{{count}} shared", + "total": "{{count}} total", + "codexOnly_few": "{{count}} Codex only", + "codexOnly_many": "{{count}} Codex only", + "codexOnly_one": "{{count}} Codex only", + "codexOnly_other": "{{count}} Codex only", + "personal_few": "{{count}} personal", + "personal_many": "{{count}} personal", + "personal_one": "{{count}} personal", + "personal_other": "{{count}} personal", + "project_few": "{{count}} project", + "project_many": "{{count}} project", + "project_one": "{{count}} project", + "project_other": "{{count}} project", + "shared_few": "{{count}} shared", + "shared_many": "{{count}} shared", + "shared_one": "{{count}} shared", + "shared_other": "{{count}} shared", + "total_few": "{{count}} total", + "total_many": "{{count}} total", + "total_one": "{{count}} total", + "total_other": "{{count}} total" + }, + "empty": { + "noMatches": "No skills match your search", + "noMatchesDescription": "Try a different search term or switch filters.", + "noSkills": "No skills yet", + "noSkillsDescription": "Create your first skill to teach a repeatable workflow, or import one you already use." + }, + "filters": { + "all": "All skills", + "codexOnly": "Codex only", + "hasScripts": "Has scripts", + "needsAttention": "Needs attention", + "personal": "Personal", + "project": "Project", + "shared": "Shared" + }, + "hero": { + "codexAvailable": "Use `.codex` when a skill should stay Codex-only.", + "codexUnavailable": "Existing `.codex` skills stay editable here, but new Codex-only skills need the Codex runtime enabled.", + "description": "Skills are reusable instructions that help the runtime handle the same kind of task more consistently.", + "guidance": "Use personal skills for habits you want everywhere. Use project skills for workflows that only make sense inside one codebase.", + "personalContext": "You are seeing only your personal skills right now.", + "projectContext": "You are seeing skills for {{project}} plus your personal skills.", + "title": "Teach repeatable work" + }, + "invocation": { + "auto": "Runs automatically when it fits", + "manualOnly": "Only runs when you explicitly ask for it" + }, + "loading": { + "loading": "Loading skills...", + "refreshing": "Refreshing skills..." + }, + "runtimeAudience": "Shared skills in `.claude`, `.cursor`, and `.agents` are available to {{audience}}. Skills stored in `.codex` stay Codex-only when Codex support is available.", + "scope": { + "project": "This project", + "user": "Personal" + }, + "searchPlaceholder": "Search by skill name or what it helps with...", + "sections": { + "personal": { + "description": "Habits and instructions you want available everywhere.", + "title": "Personal skills" + }, + "project": { + "description": "Workflows that only make sense for this codebase.", + "title": "Project skills" + } + }, + "sort": { + "label": "Sort skills", + "name": "Name", + "recent": "Recent" + }, + "status": { + "hasScripts": "Includes scripts, so review it carefully", + "needsAttention": "Needs attention before you rely on it", + "ready": "Ready to use" + }, + "success": { + "created": "Skill created successfully.", + "imported": "Skill imported successfully.", + "saved": "Skill saved successfully." + } + }, + "pluginDetail": { + "unknown": "Unknown", + "metadata": { + "author": "Author", + "category": "Category", + "source": "Source", + "version": "Version", + "capabilities": "Capabilities", + "installs": "Installs" + }, + "scope": { + "label": "Scope:", + "options": { + "user": "User (global)", + "project": "Project (shared)", + "local": "Local (gitignored)" + } + }, + "links": { + "homepage": "Homepage", + "contact": "Contact" + }, + "readme": { + "loading": "Loading README...", + "empty": "No README available." + } + }, + "skillImport": { + "title": "Import skill", + "description": "Pick an existing skill folder, review what will be copied, then import it into one of your supported skill locations.", + "steps": { + "chooseFolder": { + "title": "1. Choose a skill folder", + "description": "This should be a folder that already contains a `SKILL.md`, `Skill.md`, or `skill.md` file." + }, + "location": { + "title": "2. Decide where it belongs", + "description": "Personal skills work everywhere. Project skills only show up for one codebase." + } + }, + "fields": { + "sourceFolder": "Source folder", + "destinationFolderName": "Destination folder name", + "audience": "Who can use it", + "storage": "Where to store it" + }, + "placeholders": { + "defaultFolderName": "Defaults to source folder name" + }, + "actions": { + "browse": "Browse", + "cancel": "Cancel", + "preparing": "Preparing...", + "reviewAndImport": "Review And Import", + "importSkill": "Import Skill", + "backToImport": "Back To Import" + }, + "scope": { + "user": "User", + "project": "Project: {{project}}", + "projectUnavailable": "Project unavailable" + }, + "rootSuffix": { + "codexOnly": " - Codex only", + "shared": " - Shared" + }, + "reviewHint": "Review the copied files first, then confirm the import in the next step.", + "reviewLabel": "Importing this skill", + "errors": { + "missingSkillFile": "This folder does not look like a skill yet. It needs a SKILL.md, Skill.md, or skill.md file.", + "symbolicLinks": "This folder contains symbolic links. Import the real files instead of links.", + "tooManyFiles": "This skill folder is too large to import at once. Remove extra files and try again.", + "tooLarge": "This skill folder is too large to import safely. Trim large assets and try again.", + "invalidFolderName": "Pick a simpler destination folder name using letters, numbers, dots, dashes, or underscores.", + "mustBeDirectory": "Choose a folder to import, not a single file.", + "reviewFailed": "Failed to review import changes", + "importFailed": "Failed to import skill" + } + }, + "mcpPanel": { + "sort": { + "nameAsc": "Name A→Z", + "nameDesc": "Name Z→A", + "toolsDesc": "Most tools" + }, + "health": { + "title": "MCP Health Status", + "checkingViaRuntime": "Checking installed MCP servers via {{runtime}} ...", + "lastChecked": "Last checked {{time}}", + "description": "Run diagnostics from this page to verify installed MCP connectivity.", + "checking": "Checking...", + "checkStatus": "Check Status" + }, + "diagnostics": { + "title": "Runtime MCP Diagnostics", + "serversCount": "{{count}} servers", + "serversCount_one": "{{count}} server", + "serversCount_other": "{{count}} servers", + "waiting": "Waiting for diagnostics results...", + "disableReasons": { + "checkingRuntimeStatus": "Checking runtime status...", + "checkingRuntimeAvailability": "Checking runtime availability...", + "runtimeFailedToStart": "The configured runtime was found but failed to start. Open the Dashboard to repair or reinstall it.", + "runtimeRequired": "The configured runtime is required. Install or repair it from the Dashboard." + }, + "serversCount_few": "{{count}} servers", + "serversCount_many": "{{count}} servers" + }, + "searchPlaceholder": "Search MCP servers...", + "runtime": { + "notAvailable": "{{runtime}} not available", + "notInstalled": "{{runtime}} not installed", + "requiredDescription": "MCP health checks require {{runtime}}. Go to the Dashboard to install or repair it." + }, + "empty": { + "searchTitle": "No servers found", + "title": "No MCP servers available", + "searchDescription": "Try a different search term", + "description": "Check back later for new servers" + }, + "loadMore": "Load more" + }, + "apiKeys": { + "description": "Securely store API keys for auto-filling when installing MCP servers.", + "storage": { + "osKeychain": "Keys are encrypted via {{backend}} and stored with restricted file permissions (owner-only).", + "localEncryption": "OS keychain unavailable - keys are encrypted locally with AES-256. For stronger protection, install a keyring service (gnome-keyring, kwallet)." + }, + "actions": { + "add": "Add API Key", + "addFirst": "Add your first key", + "edit": "Edit" + }, + "empty": { + "title": "No API keys saved", + "description": "Add keys to auto-fill environment variables when installing MCP servers." + }, + "form": { + "addTitle": "Add API Key", + "editTitle": "Edit API Key", + "addDescription": "Store an API key for auto-filling in MCP server installations.", + "editDescription": "Update the key details. You must re-enter the value.", + "keychainUnavailable": "OS keychain unavailable - keys encrypted with AES-256 locally. Install gnome-keyring for OS-level protection.", + "name": "Name", + "namePlaceholder": "e.g. OpenAI Production", + "environmentVariableName": "Environment Variable Name", + "envVarPlaceholder": "e.g. OPENAI_API_KEY", + "value": "Value", + "reenterValue": "Re-enter key value", + "valuePlaceholder": "sk-...", + "scope": "Scope", + "userScopeLabel": "User (global)", + "projectScopeLabel": "Project: {{project}}", + "projectUnavailable": "Project unavailable", + "boundTo": "Bound to {{path}}", + "cancel": "Cancel", + "saving": "Saving...", + "update": "Update", + "save": "Save", + "errors": { + "invalidEnvVarFormat": "Use letters, digits, underscores. Must start with a letter or underscore.", + "nameRequired": "Name is required", + "envVarRequired": "Environment variable name is required", + "invalidEnvVar": "Invalid environment variable name", + "valueRequired": "Key value is required", + "projectScopeRequiresProject": "Project-scoped API keys require an active project", + "saveFailed": "Failed to save" + } + } + }, + "skillReview": { + "title": "Review skill changes", + "description": "{{reviewLabel}} previews the filesystem changes first. Nothing is written until you confirm below.", + "noPreview": "No preview available.", + "confirmPromptPrefix": "Review the diff below, then use", + "confirmPromptSuffix": "to apply these changes.", + "noChanges": "No file changes detected yet.", + "binaryBadge": "binary", + "binaryPreviewHidden": "Binary file preview is not shown. The file will be copied as-is.", + "summary": { + "fileChanges": "{{count}} file changes", + "fileChanges_one": "{{count}} file change", + "fileChanges_other": "{{count}} file changes", + "new": "{{count}} new", + "updated": "{{count}} updated", + "removed": "{{count}} removed", + "binary": "{{count}} binary", + "fileChanges_few": "{{count}} file changes", + "fileChanges_many": "{{count}} file changes" + } + }, + "mcpCard": { + "toolsCount": "{{count}} tools", + "toolsCount_one": "{{count}} tool", + "toolsCount_other": "{{count}} tools", + "envCount": "{{count}} envs", + "envCount_one": "{{count}} env", + "envCount_other": "{{count}} envs", + "auth": "Auth", + "byAuthor": "by {{author}}", + "hosting": { + "remote": "Remote", + "local": "Local", + "both": "Both" + }, + "toolsCount_few": "{{count}} tools", + "toolsCount_many": "{{count}} tools", + "envCount_few": "{{count}} envs", + "envCount_many": "{{count}} envs", + "repository": "Repository", + "website": "Website" + }, + "installButton": { + "installing": "Installing...", + "removing": "Removing...", + "done": "Done", + "retry": "Retry", + "uninstall": "Uninstall", + "install": "Install" + }, + "pluginCard": { + "official": "Official" + } +} diff --git a/src/features/localization/renderer/locales/en/report.json b/src/features/localization/renderer/locales/en/report.json new file mode 100644 index 00000000..6a68920e --- /dev/null +++ b/src/features/localization/renderer/locales/en/report.json @@ -0,0 +1,217 @@ +{ + "cost": { + "breakdownTitle": "Cost Breakdown (per 1M tokens)", + "cacheRead": "Cache Read", + "cacheWrite": "Cache Write", + "cost": "Cost", + "input": "Input", + "noCommits": "no commits", + "noLinesChanged": "no lines changed", + "output": "Output", + "parent": "Parent: {{cost}}", + "parentCost": "Parent Cost", + "perCommit": "Per Commit", + "perCommitFormula": "total cost ÷ {{count}} commit", + "perCommitFormula_few": "total cost ÷ {{count}} commits", + "perCommitFormula_many": "total cost ÷ {{count}} commits", + "perCommitFormula_one": "total cost ÷ {{count}} commit", + "perCommitFormula_other": "total cost ÷ {{count}} commits", + "perLineChanged": "Per Line Changed", + "perLineFormula": "total cost ÷ {{count}} line", + "perLineFormula_few": "total cost ÷ {{count}} lines", + "perLineFormula_many": "total cost ÷ {{count}} lines", + "perLineFormula_one": "total cost ÷ {{count}} line", + "perLineFormula_other": "total cost ÷ {{count}} lines", + "subagent": "Subagent: {{cost}}", + "subagentCost": "Subagent Cost", + "title": "Cost Analysis", + "total": "Total" + }, + "insights": { + "agent": "agent", + "agent_few": "agents", + "agent_many": "agents", + "agent_one": "agent", + "agent_other": "agents", + "agentTree": "Agent Tree ({{count}} {{unit}})", + "background": "(background)", + "bashCommands": "Bash Commands", + "outOfScopeFindings": "Out-of-Scope Findings ({{count}})", + "questionsAsked": "Questions Asked ({{count}})", + "repeated": "Repeated", + "skillsInvoked": "Skills Invoked ({{count}})", + "taskDispatches": "Task Dispatches ({{count}})", + "tasksCreated": "Tasks Created ({{count}})", + "teamMode": "Team Mode", + "teams": "Teams: {{teams}}", + "title": "Session Insights", + "total": "Total", + "unique": "Unique", + "skillsInvoked_few": "Skills Invoked ({{count}})", + "skillsInvoked_many": "Skills Invoked ({{count}})", + "skillsInvoked_one": "Skills Invoked ({{count}})", + "skillsInvoked_other": "Skills Invoked ({{count}})", + "taskDispatches_few": "Task Dispatches ({{count}})", + "taskDispatches_many": "Task Dispatches ({{count}})", + "taskDispatches_one": "Task Dispatches ({{count}})", + "taskDispatches_other": "Task Dispatches ({{count}})", + "tasksCreated_few": "Tasks Created ({{count}})", + "tasksCreated_many": "Tasks Created ({{count}})", + "tasksCreated_one": "Tasks Created ({{count}})", + "tasksCreated_other": "Tasks Created ({{count}})", + "questionsAsked_few": "Questions Asked ({{count}})", + "questionsAsked_many": "Questions Asked ({{count}})", + "questionsAsked_one": "Questions Asked ({{count}})", + "questionsAsked_other": "Questions Asked ({{count}})", + "agentTree_few": "Agent Tree ({{count}} {{unit}})", + "agentTree_many": "Agent Tree ({{count}} {{unit}})", + "agentTree_one": "Agent Tree ({{count}} {{unit}})", + "agentTree_other": "Agent Tree ({{count}} {{unit}})", + "outOfScopeFindings_few": "Out-of-Scope Findings ({{count}})", + "outOfScopeFindings_many": "Out-of-Scope Findings ({{count}})", + "outOfScopeFindings_one": "Out-of-Scope Findings ({{count}})", + "outOfScopeFindings_other": "Out-of-Scope Findings ({{count}})", + "keyTakeaways": "Key Takeaways" + }, + "quality": { + "chars": "chars", + "corrections": "Corrections", + "failed": "failed", + "fileReadRedundancy": "File Read Redundancy", + "firstMessage": "First Message", + "firstRun": "First Run", + "frictionRate": "Friction Rate", + "lastRun": "Last Run", + "messagesBeforeWork": "Messages Before Work", + "passed": "passed", + "promptQuality": "Prompt Quality", + "readsPerUniqueFile": "Reads/Unique File", + "snapshot": "snapshot", + "snapshot_few": "snapshots", + "snapshot_many": "snapshots", + "snapshot_one": "snapshot", + "snapshot_other": "snapshots", + "startupOverhead": "Startup Overhead", + "testProgression": "Test Progression", + "title": "Quality Signals", + "tokensBeforeWork": "Tokens Before Work", + "totalReads": "Total Reads", + "uniqueFiles": "Unique Files", + "userMessages": "User Messages", + "percentOfTotal": "% of Total" + }, + "tokens": { + "apiCalls": "API Calls", + "cacheCreate": "Cache Create", + "cacheEfficiency": "Cache Efficiency", + "cacheRead": "Cache Read", + "cacheReadPct": "Cache Read %", + "coldStart": "Cold Start", + "cost": "Cost", + "input": "Input", + "model": "Model", + "no": "No", + "output": "Output", + "readWriteRatio": "R/W Ratio", + "title": "Token Usage", + "total": "Total", + "yes": "Yes" + }, + "subagents": { + "title": "Subagents", + "metrics": { + "count": "Count", + "totalTokens": "Total Tokens", + "totalDuration": "Total Duration", + "totalCost": "Total Cost" + }, + "table": { + "description": "Description", + "type": "Type", + "tokens": "Tokens", + "duration": "Duration", + "cost": "Cost" + } + }, + "overview": { + "title": "Overview", + "yes": "Yes", + "no": "No", + "metrics": { + "duration": "Duration", + "messages": "Messages", + "contextUsage": "Context Usage", + "compactions": "Compactions", + "branch": "Branch", + "subagents": "Subagents", + "project": "Project", + "sessionId": "Session ID" + } + }, + "timeline": { + "title": "Timeline & Activity", + "idleAnalysis": "Idle Analysis", + "metrics": { + "idleGaps": "Idle Gaps", + "totalIdle": "Total Idle", + "activeTime": "Active Time", + "idlePercent": "Idle %" + }, + "modelSwitches": "Model Switches ({{count}})", + "modelSwitches_one": "Model Switches ({{count}})", + "modelSwitches_other": "Model Switches ({{count}})", + "messageNumber": "msg #{{number}}", + "keyEvents": "Key Events", + "modelSwitches_few": "Model Switches ({{count}})", + "modelSwitches_many": "Model Switches ({{count}})" + }, + "tools": { + "title": "Tool Usage", + "summary": "{{formattedCount}} total calls across {{toolCount}} tools", + "columns": { + "tool": "Tool", + "calls": "Calls", + "errors": "Errors", + "successPercent": "Success %", + "health": "Health" + } + }, + "git": { + "title": "Git Activity", + "commits": "Commits", + "pushes": "Pushes", + "linesAdded": "Lines Added", + "linesRemoved": "Lines Removed", + "branchesCreated": "Branches Created" + }, + "friction": { + "title": "Friction Signals", + "rate": "Friction Rate: {{rate}}%", + "correctionsCount": "{{count}} corrections", + "correctionsCount_one": "{{count}} correction", + "corrections": "Corrections", + "thrashingSignals": "Thrashing Signals", + "repeatedBashCommands": "Repeated Bash Commands", + "reworkedFiles": "Reworked Files (3+ edits)", + "correctionsCount_few": "{{count}} corrections", + "correctionsCount_many": "{{count}} corrections", + "correctionsCount_other": "{{count}} corrections" + }, + "errors": { + "title": "Errors", + "permissionDenied": "Permission Denied", + "messageIndex": "msg #{{index}}", + "input": "Input", + "error": "Error", + "count": "{{count}} errors", + "count_one": "{{count}} error", + "permissionDenialCount": "{{count}} permission denials", + "permissionDenialCount_one": "{{count}} permission denial", + "count_few": "{{count}} errors", + "count_many": "{{count}} errors", + "count_other": "{{count}} errors", + "permissionDenialCount_few": "{{count}} permission denials", + "permissionDenialCount_many": "{{count}} permission denials", + "permissionDenialCount_other": "{{count}} permission denials" + } +} diff --git a/src/features/localization/renderer/locales/en/settings.json b/src/features/localization/renderer/locales/en/settings.json new file mode 100644 index 00000000..e9b1876b --- /dev/null +++ b/src/features/localization/renderer/locales/en/settings.json @@ -0,0 +1,983 @@ +{ + "tabs": { + "advanced": { + "description": "Power-user options: export/import config, reset defaults, and raw configuration editing.", + "label": "Advanced" + }, + "general": { + "description": "Core app preferences like theme, language, display density, and startup behavior.", + "label": "General" + }, + "infoAriaLabel": "What is {{label}}?", + "notifications": { + "description": "Control when and how you get notified about agent activity, task completions, and errors.", + "label": "Notifications" + } + }, + "view": { + "description": "Manage your app preferences", + "loading": "Loading settings...", + "title": "Settings" + }, + "runtimeProvider": { + "actions": { + "cancel": "Cancel", + "test": "Test" + }, + "defaults": { + "allProjects": "All projects", + "allProjectsHint": "Tests use {{project}}. Default applies unless a project has an override.", + "loadingContexts": "Loading contexts...", + "projectHint": "Saving overrides only {{project}}.", + "projectOverrideContext": "Project override context", + "scopeDescriptionAllProjects": "Default for every project that does not have its own OpenCode override.", + "scopeDescriptionProject": "Override only the selected project. Running teams are not changed.", + "selectProjectContext": "Select project context", + "selectProjectHint": "Select a project before testing local models or saving defaults.", + "selectValidationContext": "Select validation context", + "setAllProjectsDefault": "Set all-projects default", + "setProjectDefault": "Set project default", + "thisProject": "This project", + "title": "OpenCode defaults", + "validationContext": "Validation context" + }, + "diagnostics": { + "copied": "Diagnostics copied", + "copiedShort": "Copied", + "copy": "Copy diagnostics", + "hints": "Hints", + "likelyCause": "Likely cause:" + }, + "models": { + "alreadyDefault": "This is already the selected OpenCode default.", + "empty": "No models found.", + "emptyFree": "No free models found.", + "emptyRecommended": "No recommended models found.", + "emptyRecommendedFree": "No recommended free models found.", + "freeOnly": "Free only", + "launchableDescription": "Routes you can test or use in the team picker: local config, free built-in models, and current default.", + "launchableTitle": "Launchable OpenCode models", + "loadingRoutes": "Loading OpenCode model routes...", + "noRoutesMatch": "No OpenCode model routes match \"{{query}}\".", + "noneReported": "No launchable OpenCode model routes were reported yet. Configure a local route in OpenCode or use the Providers tab to inspect catalog providers.", + "recommendedOnly": "Recommended only", + "searchPlaceholder": "Search models", + "selectProjectBeforeTesting": "Select a project context before testing models.", + "selectProjectBeforeTestingDefaults": "Select a project context before testing or saving OpenCode defaults.", + "useInTeamPicker": "Use in team picker" + }, + "providers": { + "catalog": "OpenCode provider catalog", + "countFallback": "OpenCode providers", + "description": "{{count}}. Connected and recommended providers are shown first.", + "loadMore": "Load more providers", + "loading": "Loading OpenCode providers", + "noMatches": "No providers match that search.", + "noneReported": "No OpenCode providers reported by the managed runtime.", + "recommended": "Recommended", + "refreshCatalog": "Refresh catalog", + "searchPlaceholder": "Search providers", + "description_few": "{{count}}. Connected and recommended providers are shown first.", + "description_many": "{{count}}. Connected and recommended providers are shown first.", + "description_one": "{{count}}. Connected and recommended providers are shown first.", + "description_other": "{{count}}. Connected and recommended providers are shown first." + }, + "setup": { + "loading": "Loading provider setup..." + }, + "summary": { + "defaultModel": "OpenCode default: {{model}}", + "loading": "Loading managed OpenCode runtime, connected providers, and model defaults...", + "source": "Source: {{source}}", + "title": "OpenCode runtime" + }, + "tabs": { + "models": "Models", + "providers": "Providers" + }, + "modelRoutes": { + "searchPlaceholder": "Search model routes" + }, + "badges": { + "usedInTeamPicker": "Used in team picker", + "free": "free", + "local": "local", + "configured": "configured", + "connected": "connected", + "verified": "verified", + "needsTest": "needs test", + "failed": "failed", + "unknown": "unknown", + "default": "default" + }, + "compatibleEndpoint": { + "baseUrlPlaceholder": "http://localhost:1234" + } + }, + "general": { + "agentLanguage": { + "description": "Language for agent communication", + "descriptionWithDetected": "Language for agent communication (detected: {{detected}})", + "emptyMessage": "No language found.", + "label": "Language", + "searchPlaceholder": "Search language...", + "selectPlaceholder": "Select language...", + "title": "Agent Language" + }, + "appLanguage": { + "description": "Language for the application interface.", + "label": "Language", + "title": "App Language" + }, + "appearance": { + "autoExpandAIGroups": { + "description": "Automatically expand each response turn when opening a transcript or receiving a new message", + "label": "Expand AI responses by default" + }, + "nativeTitleBar": { + "description": "Use the default system window frame instead of the custom title bar", + "label": "Use native title bar", + "restartConfirm": { + "confirmLabel": "Restart", + "message": "The app needs to restart to apply the title bar change. Restart now?", + "title": "Restart required" + } + }, + "theme": { + "description": "Choose your preferred color theme", + "label": "Theme", + "options": { + "dark": "Dark", + "light": "Light", + "system": "System" + } + }, + "title": "Appearance" + }, + "browserAccess": { + "serverMode": { + "description": "Start an HTTP server to access the UI from a browser or embed in iframes", + "label": "Enable server mode" + }, + "title": "Browser Access" + }, + "localClaudeRoot": { + "actions": { + "selectFolder": "Select Folder", + "selectFolderManually": "Select Folder Manually", + "useAutoDetect": "Use Auto-Detect", + "useFolder": "Use Folder", + "usePath": "Use Path", + "useThisPath": "Use This Path", + "useWsl": "Using Linux/WSL?" + }, + "confirm": { + "noProjectsDir": { + "message": "This folder does not contain a \"projects\" directory. Continue anyway?", + "title": "No projects directory found" + }, + "notClaudeDir": { + "message": "This folder is named \"{{folderName}}\", not \".claude\". Continue anyway?", + "title": "Selected folder is not .claude" + }, + "noWslPaths": { + "message": "Could not find WSL distros with Claude data automatically. Select folder manually?", + "title": "No WSL Claude paths found" + }, + "wslNoProjectsDir": { + "message": "\"{{path}}\" does not contain a \"projects\" directory. Continue anyway?", + "title": "WSL path missing projects directory" + } + }, + "current": { + "autoDetected": "Auto-detected: {{path}}", + "autoDetectedPath": "Using auto-detected path", + "customPath": "Using custom path", + "label": "Current Local Root" + }, + "description": "Choose which local folder is treated as your Claude data root", + "errors": { + "detectWslFailed": "Failed to detect WSL Claude root paths", + "loadFailed": "Failed to load local Claude root settings", + "updateFailed": "Failed to update Claude root" + }, + "title": "Local Claude Root", + "wslModal": { + "closeAriaLabel": "Close WSL path modal", + "description": "Detected WSL distributions and Claude root candidates", + "noProjectsDir": "No projects directory detected", + "title": "Select WSL Claude Root" + } + }, + "privacy": { + "telemetry": { + "description": "Help improve the app by sending anonymous crash and performance data", + "label": "Send crash reports" + }, + "title": "Privacy" + }, + "server": { + "runningOn": "Running on", + "standaloneModeDescription": "Running in standalone mode. The HTTP server is always active. System notifications are not available - notification triggers are logged in-app only.", + "title": "Server" + }, + "startup": { + "launchAtLogin": { + "description": "Automatically start the app when you log in", + "label": "Launch at login" + }, + "showDockIcon": { + "description": "Display the app icon in the dock (macOS)", + "label": "Show dock icon" + }, + "title": "Startup" + } + }, + "notifications": { + "dev": { + "descriptionPrefix": "Notifications may not work in development mode. macOS identifies the app as \"Electron\" (bundle ID", + "descriptionSuffix": ") instead of the production app name. Check System Settings > Notifications > Electron to verify permissions.", + "title": "Dev Mode" + }, + "ignoredRepositories": { + "description": "Notifications from these repositories will be ignored", + "empty": "No repositories ignored", + "selectPlaceholder": "Select repository to ignore...", + "title": "Ignored Repositories" + }, + "settings": { + "enabled": { + "description": "Show system notifications for errors and events", + "label": "Enable System Notifications" + }, + "sound": { + "description": "Play a sound when notifications appear", + "label": "Play sound" + }, + "subagentErrors": { + "description": "Detect and notify about errors in subagent sessions", + "label": "Include subagent errors" + }, + "title": "Notification Settings" + }, + "snooze": { + "clear": "Clear Snooze", + "description": "Temporarily pause notifications", + "descriptionWithTime": "Snoozed until {{time}}", + "label": "Snooze notifications", + "options": { + "15": "15 minutes", + "30": "30 minutes", + "60": "1 hour", + "120": "2 hours", + "240": "4 hours", + "-1": "Until tomorrow" + }, + "selectDuration": "Select duration..." + }, + "taskCompletion": { + "description": "Get native OS notifications when Claude finishes tasks - sounds, banners, and Dock/taskbar badges. Works on macOS, Linux, and Windows.", + "installPlugin": "Install claude-notifications-go plugin", + "title": "Task Completion Notifications" + }, + "team": { + "allTasksCompleted": { + "description": "Notify when every task in a team reaches completed status", + "label": "All tasks completed" + }, + "autoResumeOnRateLimit": { + "description": "When Claude reports a reset time, schedule a follow-up nudge for the team lead after the limit resets", + "label": "Auto-resume after rate limit" + }, + "clarifications": { + "description": "Show native OS notifications when a task needs your input", + "label": "Task clarification notifications" + }, + "crossTeamMessage": { + "description": "Notify when a message arrives from another team", + "label": "Cross-team message notifications" + }, + "leadInbox": { + "description": "Notify when teammates send messages to the team lead", + "label": "Lead inbox notifications" + }, + "statusChange": { + "description": "Show native OS notifications when a task's status changes", + "label": "Task status change notifications", + "onlySolo": { + "description": "Notify only when the team has no teammates", + "label": "Only in Solo mode" + }, + "statuses": { + "description": "Which target statuses trigger a notification", + "label": "Notify on these statuses", + "options": { + "approved": "Approved", + "completed": "Completed", + "deleted": "Deleted", + "in_progress": "Started", + "needsFix": "Needs Fixes", + "pending": "Pending", + "review": "Review" + } + } + }, + "taskComments": { + "description": "Show native OS notifications when agents comment on tasks", + "label": "Task comment notifications" + }, + "taskCreated": { + "description": "Show native OS notifications when a new task is created", + "label": "Task created notifications" + }, + "teamLaunched": { + "description": "Notify when a team finishes launching and is ready", + "label": "Team launched notifications" + }, + "title": "Team Notifications", + "toolApproval": { + "description": "Notify when a tool needs your approval (Allow/Deny) while the app is not focused", + "label": "Tool approval notifications" + }, + "userInbox": { + "description": "Notify when teammates send messages to you", + "label": "User inbox notifications" + } + }, + "test": { + "action": "Send Test", + "description": "Send a test notification to verify delivery", + "failedToSend": "Failed to send test notification", + "label": "Test notification", + "sending": "Sending...", + "sent": "Sent!", + "unknownError": "Unknown error" + } + }, + "advanced": { + "about": { + "appIconAlt": "App icon", + "description": "Assemble AI agent teams that work autonomously in parallel, communicate across teams, and manage tasks on a kanban board - with built-in code review, live process monitoring, and full tool visibility.", + "standalone": "Standalone", + "title": "About", + "version": "Version {{version}}" + }, + "configuration": { + "editConfig": "Edit Config", + "exportConfig": "Export Config", + "importConfig": "Import Config", + "openInEditor": "Open in Editor", + "resetToDefaults": "Reset to Defaults", + "title": "Configuration" + }, + "updates": { + "available": "v{{version}} available", + "check": "Check for Updates", + "checking": "Checking...", + "ready": "Update ready", + "unknownVersion": "unknown", + "upToDate": "Up to date" + }, + "appName": "Agent Teams AI" + }, + "configEditor": { + "errors": { + "loadFailed": "Failed to load config", + "saveFailed": "Failed to save config" + }, + "footer": { + "autoSave": "Changes auto-save after editing", + "toClose": "to close", + "escapeKey": "Esc" + }, + "loading": "Loading config...", + "status": { + "invalidJson": "Invalid JSON", + "saveFailed": "Save failed", + "saved": "Saved", + "saving": "Saving..." + }, + "title": "Edit Configuration" + }, + "notificationTriggers": { + "add": { + "cancel": "Cancel", + "submit": "Add Trigger", + "title": "Add Custom Trigger" + }, + "builtin": { + "description": "Default triggers that come with the application. You can enable or disable them and customize their patterns.", + "title": "Built-in Triggers" + }, + "card": { + "builtinBadge": "Builtin", + "collapseAriaLabel": "Collapse", + "deleteAriaLabel": "Delete trigger", + "editNameAriaLabel": "Edit name", + "expandAriaLabel": "Expand" + }, + "color": { + "customHexTitle": "Custom hex color", + "invalidHex": "Invalid hex" + }, + "configuration": { + "alertIfGreaterThan": "Alert if >", + "emptyPatternHint": "Leave empty to match all content. Uses JavaScript regex syntax.", + "errorStatusDescription": "Triggers when a tool execution reports an error (is_error: true).", + "tokensUnit": "tokens", + "matchPatternPlaceholder": "e.g., error|failed|exception" + }, + "custom": { + "description": "Create your own triggers to get notified for specific patterns or tool outputs.", + "empty": "No custom triggers configured yet.", + "title": "Custom Triggers" + }, + "errors": { + "invalidRegexPattern": "Invalid regex pattern" + }, + "fields": { + "contentType": "Content Type", + "matchField": "Match Field", + "matchPattern": "Match Pattern (Regex)", + "scopeToolName": "Scope / Tool Name", + "scopeToolNameOptional": "Scope / Tool Name (optional)", + "threshold": "Threshold", + "tokenType": "Token Type", + "triggerNamePlaceholder": "e.g., Build Failure Alert", + "triggerNameRequired": "Trigger Name *" + }, + "ignorePatterns": { + "hint": "Press Enter to add. Notification is skipped if any pattern matches.", + "placeholder": "Add ignore regex...", + "removeAriaLabel": "Remove ignore pattern", + "summary": "Advanced: Exclusion Rules", + "title": "Ignore Patterns (skip if matches)" + }, + "options": { + "contentTypes": { + "text": "Text Output", + "thinking": "Thinking", + "tool_result": "Tool Result", + "tool_use": "Tool Use" + }, + "matchFields": { + "args": "Arguments", + "command": "Command", + "content": "Content", + "description": "Description", + "file_path": "File Path", + "fullInput": "Full Input (JSON)", + "glob": "Glob Filter", + "new_string": "New String", + "old_string": "Old String", + "path": "Path", + "pattern": "Pattern", + "prompt": "Prompt", + "query": "Query", + "skill": "Skill Name", + "subagent_type": "Subagent Type", + "text": "Text Content", + "thinking": "Thinking Content", + "url": "URL" + }, + "modes": { + "content_match": "Content Pattern", + "error_status": "Execution Error", + "token_threshold": "High Token Usage" + }, + "tokenTypes": { + "input": "Input Tokens", + "output": "Output Tokens", + "total": "Total Tokens" + }, + "toolNames": { + "anyTool": "Any Tool" + } + }, + "preview": { + "defaultTestTriggerName": "Test Trigger", + "detectedSuffix": "errors would have been detected", + "more": "...and {{count}} more", + "more_few": "...and {{count}} more", + "more_many": "...and {{count}} more", + "more_one": "...and {{count}} more", + "more_other": "...and {{count}} more", + "testTrigger": "Test Trigger", + "testing": "Testing...", + "title": "Preview", + "truncatedWarning": "Search stopped early (timeout or count limit). Actual matches may be higher.", + "viewSession": "View Session" + }, + "repositoryScope": { + "empty": "No repositories selected - trigger applies to all repositories", + "hint": "When repositories are selected, this trigger only fires for errors in those repositories.", + "placeholder": "Select repository to add...", + "summary": "Advanced: Repository Scope", + "title": "Limit to Repositories (applies only to selected repositories)" + }, + "sections": { + "configuration": "Configuration", + "dotColor": "Dot Color", + "generalInfo": "General Info", + "triggerCondition": "Trigger Condition" + } + }, + "workspaceProfiles": { + "actions": { + "addProfile": "Add Profile", + "cancel": "Cancel", + "deleteProfile": "Delete profile", + "editProfile": "Edit profile", + "save": "Save" + }, + "authMethods": { + "agent": "SSH Agent", + "auto": "Auto (from SSH Config)", + "password": "Password", + "privateKey": "Private Key" + }, + "deleteConfirm": { + "confirmLabel": "Delete", + "message": "Are you sure you want to delete \"{{name}}\"? This cannot be undone.", + "title": "Delete Profile" + }, + "description": "Save SSH connection profiles for quick reconnection", + "empty": { + "description": "Add an SSH profile to connect quickly", + "title": "No saved profiles" + }, + "form": { + "authentication": "Authentication", + "host": "Host", + "name": "Name", + "passwordPrompt": "You will be prompted for the password when connecting.", + "port": "Port", + "privateKeyPath": "Private Key Path", + "username": "Username", + "namePlaceholder": "My Server", + "hostPlaceholder": "hostname or IP", + "usernamePlaceholder": "user" + }, + "loading": "Loading profiles...", + "title": "Workspace Profiles" + }, + "connection": { + "actions": { + "connect": "Connect", + "connecting": "Connecting...", + "disconnect": "Disconnect", + "testConnection": "Test Connection", + "testing": "Testing..." + }, + "currentMode": { + "description": "Data source for session files", + "label": "Current Mode", + "local": "Local ({{path}})" + }, + "description": "Connect to a remote machine to view Claude Code sessions running there", + "form": { + "authentication": "Authentication", + "host": "Host", + "password": "Password", + "port": "Port", + "privateKeyPath": "Private Key Path", + "username": "Username", + "hostPlaceholder": "hostname or SSH config alias", + "usernamePlaceholder": "user" + }, + "savedProfiles": { + "title": "Saved Profiles" + }, + "ssh": { + "title": "SSH Connection" + }, + "status": { + "connectedTo": "Connected to {{host}}", + "remoteSessions": "Viewing remote sessions via SSH" + }, + "test": { + "failed": "Connection failed: {{error}}", + "success": "Connection successful", + "unknownError": "Unknown error" + }, + "title": "Remote Connection" + }, + "providerRuntime": { + "actions": { + "cancel": "Cancel", + "cancelLogin": "Cancel login", + "connectChatGpt": "Connect ChatGPT", + "delete": "Delete", + "disable": "Disable", + "disconnectAccount": "Disconnect account", + "generateLink": "Generate link", + "openLogin": "Open login", + "reconnectAnthropic": "Reconnect Anthropic", + "refresh": "Refresh", + "replaceKey": "Replace key", + "saveEndpoint": "Save endpoint", + "saveKey": "Save key", + "saving": "Saving...", + "setApiKey": "Set API key", + "updateKey": "Update key", + "useCode": "Use code" + }, + "apiKey": { + "loadingStoredCredentials": "Loading stored credentials...", + "projectScope": "Project", + "scope": "Scope", + "storedIn": "Stored in {{backend}}", + "userScope": "User", + "storedInApp": "Stored in app", + "providers": { + "anthropic": { + "name": "Anthropic API Key", + "title": "API key", + "description": "Use a direct Anthropic API key for API-billed access. Your Anthropic subscription session stays available when you switch back.", + "placeholder": "sk-ant-..." + }, + "codex": { + "name": "Codex API Key", + "title": "API key", + "description": "Use an OpenAI API key as a secondary Codex auth path. If you switch Codex to API key mode, the app will mirror OPENAI_API_KEY into CODEX_API_KEY for native launches.", + "placeholder": "sk-proj-..." + }, + "gemini": { + "name": "Gemini API Key", + "title": "API access", + "description": "Use `GEMINI_API_KEY` for the Gemini API backend. CLI SDK and ADC do not require it.", + "placeholder": "AIza..." + } + } + }, + "codex": { + "account": { + "appServer": "App-server: {{state}}", + "connected": "Connected", + "description": "Manage the local Codex app-server account session that powers subscription-backed native launches.", + "loginInProgress": "Login in progress", + "plan": "Plan: {{plan}}", + "reconnectRequired": "Reconnect required", + "title": "ChatGPT account", + "hints": { + "autoUsesApiKeyUntilChatgpt": "{{message}} Auto will keep using the detected API key until ChatGPT is connected.", + "detectedApiKeyNeedsApiMode": "{{message}} The detected API key is only used after you switch Codex to API key mode.", + "localArtifactsNoSession": "Codex CLI currently reports no active ChatGPT account. Local Codex account data exists, but no active managed session is selected. Usage limits appear here only after Codex CLI sees one.", + "noActiveAccount": "Codex CLI currently reports no active ChatGPT account. Usage limits appear here only after Codex CLI sees one.", + "reconnectBeforeUsage": "Codex has a locally selected ChatGPT account, but the current session needs reconnect before usage limits can load here.", + "usageLimitsAfterReport": "Usage limits appear here after Codex reports them for the connected ChatGPT account." + } + }, + "install": { + "checking": "Checking", + "downloading": "Downloading", + "installCli": "Install Codex CLI", + "installing": "Installing", + "retryInstall": "Retry install", + "title": "Install Codex CLI into app data" + }, + "rateLimits": { + "credits": "Credits", + "creditsDescription": "Credits are shown separately from window-based subscription usage and may be unavailable for plan-backed ChatGPT sessions.", + "noSecondaryWindow": "Codex did not return a secondary window for this account snapshot.", + "notReported": "Not reported", + "primaryReset": "Primary reset", + "primaryUsed": "Primary used", + "primaryWindow": "Primary window", + "remainingLeft": "{{value}} left", + "remainingUnknown": "Remaining unknown", + "secondaryReset": "Secondary reset", + "secondaryUsed": "Secondary used", + "secondaryWindow": "Secondary window", + "usedQuotaNote": "These percentages show used quota, not remaining quota.", + "weeklyReset": "Weekly reset", + "weeklyUsed": "Weekly used", + "weeklyUsedOneWeek": "Weekly used (1w)", + "weeklyWindow": "Weekly window", + "secondaryFallback": "secondary", + "secondaryWindowNote": " Weekly limits are shown separately in the {{window}} window.", + "usageExplanationGeneric": "Shows used quota, not remaining quota.", + "usageExplanationWindowOnly": "Shows used quota in the current {{window}} window, not remaining quota.", + "usageExplanationWithRemaining": "{{used}} used - about {{remaining}} left in the current {{window}} window." + } + }, + "compatibleEndpoint": { + "authToken": "Auth token", + "authTokenMissing": "Auth token is not configured.", + "baseUrl": "Base URL", + "description": "Use an Anthropic-compatible local runtime endpoint.", + "keepSavedToken": "Leave blank to keep saved token", + "title": "Local / compatible endpoint", + "tokenStatus": "Token {{status}}", + "validation": { + "baseUrlRequired": "Base URL is required", + "firstPartyAnthropic": "Use Auto, Subscription, or API key for first-party Anthropic", + "httpRequired": "Base URL must use http:// or https://", + "invalidUrl": "Invalid URL", + "noCredentials": "Base URL must not include credentials" + }, + "status": { + "endpointDisabledTokenKept": "Endpoint disabled. Saved token was kept.", + "endpointSaved": "Endpoint saved", + "endpointSavedTokenMissing": "Endpoint saved. Auth token is not configured." + } + }, + "connection": { + "authenticationMethod": "Authentication method", + "descriptions": { + "anthropic": "Choose how app-launched Anthropic sessions authenticate.", + "codex": "Choose whether Codex should prefer your ChatGPT subscription or an API key when the native runtime launches.", + "gemini": "Configure optional API access. CLI SDK and ADC are still discovered automatically.", + "opencode": "OpenCode authentication and provider inventory are managed by the OpenCode runtime." + }, + "method": "Connection method", + "mode": "Mode: {{mode}}", + "selected": "Selected", + "switching": "Switching...", + "title": "Connection" + }, + "connectionCards": { + "apiKey": { + "title": "API key" + }, + "anthropic": { + "apiKeyDescription": "Use ANTHROPIC_API_KEY and Anthropic API billing.", + "autoDescription": "Use Anthropic runtime defaults and the best local credential available.", + "hint": "Auto keeps Anthropic on its default local credential resolution.", + "subscriptionDescription": "Use your local Anthropic sign-in session and subscription access.", + "subscriptionTitle": "Anthropic subscription" + }, + "auto": { + "title": "Auto" + }, + "codex": { + "apiKeyDescription": "Use OPENAI_API_KEY and CODEX_API_KEY billing for native Codex launches.", + "autoDescription": "Prefer your ChatGPT account and subscription. Use API key mode only if needed.", + "chatgptDescription": "Use your connected ChatGPT account and Codex subscription.", + "chatgptTitle": "ChatGPT account", + "hint": "Codex always runs through the native runtime. Auto prefers your ChatGPT account before falling back to API-key credentials." + } + }, + "description": "Manage how each provider connects and, when supported, which backend the multimodel runtime should use.", + "fastMode": { + "defaultOff": "Default Off", + "description": "Apply Claude Code Fast mode by default for new Anthropic team launches when the resolved model and runtime allow it.", + "disabledHint": "New Anthropic launches stay on normal speed unless a team explicitly enables Fast mode.", + "enabledHint": "New Anthropic launches will request Fast mode by default when the resolved model supports it.", + "notExposed": "This Anthropic runtime does not expose Fast mode.", + "preferFast": "Prefer Fast", + "title": "Fast mode default", + "unavailableForRuntime": "Fast mode is currently unavailable for this Anthropic runtime." + }, + "alerts": { + "anthropicApiKeyMissing": "API key mode is selected, but no Anthropic API credential is available yet.", + "anthropicStoredKeyAvailable": "A saved API key is available, but app-launched Anthropic sessions use it only after you switch to API key mode.", + "anthropicSubscriptionMissing": "Anthropic subscription mode is selected. Sign in with Anthropic to use this provider.", + "authTokenMissing": "Auth token is not configured. Many local Anthropic-compatible endpoints require a non-empty token.", + "chatgptLoginPending": "Waiting for ChatGPT account login to finish...", + "chatgptLoginStarting": "Starting ChatGPT login...", + "codexApiKeyMissing": "API key mode is selected, but no OPENAI_API_KEY or CODEX_API_KEY credential is available yet.", + "codexLocalArtifactsNoSession": "Codex CLI currently has no active ChatGPT account. Local Codex account data exists, but no active managed session is selected.", + "codexNeedsReconnect": "Codex has a locally selected ChatGPT account, but the current session needs reconnect.", + "codexNoChatgptAccount": "Codex CLI currently has no active ChatGPT account. Connect ChatGPT to use your subscription.", + "codexNoCredential": "No ChatGPT account or API key is available yet.", + "geminiApiUnavailable": "Gemini API is currently unavailable. Configure `GEMINI_API_KEY` here or use valid Google ADC credentials.", + "withApiKeyFallback": "{{message}} Switch to API key mode to use the detected API key." + }, + "authModeDescriptions": { + "anthropic": { + "apiKey": "Force app-launched Anthropic sessions to use an API key credential.", + "auto": "Use the runtime default behavior. Saved API keys in this app are only used after you switch to API key mode.", + "oauth": "Force app-launched Anthropic sessions to use the local Anthropic subscription session." + }, + "codex": { + "apiKey": "Force native Codex launches to use OPENAI_API_KEY / CODEX_API_KEY billing.", + "auto": "Prefer your ChatGPT account when it is available. Fall back to API key mode only when needed.", + "chatgpt": "Force native Codex launches to use your connected ChatGPT account and subscription." + } + }, + "progress": { + "applyingConnectionChanges": "Applying connection changes...", + "refreshingProviderStatus": "Refreshing provider status...", + "savingCompatibleEndpoint": "Saving compatible endpoint...", + "switchingAnthropicSubscription": "Switching to Anthropic subscription...", + "switchingApiKey": "Switching to API key...", + "switchingApiKeyMode": "Switching to API key mode...", + "switchingAuto": "Switching to Auto...", + "switchingChatgpt": "Switching to ChatGPT account mode..." + }, + "provider": "Provider", + "runtime": { + "descriptions": { + "anthropic": "Anthropic currently has no separate runtime backend selector.", + "codex": "Codex now runs only through the native runtime path.", + "gemini": "Choose which Gemini runtime backend multimodel should use.", + "opencode": "OpenCode uses its own managed runtime host. Desktop currently exposes status only." + }, + "title": "Runtime", + "updating": "Updating runtime..." + }, + "runtimeSummary": "Runtime: {{runtime}}", + "status": { + "configured": "configured", + "enabled": "Enabled", + "notConfigured": "Not configured", + "notSet": "not set", + "off": "Off", + "unknown": "Unknown" + }, + "title": "Provider Settings", + "usage": { + "apiKey": "Using API key", + "apiKeyRequired": "API key required", + "compatibleEndpoint": "Using compatible endpoint", + "notConnected": "Not connected", + "usingMethod": "Using {{method}}" + }, + "errors": { + "apiKeyDeletedRefreshFailed": "API key deleted, but failed to refresh provider status.", + "apiKeySavedRefreshFailed": "API key saved, but failed to refresh provider status.", + "connectionUpdatedRefreshFailed": "Connection updated, but failed to refresh provider status.", + "deleteApiKey": "Failed to delete API key", + "disableEndpoint": "Failed to disable endpoint", + "endpointDisabledRefreshFailed": "Endpoint disabled, but failed to refresh provider status.", + "endpointSavedRefreshFailed": "Endpoint saved, but failed to refresh provider status.", + "refreshCodexAccount": "Failed to refresh Codex account", + "saveApiKey": "Failed to save API key", + "saveEndpoint": "Failed to save endpoint", + "updateAnthropicFastMode": "Failed to update Anthropic Fast mode", + "updateConnection": "Failed to update connection", + "updateRuntimeBackend": "Failed to update runtime backend", + "apiKeyRequired": "API key is required" + }, + "connectionUi": { + "authMode": { + "auto": "Auto", + "oauth": "Subscription / OAuth", + "chatgpt": "ChatGPT account", + "apiKey": "API key", + "anthropicSubscription": "Anthropic subscription" + }, + "authMethod": { + "apiKey": "API key", + "apiKeyHelper": "API key helper", + "oauth": "OAuth", + "claudeSubscription": "Claude subscription", + "geminiCli": "Gemini CLI", + "googleAccount": "Google account", + "serviceAccount": "service account" + }, + "runtime": { + "codexNative": "Codex native", + "currentRuntime": "Current runtime", + "selectedRuntime": "Selected runtime", + "summary": "{{prefix}}: {{runtime}}" + }, + "status": { + "checking": "Checking...", + "checked": "Checked", + "providerActivity": "Provider Activity", + "notConnected": "Not connected", + "startingChatGptLogin": "Starting ChatGPT login...", + "waitingForChatGptLogin": "Waiting for ChatGPT account login...", + "chatGptVerificationDegraded": "ChatGPT account detected - account verification is currently degraded.", + "chatGptAccountReady": "ChatGPT account ready", + "apiKeyReady": "API key ready", + "codexLocalAccountNeedsReconnect": "Codex has a locally selected ChatGPT account, but the current session needs reconnect.", + "codexNoActiveManagedSession": "Codex CLI reports no active ChatGPT login. Local Codex account data exists, but no active managed session is selected.", + "codexNoActiveChatGptLogin": "Codex CLI reports no active ChatGPT login", + "connectChatGptForSubscription": "Connect a ChatGPT account to use your Codex subscription.", + "codexNativeReady": "Codex native ready", + "codexNativeUnavailable": "Codex native unavailable", + "unavailableInCurrentRuntime": "Unavailable in current runtime", + "connectedViaApiKey": "Connected via API key", + "apiKeyConfiguredNotVerified": "API key configured, but not verified yet", + "apiKeyModeMissingCredential": "API key mode selected, but no API key is configured", + "connectedVia": "Connected via {{method}}", + "unableToVerify": "Unable to verify" + }, + "mode": { + "selectedAuth": "Selected auth: {{authMode}}", + "preferredAuth": "Preferred auth: {{authMode}}" + }, + "credential": { + "apiKeyConfigured": "API key is configured", + "savedApiKeyAvailable": "Saved API key available in Manage", + "apiKeyAlsoConfigured": "API key also configured in Manage", + "apiKeyConfiguredInManage": "API key is configured in Manage", + "apiKeyFallbackInManage": "API key also available in Manage as fallback", + "availableAsFallback": "{{summary}} - available as fallback", + "savedApiKeyAvailableIfSwitch": "Saved API key available in Manage if you switch to API key mode", + "availableIfSwitch": "{{summary}} - available if you switch to API key mode", + "autoWillUseUntilChatGpt": "{{summary}} - Auto will use this until ChatGPT is connected" + }, + "actions": { + "connect": "Connect", + "connectAnthropic": "Connect Anthropic", + "connectChatGpt": "Connect ChatGPT", + "disconnect": "Disconnect", + "openLogin": "Open Login" + }, + "disconnect": { + "anthropicTitle": "Disconnect Anthropic subscription?", + "anthropic": "This removes the local Anthropic subscription session from the Claude CLI runtime.", + "anthropicWithApiKey": "This removes the local Anthropic subscription session from the Claude CLI runtime. Saved API keys in Manage stay available.", + "geminiTitle": "Disconnect Gemini CLI?", + "gemini": "This clears the local Gemini CLI session metadata. External ADC credentials and saved API keys are not removed." + } + } + }, + "cliRuntime": { + "actions": { + "checkForUpdates": "Check for Updates", + "checking": "Checking...", + "extensions": "Extensions", + "installRuntime": "Install {{runtime}}", + "manage": "Manage", + "recheck": "Re-check", + "reinstallRuntime": "Reinstall {{runtime}}", + "retry": "Retry", + "update": "Update" + }, + "installer": { + "checkingLatest": "Checking latest version...", + "downloading": "Downloading...", + "failed": "Installation failed", + "installed": "Installed v{{version}}", + "installing": "Installing...", + "latest": "latest", + "verifying": "Verifying checksum..." + }, + "labels": { + "multimodel": "Multimodel" + }, + "loading": { + "aiProviders": "Checking AI Providers...", + "claudeCli": "Checking Claude CLI..." + }, + "provider": { + "backend": "Backend: {{backend}}", + "loadingModels": "Loading models...", + "modelsUnavailable": "Models unavailable for this runtime build", + "runtime": "Runtime: {{runtime}}" + }, + "providerTerminal": { + "authFailed": "Authentication failed", + "authUpdated": "Authentication updated", + "loggedOut": "Provider logged out", + "login": "Login", + "logout": "Logout", + "logoutFailed": "Logout failed" + }, + "status": { + "configuredNotFound": "The configured {{runtime}} was not found.", + "foundButFailed": "{{runtime}} was found but failed to start", + "healthCheckFailed": "The configured {{runtime}} failed its startup health check.", + "notInstalled": "{{runtime}} not installed" + }, + "title": "CLI Runtime" + }, + "cliStatus": { + "versionUpgrade": "v{{current}} -> v{{latest}}" + } +} diff --git a/src/features/localization/renderer/locales/en/team.json b/src/features/localization/renderer/locales/en/team.json new file mode 100644 index 00000000..4e31e250 --- /dev/null +++ b/src/features/localization/renderer/locales/en/team.json @@ -0,0 +1,2415 @@ +{ + "activity": { + "actions": { + "createTaskFromMessage": "Create task from message", + "expandMessage": "Expand message", + "replyToMessage": "Reply to message", + "restartTeam": "Restart team" + }, + "authError": { + "description": "Authentication failed. Restarting the team will refresh the session and may resolve this issue. If the problem persists, check your API credentials or try again later." + }, + "automation": { + "reviewPickup": "Asked teammate to pick up review", + "stallNudge": "Asked teammate to continue stalled task", + "workSyncBody": "Asked teammate to sync current work" + }, + "badges": { + "automation": "automation", + "bootstrap": "bootstrap", + "command": "command", + "comment": "Comment", + "live": "live", + "note": "note", + "rateLimited": "Rate Limited", + "restart": "restart", + "result": "result", + "session": "session", + "stallNudge": "stall nudge", + "start": "start", + "workSync": "work sync" + }, + "bootstrap": { + "acknowledged": "Bootstrap acknowledged", + "restarting": "Restarting teammate", + "starting": "Starting teammate" + }, + "rawJson": "Raw JSON", + "unread": "Unread", + "thoughts": { + "count": "{{count}} thoughts", + "count_one": "{{count}} thought", + "expand": "Expand thoughts", + "showMore": "Show more", + "showLess": "Show less", + "count_few": "{{count}} thoughts", + "count_many": "{{count}} thoughts", + "count_other": "{{count}} thoughts", + "toolSummary": "🔧 {{summary}}", + "titleForMember": "{{name}} - thoughts" + }, + "timeline": { + "loadingMessages": "Loading messages...", + "noMessages": "No messages", + "emptyHint": "Send a message to a member to see activity.", + "newSession": "New session", + "olderCount": "+{{count}} older", + "showMore": "Show {{count}} more", + "showAll": "Show all", + "olderCount_one": "+{{count}} older", + "olderCount_few": "+{{count}} older", + "olderCount_many": "+{{count}} older", + "olderCount_other": "+{{count}} older" + }, + "pendingReplies": { + "title": "Awaiting replies", + "openMember": "Open member", + "messageSentAwaitingReply": "Message sent, awaiting reply", + "awaitingReply": "awaiting reply", + "externalTeam": "external team", + "crossTeamAwaitingReply": "Cross-team message sent, awaiting reply", + "user": "user", + "awaitingApproval": "awaiting approval" + }, + "reply": { + "replyingTo": "Replying to", + "action": "Reply" + }, + "activeTasks": { + "inProgress": "In progress" + }, + "expandDialog": { + "description": "Expanded message view" + } + }, + "create": { + "actions": { + "create": "Create", + "creating": "Creating...", + "openExisting": "Open Existing Team", + "skipPreflightAndCreate": "Skip preflight and create" + }, + "conflict": { + "description": "Running two teams in the same directory is risky - they may conflict editing the same files. Consider using a different directory or a git worktree for isolation.", + "title": "Another team \"{{team}}\" is already running for this working directory", + "workingDirectory": "Working directory:" + }, + "description": { + "copy": "Create a new team based on an existing one.", + "create": "Set up your team and choose how it starts." + }, + "errors": { + "nameExists": "Team name already exists", + "nameLaunching": "A team with this name is currently launching", + "createConfigFailed": "Failed to create team config", + "loadProjectsFailed": "Failed to load projects" + }, + "fields": { + "color": "Color (optional)", + "description": "Description (optional)", + "prompt": "Prompt for team lead (optional)", + "teamName": "Team name" + }, + "launchAfterCreate": { + "description": "Start the team immediately via local Claude CLI.", + "label": "Run command after create" + }, + "localOnly": "Available only in local Electron mode.", + "onDisk": "On disk:", + "placeholders": { + "description": "Brief description of the team purpose", + "prompt": "Instructions for the team lead during provisioning..." + }, + "saved": "Saved", + "solo": { + "description": "Only the team lead (main process) will be started - no teammates will be spawned. Works like a regular agent session in your chosen runtime (Claude Code, Codex, OpenCode, Gemini) but with access to the task board for planning. Saves tokens by avoiding teammate coordination overhead. You can add members later from the team settings.", + "label": "Solo team" + }, + "title": { + "copy": "Copy Team", + "create": "Create Team" + }, + "optional": { + "launchSettingsTitle": "Optional launch settings", + "launchSettingsDescription": "Prompt, safety, and CLI overrides live here when you need them.", + "teamDetailsTitle": "Optional team details", + "teamDetailsDescription": "Keep the default flow compact and only open this when you want extra context or a custom color." + }, + "prepare": { + "unsupportedPreload": "Current preload version does not support team:prepareProvisioning. Restart the dev app.", + "selectWorkingDirectory": "Select a working directory to validate the launch environment.", + "someProvidersNeedAttention": "Some selected providers need attention.", + "readyWithNotes": "All selected providers are ready, with notes.", + "ready": "All selected providers are ready.", + "failed": "Failed to prepare selected providers", + "checkingProviders": "Checking selected providers...", + "preparingEnvironment": "Preparing environment...", + "selectedProvidersReadyWithNotes": "Selected providers ready (with notes)", + "selectedProvidersReady": "Selected providers ready" + }, + "validation": { + "nameMustContainLetterOrDigit": "Name must contain at least one letter or digit", + "nameTooLong": "Name is too long (max 128 chars)", + "selectWorkingDirectory": "Select working directory (cwd)", + "memberNameRequired": "Member name cannot be empty", + "memberNameInvalid": "Member name must start with alphanumeric, use only [a-zA-Z0-9._-], max 128 chars", + "memberNamesUnique": "Member names must be unique", + "openCodeLeadModelRequired": "OpenCode lead requires a selected model.", + "openCodeTeammateRequired": "OpenCode lead requires at least one OpenCode teammate.", + "teamLaunching": "Team is currently launching", + "teamNameExists": "Team name already exists", + "checkFormFields": "Check form fields" + } + }, + "editTeam": { + "actions": { + "cancel": "Cancel", + "save": "Save" + }, + "addMemberLockReason": "Use the dedicated Add member dialog to add new teammates while the team is live.", + "description": "Change team name, description and color", + "errors": { + "changesSavedRefreshFailed": "Team changes were saved, but failed to refresh the latest view: {{message}}", + "liveRenameBlocked": "Existing teammates cannot be renamed while the team is live. renamed: {{names}}", + "memberNameEmpty": "Member name cannot be empty", + "memberNameInvalid": "Member name must start with alphanumeric, use only [a-zA-Z0-9._-], max 128 chars", + "memberNameNumericSuffix": "Member name \"{{name}}\" is not allowed (reserved for Claude CLI auto-suffix). Use \"{{base}}\" instead.", + "memberNameReserved": "Member name \"{{name}}\" is reserved", + "memberNamesUnique": "Member names must be unique before saving", + "newLiveTeammates": "Add new teammates from the dedicated Add member dialog while the team is live. Edit Team only supports updating existing teammates.", + "provisioning": "Team settings cannot be edited while provisioning is still in progress. Wait for launch to finish, then try again.", + "restartFailedMany": "Team saved, but failed to restart these teammates: {{failures}}", + "restartFailedOne": "Team saved, but failed to restart this teammate: {{failures}}", + "saveFailed": "Failed to save", + "settingsChanged": "Team settings changed while this dialog was open. Reopen it and review the latest state before saving.", + "settingsSavedMembersAndRefreshFailed": "Team settings were saved, but member changes failed: {{message}}. Refresh also failed: {{refreshError}}", + "settingsSavedMembersFailed": "Team settings were saved, but member changes failed: {{message}}", + "settingsSavedRefreshFailed": "Team settings were saved, but failed to refresh the latest view: {{message}}", + "teamNameEmpty": "Team name cannot be empty", + "unsupportedMixedPrimaryMutation": "Live edits to primary-owned teammates in mixed OpenCode teams are not supported yet. Stop the team, edit the roster, then relaunch. Affected: {{names}}" + }, + "fields": { + "colorOptional": "Color (optional)", + "description": "Description", + "name": "Name" + }, + "memberRestartWarning": "Saving will restart this teammate to apply role, workflow, worktree isolation, provider, model, effort, or MCP access changes.", + "notices": { + "liveRenameBlocked": "Live save is blocked because existing teammates were renamed. Revert those identity changes or stop the team first.", + "newLiveTeammates": "New teammates cannot be added from Edit Team while the team is live. Use the Add member dialog instead.", + "provisioning": "Team provisioning is still in progress. Editing is temporarily locked until launch finishes.", + "restartMany": "Saving will restart or relaunch these teammates to apply role, workflow, worktree isolation, provider, model, effort, or MCP access changes: {{names}}.", + "restartOne": "Saving will restart or relaunch this teammate to apply role, workflow, worktree isolation, provider, model, effort, or MCP access changes: {{names}}.", + "unsupportedMixedPrimaryMutation": "Live edits/removals for primary-owned teammates in mixed OpenCode teams require stopping and relaunching the team: {{names}}." + }, + "placeholders": { + "description": "Team description (optional)", + "teamName": "Team name" + }, + "teamLead": { + "changeRuntime": "Change lead runtime", + "changeRuntimeDescription": "Open Relaunch Team to change the lead provider, model, or effort.", + "modelLockReason": "Team lead runtime is managed from Relaunch Team.", + "readOnlyHint": "Team lead name and role stay read-only here. Open the runtime panel on the lead row to change provider, model, or effort.", + "role": "Team Lead" + }, + "title": "Edit Team" + }, + "memberDraft": { + "actions": { + "remove": "Remove member", + "removeAria": "Remove {{name}}", + "restore": "Restore member", + "restoreAria": "Restore {{name}}" + }, + "anthropicContext": { + "defaultSetting": "default context setting", + "description": "Anthropic context is team-wide for this launch: {{mode}}. Use the lead runtime panel's Limit context checkbox to change it.", + "limitEnabled": "200K limit enabled" + }, + "mcp": { + "buttonInherit": "MCP inherit", + "buttonScopes": "MCP scopes", + "chooseScopes": "Choose scopes", + "inheritLead": "Inherit lead", + "lockedInfo": "Agent Teams MCP only is enabled for all teammates. This teammate will launch with only the Agent Teams server.", + "mode": "MCP mode", + "scopes": { + "local": "local", + "project": "project", + "user": "user" + }, + "serverNames": "Server names", + "settingInfo": "Agent Teams MCP launches this teammate with only the Agent Teams server. Scope and allowlist modes apply only to this teammate launch.", + "strictAllowlist": "Strict allowlist", + "tooltip": "{{label}}: Control this member's MCP inheritance policy", + "agentTeamsMcp": "Agent Teams MCP" + }, + "model": { + "ariaLabel": "{{provider}} provider, {{model}}", + "currentLeadRuntime": "Current lead runtime", + "default": "Default", + "inheritedTooltip": "Provider, model, and effort are inherited from the lead while sync is enabled.", + "leadSuffix": "{{label}} (lead)", + "liveDisabled": "Provider, model, and effort changes are disabled while the team is live. Reconnect the team to apply them safely.", + "lockedActionFallback": "Lead runtime changes open Relaunch Team, where provider, model, and effort can be updated.", + "restartWholeTeam": "Saving those runtime changes restarts the whole team." + }, + "nameAria": "Member {{index}} name", + "nameFallback": "member {{index}}", + "noRole": "No role", + "removed": "Removed", + "workflow": { + "addTooltip": "Add teammate workflow", + "editTooltip": "Edit teammate workflow", + "label": "Workflow (optional)", + "placeholder": "How this agent should behave, interact with others...", + "saved": "Saved" + }, + "worktree": { + "description": "Run this teammate in a separate git worktree. Apply/reject changes targets that worktree, not the lead workspace.", + "label": "Worktree" + }, + "addMembers": { + "title": "Add Members", + "description": "Add new members to {{teamName}}" + }, + "placeholders": { + "name": "member-name", + "mcpServers": "github, sentry" + } + }, + "detail": { + "actions": { + "add": "Add", + "cancel": "Cancel", + "delete": "Delete", + "editCode": "Edit code", + "launch": "Launch", + "remove": "Remove", + "stop": "Stop", + "task": "Task", + "visualize": "Visualize" + }, + "deleteTeam": { + "description": "Delete team \"{{team}}\"? This action is irreversible. All team data and tasks will be deleted.", + "title": "Delete team" + }, + "draft": { + "descriptionPrefix": "This is a draft team -", + "descriptionSuffix": "has been configured with {{count}} {{member}} but hasn't been provisioned by CLI yet. Click Launch to select a model and start the team.", + "descriptionSuffix_few": "has been configured with {{count}} {{member}} but hasn't been provisioned by CLI yet. Click Launch to select a model and start the team.", + "descriptionSuffix_many": "has been configured with {{count}} {{member}} but hasn't been provisioned by CLI yet. Click Launch to select a model and start the team.", + "descriptionSuffix_one": "has been configured with {{count}} {{member}} but hasn't been provisioned by CLI yet. Click Launch to select a model and start the team.", + "descriptionSuffix_other": "has been configured with {{count}} {{member}} but hasn't been provisioned by CLI yet. Click Launch to select a model and start the team.", + "member": "members", + "member_few": "members", + "member_many": "members", + "member_one": "member", + "member_other": "members", + "title": "Team not launched yet" + }, + "invalidTab": "Invalid team tab", + "kanbanSafeData": "Failed to fully load kanban. Displaying safe data.", + "loadFailed": "Failed to load team", + "loading": "Loading team", + "loadingSidebar": "Loading team sidebar", + "offline": { + "offline": "Team is offline", + "partialFailed": "Last launch failed partway", + "partialMissing": "Last launch failed partway - {{missing}}/{{expected}} teammates did not join", + "reconciling": "Last launch is still reconciling" + }, + "previous": "Previous: {{paths}}", + "removeMember": { + "description": "Remove \"{{member}}\" from the team? Tasks and messages will be preserved, but this name cannot be reused.", + "title": "Remove member" + }, + "sections": { + "team": "Team" + }, + "solo": "Solo", + "status": { + "active": "Active", + "launching": "Launching...", + "running": "Running" + }, + "telemetry": { + "cpu": "CPU", + "memory": "Memory" + }, + "tooltips": { + "deleteTeam": "Delete team", + "editTeam": "Edit team", + "editUnavailableProvisioning": "Edit team is unavailable while provisioning is still in progress", + "openBuiltInEditor": "Open project in built-in editor", + "openTeamGraph": "Open team graph", + "stopTeam": "Stop team" + }, + "waitingForProvisioning": "Team data will appear once provisioning completes", + "context": { + "title": "Context" + } + }, + "review": { + "fileHeader": { + "actions": { + "accept": "Accept", + "discard": "Discard", + "discardTooltip": "Discard all edits for this file", + "keepMyDraft": "Keep my draft", + "reject": "Reject", + "reloadFromDisk": "Reload from disk", + "restore": "Restore", + "restoreTooltip": "Create/restore this file on disk from the preview", + "saveFile": "Save File", + "saveFileTooltip": "Save file to disk" + }, + "badges": { + "deleted": "DELETED", + "manualReview": "MANUAL REVIEW", + "new": "NEW", + "worktree": "WORKTREE" + }, + "contentSource": { + "disk-current": "Current Disk", + "file-history": "File History", + "git-fallback": "Git Fallback", + "ledger-exact": "Task Ledger", + "ledger-snapshot": "Ledger Snapshot", + "snippet-reconstruction": "Reconstructed", + "unavailable": "Content unavailable" + }, + "contentUnavailable": { + "badge": "Content unavailable", + "description": "The ledger recorded metadata for this change, but full text content is not available. This usually means binary, large, or hash-only content.", + "safety": "Automatic accept/reject is disabled for this file to avoid unsafe disk writes.", + "title": "Text content is unavailable" + }, + "disabled": { + "acceptRejectContentUnavailable": "Accept/Reject is disabled because full text content is unavailable.", + "acceptRejectMissingOnDisk": "Accept/Reject is disabled while the file is missing on disk.", + "rejectBaselineUnavailable": "Reject is disabled because the original baseline is unavailable.", + "rejectContentUnavailable": "Reject is disabled because full text content is unavailable.", + "rejectManualLedgerReview": "Reject is disabled because this ledger change has binary, large, or unavailable content." + }, + "externalChange": { + "changedOnDisk": "Changed on disk", + "deletedOnDisk": "Deleted on disk", + "recreatedOnDisk": "Recreated on disk" + }, + "missingOnDisk": { + "badge": "Missing on disk", + "description": "We can still show a preview from agent logs, but your filesystem is out of sync.", + "restorePrefix": "Use", + "restoreSuffix": "to write the preview content back to disk.", + "restoreUnavailable": "Full file content is not available to restore automatically.", + "title": "File is missing on disk" + }, + "pathChange": { + "from": "From {{path}}", + "to": "To {{path}}" + }, + "worktree": { + "isolated": "Isolated worktree" + } + }, + "toolbar": { + "stats": { + "pending": "{{count}} pending", + "pending_one": "{{count}} pending", + "pending_other": "{{count}} pending", + "accepted": "{{count}} accepted", + "accepted_one": "{{count}} accepted", + "accepted_other": "{{count}} accepted", + "rejected": "{{count}} rejected", + "rejected_one": "{{count}} rejected", + "rejected_other": "{{count}} rejected", + "acrossFiles": "across {{count}} files", + "acrossFiles_one": "across {{count}} file", + "acrossFiles_other": "across {{count}} files", + "edited": "{{count}} edited", + "edited_one": "{{count}} edited", + "edited_other": "{{count}} edited", + "pending_few": "{{count}} pending", + "pending_many": "{{count}} pending", + "accepted_few": "{{count}} accepted", + "accepted_many": "{{count}} accepted", + "rejected_few": "{{count}} rejected", + "rejected_many": "{{count}} rejected", + "acrossFiles_few": "across {{count}} files", + "acrossFiles_many": "across {{count}} files", + "edited_few": "{{count}} edited", + "edited_many": "{{count}} edited" + }, + "actions": { + "auto": "Auto", + "undo": "Undo", + "acceptAll": "Accept All", + "rejectAll": "Reject All", + "applying": "Applying...", + "applyRejections": "Apply Rejections" + }, + "tooltips": { + "autoOn": "Auto-mark files as viewed when scrolled to end (ON)", + "autoOff": "Auto-mark files as viewed when scrolled to end (OFF)", + "undo": "Undo last review operation (Ctrl+Z)", + "acceptAll": "Accept all changes across all files", + "rejectAll": "Reject all safely rejectable changes across all files", + "rejectAllDisabled": "No pending files have a safe original baseline to reject.", + "applyRejections": "Apply rejected hunks to disk; accepted changes are kept as-is" + } + }, + "diffError": { + "title": "Failed to render diff view", + "unexpected": "An unexpected error occurred while rendering the diff.", + "actions": { + "retry": "Retry" + }, + "raw": { + "show": "Show raw diff data", + "file": "File: {{file}}", + "original": "--- Original", + "modified": "+++ Modified", + "charsTotal": "... ({{count}} chars total)", + "charsTotal_one": "... ({{count}} char total)", + "charsTotal_other": "... ({{count}} chars total)", + "charsTotal_few": "... ({{count}} chars total)", + "charsTotal_many": "... ({{count}} chars total)" + } + }, + "fileTree": { + "viewed": "Viewed", + "badges": { + "new": "new", + "deleted": "deleted" + }, + "collapseFolder": "Collapse {{name}}", + "expandFolder": "Expand {{name}}", + "empty": { + "noChangedFiles": "No changed files", + "noMatchingFiles": "No matching files" + }, + "searchPlaceholder": "Search files…", + "filters": { + "unresolved": "Unresolved", + "rejected": "Rejected", + "new": "New", + "clear": "Clear" + } + }, + "diffControls": { + "previousChunk": "Previous chunk", + "nextChunk": "Next chunk", + "rejectChange": "Reject change (⌘N)", + "acceptChange": "Accept change (⌘Y)", + "undo": "Undo", + "keep": "Keep", + "rejectShortcut": "⌘N", + "acceptShortcut": "⌘Y" + }, + "conflict": { + "title": "Conflict Detected", + "description": "This file has been modified since the agent's changes", + "cancel": "Cancel", + "saveResolution": "Save Resolution", + "editManually": "Edit Manually", + "useOriginal": "Use Original", + "keepCurrent": "Keep Current" + }, + "fullDiffLoading": { + "titleOne": "Preparing Full Diff", + "titleMany": "Preparing {{count}} Full Diffs", + "subtitleForFile": "Finalizing the exact editor diff for {{file}}.", + "subtitleCurrentFile": "Finalizing the exact editor diff for the current file.", + "subtitleMany": "Resolving exact before/after baselines for the files currently loading.", + "previewsReady": "{{count}} previews ready", + "previewsReady_one": "{{count}} preview ready", + "editorViewLoading": "Editor view loading", + "filesInProgress": "{{count}} files in progress", + "filesInProgress_one": "{{count}} file in progress", + "filesReady": "{{ready}}/{{total}} files ready", + "progressDescription": "{{ready}} ready, {{loading}} still loading. Preview diffs stay visible below while the remaining baselines are resolved.", + "singleDescription": "Preview diffs stay visible below while the exact baseline is resolved.", + "previewsReady_few": "{{count}} previews ready", + "previewsReady_many": "{{count}} previews ready", + "previewsReady_other": "{{count}} previews ready", + "filesInProgress_few": "{{count}} files in progress", + "filesInProgress_many": "{{count}} files in progress", + "filesInProgress_other": "{{count}} files in progress" + }, + "fileMissingPrefix": "File is missing on disk. This diff may be only a preview from agent logs. Use", + "restore": "Restore", + "fileMissingSuffix": "to create the file on disk.", + "filePlaceholder": { + "loading": "Loading", + "description": "Preparing a full editor diff for this file." + }, + "loading": { + "diff": "DIFF", + "ledgerObjectsProcessed": "{{count}} ledger objects processed", + "ledgerObjectsProcessed_one": "{{count}} ledger object processed", + "ledgerObjectsProcessed_other": "{{count}} ledger objects processed", + "ledgerObjectsProcessed_few": "{{count}} ledger objects processed", + "ledgerObjectsProcessed_many": "{{count}} ledger objects processed", + "phases": { + "readingLedger": "Reading task ledger...", + "resolvingFiles": "Resolving file states...", + "checkingWorktree": "Checking worktree context...", + "preparingDiffs": "Preparing review diffs..." + } + }, + "progress": { + "viewed": "{{viewed}}/{{total}} viewed" + }, + "scope": { + "readMore": "Read more", + "tiers": { + "exact": { + "title": "Task scope determined precisely", + "detail": "Both start and completion markers found in the session log. The diff includes only changes made during this specific task - other tasks that modified the same files are excluded." + }, + "endEstimated": { + "title": "End boundary estimated", + "detail": "Only the start marker was found - the task has no completion marker yet. Changes shown from task start to end of session. If other tasks ran after this one in the same session, their changes may also be included." + }, + "startEstimated": { + "title": "Start boundary estimated", + "detail": "Only the completion marker was found - the start of work was not captured. If other tasks ran before this one in the same session, their changes to the same files may also be included." + }, + "allSession": { + "title": "Showing all session changes", + "detail": "No task markers found in the session log. Cannot isolate this task - all file changes from the entire session are shown, including changes from other tasks. This can happen with older CLI versions or non-standard workflows." + } + }, + "ledger": { + "exact": { + "title": "Changes captured by task ledger", + "detail": "The orchestrator captured these file changes while the agent was working on this task.", + "badge": "Ledger exact" + }, + "limited": { + "title": "Changes captured with limited reviewability", + "detail": "The orchestrator captured these file changes for this task, but at least one change was captured from a snapshot or metadata-only source. Review exact text diffs where available; binary or unavailable content may require manual review.", + "mixedBadge": "Mixed reviewability", + "needsReviewBadge": "Needs review" + } + }, + "workInterval": { + "title": "Scoped by persisted work interval", + "detail": "The task start marker was not available in the session log, so the diff is scoped by the task work interval stored on the board.", + "badge": "Interval scoped" + }, + "confidence": { + "high": "High confidence", + "medium": "Medium confidence", + "low": "Low confidence", + "bestEffort": "Best effort" + } + }, + "shortcuts": { + "title": "Keyboard Shortcuts", + "actions": { + "nextChange": "Next change", + "previousChange": "Previous change", + "nextFile": "Next file", + "previousFile": "Previous file", + "acceptChange": "Accept change", + "rejectChange": "Reject change", + "saveFile": "Save file", + "undo": "Undo", + "redo": "Redo", + "toggleShortcuts": "Toggle shortcuts", + "closeDialog": "Close dialog" + } + }, + "timeline": { + "empty": "No edit events", + "titleWithCount": "Edit Timeline ({{count}})" + }, + "continuousScroll": { + "empty": "No reviewable file changes" + }, + "empty": { + "noSafeDiff": "No safe diff available", + "noFileChangesRecorded": "No file changes recorded", + "noSafeDiffDescription": "The task ledger did not expose a safe file diff for this task.", + "noSafeDiffDiagnosticsDescription": "The task ledger did not expose a safe file diff for this task. The diagnostics below explain why.", + "noFileEventsYet": "The task ledger has no file events for this task yet.", + "noFileEvents": "The task ledger has no file events for this task." + } + }, + "messages": { + "actions": { + "bottomSheetActions": "Message bottom sheet actions", + "collapseAll": "Collapse all messages", + "collapseSheet": "Collapse sheet", + "expandAll": "Expand all messages", + "expandSheet": "Expand sheet", + "floatComposer": "Float composer", + "floatMessagesComposer": "Float messages composer", + "hideSearch": "Hide search", + "loadOlder": "Load older messages", + "markAllRead": "Mark all as read", + "messageActions": "Message actions", + "moveMessagesToBottomSheet": "Move messages to bottom sheet", + "moveMessagesToSidebar": "Move messages to sidebar", + "moveToBottomSheet": "Move to bottom sheet", + "moveToInline": "Move to inline", + "moveToSidebar": "Move to sidebar", + "panelActions": "Message panel actions", + "searchMessages": "Search messages" + }, + "delivery": { + "copied": "Copied", + "copyDebugDetails": "Copy debug details", + "details": "Details", + "fields": { + "acceptanceUnknown": "acceptanceUnknown", + "delivered": "delivered", + "diagnostics": "diagnostics", + "ledgerStatus": "ledgerStatus", + "messageId": "messageId", + "providerId": "providerId", + "queuedBehindMessageId": "queuedBehindMessageId", + "reason": "reason", + "responsePending": "responsePending", + "responseState": "responseState", + "statusMessageId": "statusMessageId", + "userVisibleMessage": "userVisibleMessage", + "userVisibleNextReviewAt": "userVisibleNextReviewAt", + "userVisibleReasonCode": "userVisibleReasonCode", + "userVisibleState": "userVisibleState", + "visibleReplyCorrelation": "visibleReplyCorrelation", + "visibleReplyMessageId": "visibleReplyMessageId" + } + }, + "panelMode": "Message panel mode", + "title": "Messages", + "unread": { + "new": "{{count}} new", + "unread": "{{count}} unread", + "new_few": "{{count}} new", + "new_many": "{{count}} new", + "new_one": "{{count}} new", + "new_other": "{{count}} new", + "unread_few": "{{count}} unread", + "unread_many": "{{count}} unread", + "unread_one": "{{count}} unread", + "unread_other": "{{count}} unread" + }, + "filter": { + "ariaLabel": "Filter messages", + "tooltip": "Filter messages", + "from": "From", + "to": "To", + "noData": "No data", + "showStatusUpdates": "Show status updates (idle/shutdown)", + "actions": { + "reset": "Reset", + "save": "Save" + } + }, + "status": { + "title": "Status" + }, + "actionMode": { + "label": "Action mode" + }, + "search": { + "placeholder": "Search..." + } + }, + "modelSelector": { + "badges": { + "configured": "Configured", + "connected": "Connected", + "failed": "Failed", + "free": "Free", + "local": "Local", + "needsTest": "Needs test", + "verified": "Verified", + "unavailable": "Unavailable", + "issue": "Issue" + }, + "customModelId": "Custom model id", + "label": "Model (optional)", + "multimodelRequired": "Codex and Gemini require Multimodel mode.", + "openCode": { + "allSources": "All OpenCode sources", + "filterSource": "Filter {{source}}", + "filterSources": "Filter OpenCode sources", + "freeOnly": "Free only", + "freeTooltip": "OpenCode marks this model as free.", + "loadingModels": "Loading OpenCode models...", + "noSourcesFound": "No sources found.", + "recommendedOnly": "Recommended only", + "searchSources": "Search sources", + "sourcesCount": "{{count}} OpenCode sources", + "sourcesCount_few": "{{count}} OpenCode sources", + "sourcesCount_many": "{{count}} OpenCode sources", + "sourcesCount_one": "{{count}} OpenCode sources", + "sourcesCount_other": "{{count}} OpenCode sources" + }, + "reason": "Reason: {{reason}}", + "runtimeModelsSyncing": "Explicit models load from the current runtime. Default remains available while the list is syncing.", + "fastMode": { + "codexLabel": "Fast mode (2x credits)", + "optionalLabel": "Fast mode (optional)", + "defaultOff": "Default (Off)", + "fast": "Fast", + "off": "Off", + "defaultFast": "Default (Fast)", + "defaultResolvesTo": "Default currently resolves to {{mode}}.", + "runtimeBackedHint": "Fast mode is runtime-backed and only unlocks when the resolved Anthropic launch model supports it." + }, + "anthropicExtraUsage": { + "pricingDocs": "Read Anthropic pricing docs" + }, + "searchModels": "Search models", + "defaultModel": "Default", + "empty": { + "noSearchMatches": "No models match this search.", + "recommendedFreeOpenCode": "No recommended free OpenCode models are available in the current runtime list.", + "freeOpenCode": "No free OpenCode models are available in the current runtime list.", + "recommendedOpenCode": "No recommended OpenCode models are available in the current runtime list.", + "noModels": "No models are available in the current runtime list." + }, + "openCodeStatus": { + "notReadyTitle": "OpenCode is not ready for team launch", + "freeModelsAvailableTitle": "OpenCode free models are available", + "providerNotConnectedTitle": "OpenCode provider is not connected", + "readyTitle": "OpenCode is ready", + "readyMessage": "OpenCode passed provider readiness. Select it to use OpenCode models for this team.", + "useOpenCode": "Use OpenCode", + "badges": { + "check": "Check", + "install": "Install", + "free": "Free", + "setup": "Setup" + }, + "summary": { + "checking": "OpenCode status: checking runtime", + "status": "OpenCode status: {{parts}}" + }, + "summaryParts": { + "teamLaunchBlocked": "team launch blocked", + "providerOptional": "provider connection optional", + "providerModelsNeedSetup": "provider-backed models need setup", + "teamLaunchReady": "team launch ready", + "runtimeDetected": "runtime detected", + "runtimeMissing": "runtime missing", + "freeWithoutAuth": "free models available without auth", + "providerConnected": "provider connected", + "providerNotConnected": "provider not connected" + }, + "messages": { + "checking": "The app is still checking the OpenCode runtime. Wait for provider status to finish, then try again.", + "unsupported": "OpenCode is not installed, not found, or the detected runtime is not supported. Install or update OpenCode, then refresh provider status. You can also use the Install button on the home page.", + "freeAvailable": "OpenCode is detected. You can use free OpenCode models such as Big Pickle without connecting a provider. Connect a provider only when you want provider-backed models.", + "noFreeListed": "OpenCode is detected, but no free OpenCode model is listed yet. Refresh provider status, or connect a provider in OpenCode for provider-backed models.", + "launchBlocked": "OpenCode is installed and authenticated, but Agent Teams launch readiness is blocked.", + "ready": "OpenCode is ready for team launch." + }, + "loadingRuntime": "OpenCode runtime status is still loading." + }, + "advisory": { + "pingNotConfirmed": "Ping not confirmed", + "note": "Note" + }, + "placeholders": { + "customModelId": "openai/gpt-oss-20b" + }, + "routeGroups": { + "openCodeConfig": "OpenCode config", + "builtinFree": "Free built-in", + "connectedProviders": "Connected providers", + "otherCatalog": "Other OpenCode catalog" + }, + "pricing": { + "free": "Free", + "inputShort": "in {{rate}}", + "outputShort": "out {{rate}}", + "perMillionSummary": "{{summary}} / 1M", + "inputTitle": "Input: {{rate}} per 1M tokens", + "outputTitle": "Output: {{rate}} per 1M tokens", + "cacheReadTitle": "Cache read: {{rate}} per 1M tokens", + "cacheWriteTitle": "Cache write: {{rate}} per 1M tokens" + }, + "defaultTooltip": { + "anthropicCompatibleWithResolved": "Uses the Anthropic-compatible endpoint default model.\nCurrently resolves to {{model}}.", + "anthropicCompatible": "Uses the Anthropic-compatible endpoint default model.", + "anthropic": "Uses the Claude team default model.\nResolves to {{longContextModel}} with 1M context, or {{limitedContextModel}} with 200K context when Limit context is enabled.", + "openCodeWithResolved": "Uses the OpenCode default model.\nCurrently resolves to {{model}}.", + "openCode": "Uses the OpenCode runtime default model.", + "runtime": "Uses the runtime default for the selected provider." + }, + "multimodelOff": "Multimodel off", + "unavailableInRuntime": "Unavailable in current runtime" + }, + "taskDetail": { + "actions": { + "cancel": "Cancel", + "delete": "Delete", + "markResolved": "Mark resolved", + "save": "Save" + }, + "attachments": { + "commentAttachment": "Comment attachment", + "fromComments": "From comments", + "preview": "Preview {{filename}}" + }, + "changes": { + "badges": { + "attention": "attention", + "noSafeDiff": "no safe diff" + }, + "empty": { + "noFileChangesRecorded": "No file changes recorded", + "noFileChangesRecordedYet": "No file changes recorded yet", + "noReviewableChangesRecovered": "No reviewable file changes recovered", + "noSafeDiffAvailable": "No safe diff available" + }, + "loadFailed": "Failed to load task changes summary", + "loading": "Loading changes...", + "fileCount": "{{count}} files", + "fileRowsHidden": "{{count}} file rows hidden", + "moreDiagnostics": "{{count}} more diagnostics", + "moreFiles": "{{count}} more files", + "openInEditor": "Open in editor", + "openTask": "Open task {{subject}}", + "refresh": "Refresh changes", + "refreshFailed": "Refresh failed: {{error}}", + "refreshing": "Refreshing", + "refreshingChanges": "Refreshing changes...", + "refreshTeamChanges": "Refresh team changes", + "refreshShort": "Refresh", + "reviewDiff": "Review diff", + "reviewTaskDiff": "Review task diff", + "scannedCandidateTasks": "Scanned {{requested}} of {{eligible}} candidate tasks", + "tasksDeferred": "{{count}} tasks deferred this pass", + "title": "Changes", + "fileCount_few": "{{count}} files", + "fileCount_many": "{{count}} files", + "fileCount_one": "{{count}} files", + "fileCount_other": "{{count}} files", + "fileRowsHidden_few": "{{count}} file rows hidden", + "fileRowsHidden_many": "{{count}} file rows hidden", + "fileRowsHidden_one": "{{count}} file rows hidden", + "fileRowsHidden_other": "{{count}} file rows hidden", + "moreDiagnostics_few": "{{count}} more diagnostics", + "moreDiagnostics_many": "{{count}} more diagnostics", + "moreDiagnostics_one": "{{count}} more diagnostics", + "moreDiagnostics_other": "{{count}} more diagnostics", + "moreFiles_few": "{{count}} more files", + "moreFiles_many": "{{count}} more files", + "moreFiles_one": "{{count}} more files", + "moreFiles_other": "{{count}} more files", + "tasksDeferred_few": "{{count}} tasks deferred this pass", + "tasksDeferred_many": "{{count}} tasks deferred this pass", + "tasksDeferred_one": "{{count}} tasks deferred this pass", + "tasksDeferred_other": "{{count}} tasks deferred this pass" + }, + "clarification": { + "awaitingLead": "Awaiting clarification from team lead", + "awaitingUser": "Awaiting clarification from you" + }, + "description": { + "add": "Click to add description...", + "edit": "Edit description", + "placeholder": "Task description (supports markdown)" + }, + "loading": { + "fetchingTeamData": "Fetching team data", + "title": "Loading task..." + }, + "logs": { + "newArriving": "New task logs arriving" + }, + "notFound": "Task not found", + "related": { + "blockedBy": "Blocked by", + "blocks": "Blocks", + "linkedFrom": "Linked from", + "links": "Links", + "title": "Related tasks" + }, + "review": { + "reviewer": "Reviewer: {{reviewer}}" + }, + "sections": { + "attachments": "Attachments", + "changes": "Changes", + "comments": "Comments", + "description": "Description", + "taskLogs": "Task Logs", + "workflowHistory": "Workflow History" + }, + "unassigned": "Unassigned", + "workflow": { + "implementationTimeTitle": "Implementation time from persisted work intervals", + "inProgressTime": "In progress time {{duration}}" + }, + "comments": { + "renderLimit": "Showing the most recent {{formattedCount}} comments to keep the UI responsive.", + "badges": { + "approved": "Approved", + "reviewRequested": "Review requested" + }, + "unknownTime": "unknown time", + "actions": { + "reply": "Reply", + "replyToComment": "Reply to comment", + "showMore": "Show more comments ({{visible}}/{{total}})", + "cancelReply": "Cancel reply", + "comment": "Comment" + }, + "attachments": { + "previewAlt": "Attachment preview", + "downloadFailed": "Download failed" + }, + "replyingTo": "Replying to", + "input": { + "placeholder": "Add a comment... (Enter to send)", + "charsLeft": "{{count}} chars left", + "charsLeft_one": "{{count}} char left", + "charsLeft_other": "{{count}} chars left", + "charsLeft_few": "{{count}} chars left", + "charsLeft_many": "{{count}} chars left" + } + }, + "workflowTimeline": { + "empty": "No workflow history recorded", + "currentImplementationInterval": "Current implementation interval", + "implementationIntervalEnded": "Implementation interval ended at this transition", + "runningPrefix": "running ", + "createdAs": "Created as", + "by": "by", + "reassigned": "Reassigned", + "assignedTo": "Assigned to", + "unassignedFrom": "Unassigned from", + "ownerChanged": "Owner changed", + "reviewRequested": "Review requested", + "reviewStarted": "Review started", + "changesRequested": "Changes requested", + "approved": "Approved", + "unknownEvent": "Unknown event" + }, + "reviewStates": { + "approved": "Approved", + "needsFix": "Needs fix", + "inReview": "In review" + } + }, + "tasks": { + "createTask": { + "assignee": "Assignee", + "assigneeOptional": "Assignee (optional)", + "blockedByOptional": "Blocked by tasks (optional)", + "blockedBySummary": "Task will be blocked by: {{tasks}}", + "cancel": "Cancel", + "create": "Create", + "creating": "Creating...", + "description": "The task will be created in the team's tasks/ directory and appear on the Kanban board.", + "descriptionOptional": "Description (optional)", + "detailsPlaceholder": "Task details (supports markdown)", + "hideOptionalFields": "Hide optional fields", + "offlineNotice": { + "after": "- launch the team to start execution.", + "before": "Team is offline. The task will be added to" + }, + "promptOptional": "Prompt for assignee (optional)", + "promptPlaceholder": "Custom instructions for the team member...", + "relatedOptional": "Related tasks (optional)", + "relatedSummary": "Related: {{tasks}}", + "saved": "Saved", + "searchTasks": "Search tasks...", + "selectMember": "Select a member", + "selectMemberOptional": "Select member...", + "showOptionalFields": "Show optional fields", + "startImmediately": "Start immediately", + "startOfflineHint": "Team is offline. Launch the team first to start tasks immediately.", + "subject": "Subject", + "subjectPlaceholder": "What needs to be done?", + "title": "Create Task", + "todo": "TODO" + }, + "list": { + "columns": { + "blockedBy": "Blocked By", + "blocks": "Blocks", + "id": "ID", + "owner": "Owner", + "status": "Status", + "subject": "Subject" + }, + "empty": "No tasks in this team", + "filters": { + "allOwners": "All owners", + "allStatuses": "All statuses", + "ownerAria": "Filter tasks by owner", + "statusAria": "Filter tasks by status" + }, + "showing": "Showing {{shown}} of {{total}}" + }, + "status": { + "completed": "completed", + "deleted": "deleted", + "inProgress": "in_progress", + "pending": "pending" + }, + "statusSummary": { + "progressAria": "Tasks {{completed}}/{{total}} completed", + "inProgress": "{{count}} in_progress", + "inProgress_one": "{{count}} in_progress", + "inProgress_other": "{{count}} in_progress", + "inProgress_few": "{{count}} in_progress", + "inProgress_many": "{{count}} in_progress", + "pending": "{{count}} pending", + "pending_one": "{{count}} pending", + "pending_other": "{{count}} pending", + "pending_few": "{{count}} pending", + "pending_many": "{{count}} pending", + "completed": "{{count}} completed", + "completed_one": "{{count}} completed", + "completed_other": "{{count}} completed", + "completed_few": "{{count}} completed", + "completed_many": "{{count}} completed" + }, + "unassigned": "Unassigned", + "teamPrefix": "Team:", + "openTask": "Open task", + "deleteConfirm": { + "title": "Delete task", + "message": "Move task #{{taskId}} to trash?", + "confirmLabel": "Delete", + "cancelLabel": "Cancel" + } + }, + "editor": { + "actions": { + "cancel": "Cancel", + "closeEditor": "Close editor", + "closeTab": "Close tab", + "closeTooltip": "Close editor (Esc)", + "discard": "Discard", + "discardAndClose": "Discard & Close", + "keep": "Keep", + "keepMine": "Keep mine", + "keyboardShortcuts": "Keyboard shortcuts", + "overwrite": "Overwrite", + "refreshAria": "Refresh (F5)", + "refreshTooltip": "Refresh git status (F5)", + "reload": "Reload", + "retry": "Retry", + "save": "Save", + "saveAllAndClose": "Save All & Close" + }, + "ariaLabel": "Project Editor", + "dialogs": { + "conflictDescription": "The file has been modified externally since you opened it. Overwrite with your changes?", + "conflictTitle": "Save Conflict", + "unsavedDescription": "You have unsaved changes. What would you like to do?", + "unsavedFileDescription": "This file has unsaved changes. What would you like to do?", + "unsavedTitle": "Unsaved Changes" + }, + "newFile": { + "validation": { + "nameRequired": "Name cannot be empty", + "invalidName": "Invalid name", + "invalidCharacters": "Name contains invalid characters", + "nameTooLong": "Name is too long" + }, + "placeholders": { + "fileName": "File name...", + "folderName": "Folder name..." + }, + "aria": { + "newFileName": "New file name", + "newFolderName": "New folder name" + } + }, + "draftRecovered": "Recovered unsaved changes from a previous session.", + "externalChange": { + "changed": "File changed on disk.", + "deleted": "File no longer exists on disk." + }, + "saveFailed": "Save failed: {{error}}", + "sidebar": { + "explorer": "Explorer", + "hide": "Hide sidebar", + "hideWithShortcut": "Hide sidebar ({{shortcut}})", + "show": "Show sidebar", + "showWithShortcut": "Show sidebar ({{shortcut}})" + }, + "searchInFiles": { + "title": "Search in Files", + "closeSearch": "Close search", + "closeSearchShortcut": "Close search (Esc)", + "searchPlaceholder": "Search...", + "matchCase": "Match Case", + "matchCaseToggle": "Aa", + "noResults": "No results found", + "resultsSummary": "{{count}} matches in {{fileCount}} files", + "resultsSummary_one": "{{count}} match in {{fileCount}} files", + "truncated": "(truncated)", + "resultsSummary_few": "{{count}} matches in {{fileCount}} files", + "resultsSummary_many": "{{count}} matches in {{fileCount}} files", + "resultsSummary_other": "{{count}} matches in {{fileCount}} files" + }, + "fileTree": { + "failedToLoadFiles": "Failed to load files: {{error}}", + "loading": "Loading files...", + "empty": "No files found", + "dropForProjectRoot": "Drop here for project root", + "moveToTrash": "Move to Trash", + "moveToTrashConfirm": "Move \"{{name}}\" to Trash?", + "cancel": "Cancel" + }, + "goToLine": { + "title": "Go to Line", + "position": "(current: {{current}}, total: {{total}})", + "placeholder": "Line number, +offset, -offset, or %", + "go": "Go" + }, + "searchPanel": { + "previousMatch": "Previous Match", + "nextMatch": "Next Match", + "close": "Close", + "replacePlaceholder": "Replace", + "replace": "Replace", + "replaceNext": "Replace Next", + "all": "All", + "replaceAll": "Replace All" + }, + "statusBar": { + "position": "Ln {{line}}, Col {{col}}", + "enableWatcher": "Enable file watcher", + "disableWatcher": "Disable file watcher", + "watch": "watch", + "watching": "watching", + "watchExternalChanges": "Watch for external changes", + "disableExternalWatcher": "Disable external change watcher", + "encodingUtf8": "UTF-8", + "spaces": "Spaces: {{count}}" + }, + "imagePreview": { + "loading": "Loading preview...", + "openFullSize": "Open full-size preview", + "openSystemViewer": "Open in System Viewer" + }, + "quickOpen": { + "title": "Quick Open", + "searchPlaceholder": "Search files by name...", + "loading": "Loading files...", + "empty": "No files found" + }, + "errorBoundary": { + "crashed": "Editor crashed", + "unknownError": "Unknown error" + }, + "binaryPlaceholder": { + "file": "Binary file ({{size}})" + }, + "unsavedChanges": "Unsaved changes", + "empty": { + "selectFile": "Select a file from the tree to edit" + }, + "search": { + "toggleReplace": "Toggle Replace", + "placeholder": "Search" + }, + "shortcuts": { + "title": "Keyboard Shortcuts", + "groups": { + "fileOperations": "File Operations", + "search": "Search", + "navigation": "Navigation", + "editing": "Editing", + "markdown": "Markdown", + "general": "General" + }, + "actions": { + "quickOpen": "Quick Open", + "save": "Save", + "saveAll": "Save All", + "closeTab": "Close Tab", + "findInFile": "Find in File", + "searchInFiles": "Search in Files", + "goToLine": "Go to Line", + "nextTab": "Next Tab", + "previousTab": "Previous Tab", + "cycleTabs": "Cycle Tabs", + "toggleSidebar": "Toggle Sidebar", + "undo": "Undo", + "redo": "Redo", + "selectNextMatch": "Select Next Match", + "toggleComment": "Toggle Comment", + "splitPreview": "Split Preview", + "fullPreview": "Full Preview", + "closeEditor": "Close Editor" + } + }, + "toolbar": { + "enableWordWrap": "Enable word wrap", + "disableWordWrap": "Disable word wrap", + "closeSplitPreview": "Close split preview", + "closePreview": "Close preview" + } + }, + "launch": { + "actions": { + "createSchedule": "Create Schedule", + "creating": "Creating...", + "goToDashboard": "Go to Dashboard", + "launchTeam": "Launch team", + "launching": "Launching...", + "relaunchTeam": "Relaunch team", + "relaunching": "Relaunching...", + "saveChanges": "Save Changes", + "saving": "Saving..." + }, + "billing": { + "prefix": "Starting June 15, 2026, Anthropic bills", + "readArticle": "Read Anthropic article", + "suffix": "and Agent SDK usage from the monthly Agent SDK credit, separate from interactive Claude Code limits. The credit resets each billing cycle and unused credit does not roll over." + }, + "conflict": { + "description": "Running two teams in the same directory is risky - they may conflict editing the same files. Consider using a different directory or a git worktree for isolation.", + "title": "Another team \"{{team}}\" is already running for this working directory", + "workingDirectory": "Working directory:" + }, + "description": { + "createSchedule": "Schedule automatic Claude task execution", + "createScheduleForTeam": "Schedule automatic runs for team \"{{team}}\"", + "editSchedule": "Editing schedule for team \"{{team}}\"", + "launchPrefix": "Start team", + "launchSuffix": "via local Claude CLI.", + "relaunchPrefix": "Stop the current run for", + "relaunchSuffix": "and start it again via local Claude CLI." + }, + "prepare": { + "action": { + "launch": "launch", + "relaunch": "relaunch" + }, + "blocked": "Runtime environment is not available - {{action}} is blocked", + "checkingProviders": "Checking selected providers...", + "failed": "Failed to prepare selected providers", + "preflight": "Pre-flight check to catch errors before {{action}}", + "preparingEnvironment": "Preparing environment...", + "ready": "All selected providers are ready.", + "readyWithNotes": "All selected providers are ready, with notes.", + "unsupportedPreload": "Current preload version does not support team:prepareProvisioning. Restart the dev app.", + "selectWorkingDirectory": "Select a working directory to validate the launch environment.", + "someProvidersNeedAttention": "Some selected providers need attention." + }, + "prompt": { + "label": "Prompt", + "oneShotPrefix": "This prompt will be passed to", + "oneShotSuffix": "for one-shot execution", + "saved": "Saved", + "schedulePlaceholder": "Instructions for Claude to execute on schedule...", + "teamLeadOptional": "Prompt for team lead (optional)", + "teamLeadPlaceholder": "Instructions for team lead..." + }, + "providerChanged": "Provider changed from {{from}} to {{to}}. The previous lead session will not be resumed, and the lead will start with fresh context so the new runtime is applied correctly.", + "relaunchFreshSession": "Team relaunch starts a fresh lead session. Durable team state, task board, and member configuration are rehydrated into the launch prompt.", + "relaunchWarning": { + "description": "Saving these settings will stop the current team process, persist the updated roster, and launch the team again with the new runtime.", + "title": "Relaunch will restart the current team run" + }, + "schedule": { + "labelOptional": "Label (optional)", + "labelPlaceholder": "e.g., Daily code review, Nightly tests...", + "maxBudgetUsd": "Max budget (USD)", + "maxTurns": "Max turns", + "noLimit": "No limit", + "noMatches": "No teams match your search.", + "noTeams": "No teams available. Create a team first.", + "searchTeams": "Search teams...", + "selectTeam": "Select a team...", + "team": "Team", + "title": "Schedule" + }, + "title": { + "createSchedule": "Create Schedule", + "editSchedule": "Edit Schedule", + "launch": "Launch Team", + "relaunch": "Relaunch Team" + }, + "errors": { + "loadProjectsFailed": "Failed to load projects", + "saveScheduleFailed": "Failed to save schedule", + "relaunchFailed": "Failed to relaunch team", + "launchFailed": "Failed to launch team" + }, + "validation": { + "openCodeLeadModelRequired": "OpenCode lead requires a selected model.", + "openCodeTeammateRequired": "OpenCode lead requires at least one OpenCode teammate.", + "selectWorkingDirectory": "Select working directory (cwd)", + "fixMemberNames": "Fix member names before launch", + "memberNamesUnique": "Member names must be unique before launch" + }, + "optionalSettings": { + "relaunchTitle": "Relaunch settings", + "title": "Optional launch settings", + "relaunchDescription": "Review the roster and lead runtime before restarting the team.", + "description": "Keep the launch flow focused on the project path and only expand this when you want extra control." + } + }, + "list": { + "actions": { + "copyTeam": "Copy team", + "createTeam": "Create Team", + "deleteForever": "Delete forever", + "deletePermanently": "Delete permanently", + "deleteTeam": "Delete team", + "launching": "Launching...", + "launchTeam": "Launch team", + "relaunchTeam": "Relaunch team", + "restore": "Restore", + "restoreTeam": "Restore team", + "retry": "Retry", + "stopTeam": "Stop team", + "stopping": "Stopping..." + }, + "electronOnly": { + "description": "In browser mode, access to local `~/.claude/teams` directories is not available.", + "title": "Teams is only available in Electron mode" + }, + "empty": { + "description": "Create a team here to get started. It will show up in the list automatically.", + "localOnly": "Team creation is only available in local Electron mode.", + "title": "No teams found" + }, + "filter": { + "clearAll": "Clear all", + "label": "Filter teams", + "projectPriority": "Project priority", + "status": "Status" + }, + "loadFailed": "Failed to load teams", + "loading": "Loading teams...", + "localOnly": "Only available in local Electron mode.", + "membersCount": "Members: {{count}}", + "membersCount_few": "Members: {{count}}", + "membersCount_many": "Members: {{count}}", + "membersCount_one": "Member: {{count}}", + "membersCount_other": "Members: {{count}}", + "noDescription": "No description", + "noMatches": "No teams matching current filters", + "partial": { + "pending": "Last launch is still reconciling.", + "skipped": "Last launch has skipped teammates.", + "skippedWithCount": "Last launch skipped {{count}}/{{expected}} teammate.", + "skippedWithCount_few": "Last launch skipped {{count}}/{{expected}} teammates.", + "skippedWithCount_many": "Last launch skipped {{count}}/{{expected}} teammates.", + "skippedWithCount_one": "Last launch skipped {{count}}/{{expected}} teammate.", + "skippedWithCount_other": "Last launch skipped {{count}}/{{expected}} teammates.", + "stopped": "Last launch stopped before all teammates joined.", + "stoppedWithCount": "Last launch stopped before {{count}}/{{expected}} teammate joined.", + "stoppedWithCount_few": "Last launch stopped before {{count}}/{{expected}} teammates joined.", + "stoppedWithCount_many": "Last launch stopped before {{count}}/{{expected}} teammates joined.", + "stoppedWithCount_one": "Last launch stopped before {{count}}/{{expected}} teammate joined.", + "stoppedWithCount_other": "Last launch stopped before {{count}}/{{expected}} teammates joined." + }, + "searchPlaceholder": "Search teams...", + "sections": { + "otherTeams": "Other teams", + "projectTeams": "Teams for {{project}}", + "selectedProject": "selected project" + }, + "solo": "Solo", + "status": { + "active": "Active", + "deleted": "Deleted", + "launching": "Launching...", + "offline": "Offline", + "partialFailure": "Launch failed partway", + "partialPending": "Bootstrap pending", + "partialSkipped": "Launch skipped member", + "running": "Running" + }, + "title": "Select Team", + "trash": "Trash ({{count}})", + "trash_few": "Trash ({{count}})", + "trash_many": "Trash ({{count}})", + "trash_one": "Trash ({{count}})", + "trash_other": "Trash ({{count}})", + "deleteDraft": { + "title": "Delete draft", + "message": "Delete draft team \"{{teamName}}\"? This cannot be undone.", + "confirmLabel": "Delete", + "cancelLabel": "Cancel" + }, + "moveToTrash": { + "title": "Move to trash", + "message": "Move team \"{{teamName}}\" to trash? You can restore it later.", + "confirmLabel": "Move to trash", + "cancelLabel": "Cancel" + }, + "deleteForever": { + "title": "Delete permanently", + "message": "Delete team \"{{teamName}}\" permanently? All data will be lost.", + "confirmLabel": "Delete forever", + "cancelLabel": "Cancel" + } + }, + "messageComposer": { + "crossTeam": { + "hint": "Tip: Cross-team messages go to the target team lead. If you want the reply to come back to your team lead instead of you, say that explicitly in the message." + }, + "attachments": { + "attachFiles": "Attach files (paste or drag & drop)", + "unavailable": "Attachments are unavailable", + "disabledHint": "File attachments are supported for the online team lead and online OpenCode teammates. Remove attachments or switch recipient.", + "restrictions": { + "crossTeam": "File attachments are not supported for cross-team messages", + "teamOffline": "Team must be online to attach files", + "unsupportedRecipient": "Files can be sent to the team lead or OpenCode teammates", + "openCodeOffline": "Team must be online to attach files for OpenCode teammates", + "sending": "Wait for current message to finish sending before adding files", + "maximumReached": "Maximum attachments reached", + "leadOnly": "Files can only be sent to the team lead" + } + }, + "slash": { + "restrictions": { + "attachments": "Slash commands require a live team lead and cannot be sent with attachments", + "crossTeam": "Slash commands can only be run on the current team lead", + "notLead": "Slash commands can only be sent to the team lead", + "leadOffline": "Slash commands require the team lead to be online" + } + }, + "status": { + "reusedCrossTeamRequest": "Reused recent cross-team request", + "teamOffline": "Team offline" + }, + "input": { + "charsLeft": "{{count}} chars left", + "charsLeft_one": "{{count}} char left", + "charsLeft_other": "{{count}} chars left", + "teamLaunchingPlaceholder": "Team is launching... message will be queued for inbox delivery.", + "crossTeamPlaceholder": "Cross-team message to {{team}}...", + "teamFallback": "team", + "placeholder": "Write a message... (Enter to send, Shift+Enter for new line)", + "slashTip": "Tip: You can use \"/\" to run any Claude commands.", + "charsLeft_few": "{{count}} chars left", + "charsLeft_many": "{{count}} chars left" + }, + "teamSelector": { + "thisTeam": "This team", + "current": "current", + "online": "online", + "offline": "offline", + "onlineTitle": "Online", + "offlineTitle": "Offline" + }, + "recipient": { + "select": "Select...", + "searchPlaceholder": "Search...", + "noResults": "No results" + }, + "actions": { + "voiceToText": "Voice to text", + "send": "Send", + "sendingUnavailableLaunching": "Sending unavailable while team is launching" + } + }, + "claudeLogs": { + "filter": { + "ariaLabel": "Filter Claude logs", + "tooltip": "Filter logs", + "sections": { + "stream": "Stream", + "content": "Content" + }, + "kinds": { + "output": "Output", + "thinking": "Thinking", + "tool": "Tool calls" + }, + "actions": { + "reset": "Reset", + "save": "Save" + }, + "streams": { + "stdout": "stdout", + "stderr": "stderr" + } + }, + "rawLineCount": "{{formattedCount}} raw lines", + "rawLineCount_one": "{{formattedCount}} raw line", + "rawLinesCaptured": "{{count}} captured", + "emptyRawLogs": "{{count}}; none are assistant/tool output yet.", + "noLogsYet": "No logs yet.", + "teamNotRunning": "Team is not running.", + "searchPlaceholder": "Search logs...", + "clearSearch": "Clear search", + "newCount": "+{{count}} new", + "loading": "Loading...", + "showMore": "Show more", + "noLogsCaptured": "No logs captured.", + "noMatchingLogs": "No matching logs.", + "rawLineCount_few": "{{formattedCount}} raw lines", + "rawLineCount_many": "{{formattedCount}} raw lines", + "rawLineCount_other": "{{formattedCount}} raw lines", + "openFullscreen": "Open fullscreen logs", + "fullscreen": "Fullscreen", + "viewingFullscreen": "Viewing in fullscreen mode", + "logsTitle": "Logs" + }, + "agentGraph": { + "popover": { + "externalTeam": "External team", + "process": { + "startedBy": "Started by:", + "at": "At:", + "openUrl": "Open URL" + }, + "overflow": { + "hiddenTasks": "Hidden tasks", + "empty": "No hidden tasks available." + }, + "member": { + "lead": "Lead", + "workingOn": "working on", + "recentTools": "Recent tools", + "spawn": { + "waitingToStart": "waiting to start", + "starting": "starting", + "failed": "failed" + }, + "state": { + "active": "active", + "idle": "idle", + "offline": "offline", + "runningTool": "running tool" + }, + "activeTool": { + "running": "Running tool", + "failed": "Tool failed", + "finished": "Tool finished" + }, + "actions": { + "message": "Message", + "profile": "Profile", + "task": "Task" + } + } + }, + "logPreview": { + "logs": "Logs", + "loading": "Loading logs", + "more": "+{{count}} more", + "more_one": "+{{count}} more", + "more_other": "+{{count}} more", + "more_few": "+{{count}} more", + "more_many": "+{{count}} more" + }, + "blockingEdge": { + "title": "Blocking Dependency", + "blocks": "blocks", + "close": "Close", + "blockingHiddenTasks": "Blocking hidden tasks", + "blockedHiddenTasks": "Blocked hidden tasks" + }, + "activityHud": { + "activity": "Activity", + "noRecentActivity": "No recent activity", + "more": "+{{count}} more", + "more_one": "+{{count}} more", + "more_other": "+{{count}} more", + "more_few": "+{{count}} more", + "more_many": "+{{count}} more" + }, + "provisioning": { + "launchDetails": "Launch details", + "launchDetailsDescription": "Detailed team launch progress, live output and CLI logs." + } + }, + "projectPath": { + "label": "Project", + "source": { + "claude": "Found by Claude", + "codex": "Found by Codex", + "mixed": "Found by Claude and Codex" + }, + "deleted": { + "title": "Project folder no longer exists", + "label": "Deleted" + }, + "mode": { + "projectList": "From project list", + "customPath": "Custom path" + }, + "loadingProjects": "Loading projects...", + "selectProject": "Select a project...", + "searchPlaceholder": "Search project by name or path", + "empty": "Nothing found", + "selectFromList": "Select a project from the list", + "noProjects": "No projects found, switch to custom path.", + "customWorkingDirectory": "Custom working directory", + "browse": "Browse", + "createAutomatically": "If the directory does not exist, it will be created automatically." + }, + "members": { + "badges": { + "worktree": "worktree" + }, + "runtimeTelemetry": { + "title": "Local runtime load", + "description": "Parent and child processes only. Remote LLM inference is not included.", + "cpu": "CPU", + "memory": "Memory", + "summedRss": "summed RSS", + "sharedHost": "Shared OpenCode host metric. It is not exclusive to this member.", + "processTreeCapped": "Process tree was capped for this sample.", + "rssHint": "RSS can include shared pages, so it is best read as a load signal, not exclusive memory." + }, + "editor": { + "title": "Members", + "addMember": "Add member", + "editAsJson": "Edit as JSON", + "runInSeparateWorktrees": "Run teammates in separate worktrees", + "agentTeamsMcpOnly": "Agent Teams MCP only", + "removedCount": "Removed ({{count}})", + "removedModelLockReason": "Removed members are kept for soft delete history. Restore them to edit settings.", + "memberNamesUnique": "Member names must be unique" + }, + "stats": { + "computing": "Computing stats...", + "empty": "No stats available", + "lines": "Lines", + "linesInfo": "Approximate. Accurate for Edit and Write tools. Bash file writes are estimated from command patterns (heredoc, echo, sed) and may be underreported.", + "files": "Files", + "toolCalls": "Tool Calls", + "tokens": "Tokens", + "toolUsage": "Tool Usage", + "filesTouched": "Files Touched ({{count}})", + "viewAllChanges": "View All Changes", + "showLess": "Show less", + "moreFiles": "+{{count}} more", + "footer": "{{count}} sessions · computed {{computedAgo}}", + "footer_one": "{{count}} session · computed {{computedAgo}}", + "footer_few": "{{count}} sessions · computed {{computedAgo}}", + "footer_many": "{{count}} sessions · computed {{computedAgo}}", + "footer_other": "{{count}} sessions · computed {{computedAgo}}" + }, + "logs": { + "searching": "Searching logs...", + "empty": "No logs found", + "waitingForTaskActivity": "Task is in progress - waiting for session activity (auto-refreshing)...", + "noTaskActivity": "No session activity for this task yet", + "noMemberActivity": "This member has no recorded session activity yet", + "leadSessionTooltip": "Full team lead session logs - useful for global orchestration context, not specific to this agent", + "memberSessionTooltip": "Full persistent teammate session logs - useful when work runs in a root member session instead of a subagent file", + "startedAt": "started {{time}}", + "active": "active", + "showDetails": "Show details", + "hideDetails": "Hide details", + "loadingDetails": "Loading details...", + "failedToLoadDetails": "Failed to load details" + }, + "detail": { + "relaunchOpenCode": "Relaunch OpenCode", + "restart": "Restart", + "legacyLogsFallback": "Legacy Logs Fallback", + "copyDiagnostics": "Copy diagnostics", + "pid": "PID {{pid}}", + "removedAt": "Removed {{date}}", + "failedToRestartMember": "Failed to restart member", + "sendMessage": "Send Message", + "assignTask": "Assign Task", + "remove": "Remove" + }, + "list": { + "loading": "Loading team members", + "unavailable": "Member roster unavailable", + "unavailableDescription": "{{count}} teammates are known from team metadata, but roster details are missing.", + "unavailableDescription_one": "{{count}} teammate is known from team metadata, but roster details are missing.", + "soloLeadOnly": "Solo team - lead only", + "removedCount": "Removed ({{count}})", + "unavailableDescription_few": "{{count}} teammates are known from team metadata, but roster details are missing.", + "unavailableDescription_many": "{{count}} teammates are known from team metadata, but roster details are missing.", + "unavailableDescription_other": "{{count}} teammates are known from team metadata, but roster details are missing." + }, + "executionLog": { + "empty": "Nothing to display", + "emptyUserMessage": "{{time}} - (empty)", + "agentInstructions": "Agent instructions", + "memberTurn": "{{member}} turn", + "agentTurn": "Agent turn", + "turn": "turn" + }, + "recentMessages": { + "latest": "Latest messages", + "latestForMember": "Latest messages - {{member}}", + "loadMore": "Load more", + "expand": "Expand", + "collapse": "Collapse" + }, + "leadModel": { + "defaultModel": "Default", + "providerModelAria": "{{provider}} provider, {{model}}", + "leadShort": "lead", + "teamLead": "Team Lead", + "syncWithTeammates": "Sync model with teammates", + "anthropicTeamWide": "Anthropic team-wide", + "runtimeInheritance": "Lead runtime applies to teammates unless they set their own provider or model.", + "anthropicContextLimit": "The 200K context limit is team-wide for Anthropic runtimes in this launch, including custom Anthropic teammates." + }, + "runtimeLogs": { + "autoRefresh": "Auto-refresh", + "wrapLines": "Wrap lines", + "loadingTail": "Loading process log tail...", + "empty": "No process log file captured for this member yet." + }, + "tasks": { + "empty": "No tasks assigned to this member" + }, + "messages": { + "loadOlder": "Load older messages", + "filters": { + "all": "All", + "messages": "Messages", + "comments": "Comments" + }, + "empty": { + "loading": "Loading activity...", + "noComments": "No comments for this member", + "noLoadedMessages": "No loaded messages for this member yet", + "noMessages": "No messages with this member", + "noLoadedActivity": "No loaded activity for this member yet", + "noActivity": "No activity with this member" + } + }, + "actions": { + "openProfile": "Open profile", + "editRole": "Edit role", + "sendMessage": "Send message", + "assignTask": "Assign task" + }, + "roleSelect": { + "customRolePlaceholder": "Enter custom role..." + } + }, + "schedule": { + "count": "{{count}} schedules", + "count_one": "{{count}} schedule", + "count_other": "{{count}} schedules", + "nextRun": "Next: {{next}}", + "actions": { + "runNow": "Run now", + "edit": "Edit", + "pause": "Pause", + "resume": "Resume", + "delete": "Delete", + "addSchedule": "Add Schedule" + }, + "runHistory": { + "loading": "Loading run history...", + "empty": "No runs yet" + }, + "count_few": "{{count}} schedules", + "count_many": "{{count}} schedules", + "runLog": { + "title": "Run Log", + "exitCode": "exit {{code}}", + "retryCount": "retry {{count}}/{{max}}", + "stillRunning": "Task is still running...", + "loadingLogs": "Loading logs...", + "errors": "Errors", + "close": "Close" + }, + "cron": { + "expression": "Cron expression", + "highFrequencyWarning": "High frequency schedule (less than 5 min interval)", + "nextRuns": "Next runs:", + "timezone": "Timezone", + "selectTimezone": "Select timezone", + "warmUpTime": "Warm-up time", + "warmUpDescription": "Prepares selected providers before scheduled execution", + "errors": { + "enterExpression": "Enter a cron expression", + "invalidExpression": "Invalid cron expression" + }, + "presets": { + "everyHour": "Every hour", + "everySixHours": "Every 6 hours", + "dailyAtNine": "Daily at 9am", + "weekdaysAtNine": "Weekdays at 9am", + "mondayAtNine": "Monday at 9am", + "everyThirtyMinutes": "Every 30 min" + }, + "warmUpOptions": { + "none": "No warm-up", + "fiveMinutes": "5 min", + "tenMinutes": "10 min", + "fifteenMinutes": "15 min", + "thirtyMinutes": "30 min" + } + }, + "empty": { + "title": "No schedules yet", + "description": "Create a schedule to run Claude tasks automatically on a cron schedule." + }, + "title": "Schedules", + "status": { + "active": "Active", + "paused": "Paused", + "disabled": "Disabled" + }, + "runStatus": { + "pending": "Pending", + "warmingUp": "Warming up", + "warm": "Warm", + "running": "Running", + "completed": "Completed", + "failed": "Failed", + "interrupted": "Interrupted", + "cancelled": "Cancelled" + } + }, + "openCodeContextConfigHint": { + "summary": "OpenCode local models can use an OpenCode context budget instead of prompt-only limits.", + "description": "Add matching limits to the OpenCode config for the provider and model used by this teammate. This helps OpenCode compact and prune before local models overflow their context window.", + "replacePrefix": "Replace", + "and": "and", + "replaceSuffix": "with the provider and model IDs from your OpenCode setup. Prompt instructions like", + "promptInstructionsSuffix": "are weaker because the request is assembled before the model reads them.", + "providerLimits": "Provider limits", + "compactionConfig": "Compaction config" + }, + "sessions": { + "noProjectPath": "No project path linked", + "provisioningHint": "Sessions will appear after team provisioning", + "projectNotFound": "Project not found", + "loading": "Loading sessions...", + "empty": "No sessions found", + "showAllSessions": "Show for all sessions", + "lead": "lead", + "removeFilter": "Remove filter", + "filterBySession": "Filter by this session", + "openSession": "Open session", + "title": "Sessions" + }, + "provisioning": { + "pid": "PID {{pid}}", + "cancel": "Cancel", + "moreWarningsHidden": "{{count}} more warnings hidden", + "diagnostics": "Diagnostics", + "liveOutput": "Live output", + "diagnosticsCopied": "Diagnostics copied", + "copyDiagnostics": "Copy diagnostics", + "copied": "Copied", + "noOutput": "No output captured yet.", + "cliLogs": "CLI logs", + "steps": { + "starting": "Starting", + "configuring": "Team setup", + "assembling": "Members joining", + "finalizing": "Finalizing" + }, + "providerStatus": { + "status": { + "checking": "checking...", + "ready": "OK", + "notes": "OK (notes)", + "failed": "ERR", + "pending": "waiting" + }, + "detailSummary": { + "cliBinaryMissing": "CLI binary missing", + "openCodeRuntimeMissing": "OpenCode runtime missing", + "openCodeWindowsAccessBlocked": "OpenCode Windows access blocked", + "openCodeNoOutput": "OpenCode runtime check returned no output", + "openCodeMcpUnreachable": "OpenCode app MCP unreachable", + "workingDirectoryMissing": "Working directory missing", + "cliBinaryCouldNotStart": "CLI binary could not be started", + "cliPreflightIncomplete": "CLI preflight did not complete", + "authenticationRequired": "Authentication required", + "runtimeProviderNotConfigured": "Runtime provider is not configured", + "cliPreflightFailed": "CLI preflight failed", + "selectedModelCompatible": "Selected model compatible", + "selectedModelCompatibilityPending": "Selected model compatibility pending", + "selectedModelAvailable": "Selected model available", + "selectedModelVerified": "Selected model verified", + "selectedModelUnavailable": "Selected model unavailable", + "selectedModelTimedOut": "Selected model verification timed out", + "selectedModelCheckFailed": "Selected model check failed", + "selectedModelDeferred": "Selected model verification deferred", + "selectedModelPingNotConfirmed": "Selected model ping not confirmed", + "readyWithNotes": "Ready with notes", + "needsAttention": "Needs attention" + }, + "modelChecksSummary": "Selected model checks - {{details}}", + "modelParts": { + "unavailable": "{{count}} model unavailable", + "unavailable_one": "{{count}} model unavailable", + "unavailable_other": "{{count}} models unavailable", + "checkFailed": "{{count}} model check failed", + "checkFailed_one": "{{count}} model check failed", + "checkFailed_other": "{{count}} models check failed", + "timedOut": "{{count}} model timed out", + "timedOut_one": "{{count}} model timed out", + "timedOut_other": "{{count}} models timed out", + "deferred": "{{count}} verification deferred", + "deferred_one": "{{count}} verification deferred", + "deferred_other": "{{count}} verification deferred", + "pingNotConfirmed": "{{count}} ping not confirmed", + "pingNotConfirmed_one": "{{count}} ping not confirmed", + "pingNotConfirmed_other": "{{count}} ping not confirmed", + "compatibilityPending": "{{count}} compatible, deep verification pending", + "compatibilityPending_one": "{{count}} compatible, deep verification pending", + "compatibilityPending_other": "{{count}} compatible, deep verification pending", + "compatible": "{{count}} compatible", + "compatible_one": "{{count}} compatible", + "compatible_other": "{{count}} compatible", + "checking": "{{count}} checking", + "checking_one": "{{count}} checking", + "checking_other": "{{count}} checking", + "available": "{{count}} available", + "available_one": "{{count}} available", + "available_other": "{{count}} available", + "verified": "{{count}} verified", + "verified_one": "{{count}} verified", + "verified_other": "{{count}} verified", + "unavailable_few": "{{count}} models unavailable", + "unavailable_many": "{{count}} models unavailable", + "checkFailed_few": "{{count}} models check failed", + "checkFailed_many": "{{count}} models check failed", + "timedOut_few": "{{count}} models timed out", + "timedOut_many": "{{count}} models timed out", + "deferred_few": "{{count}} verification deferred", + "deferred_many": "{{count}} verification deferred", + "pingNotConfirmed_few": "{{count}} ping not confirmed", + "pingNotConfirmed_many": "{{count}} ping not confirmed", + "compatibilityPending_few": "{{count}} compatible, deep verification pending", + "compatibilityPending_many": "{{count}} compatible, deep verification pending", + "compatible_few": "{{count}} compatible", + "compatible_many": "{{count}} compatible", + "checking_few": "{{count}} checking", + "checking_many": "{{count}} checking", + "available_few": "{{count}} available", + "available_many": "{{count}} available", + "verified_few": "{{count}} verified", + "verified_many": "{{count}} verified" + }, + "openProviderSettings": "Open {{provider}} settings", + "copied": "Copied", + "copyDiagnostics": "Copy diagnostics", + "deepVerificationPending": "Deep verification is still running. OpenCode free models may take around 20 seconds.", + "progress": { + "checkingSelectedProviders": "Checking selected providers in parallel...", + "checkingProvider": "Checking {{provider}} provider...", + "checkingProviders": "Checking {{providers}} providers..." + }, + "failureHints": { + "openCodeAccessDenied": "Fix folder permissions or move the project to a user-writable folder. Running as administrator is only a temporary workaround.", + "openCodeBridgeNoOutput": "Restart the app and OpenCode runtime, then retry. If it repeats, copy diagnostics.", + "workingDirectoryMissing": "Choose an existing working directory, then reopen this dialog.", + "authenticationRequired": "Authenticate the required provider in Claude CLI, then reopen this dialog.", + "runtimeProviderNotConfigured": "Configure the selected provider runtime, then reopen this dialog.", + "openCodeRuntimeMissing": "Install or retry OpenCode runtime from the provider status card, then reopen this dialog.", + "openCodeAppMcpUnreachable": "Retry launch to refresh the OpenCode app MCP bridge. If it repeats, restart the app and OpenCode runtime.", + "cliBinaryMissing": "Make sure the local Claude CLI binary exists and can be started, then reopen this dialog.", + "default": "Resolve the issue above, then reopen this dialog." + } + }, + "presentation": { + "awaitingPermission": "{{count}} teammate awaiting permission approval", + "nameListWithMore": "{{names}}, +{{count}} more", + "waitingForOpenCode": "Waiting for OpenCode: {{names}}", + "bootstrapStalled": "Bootstrap stalled: {{names}}", + "bootstrapStalledWithOpenCodeWait": "{{stalled}}; Waiting for OpenCode: {{names}}", + "namedPendingDiagnostic": "{{label}}: {{names}}", + "countPendingDiagnostic": "{{count}} {{label}}", + "pendingLabels": { + "bootstrapStalled": "Bootstrap stalled", + "shellOnly": "Shell-only", + "waitingForBootstrap": "Waiting for bootstrap", + "bootstrapUnconfirmed": "Bootstrap unconfirmed", + "awaitingPermission": "Awaiting permission", + "waitingForRuntime": "Waiting for runtime", + "shellOnlyLower": "shell-only", + "waitingForBootstrapLower": "waiting for bootstrap", + "bootstrapUnconfirmedLower": "bootstrap unconfirmed", + "awaitingPermissionLower": "awaiting permission", + "waitingForRuntimeLower": "waiting for runtime" + }, + "failed": { + "memberFailedToStart": "{{name}} failed to start", + "teammatesFailedToStart": "{{count}} teammates failed to start", + "teammatesFailedRatio": "{{count}}/{{total}} teammates failed to start" + }, + "skipped": { + "memberSkipped": "{{name}} skipped for this launch", + "memberSkippedWithReason": "{{name}} skipped for this launch - {{reason}}", + "memberSkippedCompact": "{{name}} skipped", + "teammatesSkipped": "{{count}} teammates skipped", + "teammatesSkippedList": "Skipped teammates: {{list}}", + "teammatesSkippedRatio": "{{count}}/{{total}} teammates skipped for this launch" + }, + "joining": { + "teammatesStillJoining": "{{count}} teammates still joining", + "teammatesConfirmedRatio": "{{count}}/{{total}} teammates confirmed" + }, + "ready": { + "leadOnline": "Lead online", + "allTeammatesJoined": "All {{count}} teammates joined", + "teamProvisionedLeadOnline": "Team provisioned - lead online", + "teamProvisionedAllJoined": "Team provisioned - all {{count}} teammates joined", + "teamProvisionedStillJoining": "Team provisioned - teammates are still joining", + "launchFinishedWithErrors": "Launch finished with errors - {{count}}/{{total}} teammates failed to start", + "launchContinuedSkipped": "Launch continued - {{count}}/{{total}} teammates skipped", + "teamLaunchedLeadOnline": "Team launched - lead online", + "teamLaunchedAllJoined": "Team launched - all {{count}} teammates joined" + }, + "panel": { + "launchFailed": "Launch failed", + "launchDetails": "Launch details", + "launchFinishedWithErrors": "Launch finished with errors", + "launchContinuedSkipped": "Launch continued with skipped teammates", + "coreTeamReady": "Core team ready", + "finishingLaunch": "Finishing launch", + "teamLaunched": "Team launched", + "launchingTeam": "Launching team" + } + } + }, + "liveRuntimeStatus": { + "title": "Live runtime status", + "description": "Display-only heartbeat and launch state. Process controls remain below.", + "source": "source: {{source}}", + "lane": "{{lane}} lane", + "diagnosticOnly": "Diagnostic only", + "updated": "updated {{value}}", + "states": { + "running": "Running", + "starting": "Starting", + "waiting": "Waiting", + "degraded": "Needs attention", + "stopped": "Stopped", + "unknown": "Unknown" + } + }, + "taskLogs": { + "exact": { + "title": "Exact Task Logs", + "loading": "Loading exact task logs...", + "description": "Exact transcript slices rendered with the same execution-log components used in Logs.", + "emptyTitle": "No exact task logs yet", + "emptyDescription": "Exact transcript bundles will appear here when explicit task-linked transcript metadata is available.", + "summaryOnly": "summary only" + }, + "executionSessions": { + "title": "Execution Sessions", + "online": "Online", + "updating": "Updating...", + "description": "Legacy session-centric transcript browsing and previews." + }, + "stream": { + "title": "Task Log Stream" + } + }, + "kanban": { + "taskCard": { + "cancelTask": "Cancel task {{taskId}}", + "cancel": "Cancel", + "moveBackToTodoConfirm": "Move this task back to TODO and notify the team?", + "confirm": "Confirm", + "keep": "Keep", + "changesNeedAttention": "Changes need attention", + "changes": "Changes", + "deleteTask": "Delete task", + "taskLogsActive": "Task logs active", + "newTaskLogsArriving": "New task logs arriving", + "awaitingUser": "Awaiting user", + "awaitingLead": "Awaiting lead", + "blockedBy": "Blocked by", + "blocks": "Blocks", + "start": "Start", + "complete": "Complete", + "approve": "Approve", + "requestReview": "Request review", + "manualReview": "Manual review", + "requestChanges": "Request changes" + }, + "filter": { + "title": "Filter tasks", + "session": "Session", + "allSessions": "All sessions", + "teammate": "Teammate", + "unassigned": "(unassigned)", + "column": "Column", + "clearAll": "Clear all" + }, + "board": { + "addTask": "Add task", + "noTasks": "No tasks", + "showMore": "Show {{count}} more", + "hiddenCount": "{{count}} hidden", + "trash": "Trash", + "gridView": "Grid view", + "columnsView": "Columns view" + }, + "trash": { + "title": "Trash", + "empty": "No deleted tasks", + "subject": "Subject", + "owner": "Owner", + "deleted": "Deleted", + "unassigned": "Unassigned", + "restoreTask": "Restore task", + "restore": "Restore", + "close": "Close" + }, + "sort": { + "title": "Sort tasks", + "sortBy": "Sort by", + "reset": "Reset", + "options": { + "updatedAt": { + "label": "Last updated", + "description": "Recently updated first" + }, + "createdAt": { + "label": "Created", + "description": "Newest first" + }, + "owner": { + "label": "Owner", + "description": "Alphabetically by assignee" + }, + "manual": { + "label": "Manual", + "description": "Drag-and-drop order" + } + } + }, + "search": { + "clearSearch": "Clear search", + "tasks": "Tasks", + "createdAgo": "created {{time}}", + "updatedAgo": "updated {{time}}", + "placeholder": "Search tasks... (#id or text)" + }, + "grid": { + "addTask": "Add task", + "noTasks": "No tasks" + }, + "title": "Kanban", + "columns": { + "todo": "TODO", + "inProgress": "IN PROGRESS", + "review": "REVIEW", + "done": "DONE", + "approved": "APPROVED" + } + }, + "worktreeGitReadiness": { + "checking": "Checking Git repository status for teammate worktrees...", + "ready": "Git worktrees are ready.", + "readyOnBranch": "Git worktrees are ready on branch {{branch}}.", + "needsSetup": "Worktree isolation needs Git setup", + "initialCommitNotice": "The initial commit action stages and commits all current files with message", + "initializeRepository": "Initialize Git repository", + "createInitialCommit": "Create initial commit", + "initialCommitMessage": "chore: initial commit" + }, + "toolApproval": { + "settings": "Settings", + "autoAllowAllTools": "Auto-allow all tools", + "autoAllowFileEdits": "Auto-allow file edits (Edit, Write, NotebookEdit)", + "autoAllowSafeCommands": "Auto-allow safe commands (git, pnpm, npm, ls...)", + "onTimeout": "On timeout:", + "after": "after", + "secondsShort": "sec", + "timeoutActions": { + "wait": "Wait forever", + "allow": "Allow", + "deny": "Deny" + }, + "submit": "Submit", + "allow": "Allow", + "deny": "Deny", + "allowAll": "Allow all", + "pendingCount": "{{count}} pending", + "autoActionIn": "Auto-{{action}} in {{time}}", + "diff": { + "previewChanges": "Preview changes", + "readingFile": "Reading file...", + "binaryFile": "Binary file - cannot preview", + "truncated": "File truncated at 2MB - diff may be incomplete", + "newFile": "New file" + } + }, + "memberWorkSync": { + "details": { + "title": "Member work sync", + "actionableItems": "Actionable items", + "fingerprint": "Fingerprint", + "report": "Report", + "none": "none", + "shadowWouldNudge": "Shadow would nudge", + "yes": "yes", + "no": "no", + "moreActionableItems": "{{count}} more actionable item(s)", + "diagnostics": "Diagnostics: {{diagnostics}}" + }, + "title": "Member work sync", + "loadingDiagnostics": "Loading member work sync diagnostics.", + "diagnosticsUnavailable": "Member work sync diagnostics are unavailable." + }, + "advancedCli": { + "title": "Advanced", + "useWorktree": "Use worktree", + "recent": "Recent", + "commandPreview": "Command preview", + "customArguments": "Custom arguments", + "validate": "Validate", + "validation": { + "allFlagsValid": "All flags valid", + "unknownFlags": "Unknown: {{flags}}", + "protectedFlags": "Protected: {{flags}}", + "failed": "Validation failed" + }, + "placeholders": { + "worktreeName": "worktree-name" + } + }, + "processes": { + "ago": "{{time}} ago", + "stoppedAgo": "stopped {{time}} ago", + "running": "Running", + "stopped": "Stopped", + "stopProcess": "Stop process (SIGTERM)", + "kill": "Kill", + "openInBrowser": "Open in browser", + "open": "Open", + "pid": "PID{{pid}}", + "title": "CLI Processes" + }, + "taskActivity": { + "loadingDetails": "Loading activity details...", + "contextUnavailable": "Detailed transcript context is no longer available for this activity.", + "loading": "Loading task activity...", + "lowSignalOnly": "No key task activity was found yet. Low-level execution details are available below in Task Log Stream.", + "empty": "No explicit task activity was found in the available transcripts yet. Older or heuristic session logs may still be available below in Execution Sessions.", + "title": "Task Activity", + "description": "Key explicit runtime activity linked to this task from transcript metadata." + }, + "sendMessage": { + "title": "Send Message", + "description": "Send a direct message to a team member.", + "recipientLabel": "Recipient", + "selectMemberPlaceholder": "Select member...", + "messageLabel": "Message", + "placeholder": "Write your message... (Enter to send)", + "send": "Send", + "sending": "Sending...", + "charsLeft": "{{count}} chars left", + "saved": "Saved", + "attachments": { + "teamOnlineRequired": "Team must be online to attach files", + "recipientUnsupported": "Files can be sent to the team lead or OpenCode teammates", + "openCodeOnlineRequired": "Team must be online to attach files for OpenCode teammates", + "disabledHint": "File attachments are supported for the online team lead and online OpenCode teammates. Remove attachments or switch recipient.", + "attachFiles": "Attach files (paste or drag & drop)", + "unavailable": "Attachments are unavailable" + }, + "quote": { + "remove": "Remove quote", + "replyingTo": "Replying to" + } + }, + "taskComments": { + "cancelReply": "Cancel reply", + "replyingTo": "Replying to", + "placeholder": "Add a comment... (Enter to send)", + "attachFile": "Attach file (or paste)", + "voiceToText": "Voice to text", + "comment": "Comment", + "charsLeft": "{{count}} chars left", + "saved": "Saved", + "awaitingReplyFrom": "Awaiting reply from", + "or": "or" + }, + "taskAttachments": { + "dropImageHere": "Drop image here", + "attachImage": "Attach image", + "pasteOrDragDrop": "or paste / drag-drop", + "fromOriginalMessage": "From original message", + "dropFilesHere": "Drop files here", + "loading": "Loading attachments..." + }, + "permissions": { + "autoApproveAllTools": "Auto-approve all tools", + "autonomousModeDescription": "Autonomous mode: team tools execute without confirmation. Be cautious with untrusted code.", + "manualModeDescription": "Manual mode: you'll approve or deny each tool call in real time." + }, + "memberLogStream": { + "tabs": { + "execution": "Execution", + "process": "Process" + }, + "filters": { + "all": "All" + }, + "logs": { + "title": "Logs", + "loading": "Loading member log stream...", + "emptyTitle": "No log stream entries were found for this member yet.", + "emptyDescription": "Member-scoped transcript or runtime logs will appear here when available." + } + }, + "reviewDialog": { + "placeholder": "Describe what needs to change... (Enter to submit)", + "submit": "Submit", + "charsLeft": "{{count}} chars left", + "saved": "Saved", + "title": "Request Changes" + }, + "dialogs": { + "actions": { + "openDashboard": "Open Dashboard", + "openTeam": "Open team", + "cancel": "Cancel" + }, + "membersJson": { + "hide": "Hide JSON" + }, + "optional": { + "badge": "Optional" + } + }, + "runningTeams": { + "title": "Running Teams" + }, + "layout": { + "maxPanesReached": "Maximum of {{count}} panes reached" + }, + "codexReconnect": { + "description": "Your Codex session appears stale. Reconnect to continue.", + "useCode": "Use code" + }, + "effortLevel": { + "label": "Effort level (optional)", + "maxDescription": "Max gives the model the most reasoning time for difficult tasks." + }, + "contextLimit": { + "limitTo200k": "Limit context to 200K tokens", + "always200k": "(always 200K for this model)", + "tooltipContent": "Keeps launches within a 200K-token context window when supported.", + "tooltipTitle": "Context limit" + }, + "roleSelect": { + "noRole": "No role", + "customRole": "Custom role...", + "searchPlaceholder": "Search roles...", + "empty": "No roles found.", + "reservedRole": "This role is reserved" + } +} diff --git a/src/features/localization/renderer/locales/ru/common.json b/src/features/localization/renderer/locales/ru/common.json new file mode 100644 index 00000000..74068192 --- /dev/null +++ b/src/features/localization/renderer/locales/ru/common.json @@ -0,0 +1,900 @@ +{ + "actions": { + "cancel": "Отмена", + "close": "Закрыть", + "copied": "Скопировано", + "copyUrl": "Копировать URL", + "open": "Открыть", + "reveal": "Показать", + "retry": "Повторить", + "save": "Сохранить", + "showLess": "Свернуть", + "showMore": "Показать больше", + "refresh": "Обновить", + "reset": "Сбросить", + "copyToClipboard": "Скопировать в буфер", + "moreActions": "Ещё действия", + "closeDialog": "Закрыть диалог", + "goToDashboard": "На дашборд", + "or": "или", + "hide": "Скрыть", + "resetSelection": "Сбросить выбор" + }, + "code": { + "line": "строка {{line}}", + "lines": "строки {{from}}-{{to}}", + "moreLines": "({{count}} строк ещё...)", + "moreLines_few": "({{count}} строки ещё...)", + "moreLines_many": "({{count}} строк ещё...)", + "moreLines_one": "({{count}} строка ещё...)", + "moreLines_other": "({{count}} строки ещё...)", + "code": "Код", + "preview": "Предпросмотр", + "markdownPreview": "Предпросмотр Markdown", + "linesParenthesized": "(строки {{from}}-{{to}})", + "mermaidSyntaxError": "Синтаксическая ошибка Mermaid" + }, + "contextBadge": { + "badge": "Контекст", + "breakdown": { + "text": "Текст", + "thinking": "Thinking" + }, + "detailsAria": "Детали инъекции контекста", + "sectionSummary": "{{title}} ({{count}}) ~{{tokens}} токенов", + "sectionSummary_few": "{{title}} ({{count}}) ~{{tokens}} токенов", + "sectionSummary_many": "{{title}} ({{count}}) ~{{tokens}} токенов", + "sectionSummary_one": "{{title}} ({{count}}) ~{{tokens}} токенов", + "sectionSummary_other": "{{title}} ({{count}}) ~{{tokens}} токенов", + "sections": { + "claudeMdFiles": "Файлы CLAUDE.md", + "mentionedFiles": "Упомянутые файлы", + "taskCoordination": "Координация задач", + "thinkingText": "Thinking + Text", + "toolOutputs": "Выводы инструментов", + "userMessages": "Сообщения пользователя" + }, + "title": "Новый контекст, добавленный в этом ходе", + "tokenCount": "~{{tokens}} токенов", + "totalNewTokens": "Всего новых токенов", + "turn": "Ход {{turn}}" + }, + "locales": { + "emptyMessage": "Язык не найден.", + "names": { + "en": "Английский", + "ru": "Русский", + "system": "Системный" + }, + "searchPlaceholder": "Поиск языка...", + "selectPlaceholder": "Выберите язык интерфейса...", + "systemWithResolved": "Системный - {{locale}}" + }, + "members": { + "emptyMessage": "Участники не найдены.", + "searchPlaceholder": "Поиск участников...", + "unassigned": "Не назначено", + "teammateFallback": "участник" + }, + "providerRuntime": { + "codex": { + "install": { + "checking": "Проверка", + "downloading": "Загрузка", + "installCli": "Установить Codex CLI", + "installing": "Установка", + "retryInstall": "Повторить установку" + } + } + }, + "search": { + "noMatchingSuggestions": "Подходящих вариантов нет", + "searching": "Поиск...", + "searchingFiles": "Поиск файлов...", + "findInConversation": "Найти в разговоре...", + "resultCount": "{{current}} из {{total}}", + "resultCountCapped": "{{current}} из {{total}}+", + "noResults": "Нет результатов", + "previousResultShortcut": "Предыдущий результат (Shift+Enter)", + "nextResultShortcut": "Следующий результат (Enter)", + "closeShortcut": "Закрыть (Esc)", + "nothingFound": "Ничего не найдено", + "placeholder": "Поиск..." + }, + "schedules": { + "actions": { + "addSchedule": "Добавить расписание", + "clearFilters": "Сбросить фильтры", + "createSchedule": "Создать расписание", + "delete": "Удалить", + "edit": "Редактировать", + "pause": "Приостановить", + "resume": "Возобновить", + "runNow": "Запустить сейчас" + }, + "empty": { + "description": "Создайте расписание в любой команде, чтобы автоматизировать выполнение Claude-задач через cron expressions. Расписания всех команд появятся здесь.", + "noMatches": "Нет расписаний под текущие фильтры", + "title": "Запланированных задач нет" + }, + "filters": { + "allTeams": "Все команды" + }, + "item": { + "loadingRunHistory": "Загрузка истории запусков...", + "nextRun": "Следующий запуск: {{value}}", + "noRunsYet": "Запусков пока нет" + }, + "loading": "Загрузка расписаний...", + "searchPlaceholder": "Поиск расписаний...", + "status": { + "active": "Активные", + "all": "Все", + "disabled": "Отключённые", + "paused": "На паузе" + }, + "title": "Расписания" + }, + "sessions": { + "actions": { + "hide": "Скрыть", + "pin": "Закрепить", + "unhide": "Показать" + }, + "empty": { + "noMatchingSessions": "Подходящих сессий нет", + "noMatchingSessionsDescription": "В этом проекте пока нет подходящих сессий.", + "noMatchingSessionsFiltered": "Попробуйте другой запрос или сбросьте фильтр provider.", + "noSessions": "Сессии не найдены", + "noSessionsDescription": "В этом проекте пока нет сессий", + "selectProject": "Выберите проект, чтобы посмотреть сессии" + }, + "errors": { + "loading": "Ошибка загрузки сессий" + }, + "loadedMatchingMore": "Загружено подходящих сессий: {{count}} - прокрутите вниз, чтобы загрузить ещё.", + "loadedMatchingMore_few": "Загружено подходящих сессий: {{count}} - прокрутите вниз, чтобы загрузить ещё.", + "loadedMatchingMore_many": "Загружено подходящих сессий: {{count}} - прокрутите вниз, чтобы загрузить ещё.", + "loadedMatchingMore_one": "Загружена подходящая сессия: {{count}} - прокрутите вниз, чтобы загрузить ещё.", + "loadedMatchingMore_other": "Загружено подходящих сессий: {{count}} - прокрутите вниз, чтобы загрузить ещё.", + "loadingMore": "Загрузка следующих сессий...", + "pinned": "Закреплённые", + "scrollToLoadMore": "Прокрутите, чтобы загрузить ещё", + "search": { + "clear": "Очистить поиск сессий", + "placeholder": "Поиск сессий..." + }, + "selection": { + "cancel": "Отменить выделение", + "exitMode": "Выйти из режима выделения", + "hideSelected": "Скрыть выбранные сессии", + "pinSelected": "Закрепить выбранные сессии", + "selectSessions": "Выбрать сессии", + "selected": "Выбрано: {{count}}", + "selected_few": "Выбрано: {{count}}", + "selected_many": "Выбрано: {{count}}", + "selected_one": "Выбрана: {{count}}", + "selected_other": "Выбрано: {{count}}", + "unhideSelected": "Показать выбранные сессии" + }, + "sort": { + "byContext": "По контексту", + "byContextTooltip": "Сортировать по потреблению контекста", + "byRecentTooltip": "Сортировать по недавним", + "contextLoadedOnly": "Сортировка по контексту ранжирует только загруженные сессии." + }, + "title": "Сессии", + "visibility": { + "hideHidden": "Скрыть скрытые сессии", + "showHidden": "Показать скрытые сессии" + }, + "worktree": { + "switch": "Переключить worktree" + }, + "failedToLoad": "Не удалось загрузить сессию", + "loading": "Загрузка сессии...", + "filter": { + "title": "Фильтр сессий" + }, + "count": "{{count}} сессий", + "count_one": "{{count}} сессия", + "count_few": "{{count}} сессии", + "count_many": "{{count}} сессий", + "count_other": "{{count}} сессий", + "inProgress": "Сессия выполняется..." + }, + "states": { + "loading": "Загрузка...", + "offline": "Офлайн", + "online": "Онлайн", + "unknown": "Неизвестно", + "error": "Ошибка" + }, + "markdown": { + "imageFallback": "[Изображение: {{label}}]", + "largeContentNotice": "Контент очень большой ({{count}} символов). Показываем raw preview, чтобы интерфейс не завис.", + "largeContentNotice_few": "Контент очень большой ({{count}} символа). Показываем raw preview, чтобы интерфейс не завис.", + "largeContentNotice_many": "Контент очень большой ({{count}} символов). Показываем raw preview, чтобы интерфейс не завис.", + "largeContentNotice_one": "Контент очень большой ({{count}} символ). Показываем raw preview, чтобы интерфейс не завис.", + "largeContentNotice_other": "Контент очень большой ({{count}} символа). Показываем raw preview, чтобы интерфейс не завис.", + "largeContentTitle": "Большой контент показан как raw, чтобы интерфейс не завис", + "raw": "Raw", + "rawPreview": "Raw preview", + "renderMarkdown": "Отрендерить Markdown", + "showAll": "Показать всё", + "showMore": "Показать ещё", + "showRaw": "Показать raw", + "showingChars": "Показано {{shown}} / {{total}} символов" + }, + "terminal": { + "checkOutputForDetails": "Подробности смотрите в выводе терминала выше", + "closingInSeconds": "Закрытие через {{count}} с...", + "closingInSeconds_few": "Закрытие через {{count}} с...", + "closingInSeconds_many": "Закрытие через {{count}} с...", + "closingInSeconds_one": "Закрытие через {{count}} с...", + "closingInSeconds_other": "Закрытие через {{count}} с...", + "completedSuccessfully": "Успешно завершено", + "exitCode": "(код выхода {{code}})", + "processFailed": "Процесс завершился с ошибкой", + "title": "Терминал" + }, + "tokens": { + "accumulatedWithoutDuplication": "Накоплено по всей сессии без дублирования", + "cacheRead": "Cache Read", + "cacheWrite": "Cache Write", + "costUsd": "Стоимость (USD)", + "inputTokens": "Входные токены", + "model": "Модель", + "outputTokens": "Выходные токены", + "phase": "Фаза {{phase}}/{{total}}", + "promptInputShare": "{{percent}}% от prompt input", + "taskCoordination": "Координация задач", + "thinkingText": "Thinking + Text", + "toolOutputs": "Выводы инструментов", + "total": "Итого", + "userMessages": "Сообщения пользователя", + "visibleContext": "Видимый контекст", + "includesClaudeMd": "вкл. CLAUDE.md ×{{count}}", + "claudeMd": "CLAUDE.md", + "mentionedFiles": "@files", + "percentValue": "({{percent}}%)", + "approxTokens": "~{{tokens}} токенов", + "approxTokensParenthesized": "(~{{tokens}})" + }, + "list": { + "actions": { + "copyTeam": "Скопировать команду", + "createTeam": "Создать команду", + "deleteForever": "Удалить навсегда", + "deletePermanently": "Удалить окончательно", + "deleteTeam": "Удалить команду", + "launching": "Запуск...", + "launchTeam": "Запустить команду", + "relaunchTeam": "Перезапустить команду", + "restore": "Восстановить", + "restoreTeam": "Восстановить команду", + "retry": "Повторить", + "stopTeam": "Остановить команду", + "stopping": "Остановка..." + }, + "status": { + "active": "Активно", + "deleted": "Удалено", + "launching": "Запуск...", + "offline": "Offline", + "partialFailure": "Запуск частично не удался", + "partialPending": "Bootstrap ожидает", + "partialSkipped": "Запуск пропустил участника", + "running": "Работает" + }, + "partial": { + "pending": "Последний запуск ещё сверяется.", + "skipped": "В последнем запуске были пропущены teammates.", + "skippedWithCount": "Последний запуск пропустил {{count}}/{{expected}} teammate.", + "skippedWithCount_few": "Последний запуск пропустил {{count}}/{{expected}} teammates.", + "skippedWithCount_many": "Последний запуск пропустил {{count}}/{{expected}} teammates.", + "skippedWithCount_one": "Последний запуск пропустил {{count}}/{{expected}} teammate.", + "skippedWithCount_other": "Последний запуск пропустил {{count}}/{{expected}} teammates.", + "stopped": "Последний запуск остановился до подключения всех teammates.", + "stoppedWithCount": "Последний запуск остановился до подключения {{count}}/{{expected}} teammate.", + "stoppedWithCount_few": "Последний запуск остановился до подключения {{count}}/{{expected}} teammates.", + "stoppedWithCount_many": "Последний запуск остановился до подключения {{count}}/{{expected}} teammates.", + "stoppedWithCount_one": "Последний запуск остановился до подключения {{count}}/{{expected}} teammate.", + "stoppedWithCount_other": "Последний запуск остановился до подключения {{count}}/{{expected}} teammates." + }, + "noDescription": "Нет описания", + "solo": "Solo", + "membersCount": "Участников: {{count}}", + "membersCount_few": "Участников: {{count}}", + "membersCount_many": "Участников: {{count}}", + "membersCount_one": "Участник: {{count}}", + "membersCount_other": "Участников: {{count}}", + "all": "Все", + "moreCount": "+{{count}} ещё", + "moreCount_one": "+{{count}} ещё", + "moreCount_few": "+{{count}} ещё", + "moreCount_many": "+{{count}} ещё", + "moreCount_other": "+{{count}} ещё" + }, + "runtimeProvider": { + "defaults": { + "scopeDescriptionAllProjects": "Default для всех проектов, у которых нет собственного OpenCode override.", + "scopeDescriptionProject": "Override только для выбранного проекта. Уже запущенные команды не изменяются.", + "setAllProjectsDefault": "Задать default для всех проектов", + "setProjectDefault": "Задать default для проекта", + "validationContext": "Validation context", + "projectOverrideContext": "Project override context", + "selectProjectHint": "Выберите проект перед тестированием local models или сохранением defaults.", + "allProjectsHint": "Тесты используют {{project}}. Default применяется, если у проекта нет своего override.", + "projectHint": "Сохранение изменит override только для {{project}}." + } + }, + "sessionContext": { + "header": { + "title": "Контекст", + "closePanel": "Закрыть панель", + "phase": "Фаза:", + "current": "Текущая", + "view": "Вид:", + "category": "Категории", + "bySize": "По размеру" + }, + "metrics": { + "unavailable": "Недоступно", + "contextUsed": "Использовано контекста", + "promptInput": "Вход prompt", + "visibleContext": "Видимый контекст", + "ofContext": "от контекста", + "ofPrompt": "от prompt", + "codexTelemetryUnavailable": "Текущий runtime пока не передаёт prompt-side usage для Codex, поэтому Prompt Input и Context Used остаются недоступными вместо фейкового нуля.", + "sessionCost": "Стоимость сессии:", + "parentPlus": "parent +", + "subagents": "subagents", + "details": "подробности" + }, + "help": { + "contextUsed": { + "title": "Использовано контекста", + "description": "Prompt input плюс output tokens, которые сейчас занимают context window модели." + }, + "promptInput": { + "title": "Вход prompt", + "description": "Tokens, отправленные модели перед генерацией. Для Claude это включает `input_tokens + cache_creation_input_tokens + cache_read_input_tokens`." + }, + "visibleContext": { + "title": "Видимый контекст", + "description": "Инспектируемая часть prompt input: файлы, CLAUDE.md, tool outputs, сообщения пользователя и похожие injections, которые можно оптимизировать напрямую." + }, + "availability": { + "title": "Доступность", + "description": "Если provider runtime пока не отдаёт prompt-side usage, панель показывает metrics как unavailable, а не притворяется, что это ноль." + } + }, + "items": { + "turn": "@Ход {{turn}}", + "tokensApprox": "~{{tokens}} токенов", + "toolsCount": "{{count}} инструментов", + "toolsCount_one": "{{count}} инструмент", + "toolsCount_few": "{{count}} инструмента", + "toolsCount_many": "{{count}} инструментов", + "toolsCount_other": "{{count}} инструментов", + "itemsCount": "{{count}} элементов", + "itemsCount_one": "{{count}} элемент", + "itemsCount_few": "{{count}} элемента", + "itemsCount_many": "{{count}} элементов", + "itemsCount_other": "{{count}} элементов", + "missing": "нет файла", + "thinking": "Размышление", + "text": "Текст" + }, + "empty": "В этой сессии контекстные вставки не обнаружены", + "view": { + "grouped": "Группировано", + "flat": "Плоско" + }, + "claudeMdFiles": "Файлы CLAUDE.md", + "mentionedFiles": "Упомянутые файлы" + }, + "chat": { + "subagent": { + "fallbackName": "Subagent", + "shutdownConfirmed": "Shutdown подтверждён", + "summary": { + "tools": "{{count}} tools", + "tools_one": "{{count}} tool", + "tools_few": "{{count}} tools", + "tools_many": "{{count}} tools", + "tools_other": "{{count}} tools" + }, + "meta": { + "type": "Тип", + "duration": "Длительность", + "model": "Модель", + "id": "ID" + }, + "metrics": { + "contextWindow": "Context Window", + "contextUsage": "Использование контекста", + "mainContext": "Основной контекст", + "totalOutput": "Общий output", + "turns": "({{count}} turns)", + "turns_one": "({{count}} turn)", + "turns_few": "({{count}} turns)", + "turns_many": "({{count}} turns)", + "turns_other": "({{count}} turns)", + "subagentContext": "Контекст subagent", + "phase": "Фаза {{phase}}" + }, + "trace": { + "title": "Execution trace" + } + }, + "user": { + "you": "Вы", + "showMore": "Показать больше", + "showLess": "Показать меньше", + "backgroundTask": "Фоновая задача", + "exitCode": "exit {{code}}", + "imagesAttached": "прикреплено изображений: {{count}}", + "imagesAttached_one": "прикреплено {{count}} изображение", + "imagesAttached_few": "прикреплено {{count}} изображения", + "imagesAttached_many": "прикреплено изображений: {{count}}", + "imagesAttached_other": "прикреплено {{count}} изображения" + }, + "compact": { + "toggle": "Переключить сжатое содержимое", + "contextCompacted": "Контекст сжат", + "freedTokens": "(освобождено {{tokens}})", + "phase": "Фаза {{phase}}", + "conversationCompacted": "Диалог сжат", + "summary": "Предыдущие сообщения были суммаризированы для экономии контекста. Полная история диалога сохранена в файле сессии.", + "compacted": "Сжато" + }, + "executionTrace": { + "empty": "Нет элементов выполнения", + "nested": "Вложенный: {{name}}", + "input": "Ввод" + }, + "items": { + "empty": "Нет элементов для отображения" + }, + "tools": { + "teammateSpawned": "Участник запущен", + "shutdownRequested": "Запрошено завершение ->", + "noResultReceived": "Результат не получен", + "duration": "Длительность: {{duration}}", + "result": "Результат", + "write": { + "createdFile": "Файл создан", + "wroteToFile": "Записано в файл" + }, + "skill": { + "instructions": "Инструкции навыка", + "unknown": "Неизвестный навык" + } + }, + "lastOutput": { + "requestInterrupted": "Запрос прерван пользователем", + "planReadyForApproval": "План готов к подтверждению" + }, + "empty": { + "icon": "💬", + "title": "История разговора пуста", + "description": "В этой сессии пока нет сообщений." + }, + "context": { + "remainingPercent": "(осталось {{percent}}%)", + "count": "Контекст ({{count}})", + "count_one": "Контекст ({{count}})", + "count_few": "Контекст ({{count}})", + "count_many": "Контекст ({{count}})", + "count_other": "Контекст ({{count}})" + }, + "scrollToBottom": "Прокрутить вниз", + "bottom": "Вниз", + "teammateMessage": { + "message": "Сообщение", + "resent": "Отправлено повторно", + "fallback": "Сообщение участника" + }, + "system": { + "label": "Система" + } + }, + "tmuxInstaller": { + "summaryTitle": "tmux не установлен", + "detectedOs": "Обнаруженная ОС: {{os}}", + "runtimePath": "Путь runtime: {{path}}", + "phase": "Фаза: {{phase}}", + "actions": { + "cancel": "Отмена", + "manualGuide": "Manual guide", + "hideSetupSteps": "Скрыть шаги настройки", + "showSetupSteps": "Показать шаги настройки ({{count}})", + "showSetupSteps_one": "Показать шаг настройки ({{count}})", + "showSetupSteps_few": "Показать шаги настройки ({{count}})", + "showSetupSteps_many": "Показать шаги настройки ({{count}})", + "showSetupSteps_other": "Показать шаги настройки ({{count}})", + "recheck": "Проверить снова" + }, + "installerProgress": "Прогресс установки", + "input": { + "placeholder": "Отправить input в installer", + "send": "Отправить input", + "passwordNotice": "Password input отправляется напрямую в terminal installer и не добавляется в log output." + }, + "details": { + "show": "Показать details", + "hide": "Скрыть details" + } + }, + "commandPalette": { + "noRecentActivity": "Нет недавней активности", + "sessionsCount": "{{count}} sessions", + "sessionsCount_one": "{{count}} session", + "sessionsCount_few": "{{count}} sessions", + "sessionsCount_many": "{{count}} sessions", + "sessionsCount_other": "{{count}} sessions", + "mode": { + "searchProjects": "Поиск проектов", + "searchAcrossProjects": "Поиск по всем проектам", + "searchInProject": "Поиск в проекте" + }, + "currentProject": "Текущий проект", + "global": "Global", + "placeholders": { + "projects": "Поиск проектов...", + "conversations": "Поиск conversations..." + }, + "empty": { + "noProjectsForQuery": "Проекты по запросу \"{{query}}\" не найдены", + "noProjects": "Проекты не найдены", + "minChars": "Введите минимум 2 символа для поиска", + "noFastResults": "В недавних sessions нет быстрых результатов по запросу \"{{query}}\"", + "noResults": "Результаты по запросу \"{{query}}\" не найдены" + }, + "footer": { + "projectsCount": "{{count}} проектов", + "projectsCount_one": "{{count}} проект", + "projectsCount_few": "{{count}} проекта", + "projectsCount_many": "{{count}} проектов", + "projectsCount_other": "{{count}} проектов", + "results": "{{count}} {{speed}}результатов", + "results_one": "{{count}} {{speed}}результат", + "results_few": "{{count}} {{speed}}результата", + "results_many": "{{count}} {{speed}}результатов", + "results_other": "{{count}} {{speed}}результатов", + "resultsAcrossProjects": "{{count}} {{speed}}результатов по всем проектам", + "resultsAcrossProjects_one": "{{count}} {{speed}}результат по всем проектам", + "resultsAcrossProjects_few": "{{count}} {{speed}}результата по всем проектам", + "resultsAcrossProjects_many": "{{count}} {{speed}}результатов по всем проектам", + "resultsAcrossProjects_other": "{{count}} {{speed}}результатов по всем проектам", + "fastPrefix": "fast ", + "typeToSearch": "Введите запрос", + "navigate": "навигация", + "select": "выбрать", + "open": "открыть", + "global": "global", + "close": "закрыть", + "upDownKey": "↑↓", + "escapeKey": "esc" + } + }, + "tasksPanel": { + "title": "Задачи", + "searchPlaceholder": "Поиск задач...", + "pinned": "Закреплённые", + "groupByLabel": "Группировать:", + "groupByAria": "Группировка", + "groupModes": { + "none": "Нет", + "project": "Проект", + "time": "Время" + }, + "showArchived": "Показать архивные", + "hideArchived": "Скрыть архивные", + "empty": { + "noMatchingTasks": "Нет подходящих задач", + "noTasks": "Задачи не найдены" + }, + "teamLabel": "Команда: {{team}}", + "showMore": "Показать ещё", + "showLess": "Показать меньше", + "deleteConfirm": { + "title": "Удалить задачу", + "message": "Переместить задачу #{{taskId}} в корзину?", + "confirmLabel": "Удалить", + "cancelLabel": "Отмена" + }, + "deleteFailed": { + "title": "Не удалось удалить задачу", + "fallbackMessage": "Произошла непредвиденная ошибка", + "confirmLabel": "OK" + }, + "sort": { + "byTime": "По времени", + "byUnread": "По непрочитанным", + "byProject": "По проекту", + "byTeam": "По команде" + } + }, + "toolViewer": { + "input": "Ввод", + "replaceAll": "(заменить все)", + "noInputRecorded": "Для этого вызова инструмента ввод не записан.", + "agent": { + "action": "действие", + "teammate": "участник", + "team": "команда", + "runtime": "runtime", + "type": "тип", + "startupInstructionsHidden": "Стартовые инструкции скрыты в интерфейсе." + } + }, + "taskContextMenu": { + "unpin": "Открепить", + "pin": "Закрепить", + "rename": "Переименовать", + "markUnread": "Пометить непрочитанной", + "unarchive": "Разархивировать", + "archive": "Архивировать", + "deleteTask": "Удалить задачу" + }, + "updateDialog": { + "closeDialog": "Закрыть диалог", + "updateAvailable": "Доступно обновление", + "updateReady": "Обновление готово", + "noReleaseNotes": "Описание релиза недоступно.", + "viewOnGitHub": "Открыть на GitHub", + "later": "Позже", + "restartNow": "Перезапустить сейчас", + "download": "Скачать" + }, + "errorBoundary": { + "title": "Что-то пошло не так", + "description": "В приложении произошла непредвиденная ошибка. Можно попробовать перезагрузить страницу или сбросить состояние ошибки.", + "componentStack": "Стек компонентов", + "tryAgain": "Попробовать снова", + "copied": "Скопировано", + "copyErrorDetails": "Скопировать детали ошибки", + "reportBugOnGitHub": "Сообщить об ошибке на GitHub", + "reloadApp": "Перезагрузить приложение", + "diagnosticsNotice": "GitHub-отчёты и скопированная диагностика включают сообщение об ошибке, stack trace, версию приложения, активную вкладку, выбранную команду, контекст задачи и сведения окружения." + }, + "runtimeBackendSelector": { + "label": "Runtime backend", + "resolved": "Определено: {{backend}}", + "current": "Текущий", + "recommended": "Рекомендуется", + "unavailable": "Недоступно", + "cannotSelectYet": "Этот backend пока нельзя выбрать.", + "auto": "Авто", + "autoCurrently": "Авто (сейчас: {{backend}})", + "audience": { + "internal": "Внутренний" + }, + "states": { + "locked": "Заблокировано", + "disabled": "Отключено", + "authRequired": "Нужен вход", + "runtimeMissing": "Runtime отсутствует", + "degraded": "Есть проблемы", + "unavailable": "Недоступно" + } + }, + "providerModelBadges": { + "checking": "Проверка", + "unavailable": "Недоступно", + "checkFailed": "Проверка не удалась", + "free": "Бесплатно", + "freeTooltip": "Передано метаданными OpenCode. Доступность и лимиты могут измениться." + }, + "taskFilters": { + "status": "Статус", + "clearAll": "Очистить всё", + "selectAll": "Выбрать всё", + "team": "Команда", + "allTeams": "Все команды", + "searchTeams": "Искать команды...", + "noTeamsFound": "Команды не найдены", + "project": "Проект", + "allProjects": "Все проекты", + "searchProjects": "Искать проекты...", + "noProjects": "Проектов нет", + "comments": "Комментарии", + "apply": "Применить", + "read": { + "all": "Все", + "unread": "Непрочитанные", + "read": "Прочитанные" + }, + "statusOptions": { + "todo": "TODO", + "inProgress": "В РАБОТЕ", + "needsFix": "НУЖНЫ ПРАВКИ", + "done": "ГОТОВО", + "review": "РЕВЬЮ", + "approved": "ОДОБРЕНО" + } + }, + "sessionItem": { + "totalContext": "Всего контекста: {{tokens}} токенов", + "context": "Контекст: {{tokens}}", + "phase": "Фаза {{phase}}:", + "compactedTo": "(сжато до {{tokens}})" + }, + "notifications": { + "row": { + "team": "команда", + "subagent": "subagent", + "markAsRead": "Пометить прочитанным", + "delete": "Удалить", + "viewInSession": "Открыть в сессии" + }, + "title": "Уведомления", + "loading": "Загрузка уведомлений...", + "actions": { + "markFilteredAsRead": "Пометить отфильтрованные прочитанными", + "markAllAsRead": "Пометить все прочитанными", + "markFilteredRead": "Пометить фильтр", + "markAllRead": "Пометить все", + "clearFilteredNotifications": "Очистить отфильтрованные уведомления", + "clearAllNotifications": "Очистить все уведомления", + "clickToConfirm": "Нажмите для подтверждения", + "clearFiltered": "Очистить фильтр", + "clearAll": "Очистить все" + }, + "counts": { + "unreadInFilter": "{{count}} непрочитанных в фильтре", + "unreadInFilter_one": "{{count}} непрочитанное в фильтре", + "unreadInFilter_few": "{{count}} непрочитанных в фильтре", + "unreadInFilter_many": "{{count}} непрочитанных в фильтре", + "unreadInFilter_other": "{{count}} непрочитанных в фильтре", + "inFilter": "{{count}} в фильтре", + "inFilter_one": "{{count}} в фильтре", + "inFilter_few": "{{count}} в фильтре", + "inFilter_many": "{{count}} в фильтре", + "inFilter_other": "{{count}} в фильтре", + "unread": "{{count}} непрочитанных", + "unread_one": "{{count}} непрочитанное", + "unread_few": "{{count}} непрочитанных", + "unread_many": "{{count}} непрочитанных", + "unread_other": "{{count}} непрочитанных", + "total": "{{count}} всего", + "total_one": "{{count}} всего", + "total_few": "{{count}} всего", + "total_many": "{{count}} всего", + "total_other": "{{count}} всего" + }, + "filters": { + "other": "Другое" + }, + "empty": { + "noMatching": "Подходящих уведомлений нет", + "noNotifications": "Уведомлений нет", + "tryDifferentFilter": "Попробуйте другой фильтр", + "allCaughtUp": "Все уведомления разобраны" + } + }, + "updates": { + "restartToUpdate": "Перезапустить для обновления", + "updateApp": "Обновить приложение", + "downloadedRestartTooltip": "Обновление загружено, перезапустите приложение для применения", + "newVersionAvailable": "Доступна новая версия", + "updatingApp": "Обновление приложения", + "updateReady": "Обновление готово", + "restartNow": "Перезапустить сейчас" + }, + "layout": { + "github": "GitHub", + "discord": "Discord", + "expandSidebar": "Развернуть боковую панель", + "collapseSidebarShortcut": "Свернуть боковую панель ({{shortcut}})", + "sidebarView": "Вид боковой панели", + "resizeSidebar": "Изменить ширину боковой панели", + "closeTab": "Закрыть вкладку", + "openedFromSearch": "Открыто из поиска", + "pinnedSession": "Закрепленная сессия", + "jumpToSection": "Перейти к разделу", + "newTab": "Новая вкладка", + "newTabDashboard": "Новая вкладка (дашборд)", + "refreshSession": "Обновить сессию", + "refreshSessionWithShortcut": "Обновить сессию ({{shortcut}})", + "loadingTab": "Загрузка вкладки", + "menu": { + "teams": "Команды", + "settings": "Настройки", + "extensions": "Расширения", + "search": "Поиск", + "schedules": "Расписания", + "docs": "Документация", + "exportMarkdown": "Экспорт в Markdown", + "exportJson": "Экспорт в JSON", + "exportPlainText": "Экспорт в обычный текст", + "analyzeSession": "Анализировать сессию" + }, + "tabMenu": { + "closeTabs": "Закрыть {{count}} вкладок", + "closeTabs_one": "Закрыть {{count}} вкладку", + "closeTabs_few": "Закрыть {{count}} вкладки", + "closeTabs_many": "Закрыть {{count}} вкладок", + "closeTabs_other": "Закрыть {{count}} вкладки", + "closeTab": "Закрыть вкладку", + "closeOtherTabs": "Закрыть остальные вкладки", + "splitRight": "Разделить вправо", + "splitLeft": "Разделить влево", + "pinToSidebar": "Закрепить в боковой панели", + "unpinFromSidebar": "Открепить от боковой панели", + "hideFromSidebar": "Скрыть из боковой панели", + "unhideFromSidebar": "Вернуть в боковую панель", + "closeAllTabs": "Закрыть все вкладки" + }, + "sections": { + "team": "Команда", + "sessions": "Сессии", + "kanban": "Канбан", + "claudeLogs": "Логи Claude", + "messages": "Сообщения" + } + }, + "editorFormatting": { + "bold": "Жирный", + "italic": "Курсив", + "strike": "Зачеркнутый", + "code": "Код" + }, + "diff": { + "changed": "Изменено", + "noChangesDetected": "Изменений нет" + }, + "codexLogin": { + "copyLoginLinkAndCode": "Скопировать ссылку входа ChatGPT и код", + "copyLoginLink": "Скопировать ссылку входа ChatGPT", + "copyFailed": "Не удалось скопировать", + "copyLinkAndCode": "Скопировать ссылку + код", + "copyLink": "Скопировать ссылку", + "enterCodeOnLoginPage": "Введите этот код на странице входа ChatGPT" + }, + "window": { + "minimize": "Свернуть", + "maximize": "Развернуть", + "restore": "Восстановить" + }, + "context": { + "local": "Локально", + "switchingTo": "Переключение на {{workspace}}", + "loadingWorkspace": "Загрузка workspace", + "switchWorkspace": "Сменить рабочую область" + }, + "repositories": { + "noneAvailable": "Репозитории недоступны", + "remove": "Удалить репозиторий" + }, + "export": { + "session": "Экспортировать сессию", + "sessionTitle": "Экспорт сессии" + }, + "brand": { + "claude": "Claude" + }, + "sessionReport": { + "noSessionData": "Данные сессии недоступны", + "title": "Отчёт по сессии" + }, + "sessionFilters": { + "project": { + "selectProject": "Выберите проект" + } + }, + "tasks": { + "date": { + "updatedPrefix": "обн.", + "updatedYesterday": "обн. вчера", + "yesterday": "Вчера" + }, + "reviewState": { + "needsFix": "Нужны правки" + }, + "unassigned": "не назначено" + } +} diff --git a/src/features/localization/renderer/locales/ru/dashboard.json b/src/features/localization/renderer/locales/ru/dashboard.json new file mode 100644 index 00000000..2ff0e122 --- /dev/null +++ b/src/features/localization/renderer/locales/ru/dashboard.json @@ -0,0 +1,197 @@ +{ + "cliStatus": { + "actions": { + "alreadyLoggedIn": "Уже вошли?", + "becomeSponsor": "Стать спонсором", + "cancel": "Отмена", + "checkNow": "Проверить сейчас", + "checkUpdates": "Проверить обновления", + "checking": "Проверка...", + "connect": "Подключить", + "extensions": "Расширения", + "login": "Войти", + "manage": "Управлять", + "manageProviders": "Управлять провайдерами", + "plan": "План", + "recheck": "Проверить снова", + "recheckProvider": "Проверить {{provider}} снова", + "retry": "Повторить", + "updateTo": "Обновить до v{{version}}", + "useCode": "Использовать код" + }, + "atlas": { + "alt": "Atlas Cloud", + "description": "Atlas Cloud - full-modal AI inference platform, который даёт разработчикам единый AI API для доступа к video generation, image generation и LLM API. Вместо нескольких интеграций с вендорами вы подключаетесь один раз и получаете единый доступ к 300+ отобранным моделям во всех модальностях. Посмотрите новую coding plan promotion Atlas Cloud для более бюджетного API-доступа.", + "openCodeProvider": "OpenCode provider", + "plan": "Coding plan Atlas Cloud", + "sponsor": "Спонсор" + }, + "errors": { + "checkStatusFailed": "Не удалось проверить статус CLI", + "installationFailed": "Установка не удалась", + "refreshFailed": "Не удалось проверить обновления. Проверьте сетевое подключение и повторите попытку.", + "runtimeUpdatedRefreshFailed": "Runtime обновлён, но не удалось обновить статус провайдера." + }, + "hints": { + "backgroundStatus": "Статус {{runtime}} будет проверен в фоне.", + "codexApiKeyFallback": "{{hint}} API key fallback доступен, если переключить режим аутентификации.", + "codexAutoApiKey": "{{hint}} Auto продолжит использовать API key, пока ChatGPT не подключён.", + "codexFinishLogin": "Завершите вход ChatGPT в браузере. Если потребуется, введите показанный код.", + "codexNoActiveLogin": "Лимиты появятся только после того, как Codex CLI увидит активный ChatGPT account. Сейчас он сообщает, что активного входа ChatGPT нет.", + "codexNoActiveManagedSession": "Лимиты появятся только после того, как Codex CLI увидит активный ChatGPT account. Локальные данные account есть, но активная managed-сессия сейчас не выбрана.", + "codexReconnectNeeded": "Лимиты появятся только после того, как Codex обновит текущую выбранную ChatGPT-сессию. Сейчас локальную сессию нужно переподключить.", + "firstCheckSlow": "Первая проверка может занять до 30 секунд", + "loginRequiredForTeams": "Просмотр сессий и проектов работает без входа. Вход нужен только для запуска agent teams.", + "troubleshootTitle": "Если вы уверены, что уже вошли, попробуйте:" + }, + "installer": { + "checkingLatest": "Проверка последней версии...", + "downloading": "Загрузка {{runtime}}...", + "installing": "Установка {{runtime}}...", + "success": "{{runtime}} успешно установлен, версия v{{version}}", + "verifying": "Проверка checksum..." + }, + "labels": { + "apiKeyRequired": "Требуется API key", + "comingSoon": "Скоро", + "collapseProviderDetails": "Свернуть детали провайдера", + "expandProviderDetails": "Развернуть детали провайдера", + "generateLink": "Создать ссылку", + "loadingRateLimits": "Загрузка лимитов", + "loggedOut": "Провайдер отключён", + "loginAuthFailed": "Аутентификация не удалась", + "loginAuthUpdated": "Аутентификация обновлена", + "loginComplete": "Вход выполнен", + "loginFailed": "Войти не удалось", + "loginTitle": "Вход", + "logoutFailed": "Выход не удался", + "logoutTitle": "Выход", + "notLoggedIn": "Вход не выполнен", + "openLogin": "Открыть вход", + "providerActionRequired": "Требуется действие с провайдером", + "resets": "сброс {{time}}", + "runtimeLoginTitle": "Вход в {{runtime}}" + }, + "loading": { + "aiProviders": "Проверка AI-провайдеров...", + "claudeCli": "Проверка Claude CLI..." + }, + "provider": { + "authenticated": "Аутентифицировано", + "backend": "Backend: {{backend}}", + "checkingAuthentication": "Проверка аутентификации...", + "checkingProviders": "Проверка провайдеров...", + "configuredLocalCount": "{{count}} локальных настроено", + "configuredLocalCount_few": "{{count}} локальных настроено", + "configuredLocalCount_many": "{{count}} локальных настроено", + "configuredLocalCount_one": "{{count}} локальный настроен", + "configuredLocalCount_other": "{{count}} локальных настроено", + "configuredLocalTitle": "Локальные маршруты OpenCode, импортированные из вашей конфигурации OpenCode.", + "connectedCount": "Провайдеры: {{connected}}/{{denominator}} подключено", + "freeModels": "Бесплатные модели", + "freeModelsTitle": "OpenCode включает бесплатные варианты моделей, например Big Pickle, если они доступны в вашей настройке. OpenRouter через OpenCode тоже может показывать бесплатные модели, но не каждая модель OpenCode/OpenRouter бесплатна. Доступность и лимиты могут меняться.", + "loadingModels": "Загрузка моделей...", + "modelsUnavailable": "Модели недоступны для этой сборки runtime", + "runtime": "Runtime: {{runtime}}", + "verifiedCount": "{{count}} проверено", + "verifiedCount_few": "{{count}} проверено", + "verifiedCount_many": "{{count}} проверено", + "verifiedCount_one": "{{count}} проверен", + "verifiedCount_other": "{{count}} проверено", + "verifiedTitle": "Маршруты OpenCode с успешным proof выполнения." + }, + "runtime": { + "configuredHealthCheckFailed": "Настроенный {{runtime}} не прошёл health check запуска.", + "configuredNotFound": "Настроенный {{runtime}} не найден.", + "foundButFailed": "{{runtime}} найден, но не запустился", + "healthCheckFailedDescription": "Приложение нашло настроенный {{runtime}}, но его startup health check не прошёл. Почините или переустановите его, затем повторите.", + "install": "Установить {{runtime}}", + "installRequiredDescription": "{{runtime}} нужен для provisioning команд и управления сессиями. Установите его, чтобы начать.", + "isRequired": "Требуется {{runtime}}", + "reinstall": "Переустановить {{runtime}}" + }, + "runtimeInstall": { + "checking": "Проверка", + "codexTitle": "Установить Codex CLI в данные приложения", + "downloading": "Загрузка", + "downloadingPercent": "Загрузка {{percent}}%", + "install": "Установить", + "installing": "Установка", + "openCodeTitle": "Установить OpenCode runtime в данные приложения", + "retryInstall": "Повторить установку" + }, + "troubleshoot": { + "again": "снова", + "authStatusCommand": "настроенную команду проверки статуса аутентификации CLI", + "checkLoggedIn": "- проверьте, показывает ли она \"Logged in\"", + "click": "Нажмите", + "loginCommand": "команду входа runtime", + "logoutCommand": "команду выхода runtime", + "openTerminal": "Откройте терминал и выполните:", + "reloginPrefix": "Если команда говорит, что вход выполнен, но приложение этого не видит, попробуйте:", + "sameRuntime": "Убедитесь, что CLI в терминале совпадает с runtime, который использует приложение", + "statusCacheHint": "- иногда статус кэшируется на несколько секунд", + "then": "затем" + }, + "warnings": { + "multipleApiKeysMissing": "Один или несколько провайдеров работают в режиме API key, но API key не настроен. Откройте управление провайдерами, чтобы добавить ключи или переключить режим подключения.", + "multipleApiKeysNeedAttention": "Один или несколько провайдеров работают в режиме API key и требуют внимания. Откройте управление провайдерами, чтобы проверить сохранённые ключи или переключить режим подключения.", + "notAuthenticated": "{{runtime}} установлен, но вход не выполнен. Вход нужен для provisioning команд и AI-функций.", + "singleApiKeyMissing": "{{provider}} работает в режиме API key, но API key не настроен. Откройте управление провайдерами, чтобы добавить ключ или переключить режим подключения.", + "singleApiKeyNeedsAttention": "{{provider}} работает в режиме API key, но не подключён. Откройте управление провайдерами, чтобы проверить сохранённый ключ или переключить режим подключения." + } + }, + "recentProjects": { + "selectFolderTitle": "Выбрать папку проекта", + "selectFolder": "Выбрать папку", + "failedToLoad": "Не удалось загрузить проекты", + "retry": "Повторить", + "noProjects": "Проекты не найдены", + "noMatches": "Нет совпадений для \"{{query}}\"", + "noRecentProjects": "Недавние проекты не найдены", + "emptyDescription": "Здесь появится недавняя активность Claude и Codex.", + "loadMore": "Загрузить ещё", + "card": { + "deleted": "Удалён", + "projectFolderMissing": "Папка проекта больше не существует", + "taskCounts": { + "active": "{{count}} активных", + "active_one": "{{count}} активная", + "active_few": "{{count}} активные", + "active_many": "{{count}} активных", + "active_other": "{{count}} активных", + "pending": "{{count}} ожидают", + "pending_one": "{{count}} ожидает", + "pending_few": "{{count}} ожидают", + "pending_many": "{{count}} ожидают", + "pending_other": "{{count}} ожидают", + "done": "{{count}} готово", + "done_one": "{{count}} готова", + "done_few": "{{count}} готово", + "done_many": "{{count}} готово", + "done_other": "{{count}} готово" + } + }, + "title": "Недавние проекты", + "searchResults": "Результаты поиска", + "searchPlaceholder": "Поиск проектов..." + }, + "actions": { + "selectTeam": "Выбрать команду", + "or": "или", + "clearSearch": "Очистить поиск" + }, + "windowsAdmin": { + "title": "Рекомендуется режим администратора Windows", + "description": "Проверки рантайма OpenCode могут завершаться по таймауту, если Agent Teams AI не запущен с повышенными правами. Перезапустите приложение через Run as administrator перед запуском команд OpenCode." + }, + "webPreview": { + "title": "Откройте desktop-приложение для полной функциональности", + "description": "Браузерная версия всё ещё в разработке. Действия с проектами, интеграции и live-статусы здесь могут быть ограничены. Используйте desktop-приложение для надёжного доступа ко всем возможностям." + }, + "updateBanner": { + "newVersionAvailable": "Доступна новая версия", + "restartNow": "Перезапустить сейчас", + "viewDetails": "Подробнее" + } +} diff --git a/src/features/localization/renderer/locales/ru/errors.json b/src/features/localization/renderer/locales/ru/errors.json new file mode 100644 index 00000000..9db1e8e4 --- /dev/null +++ b/src/features/localization/renderer/locales/ru/errors.json @@ -0,0 +1,3 @@ +{ + "fallback": "Что-то пошло не так." +} diff --git a/src/features/localization/renderer/locales/ru/extensions.json b/src/features/localization/renderer/locales/ru/extensions.json new file mode 100644 index 00000000..a3f437d8 --- /dev/null +++ b/src/features/localization/renderer/locales/ru/extensions.json @@ -0,0 +1,684 @@ +{ + "store": { + "actions": { + "addCustom": "Добавить custom", + "openDashboard": "Открыть Dashboard", + "refreshCatalog": "Обновить каталог" + }, + "capabilities": { + "mcp": "MCP: {{status}}", + "plugins": "Plugins: {{status}}", + "skills": "Skills: {{status}}" + }, + "desktopOnly": "Доступно только в desktop app.", + "provider": { + "checkingStatus": "Проверка статуса provider...", + "connected": "Подключён", + "loading": "Загрузка...", + "needsSetup": "Нужна настройка", + "readyToConfigure": "Готов к настройке", + "unsupported": "Не поддерживается" + }, + "runtime": { + "checkingAvailabilityDescription": "Extensions требуют настроенный runtime для управления plugins, MCP servers, skills и provider connections.", + "checkingAvailabilityTitle": "Проверка доступности runtime для extensions", + "failedToStartDescription": "Extensions отключены, пока runtime не пройдёт startup health check. Откройте Dashboard, чтобы исправить или переустановить его.", + "failedToStartTitle": "Настроенный runtime найден, но не запустился", + "multimodelCapabilitiesDescription": "Поддержка providers может отличаться по секциям. Plugins показываются только там, где runtime явно сообщает поддержку.", + "multimodelCapabilitiesTitle": "Возможности multimodel runtime", + "needsSignInDescription": "{{runtime}} найден{{version}}, но установка plugins отключена, пока вы не войдёте через Dashboard.", + "needsSignInTitle": "{{runtime}} требует вход", + "notAvailableDescription": "Extensions отключены, пока runtime не установлен. Откройте Dashboard, установите его и повторите попытку.", + "notAvailableTitle": "Настроенный runtime недоступен", + "readyDescription": "Plugins можно устанавливать с этой страницы{{versionSuffix}}.", + "readyTitle": "{{runtime}} готов", + "requiredForMutations": "Настроенный runtime требуется для установки или удаления extensions. Установите или исправьте его через Dashboard." + }, + "sessionsRestartWarning": "Запущенные сессии не увидят изменения extensions до перезапуска.", + "tabs": { + "apiKeys": { + "description": "Secret keys для online services. Добавьте их здесь, чтобы plugins, servers и integrations могли подключаться и работать.", + "label": "API Keys" + }, + "mcpServers": { + "description": "Подключения к внешним tools и apps. Они позволяют runtime читать данные или выполнять действия за пределами приложения.", + "label": "MCP Servers" + }, + "plugins": { + "description": "Небольшие add-ons для runtime. В multimodel mode сейчас применяются к Anthropic sessions, когда поддерживаются. Более широкая поддержка providers в разработке.", + "label": "Plugins" + }, + "skills": { + "description": "Готовые инструкции для типовых задач. Они помогают runtime стабильнее выполнять повторяемые действия.", + "label": "Skills" + } + }, + "title": "Extensions" + }, + "pluginsPanel": { + "activeFilters": "Активно: {{count}}", + "activeFilters_few": "Активно: {{count}}", + "activeFilters_many": "Активно: {{count}}", + "activeFilters_one": "Активен: {{count}}", + "activeFilters_other": "Активно: {{count}}", + "browseByFit": "Подбор по назначению", + "capabilities": "Возможности", + "categories": "Категории", + "clearAllFilters": "Сбросить все фильтры", + "clearFilters": "Сбросить фильтры", + "counts": { + "capabilities": "Возможностей: {{count}}", + "capabilities_few": "Возможностей: {{count}}", + "capabilities_many": "Возможностей: {{count}}", + "capabilities_one": "Возможность: {{count}}", + "capabilities_other": "Возможностей: {{count}}", + "categories": "Категорий: {{count}}", + "categories_few": "Категории: {{count}}", + "categories_many": "Категорий: {{count}}", + "categories_one": "Категория: {{count}}", + "categories_other": "Категории: {{count}}", + "plugins": "Plugins: {{count}}", + "plugins_few": "Plugins: {{count}}", + "plugins_many": "Plugins: {{count}}", + "plugins_one": "Plugin: {{count}}", + "plugins_other": "Plugins: {{count}}" + }, + "empty": { + "description": "Проверьте позже, когда появятся новые plugins", + "filteredDescription": "Попробуйте изменить поиск или критерии фильтра", + "filteredTitle": "Нет plugins под выбранные фильтры", + "title": "Plugins недоступны" + }, + "filterDescription": "Сужайте каталог по категории, возможностям или статусу установки.", + "installedOnly": "Только установленные", + "providerSupportNotice": "Поддержка plugins сейчас гарантирована только для Anthropic (Claude) sessions. Мы работаем над поддержкой plugins во всех agents.", + "resultsUpdateInstantly": "Результаты обновляются сразу при изменении фильтров.", + "searchPlaceholder": "Поиск plugins...", + "selectedCount": "Выбрано: {{count}}", + "selectedCount_few": "Выбрано: {{count}}", + "selectedCount_many": "Выбрано: {{count}}", + "selectedCount_one": "Выбран: {{count}}", + "selectedCount_other": "Выбрано: {{count}}", + "showing": "Показано {{shown}} из {{total}} plugins", + "sort": { + "category": "Категория", + "nameAsc": "Имя A-Z", + "nameDesc": "Имя Z-A", + "popular": "Популярные" + } + }, + "customMcp": { + "actions": { + "add": "Добавить", + "cancel": "Отмена", + "install": "Установить", + "installing": "Установка..." + }, + "description": "Добавьте server вручную без каталога.", + "errors": { + "installFailed": "Не удалось установить", + "invalidServerName": "Некорректное имя server. Используйте латинские буквы, цифры, дефисы, подчёркивания и точки.", + "npmPackageRequired": "Требуется имя npm package", + "serverNameRequired": "Требуется имя server", + "serverUrlRequired": "Требуется URL server" + }, + "fields": { + "environmentVariables": "Environment variables", + "headers": "Headers", + "npmPackage": "npm package", + "scope": "Scope", + "serverName": "Имя server", + "serverUrl": "URL server", + "transport": "Transport", + "transportType": "Тип transport", + "versionOptional": "Версия (необязательно)" + }, + "title": "Добавить custom MCP server", + "transport": { + "httpSse": "HTTP / SSE", + "stdio": "Stdio (npm)" + }, + "placeholders": { + "headerName": "Header-Name", + "envVarName": "ENV_VAR_NAME", + "serverName": "my-server", + "latest": "latest", + "value": "value", + "serverUrl": "https://api.example.com/mcp" + } + }, + "mcpDetail": { + "auth": { + "remoteMayNeedHeaders": "Remote MCP servers могут требовать custom headers или API keys, даже если registry их не описывает. Если connection после установки не работает, проверьте provider docs.", + "required": "Этот server требует authentication" + }, + "diagnostics": { + "launchTarget": "Launch Target" + }, + "form": { + "autoFilled": "Заполнено автоматически", + "environmentVariables": "Environment variables", + "headers": "Headers", + "scope": "Scope", + "serverName": "Имя server" + }, + "install": { + "httpTransport": "HTTP: {{transport}}", + "manualSetupDescription": "Этот server требует ручной настройки. Проверьте repository для инструкций по установке.", + "manualSetupRequired": "Требуется ручная настройка", + "npmPackage": "npm: {{package}}", + "manage": "Управление установкой", + "install": "Установить server" + }, + "links": { + "glama": "Glama", + "repository": "Repository", + "website": "Website" + }, + "metadata": { + "author": "Автор", + "githubStars": "GitHub Stars", + "hosting": "Hosting", + "installType": "Тип установки", + "license": "Лицензия", + "published": "Опубликовано", + "source": "Источник", + "updated": "Обновлено", + "version": "Версия" + }, + "scope": { + "local": "Local", + "project": "Project" + }, + "tools": { + "title": "Tools ({{count}})", + "title_few": "Tools ({{count}})", + "title_many": "Tools ({{count}})", + "title_one": "Tool ({{count}})", + "title_other": "Tools ({{count}})" + }, + "placeholders": { + "serverName": "my-server" + } + }, + "skillEditor": { + "actions": { + "cancel": "Отмена", + "createSkill": "Создать skill", + "preparing": "Подготовка...", + "reviewAndCreate": "Проверить и создать", + "reviewAndSave": "Проверить и сохранить", + "saveSkill": "Сохранить skill" + }, + "advanced": { + "customDescription": "Этот skill использует собственный markdown-формат, поэтому редактируйте его напрямую здесь.", + "customTitle": "2. Редактор SKILL.md", + "description": "В большинстве случаев это можно пропустить. Открывайте только если нужен прямой контроль над raw markdown-файлом.", + "hide": "Скрыть расширенный редактор", + "resetFromStructuredFields": "Сбросить из структурированных полей", + "show": "Показать расширенный редактор", + "title": "4. Расширенный редактор SKILL.md" + }, + "basics": { + "description": "Дайте skill понятное имя, выберите, кто может его использовать, и где он должен храниться.", + "title": "1. Основное" + }, + "description": { + "create": "Опишите workflow простым языком, проверьте файлы, которые будут созданы, затем сохраните skill.", + "edit": "Обновите этот skill, проверьте итоговые изменения файлов, затем сохраните." + }, + "extraFiles": { + "addedFiles": "Добавленные файлы:", + "assets": "Assets", + "assetsDescription": "Добавляйте screenshots или bundled media только если они помогают объяснить workflow.", + "description": "Добавляйте supporting docs, scripts или assets только если этот skill действительно в них нуждается.", + "lockedForEdits": "Root и folder заблокированы при редактировании", + "optionalDescription": "Добавьте starter files, которые попадут в review и будут записаны вместе с `SKILL.md`.", + "optionalTitle": "Дополнительные файлы", + "references": "References", + "referencesDescription": "Добавьте supporting docs, links или examples, которые runtime сможет использовать.", + "scripts": "Scripts", + "scriptsDescription": "Добавьте helper commands или setup notes. Внимательно проверьте перед публикацией skill.", + "title": "3. Дополнительные файлы" + }, + "fields": { + "compatibility": "Compatibility", + "description": "Описание", + "folderName": "Имя folder", + "folderNameHint": "Мы предлагаем его автоматически из имени skill, чтобы review сразу работал корректно.", + "invocation": "Как его использовать", + "license": "Лицензия", + "name": "Имя skill", + "notes": "Дополнительные notes или guardrails", + "root": "Где хранить", + "scope": "Кто может использовать", + "steps": "Основные шаги", + "whenToUse": "Когда использовать" + }, + "instructions": { + "description": "Эти секции генерируют skill-файл автоматически, поэтому markdown можно не редактировать вручную.", + "locked": "Структурированные поля заблокированы, потому что вы переключились на ручное редактирование `SKILL.md` ниже.", + "title": "2. Инструкции" + }, + "invocation": { + "auto": "Можно использовать автоматически", + "manualOnly": "Только когда вы явно попросите" + }, + "placeholders": { + "description": "С чем помогает этот skill", + "name": "Напишите короткое имя skill", + "notes": "Пример: отметьте отсутствующие тесты, регрессии и рискованные assumptions.", + "steps": "1. Проверьте релевантные файлы.\n2. Сначала объясните главный риск.\n3. Предложите самый безопасный fix.", + "whenToUse": "Пример: используйте это для code review или bug triage.", + "license": "MIT", + "compatibility": "claude-code, cursor" + }, + "review": { + "creating": "Создание skill", + "hint": "Сначала проверьте изменения файлов, затем подтвердите сохранение на следующем шаге.", + "saving": "Сохранение skill" + }, + "root": { + "codexOnly": " - только Codex", + "shared": " - общий" + }, + "scope": { + "project": "Проект: {{project}}", + "projectUnavailable": "Проект недоступен", + "user": "Пользователь" + }, + "title": { + "create": "Создать skill", + "edit": "Редактировать skill" + } + }, + "skillDetail": { + "actions": { + "cancel": "Отмена", + "delete": "Удалить", + "deleteSkill": "Удалить skill", + "deleting": "Удаление...", + "editSkill": "Редактировать skill", + "openFolder": "Открыть папку", + "openSkillFile": "Открыть SKILL.md", + "retry": "Повторить" + }, + "badges": { + "assets": "Assets", + "autoUse": "Auto use", + "hasScripts": "Есть scripts", + "manualUse": "Manual use", + "references": "References", + "storedIn": "Хранится в {{root}}" + }, + "deleteDialog": { + "description": "Удалить этот skill и переместить его в Trash?", + "descriptionWithName": "Удалить \"{{name}}\" и переместить в Trash? При необходимости его можно восстановить из Trash.", + "title": "Удалить skill?" + }, + "descriptionFallback": "Просмотр найденных metadata skill и raw instructions.", + "errors": { + "deleteFailed": "Не удалось удалить skill", + "loadFailed": "Не удалось загрузить этот skill." + }, + "files": { + "advancedDetails": "Расширенные сведения о файле", + "assets": "Assets", + "references": "References", + "scripts": "Scripts", + "storedAt": "Хранится в" + }, + "includes": { + "assets": "assets", + "instructionsOnly": "Только инструкции skill", + "references": "references", + "scripts": "scripts" + }, + "invocation": { + "auto": "Запускается автоматически, когда подходит к задаче.", + "manualOnly": "Запускается только по явному запросу." + }, + "issues": { + "bundledScripts": "Этот skill содержит bundled scripts", + "reviewCarefully": "Внимательно проверьте skill перед использованием" + }, + "loading": "Загрузка сведений о skill...", + "scope": { + "personal": "Ваши личные skills", + "projectOnly": "Только этот проект" + }, + "summary": { + "howUsed": "Как используется", + "included": "Что входит", + "whoCanUse": "Кто может использовать" + }, + "titleFallback": "Сведения о skill" + }, + "skillsPanel": { + "actions": { + "createSkill": "Создать skill", + "import": "Импорт" + }, + "badges": { + "assets": "Assets", + "hasScripts": "Есть scripts", + "needsAttention": "Требует внимания", + "references": "References", + "storedIn": "Хранится в {{root}}" + }, + "configuredRuntime": "настроенный runtime", + "counts": { + "codexOnly": "Codex only: {{count}}", + "codexOnly_few": "Codex only: {{count}}", + "codexOnly_many": "Codex only: {{count}}", + "codexOnly_one": "Codex only: {{count}}", + "codexOnly_other": "Codex only: {{count}}", + "personal": "Личных: {{count}}", + "personal_few": "Личных: {{count}}", + "personal_many": "Личных: {{count}}", + "personal_one": "Личный: {{count}}", + "personal_other": "Личных: {{count}}", + "project": "Проектных: {{count}}", + "project_few": "Проектных: {{count}}", + "project_many": "Проектных: {{count}}", + "project_one": "Проектный: {{count}}", + "project_other": "Проектных: {{count}}", + "shared": "Общих: {{count}}", + "shared_few": "Общих: {{count}}", + "shared_many": "Общих: {{count}}", + "shared_one": "Общий: {{count}}", + "shared_other": "Общих: {{count}}", + "total": "Всего: {{count}}", + "total_few": "Всего: {{count}}", + "total_many": "Всего: {{count}}", + "total_one": "Всего: {{count}}", + "total_other": "Всего: {{count}}" + }, + "empty": { + "noMatches": "Skills под поиск не найдены", + "noMatchesDescription": "Попробуйте другой запрос или переключите фильтры.", + "noSkills": "Skills пока нет", + "noSkillsDescription": "Создайте первый skill для повторяемого workflow или импортируйте уже используемый." + }, + "filters": { + "all": "Все skills", + "codexOnly": "Codex only", + "hasScripts": "Есть scripts", + "needsAttention": "Требует внимания", + "personal": "Личные", + "project": "Проектные", + "shared": "Общие" + }, + "hero": { + "codexAvailable": "Используйте `.codex`, если skill должен оставаться только для Codex.", + "codexUnavailable": "Существующие `.codex` skills можно редактировать здесь, но для новых Codex-only skills нужен включённый Codex runtime.", + "description": "Skills - это переиспользуемые инструкции, которые помогают runtime стабильнее выполнять однотипные задачи.", + "guidance": "Используйте личные skills для привычек, нужных везде. Используйте проектные skills для workflows, которые имеют смысл только внутри одного codebase.", + "personalContext": "Сейчас показаны только ваши личные skills.", + "projectContext": "Показаны skills для {{project}} плюс ваши личные skills.", + "title": "Обучить повторяемой работе" + }, + "invocation": { + "auto": "Запускается автоматически, когда подходит", + "manualOnly": "Запускается только по явному запросу" + }, + "loading": { + "loading": "Загрузка skills...", + "refreshing": "Обновление skills..." + }, + "runtimeAudience": "Shared skills в `.claude`, `.cursor` и `.agents` доступны для {{audience}}. Skills в `.codex` остаются Codex-only, когда поддержка Codex доступна.", + "scope": { + "project": "Этот проект", + "user": "Личный" + }, + "searchPlaceholder": "Поиск по имени skill или назначению...", + "sections": { + "personal": { + "description": "Привычки и инструкции, которые должны быть доступны везде.", + "title": "Личные skills" + }, + "project": { + "description": "Workflows, которые имеют смысл только для этого codebase.", + "title": "Проектные skills" + } + }, + "sort": { + "label": "Сортировать skills", + "name": "Имя", + "recent": "Недавние" + }, + "status": { + "hasScripts": "Содержит scripts, поэтому внимательно проверьте", + "needsAttention": "Требует внимания перед использованием", + "ready": "Готов к использованию" + }, + "success": { + "created": "Skill успешно создан.", + "imported": "Skill успешно импортирован.", + "saved": "Skill успешно сохранён." + } + }, + "pluginDetail": { + "unknown": "Неизвестно", + "metadata": { + "author": "Автор", + "category": "Категория", + "source": "Источник", + "version": "Версия", + "capabilities": "Возможности", + "installs": "Установки" + }, + "scope": { + "label": "Scope:", + "options": { + "user": "User (global)", + "project": "Project (shared)", + "local": "Local (gitignored)" + } + }, + "links": { + "homepage": "Homepage", + "contact": "Контакт" + }, + "readme": { + "loading": "Загрузка README...", + "empty": "README недоступен." + } + }, + "skillImport": { + "title": "Импорт skill", + "description": "Выберите существующую папку skill, проверьте, что будет скопировано, затем импортируйте её в одну из поддерживаемых локаций skills.", + "steps": { + "chooseFolder": { + "title": "1. Выберите папку skill", + "description": "Это должна быть папка, где уже есть файл `SKILL.md`, `Skill.md` или `skill.md`." + }, + "location": { + "title": "2. Выберите, где хранить skill", + "description": "Личные skills работают везде. Проектные skills показываются только для одного codebase." + } + }, + "fields": { + "sourceFolder": "Исходная папка", + "destinationFolderName": "Имя целевой папки", + "audience": "Кто может использовать", + "storage": "Где хранить" + }, + "placeholders": { + "defaultFolderName": "По умолчанию имя исходной папки" + }, + "actions": { + "browse": "Выбрать", + "cancel": "Отмена", + "preparing": "Подготовка...", + "reviewAndImport": "Проверить и импортировать", + "importSkill": "Импортировать skill", + "backToImport": "Назад к импорту" + }, + "scope": { + "user": "User", + "project": "Project: {{project}}", + "projectUnavailable": "Project недоступен" + }, + "rootSuffix": { + "codexOnly": " - Codex only", + "shared": " - Shared" + }, + "reviewHint": "Сначала проверьте скопированные файлы, затем подтвердите импорт на следующем шаге.", + "reviewLabel": "Импорт этого skill", + "errors": { + "missingSkillFile": "Эта папка пока не похожа на skill. Нужен файл SKILL.md, Skill.md или skill.md.", + "symbolicLinks": "В этой папке есть symbolic links. Импортируйте реальные файлы вместо links.", + "tooManyFiles": "В этой папке skill слишком много файлов для одного импорта. Уберите лишние файлы и повторите попытку.", + "tooLarge": "Эта папка skill слишком большая для безопасного импорта. Уменьшите крупные assets и повторите попытку.", + "invalidFolderName": "Выберите более простое имя целевой папки: буквы, цифры, точки, дефисы или подчёркивания.", + "mustBeDirectory": "Выберите папку для импорта, а не отдельный файл.", + "reviewFailed": "Не удалось подготовить review изменений импорта", + "importFailed": "Не удалось импортировать skill" + } + }, + "mcpPanel": { + "sort": { + "nameAsc": "Name A→Z", + "nameDesc": "Name Z→A", + "toolsDesc": "Больше всего tools" + }, + "health": { + "title": "MCP health status", + "checkingViaRuntime": "Проверка установленных MCP servers через {{runtime}} ...", + "lastChecked": "Последняя проверка {{time}}", + "description": "Запустите diagnostics на этой странице, чтобы проверить подключение установленных MCP.", + "checking": "Проверка...", + "checkStatus": "Проверить status" + }, + "diagnostics": { + "title": "Runtime MCP diagnostics", + "serversCount": "{{count}} servers", + "serversCount_one": "{{count}} server", + "serversCount_few": "{{count}} servers", + "serversCount_many": "{{count}} servers", + "serversCount_other": "{{count}} servers", + "waiting": "Ожидание результатов diagnostics...", + "disableReasons": { + "checkingRuntimeStatus": "Проверка runtime status...", + "checkingRuntimeAvailability": "Проверка доступности runtime...", + "runtimeFailedToStart": "Настроенный runtime найден, но не запустился. Откройте Dashboard, чтобы repair или reinstall его.", + "runtimeRequired": "Требуется настроенный runtime. Установите или repair его из Dashboard." + } + }, + "searchPlaceholder": "Поиск MCP servers...", + "runtime": { + "notAvailable": "{{runtime}} недоступен", + "notInstalled": "{{runtime}} не установлен", + "requiredDescription": "MCP health checks требуют {{runtime}}. Перейдите в Dashboard, чтобы установить или repair его." + }, + "empty": { + "searchTitle": "Servers не найдены", + "title": "MCP servers недоступны", + "searchDescription": "Попробуйте другой поисковый запрос", + "description": "Новые servers могут появиться позже" + }, + "loadMore": "Загрузить ещё" + }, + "apiKeys": { + "description": "Безопасно храните API keys для auto-fill при установке MCP servers.", + "storage": { + "osKeychain": "Keys шифруются через {{backend}} и хранятся с ограниченными file permissions (только owner).", + "localEncryption": "OS keychain недоступен - keys шифруются локально через AES-256. Для более сильной защиты установите keyring service (gnome-keyring, kwallet)." + }, + "actions": { + "add": "Добавить API key", + "addFirst": "Добавить первый key", + "edit": "Редактировать" + }, + "empty": { + "title": "API keys не сохранены", + "description": "Добавьте keys для auto-fill environment variables при установке MCP servers." + }, + "form": { + "addTitle": "Добавить API-ключ", + "editTitle": "Редактировать API-ключ", + "addDescription": "Сохраните API-ключ для автозаполнения при установке MCP-серверов.", + "editDescription": "Обновите детали ключа. Значение нужно ввести заново.", + "keychainUnavailable": "OS keychain недоступен - ключи локально шифруются AES-256. Установите gnome-keyring для защиты на уровне ОС.", + "name": "Название", + "namePlaceholder": "например OpenAI Production", + "environmentVariableName": "Имя переменной окружения", + "envVarPlaceholder": "например OPENAI_API_KEY", + "value": "Значение", + "reenterValue": "Введите значение ключа заново", + "valuePlaceholder": "sk-...", + "scope": "Область", + "userScopeLabel": "Пользователь (глобально)", + "projectScopeLabel": "Проект: {{project}}", + "projectUnavailable": "Проект недоступен", + "boundTo": "Привязано к {{path}}", + "cancel": "Отмена", + "saving": "Сохранение...", + "update": "Обновить", + "save": "Сохранить", + "errors": { + "invalidEnvVarFormat": "Используйте буквы, цифры и подчёркивания. Должно начинаться с буквы или подчёркивания.", + "nameRequired": "Название обязательно", + "envVarRequired": "Имя переменной окружения обязательно", + "invalidEnvVar": "Некорректное имя переменной окружения", + "valueRequired": "Значение ключа обязательно", + "projectScopeRequiresProject": "API-ключи уровня проекта требуют активный проект", + "saveFailed": "Не удалось сохранить" + } + } + }, + "skillReview": { + "title": "Проверка изменений навыка", + "description": "{{reviewLabel}} сначала показывает изменения файловой системы. Ничего не будет записано до подтверждения ниже.", + "noPreview": "Предпросмотр недоступен.", + "confirmPromptPrefix": "Проверьте diff ниже, затем нажмите", + "confirmPromptSuffix": "чтобы применить изменения.", + "noChanges": "Изменения файлов пока не обнаружены.", + "binaryBadge": "бинарный", + "binaryPreviewHidden": "Предпросмотр бинарного файла не показывается. Файл будет скопирован как есть.", + "summary": { + "fileChanges": "{{count}} изменений файлов", + "fileChanges_one": "{{count}} изменение файла", + "fileChanges_few": "{{count}} изменения файлов", + "fileChanges_many": "{{count}} изменений файлов", + "fileChanges_other": "{{count}} изменений файлов", + "new": "{{count}} новых", + "updated": "{{count}} обновлено", + "removed": "{{count}} удалено", + "binary": "{{count}} бинарных" + } + }, + "mcpCard": { + "toolsCount": "{{count}} инструментов", + "toolsCount_one": "{{count}} инструмент", + "toolsCount_few": "{{count}} инструмента", + "toolsCount_many": "{{count}} инструментов", + "toolsCount_other": "{{count}} инструментов", + "envCount": "{{count}} env-переменных", + "envCount_one": "{{count}} env-переменная", + "envCount_few": "{{count}} env-переменные", + "envCount_many": "{{count}} env-переменных", + "envCount_other": "{{count}} env-переменных", + "auth": "Авторизация", + "byAuthor": "от {{author}}", + "hosting": { + "remote": "Удаленный", + "local": "Локальный", + "both": "Оба варианта" + }, + "repository": "Репозиторий", + "website": "Сайт" + }, + "installButton": { + "installing": "Установка...", + "removing": "Удаление...", + "done": "Готово", + "retry": "Повторить", + "uninstall": "Удалить", + "install": "Установить" + }, + "pluginCard": { + "official": "Официальный" + } +} diff --git a/src/features/localization/renderer/locales/ru/report.json b/src/features/localization/renderer/locales/ru/report.json new file mode 100644 index 00000000..c5d958d1 --- /dev/null +++ b/src/features/localization/renderer/locales/ru/report.json @@ -0,0 +1,217 @@ +{ + "cost": { + "breakdownTitle": "Разбивка стоимости (за 1M токенов)", + "cacheRead": "Cache Read", + "cacheWrite": "Cache Write", + "cost": "Стоимость", + "input": "Вход", + "noCommits": "коммитов нет", + "noLinesChanged": "изменённых строк нет", + "output": "Выход", + "parent": "Parent: {{cost}}", + "parentCost": "Стоимость parent-сессии", + "perCommit": "На коммит", + "perCommitFormula": "общая стоимость ÷ {{count}} коммит", + "perCommitFormula_few": "общая стоимость ÷ {{count}} коммита", + "perCommitFormula_many": "общая стоимость ÷ {{count}} коммитов", + "perCommitFormula_one": "общая стоимость ÷ {{count}} коммит", + "perCommitFormula_other": "общая стоимость ÷ {{count}} коммита", + "perLineChanged": "На изменённую строку", + "perLineFormula": "общая стоимость ÷ {{count}} строка", + "perLineFormula_few": "общая стоимость ÷ {{count}} строки", + "perLineFormula_many": "общая стоимость ÷ {{count}} строк", + "perLineFormula_one": "общая стоимость ÷ {{count}} строка", + "perLineFormula_other": "общая стоимость ÷ {{count}} строки", + "subagent": "Субагенты: {{cost}}", + "subagentCost": "Стоимость субагентов", + "title": "Анализ стоимости", + "total": "Итого" + }, + "insights": { + "agent": "агент", + "agent_few": "агента", + "agent_many": "агентов", + "agent_one": "агент", + "agent_other": "агента", + "agentTree": "Дерево агентов ({{count}} {{unit}})", + "background": "(в фоне)", + "bashCommands": "Bash-команды", + "outOfScopeFindings": "Находки вне scope ({{count}})", + "questionsAsked": "Заданные вопросы ({{count}})", + "repeated": "Повторные", + "skillsInvoked": "Вызванные skills ({{count}})", + "taskDispatches": "Запуски Task ({{count}})", + "tasksCreated": "Созданные задачи ({{count}})", + "teamMode": "Командный режим", + "teams": "Команды: {{teams}}", + "title": "Инсайты сессии", + "total": "Всего", + "unique": "Уникальные", + "skillsInvoked_few": "Вызванные skills ({{count}})", + "skillsInvoked_many": "Вызванные skills ({{count}})", + "skillsInvoked_one": "Вызванные skills ({{count}})", + "skillsInvoked_other": "Вызванные skills ({{count}})", + "taskDispatches_few": "Запуски Task ({{count}})", + "taskDispatches_many": "Запуски Task ({{count}})", + "taskDispatches_one": "Запуски Task ({{count}})", + "taskDispatches_other": "Запуски Task ({{count}})", + "tasksCreated_few": "Созданные задачи ({{count}})", + "tasksCreated_many": "Созданные задачи ({{count}})", + "tasksCreated_one": "Созданные задачи ({{count}})", + "tasksCreated_other": "Созданные задачи ({{count}})", + "questionsAsked_few": "Заданные вопросы ({{count}})", + "questionsAsked_many": "Заданные вопросы ({{count}})", + "questionsAsked_one": "Заданные вопросы ({{count}})", + "questionsAsked_other": "Заданные вопросы ({{count}})", + "agentTree_few": "Дерево агентов ({{count}} {{unit}})", + "agentTree_many": "Дерево агентов ({{count}} {{unit}})", + "agentTree_one": "Дерево агентов ({{count}} {{unit}})", + "agentTree_other": "Дерево агентов ({{count}} {{unit}})", + "outOfScopeFindings_few": "Находки вне scope ({{count}})", + "outOfScopeFindings_many": "Находки вне scope ({{count}})", + "outOfScopeFindings_one": "Находки вне scope ({{count}})", + "outOfScopeFindings_other": "Находки вне scope ({{count}})", + "keyTakeaways": "Ключевые выводы" + }, + "quality": { + "chars": "симв.", + "corrections": "Исправления", + "failed": "failed", + "fileReadRedundancy": "Повторное чтение файлов", + "firstMessage": "Первое сообщение", + "firstRun": "Первый запуск", + "frictionRate": "Уровень friction", + "lastRun": "Последний запуск", + "messagesBeforeWork": "Сообщений до работы", + "passed": "passed", + "promptQuality": "Качество промпта", + "readsPerUniqueFile": "Чтений на уникальный файл", + "snapshot": "snapshot", + "snapshot_few": "snapshots", + "snapshot_many": "snapshots", + "snapshot_one": "snapshot", + "snapshot_other": "snapshots", + "startupOverhead": "Startup overhead", + "testProgression": "Динамика тестов", + "title": "Сигналы качества", + "tokensBeforeWork": "Токенов до работы", + "totalReads": "Всего чтений", + "uniqueFiles": "Уникальные файлы", + "userMessages": "Сообщения пользователя", + "percentOfTotal": "% от общего" + }, + "tokens": { + "apiCalls": "API-вызовы", + "cacheCreate": "Cache Create", + "cacheEfficiency": "Эффективность cache", + "cacheRead": "Cache Read", + "cacheReadPct": "Cache Read %", + "coldStart": "Cold Start", + "cost": "Стоимость", + "input": "Вход", + "model": "Модель", + "no": "Нет", + "output": "Выход", + "readWriteRatio": "R/W Ratio", + "title": "Использование токенов", + "total": "Итого", + "yes": "Да" + }, + "subagents": { + "title": "Subagents", + "metrics": { + "count": "Количество", + "totalTokens": "Всего tokens", + "totalDuration": "Общая длительность", + "totalCost": "Общая стоимость" + }, + "table": { + "description": "Описание", + "type": "Тип", + "tokens": "Tokens", + "duration": "Длительность", + "cost": "Стоимость" + } + }, + "overview": { + "title": "Обзор", + "yes": "Да", + "no": "Нет", + "metrics": { + "duration": "Длительность", + "messages": "Сообщения", + "contextUsage": "Использование контекста", + "compactions": "Compactions", + "branch": "Branch", + "subagents": "Subagents", + "project": "Проект", + "sessionId": "Session ID" + } + }, + "timeline": { + "title": "Timeline и активность", + "idleAnalysis": "Idle analysis", + "metrics": { + "idleGaps": "Idle gaps", + "totalIdle": "Всего idle", + "activeTime": "Активное время", + "idlePercent": "Idle %" + }, + "modelSwitches": "Смены модели ({{count}})", + "modelSwitches_one": "Смена модели ({{count}})", + "modelSwitches_few": "Смены модели ({{count}})", + "modelSwitches_many": "Смены модели ({{count}})", + "modelSwitches_other": "Смены модели ({{count}})", + "messageNumber": "msg #{{number}}", + "keyEvents": "Ключевые события" + }, + "tools": { + "title": "Использование инструментов", + "summary": "{{formattedCount}} вызовов всего по {{toolCount}} инструментам", + "columns": { + "tool": "Инструмент", + "calls": "Вызовы", + "errors": "Ошибки", + "successPercent": "Успех %", + "health": "Состояние" + } + }, + "git": { + "title": "Git-активность", + "commits": "Коммиты", + "pushes": "Pushes", + "linesAdded": "Строк добавлено", + "linesRemoved": "Строк удалено", + "branchesCreated": "Созданные ветки" + }, + "friction": { + "title": "Сигналы friction", + "rate": "Friction rate: {{rate}}%", + "correctionsCount": "исправлений: {{count}}", + "correctionsCount_one": "{{count}} исправление", + "correctionsCount_few": "исправлений: {{count}}", + "correctionsCount_many": "исправлений: {{count}}", + "correctionsCount_other": "исправлений: {{count}}", + "corrections": "Исправления", + "thrashingSignals": "Сигналы thrashing", + "repeatedBashCommands": "Повторяющиеся Bash-команды", + "reworkedFiles": "Переделанные файлы (3+ правки)" + }, + "errors": { + "title": "Ошибки", + "permissionDenied": "Доступ запрещён", + "messageIndex": "сообщ. #{{index}}", + "input": "Ввод", + "error": "Ошибка", + "count": "ошибок: {{count}}", + "count_one": "{{count}} ошибка", + "count_few": "ошибок: {{count}}", + "count_many": "ошибок: {{count}}", + "count_other": "ошибок: {{count}}", + "permissionDenialCount": "permission denial: {{count}}", + "permissionDenialCount_one": "{{count}} permission denial", + "permissionDenialCount_few": "permission denial: {{count}}", + "permissionDenialCount_many": "permission denial: {{count}}", + "permissionDenialCount_other": "permission denial: {{count}}" + } +} diff --git a/src/features/localization/renderer/locales/ru/settings.json b/src/features/localization/renderer/locales/ru/settings.json new file mode 100644 index 00000000..6e346b9f --- /dev/null +++ b/src/features/localization/renderer/locales/ru/settings.json @@ -0,0 +1,983 @@ +{ + "tabs": { + "advanced": { + "description": "Расширенные параметры: экспорт и импорт конфигурации, сброс настроек и редактирование raw-конфигурации.", + "label": "Расширенные" + }, + "general": { + "description": "Основные настройки приложения: тема, язык, плотность интерфейса и поведение при запуске.", + "label": "Общие" + }, + "infoAriaLabel": "Что такое «{{label}}»?", + "notifications": { + "description": "Настройте, когда и как получать уведомления об активности агентов, завершении задач и ошибках.", + "label": "Уведомления" + } + }, + "view": { + "description": "Управление настройками приложения", + "loading": "Загрузка настроек...", + "title": "Настройки" + }, + "runtimeProvider": { + "actions": { + "cancel": "Отмена", + "test": "Тест" + }, + "defaults": { + "allProjects": "Все проекты", + "allProjectsHint": "Тесты используют {{project}}. Default применяется, если у проекта нет своего override.", + "loadingContexts": "Загрузка contexts...", + "projectHint": "Сохранение изменит override только для {{project}}.", + "projectOverrideContext": "Project override context", + "scopeDescriptionAllProjects": "Default для всех проектов, у которых нет собственного OpenCode override.", + "scopeDescriptionProject": "Override только для выбранного проекта. Уже запущенные команды не изменяются.", + "selectProjectContext": "Выберите project context", + "selectProjectHint": "Выберите проект перед тестированием local models или сохранением defaults.", + "selectValidationContext": "Выберите validation context", + "setAllProjectsDefault": "Задать default для всех проектов", + "setProjectDefault": "Задать default для проекта", + "thisProject": "Этот проект", + "title": "OpenCode defaults", + "validationContext": "Validation context" + }, + "diagnostics": { + "copied": "Diagnostics скопированы", + "copiedShort": "Скопировано", + "copy": "Скопировать diagnostics", + "hints": "Подсказки", + "likelyCause": "Вероятная причина:" + }, + "models": { + "alreadyDefault": "Это уже выбранный OpenCode default.", + "empty": "Модели не найдены.", + "emptyFree": "Free models не найдены.", + "emptyRecommended": "Recommended models не найдены.", + "emptyRecommendedFree": "Recommended free models не найдены.", + "freeOnly": "Только free", + "launchableDescription": "Routes, которые можно тестировать или использовать в team picker: local config, free built-in models и текущий default.", + "launchableTitle": "Launchable OpenCode models", + "loadingRoutes": "Загрузка OpenCode model routes...", + "noRoutesMatch": "OpenCode model routes не найдены по запросу \"{{query}}\".", + "noneReported": "Launchable OpenCode model routes пока не получены. Настройте local route в OpenCode или используйте вкладку Providers для просмотра catalog providers.", + "recommendedOnly": "Только recommended", + "searchPlaceholder": "Поиск моделей", + "selectProjectBeforeTesting": "Выберите project context перед тестированием моделей.", + "selectProjectBeforeTestingDefaults": "Выберите project context перед тестированием или сохранением OpenCode defaults.", + "useInTeamPicker": "Использовать в team picker" + }, + "providers": { + "catalog": "OpenCode provider catalog", + "countFallback": "OpenCode providers", + "description": "{{count}}. Connected и recommended providers показаны первыми.", + "description_few": "{{count}}. Connected и recommended providers показаны первыми.", + "description_many": "{{count}}. Connected и recommended providers показаны первыми.", + "description_one": "{{count}}. Connected и recommended providers показаны первыми.", + "description_other": "{{count}}. Connected и recommended providers показаны первыми.", + "loadMore": "Загрузить ещё providers", + "loading": "Загрузка OpenCode providers", + "noMatches": "Providers не найдены по поиску.", + "noneReported": "Managed runtime не сообщил OpenCode providers.", + "recommended": "Recommended", + "refreshCatalog": "Обновить catalog", + "searchPlaceholder": "Поиск providers" + }, + "setup": { + "loading": "Загрузка provider setup..." + }, + "summary": { + "defaultModel": "OpenCode default: {{model}}", + "loading": "Загрузка managed OpenCode runtime, connected providers и model defaults...", + "source": "Источник: {{source}}", + "title": "OpenCode runtime" + }, + "tabs": { + "models": "Модели", + "providers": "Providers" + }, + "modelRoutes": { + "searchPlaceholder": "Поиск маршрутов моделей" + }, + "badges": { + "usedInTeamPicker": "Используется в выборе команды", + "free": "free", + "local": "local", + "configured": "настроено", + "connected": "подключено", + "verified": "проверено", + "needsTest": "нужен тест", + "failed": "ошибка", + "unknown": "неизвестно", + "default": "по умолчанию" + }, + "compatibleEndpoint": { + "baseUrlPlaceholder": "http://localhost:1234" + } + }, + "general": { + "agentLanguage": { + "description": "Язык общения с агентами", + "descriptionWithDetected": "Язык общения с агентами (определён: {{detected}})", + "emptyMessage": "Язык не найден.", + "label": "Язык", + "searchPlaceholder": "Поиск языка...", + "selectPlaceholder": "Выберите язык...", + "title": "Язык агентов" + }, + "appLanguage": { + "description": "Язык интерфейса приложения.", + "label": "Язык", + "title": "Язык приложения" + }, + "appearance": { + "autoExpandAIGroups": { + "description": "Автоматически раскрывать каждый ответ при открытии транскрипта или получении нового сообщения", + "label": "Раскрывать ответы AI по умолчанию" + }, + "nativeTitleBar": { + "description": "Использовать стандартную системную рамку окна вместо кастомной панели заголовка", + "label": "Использовать системную панель заголовка", + "restartConfirm": { + "confirmLabel": "Перезапустить", + "message": "Чтобы применить изменение панели заголовка, приложение нужно перезапустить. Перезапустить сейчас?", + "title": "Требуется перезапуск" + } + }, + "theme": { + "description": "Выберите предпочитаемую цветовую тему", + "label": "Тема", + "options": { + "dark": "Тёмная", + "light": "Светлая", + "system": "Системная" + } + }, + "title": "Внешний вид" + }, + "browserAccess": { + "serverMode": { + "description": "Запустить HTTP-сервер для доступа к интерфейсу из браузера или встраивания в iframe", + "label": "Включить режим сервера" + }, + "title": "Доступ из браузера" + }, + "localClaudeRoot": { + "actions": { + "selectFolder": "Выбрать папку", + "selectFolderManually": "Выбрать папку вручную", + "useAutoDetect": "Использовать автоопределение", + "useFolder": "Использовать папку", + "usePath": "Использовать путь", + "useThisPath": "Использовать этот путь", + "useWsl": "Используете Linux/WSL?" + }, + "confirm": { + "noProjectsDir": { + "message": "В этой папке нет директории \"projects\". Всё равно продолжить?", + "title": "Директория projects не найдена" + }, + "notClaudeDir": { + "message": "Эта папка называется \"{{folderName}}\", а не \".claude\". Всё равно продолжить?", + "title": "Выбранная папка не является .claude" + }, + "noWslPaths": { + "message": "Не удалось автоматически найти WSL-дистрибутивы с данными Claude. Выбрать папку вручную?", + "title": "Пути Claude в WSL не найдены" + }, + "wslNoProjectsDir": { + "message": "В \"{{path}}\" нет директории \"projects\". Всё равно продолжить?", + "title": "В пути WSL нет директории projects" + } + }, + "current": { + "autoDetected": "Автоопределено: {{path}}", + "autoDetectedPath": "Используется автоопределённый путь", + "customPath": "Используется пользовательский путь", + "label": "Текущий локальный корень" + }, + "description": "Выберите локальную папку, которая будет считаться корнем данных Claude", + "errors": { + "detectWslFailed": "Не удалось определить пути корня Claude в WSL", + "loadFailed": "Не удалось загрузить настройки локального корня Claude", + "updateFailed": "Не удалось обновить корень Claude" + }, + "title": "Локальный корень Claude", + "wslModal": { + "closeAriaLabel": "Закрыть окно выбора пути WSL", + "description": "Найденные WSL-дистрибутивы и кандидаты корня Claude", + "noProjectsDir": "Директория projects не найдена", + "title": "Выберите корень Claude в WSL" + } + }, + "privacy": { + "telemetry": { + "description": "Помогите улучшить приложение, отправляя анонимные отчёты о сбоях и производительности", + "label": "Отправлять отчёты о сбоях" + }, + "title": "Приватность" + }, + "server": { + "runningOn": "Запущено на", + "standaloneModeDescription": "Приложение работает в автономном режиме. HTTP-сервер всегда активен. Системные уведомления недоступны - триггеры уведомлений записываются только внутри приложения.", + "title": "Сервер" + }, + "startup": { + "launchAtLogin": { + "description": "Автоматически запускать приложение при входе в систему", + "label": "Запускать при входе" + }, + "showDockIcon": { + "description": "Показывать значок приложения в Dock (macOS)", + "label": "Показывать значок в Dock" + }, + "title": "Запуск" + } + }, + "notifications": { + "dev": { + "descriptionPrefix": "В режиме разработки уведомления могут работать некорректно. macOS определяет приложение как \"Electron\" (bundle ID", + "descriptionSuffix": "), а не как production-приложение. Проверьте разрешения в System Settings > Notifications > Electron.", + "title": "Режим разработки" + }, + "ignoredRepositories": { + "description": "Уведомления из этих репозиториев будут игнорироваться", + "empty": "Игнорируемых репозиториев нет", + "selectPlaceholder": "Выберите репозиторий для игнорирования...", + "title": "Игнорируемые репозитории" + }, + "settings": { + "enabled": { + "description": "Показывать системные уведомления об ошибках и событиях", + "label": "Включить системные уведомления" + }, + "sound": { + "description": "Воспроизводить звук при появлении уведомлений", + "label": "Воспроизводить звук" + }, + "subagentErrors": { + "description": "Обнаруживать ошибки в сессиях субагентов и уведомлять о них", + "label": "Включать ошибки субагентов" + }, + "title": "Настройки уведомлений" + }, + "snooze": { + "clear": "Снять паузу", + "description": "Временно приостановить уведомления", + "descriptionWithTime": "Приостановлено до {{time}}", + "label": "Приостановить уведомления", + "options": { + "15": "15 минут", + "30": "30 минут", + "60": "1 час", + "120": "2 часа", + "240": "4 часа", + "-1": "До завтра" + }, + "selectDuration": "Выберите длительность..." + }, + "taskCompletion": { + "description": "Получайте системные уведомления, когда Claude завершает задачи: звуки, баннеры и бейджи Dock/панели задач. Работает на macOS, Linux и Windows.", + "installPlugin": "Установить плагин claude-notifications-go", + "title": "Уведомления о завершении задач" + }, + "team": { + "allTasksCompleted": { + "description": "Уведомлять, когда все задачи в команде переходят в статус completed", + "label": "Все задачи завершены" + }, + "autoResumeOnRateLimit": { + "description": "Когда Claude сообщает время сброса лимита, запланировать follow-up для лида команды после восстановления лимита", + "label": "Автовозобновление после rate limit" + }, + "clarifications": { + "description": "Показывать системные уведомления, когда задаче нужен ваш ввод", + "label": "Уведомления об уточнениях по задачам" + }, + "crossTeamMessage": { + "description": "Уведомлять, когда приходит сообщение от другой команды", + "label": "Уведомления о сообщениях между командами" + }, + "leadInbox": { + "description": "Уведомлять, когда участники команды отправляют сообщения лиду команды", + "label": "Уведомления inbox лида" + }, + "statusChange": { + "description": "Показывать системные уведомления при изменении статуса задачи", + "label": "Уведомления об изменении статуса задач", + "onlySolo": { + "description": "Уведомлять только когда в команде нет участников", + "label": "Только в Solo-режиме" + }, + "statuses": { + "description": "Какие целевые статусы вызывают уведомление", + "label": "Уведомлять по этим статусам", + "options": { + "approved": "Одобрено", + "completed": "Завершено", + "deleted": "Удалено", + "in_progress": "Запущено", + "needsFix": "Нужны исправления", + "pending": "Ожидание", + "review": "Ревью" + } + } + }, + "taskComments": { + "description": "Показывать системные уведомления, когда агенты комментируют задачи", + "label": "Уведомления о комментариях к задачам" + }, + "taskCreated": { + "description": "Показывать системные уведомления при создании новой задачи", + "label": "Уведомления о новых задачах" + }, + "teamLaunched": { + "description": "Уведомлять, когда команда завершила запуск и готова к работе", + "label": "Уведомления о запуске команды" + }, + "title": "Уведомления команды", + "toolApproval": { + "description": "Уведомлять, когда инструменту нужно ваше подтверждение (Allow/Deny), пока приложение не в фокусе", + "label": "Уведомления о подтверждении инструментов" + }, + "userInbox": { + "description": "Уведомлять, когда участники команды отправляют сообщения вам", + "label": "Уведомления вашего inbox" + } + }, + "test": { + "action": "Отправить тест", + "description": "Отправить тестовое уведомление, чтобы проверить доставку", + "failedToSend": "Не удалось отправить тестовое уведомление", + "label": "Тестовое уведомление", + "sending": "Отправка...", + "sent": "Отправлено!", + "unknownError": "Неизвестная ошибка" + } + }, + "advanced": { + "about": { + "appIconAlt": "Значок приложения", + "description": "Собирайте команды AI-агентов, которые автономно работают параллельно, общаются между командами и управляют задачами на kanban-доске - со встроенным code review, живым мониторингом процессов и полной видимостью инструментов.", + "standalone": "Автономно", + "title": "О приложении", + "version": "Версия {{version}}" + }, + "configuration": { + "editConfig": "Редактировать конфиг", + "exportConfig": "Экспортировать конфиг", + "importConfig": "Импортировать конфиг", + "openInEditor": "Открыть в редакторе", + "resetToDefaults": "Сбросить по умолчанию", + "title": "Конфигурация" + }, + "updates": { + "available": "Доступна v{{version}}", + "check": "Проверить обновления", + "checking": "Проверка...", + "ready": "Обновление готово", + "unknownVersion": "неизвестная", + "upToDate": "Актуальная версия" + }, + "appName": "Agent Teams AI" + }, + "configEditor": { + "errors": { + "loadFailed": "Не удалось загрузить конфиг", + "saveFailed": "Не удалось сохранить конфиг" + }, + "footer": { + "autoSave": "Изменения сохраняются автоматически после редактирования", + "toClose": "чтобы закрыть", + "escapeKey": "Esc" + }, + "loading": "Загрузка конфига...", + "status": { + "invalidJson": "Некорректный JSON", + "saveFailed": "Сохранение не удалось", + "saved": "Сохранено", + "saving": "Сохранение..." + }, + "title": "Редактирование конфигурации" + }, + "notificationTriggers": { + "add": { + "cancel": "Отмена", + "submit": "Добавить триггер", + "title": "Добавить свой триггер" + }, + "builtin": { + "description": "Стандартные триггеры, встроенные в приложение. Их можно включать или отключать и настраивать их паттерны.", + "title": "Встроенные триггеры" + }, + "card": { + "builtinBadge": "Встроенный", + "collapseAriaLabel": "Свернуть", + "deleteAriaLabel": "Удалить триггер", + "editNameAriaLabel": "Редактировать имя", + "expandAriaLabel": "Развернуть" + }, + "color": { + "customHexTitle": "Пользовательский HEX-цвет", + "invalidHex": "Некорректный HEX" + }, + "configuration": { + "alertIfGreaterThan": "Уведомить если >", + "emptyPatternHint": "Оставьте пустым, чтобы совпадало с любым содержимым. Используется синтаксис JavaScript regex.", + "errorStatusDescription": "Срабатывает, когда выполнение инструмента сообщает об ошибке (is_error: true).", + "tokensUnit": "токенов", + "matchPatternPlaceholder": "например, error|failed|exception" + }, + "custom": { + "description": "Создавайте собственные триггеры для уведомлений по конкретным паттернам или выводам инструментов.", + "empty": "Пользовательские триггеры пока не настроены.", + "title": "Пользовательские триггеры" + }, + "errors": { + "invalidRegexPattern": "Некорректный regex-паттерн" + }, + "fields": { + "contentType": "Тип содержимого", + "matchField": "Поле для поиска", + "matchPattern": "Паттерн совпадения (Regex)", + "scopeToolName": "Область / инструмент", + "scopeToolNameOptional": "Область / инструмент (необязательно)", + "threshold": "Порог", + "tokenType": "Тип токенов", + "triggerNamePlaceholder": "например, Ошибка сборки", + "triggerNameRequired": "Название триггера *" + }, + "ignorePatterns": { + "hint": "Нажмите Enter, чтобы добавить. Уведомление пропускается, если совпал любой паттерн.", + "placeholder": "Добавить ignore regex...", + "removeAriaLabel": "Удалить ignore-паттерн", + "summary": "Дополнительно: правила исключения", + "title": "Ignore-паттерны (пропустить при совпадении)" + }, + "options": { + "contentTypes": { + "text": "Текстовый вывод", + "thinking": "Thinking", + "tool_result": "Результат инструмента", + "tool_use": "Вызов инструмента" + }, + "matchFields": { + "args": "Аргументы", + "command": "Команда", + "content": "Содержимое", + "description": "Описание", + "file_path": "Путь к файлу", + "fullInput": "Весь ввод (JSON)", + "glob": "Glob-фильтр", + "new_string": "Новая строка", + "old_string": "Старая строка", + "path": "Путь", + "pattern": "Паттерн", + "prompt": "Промпт", + "query": "Запрос", + "skill": "Название skill", + "subagent_type": "Тип субагента", + "text": "Текстовое содержимое", + "thinking": "Thinking-содержимое", + "url": "URL" + }, + "modes": { + "content_match": "Паттерн в содержимом", + "error_status": "Ошибка выполнения", + "token_threshold": "Высокий расход токенов" + }, + "tokenTypes": { + "input": "Входные токены", + "output": "Выходные токены", + "total": "Всего токенов" + }, + "toolNames": { + "anyTool": "Любой инструмент" + } + }, + "preview": { + "defaultTestTriggerName": "Тестовый триггер", + "detectedSuffix": "ошибок было бы обнаружено", + "more": "...и ещё {{count}}", + "more_few": "...и ещё {{count}}", + "more_many": "...и ещё {{count}}", + "more_one": "...и ещё {{count}}", + "more_other": "...и ещё {{count}}", + "testTrigger": "Проверить триггер", + "testing": "Проверка...", + "title": "Предпросмотр", + "truncatedWarning": "Поиск остановлен раньше времени (таймаут или лимит количества). Фактических совпадений может быть больше.", + "viewSession": "Открыть сессию" + }, + "repositoryScope": { + "empty": "Репозитории не выбраны - триггер применяется ко всем репозиториям", + "hint": "Если репозитории выбраны, триггер срабатывает только для ошибок в этих репозиториях.", + "placeholder": "Выберите репозиторий для добавления...", + "summary": "Дополнительно: область репозиториев", + "title": "Ограничить репозиториями (применяется только к выбранным)" + }, + "sections": { + "configuration": "Конфигурация", + "dotColor": "Цвет точки", + "generalInfo": "Основная информация", + "triggerCondition": "Условие триггера" + } + }, + "workspaceProfiles": { + "actions": { + "addProfile": "Добавить профиль", + "cancel": "Отмена", + "deleteProfile": "Удалить профиль", + "editProfile": "Редактировать профиль", + "save": "Сохранить" + }, + "authMethods": { + "agent": "SSH Agent", + "auto": "Auto (из SSH Config)", + "password": "Пароль", + "privateKey": "Приватный ключ" + }, + "deleteConfirm": { + "confirmLabel": "Удалить", + "message": "Вы уверены, что хотите удалить \"{{name}}\"? Это действие нельзя отменить.", + "title": "Удалить профиль" + }, + "description": "Сохраняйте SSH-профили для быстрого повторного подключения", + "empty": { + "description": "Добавьте SSH-профиль, чтобы быстро подключаться", + "title": "Сохранённых профилей нет" + }, + "form": { + "authentication": "Аутентификация", + "host": "Хост", + "name": "Название", + "passwordPrompt": "Пароль будет запрошен при подключении.", + "port": "Порт", + "privateKeyPath": "Путь к приватному ключу", + "username": "Имя пользователя", + "namePlaceholder": "Мой сервер", + "hostPlaceholder": "hostname или IP", + "usernamePlaceholder": "user" + }, + "loading": "Загрузка профилей...", + "title": "Профили рабочих окружений" + }, + "connection": { + "actions": { + "connect": "Подключиться", + "connecting": "Подключение...", + "disconnect": "Отключиться", + "testConnection": "Проверить подключение", + "testing": "Проверка..." + }, + "currentMode": { + "description": "Источник данных для файлов сессий", + "label": "Текущий режим", + "local": "Локально ({{path}})" + }, + "description": "Подключитесь к удалённой машине, чтобы просматривать сессии Claude Code, запущенные там", + "form": { + "authentication": "Аутентификация", + "host": "Хост", + "password": "Пароль", + "port": "Порт", + "privateKeyPath": "Путь к приватному ключу", + "username": "Имя пользователя", + "hostPlaceholder": "hostname или alias из SSH config", + "usernamePlaceholder": "user" + }, + "savedProfiles": { + "title": "Сохранённые профили" + }, + "ssh": { + "title": "SSH-подключение" + }, + "status": { + "connectedTo": "Подключено к {{host}}", + "remoteSessions": "Просмотр удалённых сессий через SSH" + }, + "test": { + "failed": "Подключение не удалось: {{error}}", + "success": "Подключение успешно", + "unknownError": "Неизвестная ошибка" + }, + "title": "Удалённое подключение" + }, + "providerRuntime": { + "actions": { + "cancel": "Отмена", + "cancelLogin": "Отменить вход", + "connectChatGpt": "Подключить ChatGPT", + "delete": "Удалить", + "disable": "Отключить", + "disconnectAccount": "Отключить аккаунт", + "generateLink": "Создать ссылку", + "openLogin": "Открыть вход", + "reconnectAnthropic": "Переподключить Anthropic", + "refresh": "Обновить", + "replaceKey": "Заменить ключ", + "saveEndpoint": "Сохранить endpoint", + "saveKey": "Сохранить ключ", + "saving": "Сохранение...", + "setApiKey": "Задать API key", + "updateKey": "Обновить ключ", + "useCode": "Использовать код" + }, + "apiKey": { + "loadingStoredCredentials": "Загрузка сохранённых credentials...", + "projectScope": "Проект", + "scope": "Scope", + "storedIn": "Хранится в {{backend}}", + "userScope": "Пользователь", + "storedInApp": "Сохранено в приложении", + "providers": { + "anthropic": { + "name": "Anthropic API Key", + "title": "API key", + "description": "Используйте прямой Anthropic API key для доступа с API-billing. Сессия подписки Anthropic останется доступной после переключения обратно.", + "placeholder": "sk-ant-..." + }, + "codex": { + "name": "Codex API Key", + "title": "API key", + "description": "Используйте OpenAI API key как дополнительный способ аутентификации Codex. При переключении Codex в режим API key приложение отзеркалит OPENAI_API_KEY в CODEX_API_KEY для native launches.", + "placeholder": "sk-proj-..." + }, + "gemini": { + "name": "Gemini API Key", + "title": "API access", + "description": "Используйте `GEMINI_API_KEY` для Gemini API backend. CLI SDK и ADC не требуют его.", + "placeholder": "AIza..." + } + } + }, + "codex": { + "account": { + "appServer": "App-server: {{state}}", + "connected": "Подключено", + "description": "Управляйте локальной сессией Codex app-server account, которая используется для subscription-backed native launches.", + "loginInProgress": "Вход выполняется", + "plan": "План: {{plan}}", + "reconnectRequired": "Нужно переподключить", + "title": "ChatGPT account", + "hints": { + "autoUsesApiKeyUntilChatgpt": "{{message}} Auto продолжит использовать найденный API key, пока ChatGPT не подключён.", + "detectedApiKeyNeedsApiMode": "{{message}} Найденный API key используется только после переключения Codex в API key mode.", + "localArtifactsNoSession": "Codex CLI сейчас не видит активный ChatGPT account. Локальные данные Codex account есть, но активная managed session не выбрана. Лимиты появятся здесь только после того, как Codex CLI увидит аккаунт.", + "noActiveAccount": "Codex CLI сейчас не видит активный ChatGPT account. Лимиты появятся здесь только после того, как Codex CLI увидит аккаунт.", + "reconnectBeforeUsage": "В Codex локально выбран ChatGPT account, но текущей сессии нужно переподключение, прежде чем здесь загрузятся лимиты.", + "usageLimitsAfterReport": "Лимиты появятся здесь после того, как Codex сообщит их для подключённого ChatGPT account." + } + }, + "install": { + "checking": "Проверка", + "downloading": "Загрузка", + "installCli": "Установить Codex CLI", + "installing": "Установка", + "retryInstall": "Повторить установку", + "title": "Установить Codex CLI в данные приложения" + }, + "rateLimits": { + "credits": "Credits", + "creditsDescription": "Credits показываются отдельно от window-based subscription usage и могут быть недоступны для plan-backed ChatGPT-сессий.", + "noSecondaryWindow": "Codex не вернул secondary window для этого account snapshot.", + "notReported": "Не передано", + "primaryReset": "Сброс primary", + "primaryUsed": "Primary использовано", + "primaryWindow": "Primary window", + "remainingLeft": "{{value}} осталось", + "remainingUnknown": "Остаток неизвестен", + "secondaryReset": "Сброс secondary", + "secondaryUsed": "Secondary использовано", + "secondaryWindow": "Secondary window", + "usedQuotaNote": "Эти проценты показывают использованную квоту, а не остаток.", + "weeklyReset": "Сброс weekly", + "weeklyUsed": "Weekly использовано", + "weeklyUsedOneWeek": "Weekly использовано (1w)", + "weeklyWindow": "Weekly window", + "secondaryFallback": "secondary", + "secondaryWindowNote": " Weekly-лимиты показаны отдельно в окне {{window}}.", + "usageExplanationGeneric": "Показывает использованную квоту, а не остаток.", + "usageExplanationWindowOnly": "Показывает использованную квоту в текущем окне {{window}}, а не остаток.", + "usageExplanationWithRemaining": "Использовано {{used}} - примерно {{remaining}} осталось в текущем окне {{window}}." + } + }, + "compatibleEndpoint": { + "authToken": "Auth token", + "authTokenMissing": "Auth token не настроен.", + "baseUrl": "Base URL", + "description": "Использовать локальный runtime endpoint, совместимый с Anthropic.", + "keepSavedToken": "Оставьте пустым, чтобы сохранить текущий token", + "title": "Локальный / compatible endpoint", + "tokenStatus": "Token {{status}}", + "validation": { + "baseUrlRequired": "Base URL обязателен", + "firstPartyAnthropic": "Для первого-party Anthropic используйте Auto, Subscription или API key", + "httpRequired": "Base URL должен использовать http:// или https://", + "invalidUrl": "Недопустимый URL", + "noCredentials": "Base URL не должен содержать credentials" + }, + "status": { + "endpointDisabledTokenKept": "Endpoint отключён. Сохранённый token оставлен.", + "endpointSaved": "Endpoint сохранён", + "endpointSavedTokenMissing": "Endpoint сохранён. Auth token не настроен." + } + }, + "connection": { + "authenticationMethod": "Метод аутентификации", + "descriptions": { + "anthropic": "Выберите, как запуски Anthropic из приложения проходят аутентификацию.", + "codex": "Выберите, должен ли Codex предпочитать ChatGPT subscription или API key при native runtime запуске.", + "gemini": "Настройте опциональный API-доступ. CLI SDK и ADC всё равно определяются автоматически.", + "opencode": "Аутентификация OpenCode и список провайдеров управляются runtime OpenCode." + }, + "method": "Метод подключения", + "mode": "Режим: {{mode}}", + "selected": "Выбрано", + "switching": "Переключение...", + "title": "Подключение" + }, + "connectionCards": { + "apiKey": { + "title": "API key" + }, + "anthropic": { + "apiKeyDescription": "Использовать ANTHROPIC_API_KEY и биллинг Anthropic API.", + "autoDescription": "Использовать runtime-настройки Anthropic по умолчанию и лучший локальный credential.", + "hint": "Auto оставляет Anthropic на стандартном локальном выборе credentials.", + "subscriptionDescription": "Использовать локальную Anthropic sign-in сессию и subscription access.", + "subscriptionTitle": "Anthropic subscription" + }, + "auto": { + "title": "Авто" + }, + "codex": { + "apiKeyDescription": "Использовать OPENAI_API_KEY и CODEX_API_KEY billing для native Codex launches.", + "autoDescription": "Предпочитать ChatGPT account и subscription. API key mode использовать только при необходимости.", + "chatgptDescription": "Использовать подключённый ChatGPT account и Codex subscription.", + "chatgptTitle": "ChatGPT account", + "hint": "Codex всегда работает через native runtime. Auto предпочитает ChatGPT account перед API-key credentials." + } + }, + "description": "Управляйте тем, как каждый провайдер подключается, и какой backend должен использовать multimodel runtime, если это поддерживается.", + "fastMode": { + "defaultOff": "По умолчанию выкл.", + "description": "Включать Claude Code Fast mode по умолчанию для новых запусков Anthropic-команд, когда выбранные модель и runtime это поддерживают.", + "disabledHint": "Новые Anthropic-запуски остаются на обычной скорости, если команда явно не включает Fast mode.", + "enabledHint": "Новые Anthropic-запуски будут запрашивать Fast mode по умолчанию, когда выбранная модель это поддерживает.", + "notExposed": "Этот Anthropic runtime не предоставляет Fast mode.", + "preferFast": "Предпочитать Fast", + "title": "Fast mode по умолчанию", + "unavailableForRuntime": "Fast mode сейчас недоступен для этого Anthropic runtime." + }, + "alerts": { + "anthropicApiKeyMissing": "Выбран API key mode, но Anthropic API credential пока недоступен.", + "anthropicStoredKeyAvailable": "Сохранённый API key доступен, но запуски Anthropic из приложения используют его только после переключения в API key mode.", + "anthropicSubscriptionMissing": "Выбран Anthropic subscription mode. Войдите в Anthropic, чтобы использовать этого провайдера.", + "authTokenMissing": "Auth token не настроен. Многим локальным Anthropic-compatible endpoints нужен непустой token.", + "chatgptLoginPending": "Ожидание завершения входа в ChatGPT account...", + "chatgptLoginStarting": "Запуск входа в ChatGPT...", + "codexApiKeyMissing": "Выбран API key mode, но OPENAI_API_KEY или CODEX_API_KEY credential пока недоступен.", + "codexLocalArtifactsNoSession": "Codex CLI сейчас не видит активный ChatGPT account. Локальные данные Codex account есть, но активная managed session не выбрана.", + "codexNeedsReconnect": "В Codex локально выбран ChatGPT account, но текущей сессии нужно переподключение.", + "codexNoChatgptAccount": "Codex CLI сейчас не видит активный ChatGPT account. Подключите ChatGPT, чтобы использовать subscription.", + "codexNoCredential": "ChatGPT account или API key пока недоступны.", + "geminiApiUnavailable": "Gemini API сейчас недоступен. Настройте `GEMINI_API_KEY` здесь или используйте корректные Google ADC credentials.", + "withApiKeyFallback": "{{message}} Переключитесь в API key mode, чтобы использовать найденный API key." + }, + "authModeDescriptions": { + "anthropic": { + "apiKey": "Принудительно использовать API key credential для Anthropic-запусков из приложения.", + "auto": "Использовать стандартное поведение runtime. Сохранённые API keys в приложении используются только после переключения в API key mode.", + "oauth": "Принудительно использовать локальную Anthropic subscription session для Anthropic-запусков из приложения." + }, + "codex": { + "apiKey": "Принудительно использовать OPENAI_API_KEY / CODEX_API_KEY billing для native Codex launches.", + "auto": "Предпочитать ChatGPT account, когда он доступен. Переходить к API key mode только при необходимости.", + "chatgpt": "Принудительно использовать подключённый ChatGPT account и subscription для native Codex launches." + } + }, + "progress": { + "applyingConnectionChanges": "Применение изменений подключения...", + "refreshingProviderStatus": "Обновление статуса провайдера...", + "savingCompatibleEndpoint": "Сохранение compatible endpoint...", + "switchingAnthropicSubscription": "Переключение на Anthropic subscription...", + "switchingApiKey": "Переключение на API key...", + "switchingApiKeyMode": "Переключение в API key mode...", + "switchingAuto": "Переключение на Авто...", + "switchingChatgpt": "Переключение в ChatGPT account mode..." + }, + "provider": "Провайдер", + "runtime": { + "descriptions": { + "anthropic": "У Anthropic сейчас нет отдельного выбора runtime backend.", + "codex": "Codex теперь работает только через native runtime path.", + "gemini": "Выберите, какой Gemini runtime backend должен использовать multimodel.", + "opencode": "OpenCode использует собственный managed runtime host. Desktop сейчас показывает только статус." + }, + "title": "Runtime", + "updating": "Обновление runtime..." + }, + "runtimeSummary": "Runtime: {{runtime}}", + "status": { + "configured": "настроен", + "enabled": "Включено", + "notConfigured": "Не настроен", + "notSet": "не задан", + "off": "Выкл.", + "unknown": "Неизвестно" + }, + "title": "Настройки провайдера", + "usage": { + "apiKey": "Используется API key", + "apiKeyRequired": "Нужен API key", + "compatibleEndpoint": "Используется compatible endpoint", + "notConnected": "Не подключено", + "usingMethod": "Используется {{method}}" + }, + "errors": { + "apiKeyDeletedRefreshFailed": "API key удалён, но не удалось обновить статус провайдера.", + "apiKeySavedRefreshFailed": "API key сохранён, но не удалось обновить статус провайдера.", + "connectionUpdatedRefreshFailed": "Подключение обновлено, но не удалось обновить статус провайдера.", + "deleteApiKey": "Не удалось удалить API key", + "disableEndpoint": "Не удалось отключить endpoint", + "endpointDisabledRefreshFailed": "Endpoint отключён, но не удалось обновить статус провайдера.", + "endpointSavedRefreshFailed": "Endpoint сохранён, но не удалось обновить статус провайдера.", + "refreshCodexAccount": "Не удалось обновить Codex account", + "saveApiKey": "Не удалось сохранить API key", + "saveEndpoint": "Не удалось сохранить endpoint", + "updateAnthropicFastMode": "Не удалось обновить Anthropic Fast mode", + "updateConnection": "Не удалось обновить подключение", + "updateRuntimeBackend": "Не удалось обновить runtime backend", + "apiKeyRequired": "API key обязателен" + }, + "connectionUi": { + "authMode": { + "auto": "Авто", + "oauth": "Подписка / OAuth", + "chatgpt": "Аккаунт ChatGPT", + "apiKey": "API key", + "anthropicSubscription": "Подписка Anthropic" + }, + "authMethod": { + "apiKey": "API key", + "apiKeyHelper": "API key helper", + "oauth": "OAuth", + "claudeSubscription": "Подписка Claude", + "geminiCli": "Gemini CLI", + "googleAccount": "Аккаунт Google", + "serviceAccount": "service account" + }, + "runtime": { + "codexNative": "Codex native", + "currentRuntime": "Текущий runtime", + "selectedRuntime": "Выбранный runtime", + "summary": "{{prefix}}: {{runtime}}" + }, + "status": { + "checking": "Проверка...", + "checked": "Проверено", + "providerActivity": "Активность провайдеров", + "notConnected": "Не подключено", + "startingChatGptLogin": "Запускается вход в ChatGPT...", + "waitingForChatGptLogin": "Ожидание входа в аккаунт ChatGPT...", + "chatGptVerificationDegraded": "Аккаунт ChatGPT найден - проверка аккаунта сейчас работает в ограниченном режиме.", + "chatGptAccountReady": "Аккаунт ChatGPT готов", + "apiKeyReady": "API key готов", + "codexLocalAccountNeedsReconnect": "В Codex локально выбран аккаунт ChatGPT, но текущей сессии нужно переподключение.", + "codexNoActiveManagedSession": "Codex CLI сообщает, что активного входа ChatGPT нет. Локальные данные аккаунта Codex есть, но активная управляемая сессия не выбрана.", + "codexNoActiveChatGptLogin": "Codex CLI сообщает, что активного входа ChatGPT нет", + "connectChatGptForSubscription": "Подключите аккаунт ChatGPT, чтобы использовать подписку Codex.", + "codexNativeReady": "Codex native готов", + "codexNativeUnavailable": "Codex native недоступен", + "unavailableInCurrentRuntime": "Недоступно в текущем runtime", + "connectedViaApiKey": "Подключено через API key", + "apiKeyConfiguredNotVerified": "API key настроен, но ещё не проверен", + "apiKeyModeMissingCredential": "Выбран режим API key, но API key не настроен", + "connectedVia": "Подключено через {{method}}", + "unableToVerify": "Не удалось проверить" + }, + "mode": { + "selectedAuth": "Выбранная аутентификация: {{authMode}}", + "preferredAuth": "Предпочитаемая аутентификация: {{authMode}}" + }, + "credential": { + "apiKeyConfigured": "API key настроен", + "savedApiKeyAvailable": "Сохранённый API key доступен в Manage", + "apiKeyAlsoConfigured": "API key также настроен в Manage", + "apiKeyConfiguredInManage": "API key настроен в Manage", + "apiKeyFallbackInManage": "API key также доступен в Manage как fallback", + "availableAsFallback": "{{summary}} - доступен как fallback", + "savedApiKeyAvailableIfSwitch": "Сохранённый API key доступен в Manage, если переключиться в режим API key", + "availableIfSwitch": "{{summary}} - доступен при переключении в режим API key", + "autoWillUseUntilChatGpt": "{{summary}} - Auto будет использовать его, пока ChatGPT не подключён" + }, + "actions": { + "connect": "Подключить", + "connectAnthropic": "Подключить Anthropic", + "connectChatGpt": "Подключить ChatGPT", + "disconnect": "Отключить", + "openLogin": "Открыть вход" + }, + "disconnect": { + "anthropicTitle": "Отключить подписку Anthropic?", + "anthropic": "Это удалит локальную сессию подписки Anthropic из runtime Claude CLI.", + "anthropicWithApiKey": "Это удалит локальную сессию подписки Anthropic из runtime Claude CLI. Сохранённые API keys в Manage останутся доступными.", + "geminiTitle": "Отключить Gemini CLI?", + "gemini": "Это очистит локальные метаданные сессии Gemini CLI. Внешние ADC credentials и сохранённые API keys не удаляются." + } + } + }, + "cliRuntime": { + "actions": { + "checkForUpdates": "Проверить обновления", + "checking": "Проверка...", + "extensions": "Расширения", + "installRuntime": "Установить {{runtime}}", + "manage": "Управлять", + "recheck": "Проверить снова", + "reinstallRuntime": "Переустановить {{runtime}}", + "retry": "Повторить", + "update": "Обновить" + }, + "installer": { + "checkingLatest": "Проверка последней версии...", + "downloading": "Загрузка...", + "failed": "Установка не удалась", + "installed": "Установлено v{{version}}", + "installing": "Установка...", + "latest": "latest", + "verifying": "Проверка checksum..." + }, + "labels": { + "multimodel": "Multimodel" + }, + "loading": { + "aiProviders": "Проверка AI-провайдеров...", + "claudeCli": "Проверка Claude CLI..." + }, + "provider": { + "backend": "Backend: {{backend}}", + "loadingModels": "Загрузка моделей...", + "modelsUnavailable": "Модели недоступны для этой сборки runtime", + "runtime": "Runtime: {{runtime}}" + }, + "providerTerminal": { + "authFailed": "Аутентификация не удалась", + "authUpdated": "Аутентификация обновлена", + "loggedOut": "Провайдер отключён", + "login": "Вход", + "logout": "Выход", + "logoutFailed": "Выход не удался" + }, + "status": { + "configuredNotFound": "Настроенный {{runtime}} не найден.", + "foundButFailed": "{{runtime}} найден, но не запустился", + "healthCheckFailed": "Настроенный {{runtime}} не прошёл health check запуска.", + "notInstalled": "{{runtime}} не установлен" + }, + "title": "CLI Runtime" + }, + "cliStatus": { + "versionUpgrade": "v{{current}} -> v{{latest}}" + } +} diff --git a/src/features/localization/renderer/locales/ru/team.json b/src/features/localization/renderer/locales/ru/team.json new file mode 100644 index 00000000..357a7b52 --- /dev/null +++ b/src/features/localization/renderer/locales/ru/team.json @@ -0,0 +1,2415 @@ +{ + "activity": { + "actions": { + "createTaskFromMessage": "Создать задачу из сообщения", + "expandMessage": "Развернуть сообщение", + "replyToMessage": "Ответить на сообщение", + "restartTeam": "Перезапустить команду" + }, + "authError": { + "description": "Не удалось пройти authentication. Перезапуск команды обновит session и может исправить проблему. Если ошибка повторится, проверьте API credentials или попробуйте позже." + }, + "automation": { + "reviewPickup": "Teammate получил просьбу забрать review", + "stallNudge": "Teammate получил просьбу продолжить stalled task", + "workSyncBody": "Teammate получил просьбу синхронизировать текущую работу" + }, + "badges": { + "automation": "automation", + "bootstrap": "bootstrap", + "command": "command", + "comment": "Комментарий", + "live": "live", + "note": "note", + "rateLimited": "Rate limit", + "restart": "restart", + "result": "result", + "session": "session", + "stallNudge": "stall nudge", + "start": "start", + "workSync": "work sync" + }, + "bootstrap": { + "acknowledged": "Bootstrap подтверждён", + "restarting": "Перезапуск teammate", + "starting": "Запуск teammate" + }, + "rawJson": "Raw JSON", + "unread": "Непрочитано", + "thoughts": { + "count": "{{count}} мыслей", + "count_one": "{{count}} мысль", + "count_few": "{{count}} мысли", + "count_many": "{{count}} мыслей", + "count_other": "{{count}} мысли", + "expand": "Развернуть мысли", + "showMore": "Показать больше", + "showLess": "Показать меньше", + "toolSummary": "🔧 {{summary}}", + "titleForMember": "{{name}} - мысли" + }, + "timeline": { + "loadingMessages": "Загрузка сообщений...", + "noMessages": "Нет сообщений", + "emptyHint": "Отправьте сообщение участнику, чтобы увидеть активность.", + "newSession": "Новая сессия", + "olderCount": "+{{count}} старых", + "olderCount_one": "+{{count}} старое", + "olderCount_few": "+{{count}} старых", + "olderCount_many": "+{{count}} старых", + "olderCount_other": "+{{count}} старых", + "showMore": "Показать ещё {{count}}", + "showAll": "Показать все" + }, + "pendingReplies": { + "title": "Ожидают ответа", + "openMember": "Открыть участника", + "messageSentAwaitingReply": "Сообщение отправлено, ожидается ответ", + "awaitingReply": "ожидает ответа", + "externalTeam": "внешняя команда", + "crossTeamAwaitingReply": "Межкомандное сообщение отправлено, ожидается ответ", + "user": "пользователь", + "awaitingApproval": "ожидает подтверждения" + }, + "reply": { + "replyingTo": "Ответ на", + "action": "Ответить" + }, + "activeTasks": { + "inProgress": "В работе" + }, + "expandDialog": { + "description": "Развёрнутый просмотр сообщения" + } + }, + "create": { + "actions": { + "create": "Создать", + "creating": "Создание...", + "openExisting": "Открыть существующую команду", + "skipPreflightAndCreate": "Пропустить preflight и создать" + }, + "conflict": { + "description": "Запуск двух команд в одной директории рискован - они могут конфликтовать при изменении одних и тех же файлов. Лучше выбрать другую директорию или git worktree для изоляции.", + "title": "Другая команда \"{{team}}\" уже работает в этой working directory", + "workingDirectory": "Working directory:" + }, + "description": { + "copy": "Создать новую команду на основе существующей.", + "create": "Настройте команду и выберите, как она будет запускаться." + }, + "errors": { + "nameExists": "Команда с таким именем уже существует", + "nameLaunching": "Команда с таким именем сейчас запускается", + "createConfigFailed": "Не удалось создать конфиг команды", + "loadProjectsFailed": "Не удалось загрузить проекты" + }, + "fields": { + "color": "Цвет (optional)", + "description": "Описание (optional)", + "prompt": "Prompt для team lead (optional)", + "teamName": "Имя команды" + }, + "launchAfterCreate": { + "description": "Сразу запустить команду через локальный Claude CLI.", + "label": "Выполнить команду после создания" + }, + "localOnly": "Доступно только в локальном Electron mode.", + "onDisk": "На диске:", + "placeholders": { + "description": "Краткое описание назначения команды", + "prompt": "Инструкции для team lead во время provisioning..." + }, + "saved": "Сохранено", + "solo": { + "description": "Будет запущен только team lead (main process) - teammates не будут созданы. Работает как обычная agent session в выбранном runtime (Claude Code, Codex, OpenCode, Gemini), но с доступом к task board для планирования. Экономит tokens за счёт отсутствия coordination overhead между teammates. Участников можно добавить позже в настройках команды.", + "label": "Solo team" + }, + "title": { + "copy": "Копировать команду", + "create": "Создать команду" + }, + "optional": { + "launchSettingsTitle": "Опциональные настройки запуска", + "launchSettingsDescription": "Промпт, безопасность и переопределения CLI находятся здесь, когда они нужны.", + "teamDetailsTitle": "Опциональные детали команды", + "teamDetailsDescription": "Основной поток остаётся компактным. Открывайте этот блок, когда нужен дополнительный контекст или свой цвет." + }, + "prepare": { + "unsupportedPreload": "Текущая версия preload не поддерживает team:prepareProvisioning. Перезапустите dev app.", + "selectWorkingDirectory": "Выберите рабочую директорию, чтобы проверить окружение запуска.", + "someProvidersNeedAttention": "Некоторым выбранным providers нужно внимание.", + "readyWithNotes": "Все выбранные providers готовы, есть заметки.", + "ready": "Все выбранные providers готовы.", + "failed": "Не удалось подготовить выбранных providers", + "checkingProviders": "Проверка выбранных providers...", + "preparingEnvironment": "Подготовка окружения...", + "selectedProvidersReadyWithNotes": "Выбранные providers готовы (есть заметки)", + "selectedProvidersReady": "Выбранные providers готовы" + }, + "validation": { + "nameMustContainLetterOrDigit": "Имя должно содержать хотя бы одну букву или цифру", + "nameTooLong": "Имя слишком длинное (максимум 128 символов)", + "selectWorkingDirectory": "Выберите рабочую директорию (cwd)", + "memberNameRequired": "Имя участника не может быть пустым", + "memberNameInvalid": "Имя участника должно начинаться с буквы или цифры и содержать только [a-zA-Z0-9._-], максимум 128 символов", + "memberNamesUnique": "Имена участников должны быть уникальными", + "openCodeLeadModelRequired": "Для lead на OpenCode нужно выбрать модель.", + "openCodeTeammateRequired": "Для lead на OpenCode нужен хотя бы один teammate OpenCode.", + "teamLaunching": "Команда сейчас запускается", + "teamNameExists": "Команда с таким именем уже существует", + "checkFormFields": "Проверьте поля формы" + } + }, + "editTeam": { + "actions": { + "cancel": "Отмена", + "save": "Сохранить" + }, + "addMemberLockReason": "Используйте отдельный диалог добавления участника, чтобы добавлять teammates во время работы команды.", + "description": "Измените имя, описание и цвет команды", + "errors": { + "changesSavedRefreshFailed": "Изменения команды сохранены, но не удалось обновить последний вид: {{message}}", + "liveRenameBlocked": "Нельзя переименовывать существующих teammates, пока команда работает. Переименованы: {{names}}", + "memberNameEmpty": "Имя участника не может быть пустым", + "memberNameInvalid": "Имя участника должно начинаться с буквы или цифры, использовать только [a-zA-Z0-9._-] и быть не длиннее 128 символов", + "memberNameNumericSuffix": "Имя участника \"{{name}}\" недоступно, потому что зарезервировано для auto-suffix Claude CLI. Используйте \"{{base}}\".", + "memberNameReserved": "Имя участника \"{{name}}\" зарезервировано", + "memberNamesUnique": "Перед сохранением имена участников должны быть уникальными", + "newLiveTeammates": "Добавляйте новых teammates через отдельный диалог добавления участника, пока команда работает. Edit Team поддерживает только изменение существующих teammates.", + "provisioning": "Настройки команды нельзя редактировать, пока provisioning ещё идёт. Дождитесь завершения запуска и попробуйте снова.", + "restartFailedMany": "Команда сохранена, но не удалось перезапустить этих teammates: {{failures}}", + "restartFailedOne": "Команда сохранена, но не удалось перезапустить этого teammate: {{failures}}", + "saveFailed": "Не удалось сохранить", + "settingsChanged": "Настройки команды изменились, пока диалог был открыт. Откройте его заново и проверьте актуальное состояние перед сохранением.", + "settingsSavedMembersAndRefreshFailed": "Настройки команды сохранены, но изменения участников не применились: {{message}}. Обновление тоже завершилось ошибкой: {{refreshError}}", + "settingsSavedMembersFailed": "Настройки команды сохранены, но изменения участников не применились: {{message}}", + "settingsSavedRefreshFailed": "Настройки команды сохранены, но не удалось обновить последний вид: {{message}}", + "teamNameEmpty": "Имя команды не может быть пустым", + "unsupportedMixedPrimaryMutation": "Live-изменения primary-owned teammates в mixed OpenCode teams пока не поддерживаются. Остановите команду, измените roster и запустите заново. Затронуты: {{names}}" + }, + "fields": { + "colorOptional": "Цвет (необязательно)", + "description": "Описание", + "name": "Имя" + }, + "memberRestartWarning": "Сохранение перезапустит этого teammate, чтобы применить изменения роли, workflow, worktree isolation, provider, model, effort или MCP access.", + "notices": { + "liveRenameBlocked": "Live-сохранение заблокировано, потому что существующие teammates были переименованы. Отмените эти identity changes или сначала остановите команду.", + "newLiveTeammates": "Новых teammates нельзя добавлять из Edit Team, пока команда работает. Используйте диалог добавления участника.", + "provisioning": "Provisioning команды ещё идёт. Редактирование временно заблокировано до завершения запуска.", + "restartMany": "Сохранение перезапустит или перезапустит заново этих teammates, чтобы применить изменения роли, workflow, worktree isolation, provider, model, effort или MCP access: {{names}}.", + "restartOne": "Сохранение перезапустит или перезапустит заново этого teammate, чтобы применить изменения роли, workflow, worktree isolation, provider, model, effort или MCP access: {{names}}.", + "unsupportedMixedPrimaryMutation": "Live-изменения или удаления primary-owned teammates в mixed OpenCode teams требуют остановки и повторного запуска команды: {{names}}." + }, + "placeholders": { + "description": "Описание команды (необязательно)", + "teamName": "Имя команды" + }, + "teamLead": { + "changeRuntime": "Изменить runtime lead", + "changeRuntimeDescription": "Откройте Relaunch Team, чтобы изменить provider, model или effort для lead.", + "modelLockReason": "Runtime team lead управляется через Relaunch Team.", + "readOnlyHint": "Имя и роль team lead здесь доступны только для чтения. Откройте runtime panel в строке lead, чтобы изменить provider, model или effort.", + "role": "Team Lead" + }, + "title": "Редактировать команду" + }, + "memberDraft": { + "actions": { + "remove": "Удалить участника", + "removeAria": "Удалить {{name}}", + "restore": "Восстановить участника", + "restoreAria": "Восстановить {{name}}" + }, + "anthropicContext": { + "defaultSetting": "настройка контекста по умолчанию", + "description": "Контекст Anthropic действует на всю команду для этого запуска: {{mode}}. Используйте checkbox Limit context в runtime panel lead, чтобы изменить это.", + "limitEnabled": "лимит 200K включён" + }, + "mcp": { + "buttonInherit": "MCP inherit", + "buttonScopes": "MCP scopes", + "chooseScopes": "Выбрать scopes", + "inheritLead": "Наследовать lead", + "lockedInfo": "Для всех teammates включён режим только Agent Teams MCP. Этот teammate запустится только с Agent Teams server.", + "mode": "MCP mode", + "scopes": { + "local": "local", + "project": "project", + "user": "user" + }, + "serverNames": "Имена servers", + "settingInfo": "Agent Teams MCP запускает этого teammate только с Agent Teams server. Scope и allowlist modes применяются только к запуску этого teammate.", + "strictAllowlist": "Строгий allowlist", + "tooltip": "{{label}}: управление MCP inheritance policy этого участника", + "agentTeamsMcp": "Agent Teams MCP" + }, + "model": { + "ariaLabel": "{{provider}} provider, {{model}}", + "currentLeadRuntime": "Текущий runtime lead", + "default": "По умолчанию", + "inheritedTooltip": "Provider, model и effort наследуются от lead, пока включена синхронизация.", + "leadSuffix": "{{label}} (lead)", + "liveDisabled": "Изменения provider, model и effort отключены, пока команда работает. Переподключите команду, чтобы применить их безопасно.", + "lockedActionFallback": "Изменения runtime lead открывают Relaunch Team, где можно обновить provider, model и effort.", + "restartWholeTeam": "Сохранение этих runtime changes перезапустит всю команду." + }, + "nameAria": "Имя участника {{index}}", + "nameFallback": "участник {{index}}", + "noRole": "Без роли", + "removed": "Удалён", + "workflow": { + "addTooltip": "Добавить workflow teammate", + "editTooltip": "Редактировать workflow teammate", + "label": "Workflow (необязательно)", + "placeholder": "Как этот agent должен работать и взаимодействовать с другими...", + "saved": "Сохранено" + }, + "worktree": { + "description": "Запускать этого teammate в отдельном git worktree. Apply/reject changes будут работать с этим worktree, а не с workspace lead.", + "label": "Worktree" + }, + "addMembers": { + "title": "Добавить участников", + "description": "Добавить новых участников в {{teamName}}" + }, + "placeholders": { + "name": "member-name", + "mcpServers": "github, sentry" + } + }, + "detail": { + "actions": { + "add": "Добавить", + "cancel": "Отмена", + "delete": "Удалить", + "editCode": "Редактировать код", + "launch": "Запустить", + "remove": "Удалить", + "stop": "Остановить", + "task": "Задача", + "visualize": "Визуализировать" + }, + "deleteTeam": { + "description": "Удалить команду \"{{team}}\"? Это действие необратимо. Все данные команды и задачи будут удалены.", + "title": "Удалить команду" + }, + "draft": { + "descriptionPrefix": "Это draft-команда -", + "descriptionSuffix": "настроена с {{count}} {{member}}, но ещё не provisioned через CLI. Нажмите «Запустить», чтобы выбрать модель и стартовать команду.", + "descriptionSuffix_few": "настроена с {{count}} {{member}}, но ещё не provisioned через CLI. Нажмите «Запустить», чтобы выбрать модель и стартовать команду.", + "descriptionSuffix_many": "настроена с {{count}} {{member}}, но ещё не provisioned через CLI. Нажмите «Запустить», чтобы выбрать модель и стартовать команду.", + "descriptionSuffix_one": "настроена с {{count}} {{member}}, но ещё не provisioned через CLI. Нажмите «Запустить», чтобы выбрать модель и стартовать команду.", + "descriptionSuffix_other": "настроена с {{count}} {{member}}, но ещё не provisioned через CLI. Нажмите «Запустить», чтобы выбрать модель и стартовать команду.", + "member": "участниками", + "member_few": "участниками", + "member_many": "участниками", + "member_one": "участником", + "member_other": "участниками", + "title": "Команда ещё не запущена" + }, + "invalidTab": "Некорректная вкладка команды", + "kanbanSafeData": "Не удалось полностью загрузить kanban. Показаны безопасные данные.", + "loadFailed": "Не удалось загрузить команду", + "loading": "Загрузка команды", + "loadingSidebar": "Загрузка sidebar команды", + "offline": { + "offline": "Команда offline", + "partialFailed": "Последний запуск частично не удался", + "partialMissing": "Последний запуск частично не удался - {{missing}}/{{expected}} teammates не подключились", + "reconciling": "Последний запуск ещё сверяется" + }, + "previous": "Предыдущие: {{paths}}", + "removeMember": { + "description": "Удалить \"{{member}}\" из команды? Задачи и сообщения сохранятся, но это имя нельзя будет использовать повторно.", + "title": "Удалить участника" + }, + "sections": { + "team": "Команда" + }, + "solo": "Solo", + "status": { + "active": "Активно", + "launching": "Запуск...", + "running": "Работает" + }, + "telemetry": { + "cpu": "CPU", + "memory": "Memory" + }, + "tooltips": { + "deleteTeam": "Удалить команду", + "editTeam": "Редактировать команду", + "editUnavailableProvisioning": "Редактирование недоступно, пока provisioning ещё выполняется", + "openBuiltInEditor": "Открыть проект во встроенном редакторе", + "openTeamGraph": "Открыть граф команды", + "stopTeam": "Остановить команду" + }, + "waitingForProvisioning": "Данные команды появятся после завершения provisioning", + "context": { + "title": "Контекст" + } + }, + "review": { + "fileHeader": { + "actions": { + "accept": "Принять", + "discard": "Отменить", + "discardTooltip": "Отменить все правки в этом файле", + "keepMyDraft": "Оставить мой черновик", + "reject": "Отклонить", + "reloadFromDisk": "Перезагрузить с диска", + "restore": "Восстановить", + "restoreTooltip": "Создать или восстановить этот файл на диске из предпросмотра", + "saveFile": "Сохранить файл", + "saveFileTooltip": "Сохранить файл на диск" + }, + "badges": { + "deleted": "УДАЛЁН", + "manualReview": "РУЧНОЙ REVIEW", + "new": "НОВЫЙ", + "worktree": "WORKTREE" + }, + "contentSource": { + "disk-current": "Текущее состояние диска", + "file-history": "История файла", + "git-fallback": "Git fallback", + "ledger-exact": "Task ledger", + "ledger-snapshot": "Снимок ledger", + "snippet-reconstruction": "Реконструировано", + "unavailable": "Содержимое недоступно" + }, + "contentUnavailable": { + "badge": "Содержимое недоступно", + "description": "В ledger записаны metadata для этого изменения, но полный текст недоступен. Обычно это binary, большой файл или hash-only content.", + "safety": "Автоматические accept/reject отключены для этого файла, чтобы избежать небезопасной записи на диск.", + "title": "Текстовое содержимое недоступно" + }, + "disabled": { + "acceptRejectContentUnavailable": "Accept/Reject отключён, потому что полный текст недоступен.", + "acceptRejectMissingOnDisk": "Accept/Reject отключён, пока файл отсутствует на диске.", + "rejectBaselineUnavailable": "Reject отключён, потому что исходный baseline недоступен.", + "rejectContentUnavailable": "Reject отключён, потому что полный текст недоступен.", + "rejectManualLedgerReview": "Reject отключён, потому что это ledger-изменение содержит binary, большой или недоступный content." + }, + "externalChange": { + "changedOnDisk": "Изменён на диске", + "deletedOnDisk": "Удалён на диске", + "recreatedOnDisk": "Создан заново на диске" + }, + "missingOnDisk": { + "badge": "Отсутствует на диске", + "description": "Предпросмотр из agent logs всё ещё доступен, но filesystem не синхронизирован.", + "restorePrefix": "Нажмите", + "restoreSuffix": "чтобы записать preview content обратно на диск.", + "restoreUnavailable": "Полное содержимое файла недоступно для автоматического восстановления.", + "title": "Файл отсутствует на диске" + }, + "pathChange": { + "from": "Из {{path}}", + "to": "В {{path}}" + }, + "worktree": { + "isolated": "Изолированный worktree" + } + }, + "toolbar": { + "stats": { + "pending": "{{count}} pending", + "pending_one": "{{count}} pending", + "pending_few": "{{count}} pending", + "pending_many": "{{count}} pending", + "pending_other": "{{count}} pending", + "accepted": "{{count}} accepted", + "accepted_one": "{{count}} accepted", + "accepted_few": "{{count}} accepted", + "accepted_many": "{{count}} accepted", + "accepted_other": "{{count}} accepted", + "rejected": "{{count}} rejected", + "rejected_one": "{{count}} rejected", + "rejected_few": "{{count}} rejected", + "rejected_many": "{{count}} rejected", + "rejected_other": "{{count}} rejected", + "acrossFiles": "в {{count}} файлах", + "acrossFiles_one": "в {{count}} файле", + "acrossFiles_few": "в {{count}} файлах", + "acrossFiles_many": "в {{count}} файлах", + "acrossFiles_other": "в {{count}} файлах", + "edited": "{{count}} edited", + "edited_one": "{{count}} edited", + "edited_few": "{{count}} edited", + "edited_many": "{{count}} edited", + "edited_other": "{{count}} edited" + }, + "actions": { + "auto": "Auto", + "undo": "Отменить", + "acceptAll": "Принять всё", + "rejectAll": "Отклонить всё", + "applying": "Применение...", + "applyRejections": "Применить отклонения" + }, + "tooltips": { + "autoOn": "Auto-mark файлов как viewed при прокрутке до конца (ON)", + "autoOff": "Auto-mark файлов как viewed при прокрутке до конца (OFF)", + "undo": "Отменить последнюю review operation (Ctrl+Z)", + "acceptAll": "Принять все изменения во всех файлах", + "rejectAll": "Отклонить все безопасно отклоняемые изменения во всех файлах", + "rejectAllDisabled": "У pending файлов нет безопасного original baseline для отклонения.", + "applyRejections": "Применить отклонённые hunks на диск; принятые изменения останутся как есть" + } + }, + "diffError": { + "title": "Не удалось отрендерить diff view", + "unexpected": "При рендеринге diff произошла неожиданная ошибка.", + "actions": { + "retry": "Повторить" + }, + "raw": { + "show": "Показать raw diff data", + "file": "Файл: {{file}}", + "original": "--- Original", + "modified": "+++ Modified", + "charsTotal": "... (всего символов: {{count}})", + "charsTotal_one": "... (всего {{count}} символ)", + "charsTotal_few": "... (всего {{count}} символа)", + "charsTotal_many": "... (всего {{count}} символов)", + "charsTotal_other": "... (всего {{count}} символов)" + } + }, + "fileTree": { + "viewed": "Просмотрено", + "badges": { + "new": "new", + "deleted": "deleted" + }, + "collapseFolder": "Свернуть {{name}}", + "expandFolder": "Развернуть {{name}}", + "empty": { + "noChangedFiles": "Нет изменённых файлов", + "noMatchingFiles": "Нет подходящих файлов" + }, + "searchPlaceholder": "Поиск файлов…", + "filters": { + "unresolved": "Нерешённые", + "rejected": "Отклонённые", + "new": "Новые", + "clear": "Очистить" + } + }, + "diffControls": { + "previousChunk": "Предыдущий chunk", + "nextChunk": "Следующий chunk", + "rejectChange": "Отклонить изменение (⌘N)", + "acceptChange": "Принять изменение (⌘Y)", + "undo": "Отменить", + "keep": "Оставить", + "rejectShortcut": "⌘N", + "acceptShortcut": "⌘Y" + }, + "conflict": { + "title": "Обнаружен конфликт", + "description": "Файл был изменён после правок агента", + "cancel": "Отмена", + "saveResolution": "Сохранить решение", + "editManually": "Редактировать вручную", + "useOriginal": "Использовать исходный вариант", + "keepCurrent": "Оставить текущий вариант" + }, + "fullDiffLoading": { + "titleOne": "Подготовка полного diff", + "titleMany": "Подготовка полных diff: {{count}}", + "subtitleForFile": "Финализируется точный editor diff для {{file}}.", + "subtitleCurrentFile": "Финализируется точный editor diff для текущего файла.", + "subtitleMany": "Определяются точные before/after baseline для загружаемых файлов.", + "previewsReady": "готово preview: {{count}}", + "previewsReady_one": "готово {{count}} preview", + "previewsReady_few": "готово preview: {{count}}", + "previewsReady_many": "готово preview: {{count}}", + "previewsReady_other": "готово preview: {{count}}", + "editorViewLoading": "Загружается editor view", + "filesInProgress": "файлов в процессе: {{count}}", + "filesInProgress_one": "{{count}} файл в процессе", + "filesInProgress_few": "файлов в процессе: {{count}}", + "filesInProgress_many": "файлов в процессе: {{count}}", + "filesInProgress_other": "файлов в процессе: {{count}}", + "filesReady": "готово файлов: {{ready}}/{{total}}", + "progressDescription": "Готово: {{ready}}, ещё загружается: {{loading}}. Preview diff остаются видимыми ниже, пока остальные baseline определяются.", + "singleDescription": "Preview diff остаются видимыми ниже, пока определяется точный baseline." + }, + "fileMissingPrefix": "Файл отсутствует на диске. Этот diff может быть только предпросмотром из логов агента. Используйте", + "restore": "Восстановить", + "fileMissingSuffix": "чтобы создать файл на диске.", + "filePlaceholder": { + "loading": "Загрузка", + "description": "Подготовка полного editor diff для этого файла." + }, + "loading": { + "diff": "DIFF", + "ledgerObjectsProcessed": "обработано ledger-объектов: {{count}}", + "ledgerObjectsProcessed_one": "обработан {{count}} ledger-объект", + "ledgerObjectsProcessed_few": "обработано {{count}} ledger-объекта", + "ledgerObjectsProcessed_many": "обработано {{count}} ledger-объектов", + "ledgerObjectsProcessed_other": "обработано {{count}} ledger-объектов", + "phases": { + "readingLedger": "Чтение task ledger...", + "resolvingFiles": "Определение состояния файлов...", + "checkingWorktree": "Проверка worktree...", + "preparingDiffs": "Подготовка review diff..." + } + }, + "progress": { + "viewed": "{{viewed}}/{{total}} просмотрено" + }, + "scope": { + "readMore": "Подробнее", + "tiers": { + "exact": { + "title": "Область задачи определена точно", + "detail": "В логе сессии найдены маркеры начала и завершения. Diff включает только изменения этой задачи - изменения других задач в тех же файлах исключены." + }, + "endEstimated": { + "title": "Граница завершения оценена приблизительно", + "detail": "Найден только маркер начала - у задачи ещё нет маркера завершения. Показаны изменения от старта задачи до конца сессии. Если после неё в той же сессии выполнялись другие задачи, их изменения тоже могут попасть в diff." + }, + "startEstimated": { + "title": "Граница начала оценена приблизительно", + "detail": "Найден только маркер завершения - начало работы не было зафиксировано. Если до этой задачи в той же сессии выполнялись другие задачи, их изменения в тех же файлах тоже могут попасть в diff." + }, + "allSession": { + "title": "Показаны все изменения сессии", + "detail": "В логе сессии нет маркеров задачи. Нельзя изолировать конкретную задачу - показаны все изменения файлов за всю сессию, включая изменения других задач. Такое возможно со старыми версиями CLI или нестандартными workflows." + } + }, + "ledger": { + "exact": { + "title": "Изменения зафиксированы task ledger", + "detail": "Orchestrator зафиксировал эти изменения файлов во время работы агента над задачей.", + "badge": "Точно по ledger" + }, + "limited": { + "title": "Изменения зафиксированы с ограниченной проверяемостью", + "detail": "Orchestrator зафиксировал эти изменения для задачи, но хотя бы одно изменение пришло из snapshot или metadata-only источника. Проверяйте точные текстовые diff там, где они доступны; binary или недоступный контент может требовать ручного review.", + "mixedBadge": "Смешанная проверяемость", + "needsReviewBadge": "Нужен review" + } + }, + "workInterval": { + "title": "Область задана сохранённым рабочим интервалом", + "detail": "Маркер начала задачи недоступен в логе сессии, поэтому diff ограничен рабочим интервалом задачи, сохранённым на доске.", + "badge": "По интервалу" + }, + "confidence": { + "high": "Высокая уверенность", + "medium": "Средняя уверенность", + "low": "Низкая уверенность", + "bestEffort": "Best effort" + } + }, + "shortcuts": { + "title": "Горячие клавиши", + "actions": { + "nextChange": "Следующее изменение", + "previousChange": "Предыдущее изменение", + "nextFile": "Следующий файл", + "previousFile": "Предыдущий файл", + "acceptChange": "Принять изменение", + "rejectChange": "Отклонить изменение", + "saveFile": "Сохранить файл", + "undo": "Отменить", + "redo": "Повторить", + "toggleShortcuts": "Показать/скрыть подсказки", + "closeDialog": "Закрыть диалог" + } + }, + "timeline": { + "empty": "Нет событий редактирования", + "titleWithCount": "История правок ({{count}})" + }, + "continuousScroll": { + "empty": "Нет изменений файлов для ревью" + }, + "empty": { + "noSafeDiff": "Нет безопасного diff", + "noFileChangesRecorded": "Изменения файлов не записаны", + "noSafeDiffDescription": "Журнал задачи не предоставил безопасный diff файлов для этой задачи.", + "noSafeDiffDiagnosticsDescription": "Журнал задачи не предоставил безопасный diff файлов для этой задачи. Диагностика ниже объясняет причину.", + "noFileEventsYet": "В журнале задачи пока нет событий файлов.", + "noFileEvents": "В журнале задачи нет событий файлов." + } + }, + "messages": { + "actions": { + "bottomSheetActions": "Действия нижней панели сообщений", + "collapseAll": "Свернуть все сообщения", + "collapseSheet": "Свернуть панель", + "expandAll": "Развернуть все сообщения", + "expandSheet": "Развернуть панель", + "floatComposer": "Открепить composer", + "floatMessagesComposer": "Открепить composer сообщений", + "hideSearch": "Скрыть поиск", + "loadOlder": "Загрузить старые сообщения", + "markAllRead": "Отметить всё прочитанным", + "messageActions": "Действия сообщений", + "moveMessagesToBottomSheet": "Переместить сообщения в нижнюю панель", + "moveMessagesToSidebar": "Переместить сообщения в sidebar", + "moveToBottomSheet": "Переместить в нижнюю панель", + "moveToInline": "Переместить inline", + "moveToSidebar": "Переместить в sidebar", + "panelActions": "Действия панели сообщений", + "searchMessages": "Искать сообщения" + }, + "delivery": { + "copied": "Скопировано", + "copyDebugDetails": "Копировать debug details", + "details": "Детали", + "fields": { + "acceptanceUnknown": "acceptanceUnknown", + "delivered": "delivered", + "diagnostics": "diagnostics", + "ledgerStatus": "ledgerStatus", + "messageId": "messageId", + "providerId": "providerId", + "queuedBehindMessageId": "queuedBehindMessageId", + "reason": "reason", + "responsePending": "responsePending", + "responseState": "responseState", + "statusMessageId": "statusMessageId", + "userVisibleMessage": "userVisibleMessage", + "userVisibleNextReviewAt": "userVisibleNextReviewAt", + "userVisibleReasonCode": "userVisibleReasonCode", + "userVisibleState": "userVisibleState", + "visibleReplyCorrelation": "visibleReplyCorrelation", + "visibleReplyMessageId": "visibleReplyMessageId" + } + }, + "panelMode": "Режим панели сообщений", + "title": "Сообщения", + "unread": { + "new": "Новых: {{count}}", + "new_few": "Новых: {{count}}", + "new_many": "Новых: {{count}}", + "new_one": "Новое: {{count}}", + "new_other": "Новых: {{count}}", + "unread": "Непрочитано: {{count}}", + "unread_few": "Непрочитано: {{count}}", + "unread_many": "Непрочитано: {{count}}", + "unread_one": "Непрочитанное: {{count}}", + "unread_other": "Непрочитано: {{count}}" + }, + "filter": { + "ariaLabel": "Фильтровать сообщения", + "tooltip": "Фильтровать сообщения", + "from": "От", + "to": "Кому", + "noData": "Нет данных", + "showStatusUpdates": "Показывать status updates (idle/shutdown)", + "actions": { + "reset": "Сбросить", + "save": "Сохранить" + } + }, + "status": { + "title": "Статус" + }, + "actionMode": { + "label": "Режим действия" + }, + "search": { + "placeholder": "Поиск..." + } + }, + "modelSelector": { + "badges": { + "configured": "Настроено", + "connected": "Подключено", + "failed": "Ошибка", + "free": "Бесплатно", + "local": "Локально", + "needsTest": "Нужен тест", + "verified": "Проверено", + "unavailable": "Недоступно", + "issue": "Проблема" + }, + "customModelId": "Custom model id", + "label": "Модель (optional)", + "multimodelRequired": "Codex и Gemini требуют Multimodel mode.", + "openCode": { + "allSources": "Все OpenCode sources", + "filterSource": "Фильтровать {{source}}", + "filterSources": "Фильтровать OpenCode sources", + "freeOnly": "Только бесплатные", + "freeTooltip": "OpenCode помечает эту модель как бесплатную.", + "loadingModels": "Загрузка моделей OpenCode...", + "noSourcesFound": "Sources не найдены.", + "recommendedOnly": "Только рекомендованные", + "searchSources": "Поиск sources", + "sourcesCount": "OpenCode sources: {{count}}", + "sourcesCount_few": "OpenCode sources: {{count}}", + "sourcesCount_many": "OpenCode sources: {{count}}", + "sourcesCount_one": "OpenCode source: {{count}}", + "sourcesCount_other": "OpenCode sources: {{count}}" + }, + "reason": "Причина: {{reason}}", + "runtimeModelsSyncing": "Явные модели загружаются из текущего runtime. Default остаётся доступным, пока список синхронизируется.", + "fastMode": { + "codexLabel": "Быстрый режим (2x credits)", + "optionalLabel": "Быстрый режим (опционально)", + "defaultOff": "По умолчанию (выкл.)", + "fast": "Fast", + "off": "Выкл.", + "defaultFast": "По умолчанию (Fast)", + "defaultResolvesTo": "Сейчас default resolves to {{mode}}.", + "runtimeBackedHint": "Fast mode зависит от runtime и доступен только когда resolved Anthropic launch model его поддерживает." + }, + "anthropicExtraUsage": { + "pricingDocs": "Открыть документацию Anthropic по ценам" + }, + "searchModels": "Поиск моделей", + "defaultModel": "По умолчанию", + "empty": { + "noSearchMatches": "По этому поиску модели не найдены.", + "recommendedFreeOpenCode": "В текущем списке runtime нет рекомендованных бесплатных моделей OpenCode.", + "freeOpenCode": "В текущем списке runtime нет бесплатных моделей OpenCode.", + "recommendedOpenCode": "В текущем списке runtime нет рекомендованных моделей OpenCode.", + "noModels": "В текущем списке runtime нет моделей." + }, + "openCodeStatus": { + "notReadyTitle": "OpenCode не готов к запуску команды", + "freeModelsAvailableTitle": "Доступны бесплатные модели OpenCode", + "providerNotConnectedTitle": "Provider OpenCode не подключён", + "readyTitle": "OpenCode готов", + "readyMessage": "OpenCode прошёл проверку готовности provider. Выберите его, чтобы использовать модели OpenCode для этой команды.", + "useOpenCode": "Использовать OpenCode", + "badges": { + "check": "Проверка", + "install": "Установить", + "free": "Free", + "setup": "Настроить" + }, + "summary": { + "checking": "Статус OpenCode: проверка runtime", + "status": "Статус OpenCode: {{parts}}" + }, + "summaryParts": { + "teamLaunchBlocked": "запуск команды заблокирован", + "providerOptional": "подключение provider необязательно", + "providerModelsNeedSetup": "provider-backed модели требуют настройки", + "teamLaunchReady": "запуск команды готов", + "runtimeDetected": "runtime найден", + "runtimeMissing": "runtime не найден", + "freeWithoutAuth": "free модели доступны без auth", + "providerConnected": "provider подключён", + "providerNotConnected": "provider не подключён" + }, + "messages": { + "checking": "Приложение ещё проверяет OpenCode runtime. Дождитесь завершения проверки provider status и попробуйте снова.", + "unsupported": "OpenCode не установлен, не найден или найденный runtime не поддерживается. Установите или обновите OpenCode, затем обновите статус provider. Также можно использовать кнопку Install на домашней странице.", + "freeAvailable": "OpenCode найден. Можно использовать free модели OpenCode, например Big Pickle, без подключения provider. Подключайте provider только если нужны provider-backed модели.", + "noFreeListed": "OpenCode найден, но free модель OpenCode пока не указана. Обновите provider status или подключите provider в OpenCode для provider-backed моделей.", + "launchBlocked": "OpenCode установлен и authenticated, но готовность Agent Teams launch заблокирована.", + "ready": "OpenCode готов к запуску команды." + }, + "loadingRuntime": "Статус OpenCode runtime ещё загружается." + }, + "advisory": { + "pingNotConfirmed": "Ping не подтверждён", + "note": "Заметка" + }, + "placeholders": { + "customModelId": "openai/gpt-oss-20b" + }, + "routeGroups": { + "openCodeConfig": "Конфиг OpenCode", + "builtinFree": "Бесплатные встроенные", + "connectedProviders": "Подключённые providers", + "otherCatalog": "Другой каталог OpenCode" + }, + "pricing": { + "free": "Бесплатно", + "inputShort": "in {{rate}}", + "outputShort": "out {{rate}}", + "perMillionSummary": "{{summary}} / 1M", + "inputTitle": "Input: {{rate}} за 1M tokens", + "outputTitle": "Output: {{rate}} за 1M tokens", + "cacheReadTitle": "Cache read: {{rate}} за 1M tokens", + "cacheWriteTitle": "Cache write: {{rate}} за 1M tokens" + }, + "defaultTooltip": { + "anthropicCompatibleWithResolved": "Использует default model Anthropic-compatible endpoint.\nСейчас resolves to {{model}}.", + "anthropicCompatible": "Использует default model Anthropic-compatible endpoint.", + "anthropic": "Использует default model команды Claude.\nResolves to {{longContextModel}} с 1M context или {{limitedContextModel}} с 200K context, когда включён Limit context.", + "openCodeWithResolved": "Использует default model OpenCode.\nСейчас resolves to {{model}}.", + "openCode": "Использует runtime default model OpenCode.", + "runtime": "Использует runtime default для выбранного provider." + }, + "multimodelOff": "Multimodel выключен", + "unavailableInRuntime": "Недоступно в текущем runtime" + }, + "taskDetail": { + "actions": { + "cancel": "Отмена", + "delete": "Удалить", + "markResolved": "Отметить решённым", + "save": "Сохранить" + }, + "attachments": { + "commentAttachment": "Вложение комментария", + "fromComments": "Из комментариев", + "preview": "Предпросмотр {{filename}}" + }, + "changes": { + "badges": { + "attention": "требует внимания", + "noSafeDiff": "нет безопасного diff" + }, + "empty": { + "noFileChangesRecorded": "Изменения файлов не записаны", + "noFileChangesRecordedYet": "Изменения файлов пока не записаны", + "noReviewableChangesRecovered": "Не удалось восстановить изменения файлов для review", + "noSafeDiffAvailable": "Безопасный diff недоступен" + }, + "loadFailed": "Не удалось загрузить сводку изменений задачи", + "loading": "Загрузка изменений...", + "fileCount": "Файлов: {{count}}", + "fileCount_few": "Файла: {{count}}", + "fileCount_many": "Файлов: {{count}}", + "fileCount_one": "Файл: {{count}}", + "fileCount_other": "Файла: {{count}}", + "fileRowsHidden": "Скрыто строк файлов: {{count}}", + "fileRowsHidden_few": "Скрыто строки файлов: {{count}}", + "fileRowsHidden_many": "Скрыто строк файлов: {{count}}", + "fileRowsHidden_one": "Скрыта строка файла: {{count}}", + "fileRowsHidden_other": "Скрыто строки файлов: {{count}}", + "moreDiagnostics": "Ещё диагностик: {{count}}", + "moreDiagnostics_few": "Ещё диагностики: {{count}}", + "moreDiagnostics_many": "Ещё диагностик: {{count}}", + "moreDiagnostics_one": "Ещё диагностика: {{count}}", + "moreDiagnostics_other": "Ещё диагностики: {{count}}", + "moreFiles": "Ещё файлов: {{count}}", + "moreFiles_few": "Ещё файла: {{count}}", + "moreFiles_many": "Ещё файлов: {{count}}", + "moreFiles_one": "Ещё файл: {{count}}", + "moreFiles_other": "Ещё файла: {{count}}", + "openInEditor": "Открыть в редакторе", + "openTask": "Открыть задачу {{subject}}", + "refresh": "Обновить изменения", + "refreshFailed": "Не удалось обновить: {{error}}", + "refreshing": "Обновление", + "refreshingChanges": "Обновление изменений...", + "refreshTeamChanges": "Обновить изменения команды", + "refreshShort": "Обновить", + "reviewDiff": "Открыть diff для review", + "reviewTaskDiff": "Открыть diff задачи для review", + "scannedCandidateTasks": "Просканировано {{requested}} из {{eligible}} candidate tasks", + "tasksDeferred": "Отложено задач за этот проход: {{count}}", + "tasksDeferred_few": "Отложено задачи за этот проход: {{count}}", + "tasksDeferred_many": "Отложено задач за этот проход: {{count}}", + "tasksDeferred_one": "Отложена задача за этот проход: {{count}}", + "tasksDeferred_other": "Отложено задачи за этот проход: {{count}}", + "title": "Изменения" + }, + "clarification": { + "awaitingLead": "Ожидается уточнение от team lead", + "awaitingUser": "Ожидается уточнение от вас" + }, + "description": { + "add": "Добавить описание...", + "edit": "Редактировать описание", + "placeholder": "Описание задачи (поддерживает markdown)" + }, + "loading": { + "fetchingTeamData": "Загрузка данных команды", + "title": "Загрузка задачи..." + }, + "logs": { + "newArriving": "Поступают новые task logs" + }, + "notFound": "Задача не найдена", + "related": { + "blockedBy": "Заблокировано", + "blocks": "Блокирует", + "linkedFrom": "Ссылаются из", + "links": "Связано", + "title": "Связанные задачи" + }, + "review": { + "reviewer": "Reviewer: {{reviewer}}" + }, + "sections": { + "attachments": "Вложения", + "changes": "Изменения", + "comments": "Комментарии", + "description": "Описание", + "taskLogs": "Логи задачи", + "workflowHistory": "История workflow" + }, + "unassigned": "Не назначено", + "workflow": { + "implementationTimeTitle": "Время реализации по сохранённым рабочим интервалам", + "inProgressTime": "Время в работе {{duration}}" + }, + "comments": { + "renderLimit": "Показаны последние {{formattedCount}} комментариев, чтобы интерфейс оставался отзывчивым.", + "badges": { + "approved": "Одобрено", + "reviewRequested": "Запрошен review" + }, + "unknownTime": "неизвестное время", + "actions": { + "reply": "Ответить", + "replyToComment": "Ответить на комментарий", + "showMore": "Показать ещё комментарии ({{visible}}/{{total}})", + "cancelReply": "Отменить ответ", + "comment": "Комментировать" + }, + "attachments": { + "previewAlt": "Предпросмотр вложения", + "downloadFailed": "Не удалось скачать" + }, + "replyingTo": "Ответ на", + "input": { + "placeholder": "Добавьте комментарий... (Enter для отправки)", + "charsLeft": "Осталось символов: {{count}}", + "charsLeft_one": "Остался {{count}} символ", + "charsLeft_few": "Осталось {{count}} символа", + "charsLeft_many": "Осталось {{count}} символов", + "charsLeft_other": "Осталось {{count}} символов" + } + }, + "workflowTimeline": { + "empty": "История workflow не записана", + "currentImplementationInterval": "Текущий интервал реализации", + "implementationIntervalEnded": "Интервал реализации завершился на этом переходе", + "runningPrefix": "идёт ", + "createdAs": "Создано как", + "by": "от", + "reassigned": "Переназначено", + "assignedTo": "Назначено", + "unassignedFrom": "Снято с", + "ownerChanged": "Владелец изменён", + "reviewRequested": "Запрошен review", + "reviewStarted": "Review начат", + "changesRequested": "Запрошены изменения", + "approved": "Одобрено", + "unknownEvent": "Неизвестное событие" + }, + "reviewStates": { + "approved": "Одобрено", + "needsFix": "Нужны правки", + "inReview": "На review" + } + }, + "tasks": { + "createTask": { + "assignee": "Исполнитель", + "assigneeOptional": "Исполнитель (необязательно)", + "blockedByOptional": "Блокируется задачами (необязательно)", + "blockedBySummary": "Задачу будут блокировать: {{tasks}}", + "cancel": "Отмена", + "create": "Создать", + "creating": "Создание...", + "description": "Задача будет создана в директории tasks/ команды и появится на Kanban-доске.", + "descriptionOptional": "Описание (необязательно)", + "detailsPlaceholder": "Детали задачи (поддерживается Markdown)", + "hideOptionalFields": "Скрыть дополнительные поля", + "offlineNotice": { + "after": "- запустите команду, чтобы начать выполнение.", + "before": "Команда офлайн. Задача будет добавлена в" + }, + "promptOptional": "Prompt для исполнителя (необязательно)", + "promptPlaceholder": "Дополнительные инструкции для участника команды...", + "relatedOptional": "Связанные задачи (необязательно)", + "relatedSummary": "Связанные: {{tasks}}", + "saved": "Сохранено", + "searchTasks": "Поиск задач...", + "selectMember": "Выберите участника", + "selectMemberOptional": "Выберите участника...", + "showOptionalFields": "Показать дополнительные поля", + "startImmediately": "Запустить сразу", + "startOfflineHint": "Команда офлайн. Сначала запустите команду, чтобы сразу стартовать задачи.", + "subject": "Тема", + "subjectPlaceholder": "Что нужно сделать?", + "title": "Создать задачу", + "todo": "TODO" + }, + "list": { + "columns": { + "blockedBy": "Заблокировано", + "blocks": "Блокирует", + "id": "ID", + "owner": "Владелец", + "status": "Статус", + "subject": "Тема" + }, + "empty": "В этой команде нет задач", + "filters": { + "allOwners": "Все владельцы", + "allStatuses": "Все статусы", + "ownerAria": "Фильтр задач по владельцу", + "statusAria": "Фильтр задач по статусу" + }, + "showing": "Показано {{shown}} из {{total}}" + }, + "status": { + "completed": "completed", + "deleted": "deleted", + "inProgress": "in_progress", + "pending": "pending" + }, + "statusSummary": { + "progressAria": "Задачи: выполнено {{completed}}/{{total}}", + "inProgress": "{{count}} в работе", + "inProgress_one": "{{count}} в работе", + "inProgress_few": "{{count}} в работе", + "inProgress_many": "{{count}} в работе", + "inProgress_other": "{{count}} в работе", + "pending": "{{count}} ожидают", + "pending_one": "{{count}} ожидает", + "pending_few": "{{count}} ожидают", + "pending_many": "{{count}} ожидают", + "pending_other": "{{count}} ожидают", + "completed": "{{count}} выполнено", + "completed_one": "{{count}} выполнена", + "completed_few": "{{count}} выполнено", + "completed_many": "{{count}} выполнено", + "completed_other": "{{count}} выполнено" + }, + "unassigned": "Не назначено", + "teamPrefix": "Команда:", + "openTask": "Открыть задачу", + "deleteConfirm": { + "title": "Удалить задачу", + "message": "Переместить задачу #{{taskId}} в корзину?", + "confirmLabel": "Удалить", + "cancelLabel": "Отмена" + } + }, + "editor": { + "actions": { + "cancel": "Отмена", + "closeEditor": "Закрыть редактор", + "closeTab": "Закрыть вкладку", + "closeTooltip": "Закрыть редактор (Esc)", + "discard": "Отменить изменения", + "discardAndClose": "Отменить и закрыть", + "keep": "Оставить", + "keepMine": "Оставить мои изменения", + "keyboardShortcuts": "Горячие клавиши", + "overwrite": "Перезаписать", + "refreshAria": "Обновить (F5)", + "refreshTooltip": "Обновить git status (F5)", + "reload": "Перезагрузить", + "retry": "Повторить", + "save": "Сохранить", + "saveAllAndClose": "Сохранить всё и закрыть" + }, + "ariaLabel": "Редактор проекта", + "dialogs": { + "conflictDescription": "Файл был изменён извне после открытия. Перезаписать его вашими изменениями?", + "conflictTitle": "Конфликт сохранения", + "unsavedDescription": "Есть несохранённые изменения. Что сделать?", + "unsavedFileDescription": "В этом файле есть несохранённые изменения. Что сделать?", + "unsavedTitle": "Несохранённые изменения" + }, + "newFile": { + "validation": { + "nameRequired": "Имя не может быть пустым", + "invalidName": "Недопустимое имя", + "invalidCharacters": "Имя содержит недопустимые символы", + "nameTooLong": "Имя слишком длинное" + }, + "placeholders": { + "fileName": "Имя файла...", + "folderName": "Имя папки..." + }, + "aria": { + "newFileName": "Имя нового файла", + "newFolderName": "Имя новой папки" + } + }, + "draftRecovered": "Восстановлены несохранённые изменения из предыдущей сессии.", + "externalChange": { + "changed": "Файл изменён на диске.", + "deleted": "Файла больше нет на диске." + }, + "saveFailed": "Не удалось сохранить: {{error}}", + "sidebar": { + "explorer": "Проводник", + "hide": "Скрыть sidebar", + "hideWithShortcut": "Скрыть sidebar ({{shortcut}})", + "show": "Показать sidebar", + "showWithShortcut": "Показать sidebar ({{shortcut}})" + }, + "searchInFiles": { + "title": "Поиск по файлам", + "closeSearch": "Закрыть поиск", + "closeSearchShortcut": "Закрыть поиск (Esc)", + "searchPlaceholder": "Искать...", + "matchCase": "Учитывать регистр", + "matchCaseToggle": "Aa", + "noResults": "Ничего не найдено", + "resultsSummary": "{{count}} совпадений в {{fileCount}} файлах", + "resultsSummary_one": "{{count}} совпадение в {{fileCount}} файлах", + "resultsSummary_few": "{{count}} совпадения в {{fileCount}} файлах", + "resultsSummary_many": "{{count}} совпадений в {{fileCount}} файлах", + "resultsSummary_other": "{{count}} совпадений в {{fileCount}} файлах", + "truncated": "(обрезано)" + }, + "fileTree": { + "failedToLoadFiles": "Не удалось загрузить файлы: {{error}}", + "loading": "Загрузка файлов...", + "empty": "Файлы не найдены", + "dropForProjectRoot": "Перетащите сюда для корня проекта", + "moveToTrash": "Переместить в корзину", + "moveToTrashConfirm": "Переместить \"{{name}}\" в корзину?", + "cancel": "Отмена" + }, + "goToLine": { + "title": "Перейти к строке", + "position": "(текущая: {{current}}, всего: {{total}})", + "placeholder": "Номер строки, +смещение, -смещение или %", + "go": "Перейти" + }, + "searchPanel": { + "previousMatch": "Предыдущее совпадение", + "nextMatch": "Следующее совпадение", + "close": "Закрыть", + "replacePlaceholder": "Замена", + "replace": "Заменить", + "replaceNext": "Заменить следующее", + "all": "Все", + "replaceAll": "Заменить все" + }, + "statusBar": { + "position": "Стр {{line}}, Кол {{col}}", + "enableWatcher": "Включить отслеживание файлов", + "disableWatcher": "Отключить отслеживание файлов", + "watch": "следить", + "watching": "слежение", + "watchExternalChanges": "Следить за внешними изменениями", + "disableExternalWatcher": "Отключить отслеживание внешних изменений", + "encodingUtf8": "UTF-8", + "spaces": "Пробелы: {{count}}" + }, + "imagePreview": { + "loading": "Загрузка предпросмотра...", + "openFullSize": "Открыть полноразмерный предпросмотр", + "openSystemViewer": "Открыть в системном просмотрщике" + }, + "quickOpen": { + "title": "Быстрое открытие", + "searchPlaceholder": "Поиск файлов по имени...", + "loading": "Загрузка файлов...", + "empty": "Файлы не найдены" + }, + "errorBoundary": { + "crashed": "Редактор упал", + "unknownError": "Неизвестная ошибка" + }, + "binaryPlaceholder": { + "file": "Бинарный файл ({{size}})" + }, + "unsavedChanges": "Несохранённые изменения", + "empty": { + "selectFile": "Выберите файл в дереве, чтобы редактировать" + }, + "search": { + "toggleReplace": "Переключить замену", + "placeholder": "Поиск" + }, + "shortcuts": { + "title": "Горячие клавиши", + "groups": { + "fileOperations": "Операции с файлами", + "search": "Поиск", + "navigation": "Навигация", + "editing": "Редактирование", + "markdown": "Markdown", + "general": "Общее" + }, + "actions": { + "quickOpen": "Быстрое открытие", + "save": "Сохранить", + "saveAll": "Сохранить всё", + "closeTab": "Закрыть вкладку", + "findInFile": "Найти в файле", + "searchInFiles": "Поиск по файлам", + "goToLine": "Перейти к строке", + "nextTab": "Следующая вкладка", + "previousTab": "Предыдущая вкладка", + "cycleTabs": "Переключать вкладки", + "toggleSidebar": "Переключить боковую панель", + "undo": "Отменить", + "redo": "Повторить", + "selectNextMatch": "Выбрать следующее совпадение", + "toggleComment": "Переключить комментарий", + "splitPreview": "Разделённый предпросмотр", + "fullPreview": "Полный предпросмотр", + "closeEditor": "Закрыть редактор" + } + }, + "toolbar": { + "enableWordWrap": "Включить перенос строк", + "disableWordWrap": "Отключить перенос строк", + "closeSplitPreview": "Закрыть разделённый предпросмотр", + "closePreview": "Закрыть предпросмотр" + } + }, + "launch": { + "actions": { + "createSchedule": "Создать расписание", + "creating": "Создание...", + "goToDashboard": "Перейти в Dashboard", + "launchTeam": "Запустить команду", + "launching": "Запуск...", + "relaunchTeam": "Перезапустить команду", + "relaunching": "Перезапуск...", + "saveChanges": "Сохранить изменения", + "saving": "Сохранение..." + }, + "billing": { + "prefix": "С 15 июня 2026 года Anthropic списывает использование", + "readArticle": "Открыть статью Anthropic", + "suffix": "и Agent SDK из ежемесячного Agent SDK credit отдельно от интерактивных лимитов Claude Code. Credit обновляется каждый billing cycle, неиспользованный остаток не переносится." + }, + "conflict": { + "description": "Запуск двух команд в одной директории рискован - они могут конфликтовать при изменении одних и тех же файлов. Лучше выбрать другую директорию или git worktree для изоляции.", + "title": "Другая команда \"{{team}}\" уже работает в этой working directory", + "workingDirectory": "Working directory:" + }, + "description": { + "createSchedule": "Настроить автоматическое выполнение Claude task", + "createScheduleForTeam": "Настроить автоматические запуски для команды \"{{team}}\"", + "editSchedule": "Редактирование расписания для команды \"{{team}}\"", + "launchPrefix": "Запустить команду", + "launchSuffix": "через локальный Claude CLI.", + "relaunchPrefix": "Остановить текущий запуск для", + "relaunchSuffix": "и запустить его заново через локальный Claude CLI." + }, + "prepare": { + "action": { + "launch": "запуск", + "relaunch": "перезапуск" + }, + "blocked": "Runtime environment недоступен - {{action}} заблокирован", + "checkingProviders": "Проверка выбранных providers...", + "failed": "Не удалось подготовить выбранных providers", + "preflight": "Pre-flight проверка, чтобы поймать ошибки до действия: {{action}}", + "preparingEnvironment": "Подготовка environment...", + "ready": "Все выбранные providers готовы.", + "readyWithNotes": "Все выбранные providers готовы, есть заметки.", + "unsupportedPreload": "Текущая версия preload не поддерживает team:prepareProvisioning. Перезапустите dev app.", + "selectWorkingDirectory": "Выберите рабочую директорию, чтобы проверить окружение запуска.", + "someProvidersNeedAttention": "Некоторым выбранным providers нужно внимание." + }, + "prompt": { + "label": "Prompt", + "oneShotPrefix": "Этот prompt будет передан в", + "oneShotSuffix": "для one-shot выполнения", + "saved": "Сохранено", + "schedulePlaceholder": "Инструкции для Claude, которые нужно выполнить по расписанию...", + "teamLeadOptional": "Prompt для team lead (optional)", + "teamLeadPlaceholder": "Инструкции для team lead..." + }, + "providerChanged": "Provider изменён с {{from}} на {{to}}. Предыдущая lead session не будет возобновлена, lead начнёт с fresh context, чтобы новый runtime применился корректно.", + "relaunchFreshSession": "Team relaunch запускает fresh lead session. Durable team state, task board и настройки участников будут rehydrated в launch prompt.", + "relaunchWarning": { + "description": "Сохранение этих настроек остановит текущий team process, сохранит обновлённый roster и снова запустит команду с новым runtime.", + "title": "Relaunch перезапустит текущий team run" + }, + "schedule": { + "labelOptional": "Label (optional)", + "labelPlaceholder": "Например: Daily code review, Nightly tests...", + "maxBudgetUsd": "Max budget (USD)", + "maxTurns": "Max turns", + "noLimit": "Без лимита", + "noMatches": "Команды не найдены по поиску.", + "noTeams": "Нет доступных команд. Сначала создайте команду.", + "searchTeams": "Поиск команд...", + "selectTeam": "Выберите команду...", + "team": "Команда", + "title": "Расписание" + }, + "title": { + "createSchedule": "Создать расписание", + "editSchedule": "Редактировать расписание", + "launch": "Запуск команды", + "relaunch": "Перезапуск команды" + }, + "errors": { + "loadProjectsFailed": "Не удалось загрузить проекты", + "saveScheduleFailed": "Не удалось сохранить расписание", + "relaunchFailed": "Не удалось перезапустить команду", + "launchFailed": "Не удалось запустить команду" + }, + "validation": { + "openCodeLeadModelRequired": "Для lead на OpenCode нужно выбрать модель.", + "openCodeTeammateRequired": "Для lead на OpenCode нужен хотя бы один teammate OpenCode.", + "selectWorkingDirectory": "Выберите рабочую директорию (cwd)", + "fixMemberNames": "Исправьте имена участников перед запуском", + "memberNamesUnique": "Имена участников должны быть уникальными перед запуском" + }, + "optionalSettings": { + "relaunchTitle": "Настройки перезапуска", + "title": "Дополнительные настройки запуска", + "relaunchDescription": "Проверьте состав и runtime lead перед перезапуском команды.", + "description": "Оставьте запуск сфокусированным на пути проекта и раскрывайте этот блок только когда нужен дополнительный контроль." + } + }, + "list": { + "actions": { + "copyTeam": "Скопировать команду", + "createTeam": "Создать команду", + "deleteForever": "Удалить навсегда", + "deletePermanently": "Удалить окончательно", + "deleteTeam": "Удалить команду", + "launching": "Запуск...", + "launchTeam": "Запустить команду", + "relaunchTeam": "Перезапустить команду", + "restore": "Восстановить", + "restoreTeam": "Восстановить команду", + "retry": "Повторить", + "stopTeam": "Остановить команду", + "stopping": "Остановка..." + }, + "electronOnly": { + "description": "В browser mode доступ к локальным директориям `~/.claude/teams` недоступен.", + "title": "Команды доступны только в Electron mode" + }, + "empty": { + "description": "Создайте команду здесь, чтобы начать. Она автоматически появится в списке.", + "localOnly": "Создание команд доступно только в локальном Electron mode.", + "title": "Команды не найдены" + }, + "filter": { + "clearAll": "Очистить всё", + "label": "Фильтровать команды", + "projectPriority": "Приоритет проекта", + "status": "Статус" + }, + "loadFailed": "Не удалось загрузить команды", + "loading": "Загрузка команд...", + "localOnly": "Доступно только в локальном Electron mode.", + "membersCount": "Участников: {{count}}", + "membersCount_few": "Участников: {{count}}", + "membersCount_many": "Участников: {{count}}", + "membersCount_one": "Участник: {{count}}", + "membersCount_other": "Участников: {{count}}", + "noDescription": "Нет описания", + "noMatches": "Нет команд по текущим фильтрам", + "partial": { + "pending": "Последний запуск ещё сверяется.", + "skipped": "В последнем запуске были пропущены teammates.", + "skippedWithCount": "Последний запуск пропустил {{count}}/{{expected}} teammate.", + "skippedWithCount_few": "Последний запуск пропустил {{count}}/{{expected}} teammates.", + "skippedWithCount_many": "Последний запуск пропустил {{count}}/{{expected}} teammates.", + "skippedWithCount_one": "Последний запуск пропустил {{count}}/{{expected}} teammate.", + "skippedWithCount_other": "Последний запуск пропустил {{count}}/{{expected}} teammates.", + "stopped": "Последний запуск остановился до подключения всех teammates.", + "stoppedWithCount": "Последний запуск остановился до подключения {{count}}/{{expected}} teammate.", + "stoppedWithCount_few": "Последний запуск остановился до подключения {{count}}/{{expected}} teammates.", + "stoppedWithCount_many": "Последний запуск остановился до подключения {{count}}/{{expected}} teammates.", + "stoppedWithCount_one": "Последний запуск остановился до подключения {{count}}/{{expected}} teammate.", + "stoppedWithCount_other": "Последний запуск остановился до подключения {{count}}/{{expected}} teammates." + }, + "searchPlaceholder": "Поиск команд...", + "sections": { + "otherTeams": "Другие команды", + "projectTeams": "Команды для {{project}}", + "selectedProject": "выбранного проекта" + }, + "solo": "Solo", + "status": { + "active": "Активно", + "deleted": "Удалено", + "launching": "Запуск...", + "offline": "Offline", + "partialFailure": "Запуск частично не удался", + "partialPending": "Bootstrap ожидает", + "partialSkipped": "Запуск пропустил участника", + "running": "Работает" + }, + "title": "Выбор команды", + "trash": "Корзина ({{count}})", + "trash_few": "Корзина ({{count}})", + "trash_many": "Корзина ({{count}})", + "trash_one": "Корзина ({{count}})", + "trash_other": "Корзина ({{count}})", + "deleteDraft": { + "title": "Удалить черновик", + "message": "Удалить черновик команды «{{teamName}}»? Это действие нельзя отменить.", + "confirmLabel": "Удалить", + "cancelLabel": "Отмена" + }, + "moveToTrash": { + "title": "Переместить в корзину", + "message": "Переместить команду «{{teamName}}» в корзину? Её можно будет восстановить позже.", + "confirmLabel": "В корзину", + "cancelLabel": "Отмена" + }, + "deleteForever": { + "title": "Удалить навсегда", + "message": "Удалить команду «{{teamName}}» навсегда? Все данные будут потеряны.", + "confirmLabel": "Удалить навсегда", + "cancelLabel": "Отмена" + } + }, + "messageComposer": { + "crossTeam": { + "hint": "Совет: cross-team сообщения идут team lead целевой команды. Если ответ должен вернуться вашему team lead, а не вам, явно напишите это в сообщении." + }, + "attachments": { + "attachFiles": "Прикрепить файлы (paste или drag & drop)", + "unavailable": "Вложения недоступны", + "disabledHint": "Файлы можно отправлять online team lead и online OpenCode teammates. Удалите вложения или смените получателя.", + "restrictions": { + "crossTeam": "Файловые вложения не поддерживаются для cross-team сообщений", + "teamOffline": "Команда должна быть online, чтобы прикреплять файлы", + "unsupportedRecipient": "Файлы можно отправлять team lead или OpenCode teammates", + "openCodeOffline": "Команда должна быть online, чтобы прикреплять файлы для OpenCode teammates", + "sending": "Дождитесь завершения текущей отправки перед добавлением файлов", + "maximumReached": "Достигнут лимит вложений", + "leadOnly": "Файлы можно отправлять только team lead" + } + }, + "slash": { + "restrictions": { + "attachments": "Slash commands требуют live team lead и не отправляются с вложениями", + "crossTeam": "Slash commands можно запускать только на team lead текущей команды", + "notLead": "Slash commands можно отправлять только team lead", + "leadOffline": "Slash commands требуют, чтобы team lead был online" + } + }, + "status": { + "reusedCrossTeamRequest": "Повторно использован недавний cross-team request", + "teamOffline": "Команда offline" + }, + "input": { + "charsLeft": "Осталось символов: {{count}}", + "charsLeft_one": "Остался {{count}} символ", + "charsLeft_few": "Осталось {{count}} символа", + "charsLeft_many": "Осталось {{count}} символов", + "charsLeft_other": "Осталось {{count}} символов", + "teamLaunchingPlaceholder": "Команда запускается... сообщение будет поставлено в очередь inbox delivery.", + "crossTeamPlaceholder": "Cross-team сообщение для {{team}}...", + "teamFallback": "команда", + "placeholder": "Напишите сообщение... (Enter для отправки, Shift+Enter для новой строки)", + "slashTip": "Совет: используйте \"/\", чтобы запускать любые Claude commands." + }, + "teamSelector": { + "thisTeam": "Эта команда", + "current": "текущая", + "online": "online", + "offline": "offline", + "onlineTitle": "Online", + "offlineTitle": "Offline" + }, + "recipient": { + "select": "Выбрать...", + "searchPlaceholder": "Поиск...", + "noResults": "Ничего не найдено" + }, + "actions": { + "voiceToText": "Voice to text", + "send": "Отправить", + "sendingUnavailableLaunching": "Отправка недоступна, пока команда запускается" + } + }, + "claudeLogs": { + "filter": { + "ariaLabel": "Фильтровать Claude logs", + "tooltip": "Фильтровать logs", + "sections": { + "stream": "Stream", + "content": "Content" + }, + "kinds": { + "output": "Output", + "thinking": "Thinking", + "tool": "Tool calls" + }, + "actions": { + "reset": "Сбросить", + "save": "Сохранить" + }, + "streams": { + "stdout": "stdout", + "stderr": "stderr" + } + }, + "rawLineCount": "сырых строк: {{formattedCount}}", + "rawLineCount_one": "{{formattedCount}} сырая строка", + "rawLineCount_few": "сырых строки: {{formattedCount}}", + "rawLineCount_many": "сырых строк: {{formattedCount}}", + "rawLineCount_other": "сырых строки: {{formattedCount}}", + "rawLinesCaptured": "{{count}} записано", + "emptyRawLogs": "{{count}}; среди них пока нет assistant/tool output.", + "noLogsYet": "Логов пока нет.", + "teamNotRunning": "Команда не запущена.", + "searchPlaceholder": "Искать в логах...", + "clearSearch": "Очистить поиск", + "newCount": "+{{count}} новых", + "loading": "Загрузка...", + "showMore": "Показать больше", + "noLogsCaptured": "Логи не записаны.", + "noMatchingLogs": "Подходящих логов нет.", + "openFullscreen": "Открыть логи на весь экран", + "fullscreen": "На весь экран", + "viewingFullscreen": "Просмотр в полноэкранном режиме", + "logsTitle": "Логи" + }, + "agentGraph": { + "popover": { + "externalTeam": "Внешняя команда", + "process": { + "startedBy": "Запущено:", + "at": "Время:", + "openUrl": "Открыть URL" + }, + "overflow": { + "hiddenTasks": "Скрытые задачи", + "empty": "Нет доступных скрытых задач." + }, + "member": { + "lead": "Lead", + "workingOn": "работает над", + "recentTools": "Недавние tools", + "spawn": { + "waitingToStart": "ожидает запуска", + "starting": "запускается", + "failed": "ошибка" + }, + "state": { + "active": "активен", + "idle": "ожидает", + "offline": "offline", + "runningTool": "запускает tool" + }, + "activeTool": { + "running": "Tool выполняется", + "failed": "Tool завершился ошибкой", + "finished": "Tool завершён" + }, + "actions": { + "message": "Сообщение", + "profile": "Профиль", + "task": "Задача" + } + } + }, + "logPreview": { + "logs": "Логи", + "loading": "Загрузка логов", + "more": "+{{count}} ещё", + "more_one": "+{{count}} ещё", + "more_few": "+{{count}} ещё", + "more_many": "+{{count}} ещё", + "more_other": "+{{count}} ещё" + }, + "blockingEdge": { + "title": "Блокирующая зависимость", + "blocks": "блокирует", + "close": "Закрыть", + "blockingHiddenTasks": "Скрытые блокирующие задачи", + "blockedHiddenTasks": "Скрытые заблокированные задачи" + }, + "activityHud": { + "activity": "Активность", + "noRecentActivity": "Недавней активности нет", + "more": "+{{count}} ещё", + "more_one": "+{{count}} ещё", + "more_few": "+{{count}} ещё", + "more_many": "+{{count}} ещё", + "more_other": "+{{count}} ещё" + }, + "provisioning": { + "launchDetails": "Детали запуска", + "launchDetailsDescription": "Подробный прогресс запуска команды, live-вывод и логи CLI." + } + }, + "projectPath": { + "label": "Проект", + "source": { + "claude": "Найдено Claude", + "codex": "Найдено Codex", + "mixed": "Найдено Claude и Codex" + }, + "deleted": { + "title": "Папка проекта больше не существует", + "label": "Удалён" + }, + "mode": { + "projectList": "Из списка проектов", + "customPath": "Свой путь" + }, + "loadingProjects": "Загрузка проектов...", + "selectProject": "Выберите проект...", + "searchPlaceholder": "Поиск проекта по имени или пути", + "empty": "Ничего не найдено", + "selectFromList": "Выберите проект из списка", + "noProjects": "Проекты не найдены, переключитесь на свой путь.", + "customWorkingDirectory": "Custom working directory", + "browse": "Выбрать", + "createAutomatically": "Если директории нет, она будет создана автоматически." + }, + "members": { + "badges": { + "worktree": "worktree" + }, + "runtimeTelemetry": { + "title": "Локальная нагрузка runtime", + "description": "Только parent и child processes. Remote LLM inference не учитывается.", + "cpu": "CPU", + "memory": "Память", + "summedRss": "суммарный RSS", + "sharedHost": "Метрика shared OpenCode host. Она не эксклюзивна для этого участника.", + "processTreeCapped": "Process tree был ограничен для этого sample.", + "rssHint": "RSS может включать shared pages, поэтому это лучше читать как сигнал нагрузки, а не эксклюзивную память." + }, + "editor": { + "title": "Участники", + "addMember": "Добавить участника", + "editAsJson": "Редактировать как JSON", + "runInSeparateWorktrees": "Запускать участников в отдельных worktree", + "agentTeamsMcpOnly": "Только Agent Teams MCP", + "removedCount": "Удалённые ({{count}})", + "removedModelLockReason": "Удалённые участники сохранены для истории soft delete. Восстановите их, чтобы редактировать настройки.", + "memberNamesUnique": "Имена участников должны быть уникальными" + }, + "stats": { + "computing": "Расчёт статистики...", + "empty": "Статистика недоступна", + "lines": "Строки", + "linesInfo": "Приблизительно. Точно для инструментов Edit и Write. Записи файлов через Bash оцениваются по паттернам команд (heredoc, echo, sed) и могут быть занижены.", + "files": "Файлы", + "toolCalls": "Вызовы инструментов", + "tokens": "Токены", + "toolUsage": "Использование инструментов", + "filesTouched": "Затронутые файлы ({{count}})", + "viewAllChanges": "Показать все изменения", + "showLess": "Показать меньше", + "moreFiles": "+{{count}} ещё", + "footer": "{{count}} сессий · рассчитано {{computedAgo}}", + "footer_one": "{{count}} сессия · рассчитано {{computedAgo}}", + "footer_few": "{{count}} сессии · рассчитано {{computedAgo}}", + "footer_many": "{{count}} сессий · рассчитано {{computedAgo}}", + "footer_other": "{{count}} сессии · рассчитано {{computedAgo}}" + }, + "logs": { + "searching": "Поиск логов...", + "empty": "Логи не найдены", + "waitingForTaskActivity": "Задача выполняется - ожидаем активность сессии (автообновление)...", + "noTaskActivity": "Для этой задачи пока нет активности сессии", + "noMemberActivity": "У этого участника пока нет записанной активности сессии", + "leadSessionTooltip": "Полные логи сессии team lead - полезны для общего orchestration-контекста, не специфичного для этого агента", + "memberSessionTooltip": "Полные логи постоянной сессии участника - полезны, когда работа идёт в корневой member session, а не в subagent-файле", + "startedAt": "начато {{time}}", + "active": "активно", + "showDetails": "Показать детали", + "hideDetails": "Скрыть детали", + "loadingDetails": "Загрузка деталей...", + "failedToLoadDetails": "Не удалось загрузить детали" + }, + "detail": { + "relaunchOpenCode": "Перезапустить OpenCode", + "restart": "Перезапустить", + "legacyLogsFallback": "Fallback legacy-логов", + "copyDiagnostics": "Скопировать диагностику", + "pid": "PID {{pid}}", + "removedAt": "Удалён {{date}}", + "failedToRestartMember": "Не удалось перезапустить участника", + "sendMessage": "Отправить сообщение", + "assignTask": "Назначить задачу", + "remove": "Удалить" + }, + "list": { + "loading": "Загрузка участников команды", + "unavailable": "Состав участников недоступен", + "unavailableDescription": "{{count}} участников известны из метаданных команды, но детали состава отсутствуют.", + "unavailableDescription_one": "{{count}} участник известен из метаданных команды, но детали состава отсутствуют.", + "unavailableDescription_few": "{{count}} участника известны из метаданных команды, но детали состава отсутствуют.", + "unavailableDescription_many": "{{count}} участников известны из метаданных команды, но детали состава отсутствуют.", + "unavailableDescription_other": "{{count}} участника известны из метаданных команды, но детали состава отсутствуют.", + "soloLeadOnly": "Solo team - только lead", + "removedCount": "Удалённые ({{count}})" + }, + "executionLog": { + "empty": "Нечего показать", + "emptyUserMessage": "{{time}} - (пусто)", + "agentInstructions": "Инструкции агента", + "memberTurn": "{{member}} turn", + "agentTurn": "Ход агента", + "turn": "ход" + }, + "recentMessages": { + "latest": "Последние сообщения", + "latestForMember": "Последние сообщения - {{member}}", + "loadMore": "Загрузить ещё", + "expand": "Развернуть", + "collapse": "Свернуть" + }, + "leadModel": { + "defaultModel": "По умолчанию", + "providerModelAria": "Провайдер {{provider}}, модель {{model}}", + "leadShort": "лид", + "teamLead": "Лид команды", + "syncWithTeammates": "Синхронизировать модель с участниками", + "anthropicTeamWide": "Anthropic для всей команды", + "runtimeInheritance": "Рантайм лида применяется к участникам, если они не задали собственного провайдера или модель.", + "anthropicContextLimit": "Лимит контекста 200K действует на всю команду для рантаймов Anthropic в этом запуске, включая кастомных участников Anthropic." + }, + "runtimeLogs": { + "autoRefresh": "Автообновление", + "wrapLines": "Перенос строк", + "loadingTail": "Загрузка хвоста лога процесса...", + "empty": "Лог процесса для этого участника пока не сохранен." + }, + "tasks": { + "empty": "У этого участника нет назначенных задач" + }, + "messages": { + "loadOlder": "Загрузить более старые сообщения", + "filters": { + "all": "Все", + "messages": "Сообщения", + "comments": "Комментарии" + }, + "empty": { + "loading": "Загрузка активности...", + "noComments": "У этого участника нет комментариев", + "noLoadedMessages": "Загруженных сообщений этого участника пока нет", + "noMessages": "Сообщений с этим участником нет", + "noLoadedActivity": "Загруженной активности этого участника пока нет", + "noActivity": "Активности с этим участником нет" + } + }, + "actions": { + "openProfile": "Открыть профиль", + "editRole": "Редактировать роль", + "sendMessage": "Отправить сообщение", + "assignTask": "Назначить задачу" + }, + "roleSelect": { + "customRolePlaceholder": "Введите свою роль..." + } + }, + "schedule": { + "count": "{{count}} schedules", + "count_one": "{{count}} schedule", + "count_few": "{{count}} schedules", + "count_many": "{{count}} schedules", + "count_other": "{{count}} schedules", + "nextRun": "Next: {{next}}", + "actions": { + "runNow": "Запустить сейчас", + "edit": "Редактировать", + "pause": "Пауза", + "resume": "Возобновить", + "delete": "Удалить", + "addSchedule": "Добавить schedule" + }, + "runHistory": { + "loading": "Загрузка run history...", + "empty": "Запусков ещё нет" + }, + "runLog": { + "title": "Лог запуска", + "exitCode": "exit {{code}}", + "retryCount": "retry {{count}}/{{max}}", + "stillRunning": "Задача ещё выполняется...", + "loadingLogs": "Загрузка логов...", + "errors": "Ошибки", + "close": "Закрыть" + }, + "cron": { + "expression": "Cron-выражение", + "highFrequencyWarning": "Высокая частота расписания (интервал меньше 5 минут)", + "nextRuns": "Следующие запуски:", + "timezone": "Часовой пояс", + "selectTimezone": "Выберите часовой пояс", + "warmUpTime": "Время прогрева", + "warmUpDescription": "Подготавливает выбранных провайдеров перед запуском по расписанию", + "errors": { + "enterExpression": "Введите cron-выражение", + "invalidExpression": "Некорректное cron-выражение" + }, + "presets": { + "everyHour": "Каждый час", + "everySixHours": "Каждые 6 часов", + "dailyAtNine": "Ежедневно в 9:00", + "weekdaysAtNine": "По будням в 9:00", + "mondayAtNine": "В понедельник в 9:00", + "everyThirtyMinutes": "Каждые 30 минут" + }, + "warmUpOptions": { + "none": "Без прогрева", + "fiveMinutes": "5 мин", + "tenMinutes": "10 мин", + "fifteenMinutes": "15 мин", + "thirtyMinutes": "30 мин" + } + }, + "empty": { + "title": "Расписаний пока нет", + "description": "Создайте расписание, чтобы автоматически запускать задачи Claude по cron." + }, + "title": "Расписания", + "status": { + "active": "Активно", + "paused": "Пауза", + "disabled": "Отключено" + }, + "runStatus": { + "pending": "Ожидает", + "warmingUp": "Прогрев", + "warm": "Готово к запуску", + "running": "Выполняется", + "completed": "Завершено", + "failed": "Ошибка", + "interrupted": "Прервано", + "cancelled": "Отменено" + } + }, + "openCodeContextConfigHint": { + "summary": "Локальные модели OpenCode могут использовать бюджет контекста OpenCode вместо ограничений только в промпте.", + "description": "Добавьте соответствующие лимиты в конфиг OpenCode для провайдера и модели этого участника. Это поможет OpenCode выполнять compact и prune до того, как локальная модель переполнит контекстное окно.", + "replacePrefix": "Замените", + "and": "и", + "replaceSuffix": "на ID провайдера и модели из вашей настройки OpenCode. Инструкции в промпте вроде", + "promptInstructionsSuffix": "слабее, потому что запрос собирается до того, как модель их прочитает.", + "providerLimits": "Лимиты провайдера", + "compactionConfig": "Конфиг compaction" + }, + "sessions": { + "noProjectPath": "Путь проекта не привязан", + "provisioningHint": "Сессии появятся после provisioning команды", + "projectNotFound": "Проект не найден", + "loading": "Загрузка сессий...", + "empty": "Сессии не найдены", + "showAllSessions": "Показать для всех сессий", + "lead": "lead", + "removeFilter": "Убрать фильтр", + "filterBySession": "Фильтровать по этой сессии", + "openSession": "Открыть сессию", + "title": "Сессии" + }, + "provisioning": { + "pid": "PID {{pid}}", + "cancel": "Отменить", + "moreWarningsHidden": "Скрыто ещё {{count}} предупреждений", + "diagnostics": "Диагностика", + "liveOutput": "Живой вывод", + "diagnosticsCopied": "Диагностика скопирована", + "copyDiagnostics": "Скопировать диагностику", + "copied": "Скопировано", + "noOutput": "Вывод пока не записан.", + "cliLogs": "Логи CLI", + "steps": { + "starting": "Запуск", + "configuring": "Настройка команды", + "assembling": "Подключение участников", + "finalizing": "Завершение" + }, + "providerStatus": { + "status": { + "checking": "проверка...", + "ready": "OK", + "notes": "OK (есть заметки)", + "failed": "ERR", + "pending": "ожидание" + }, + "detailSummary": { + "cliBinaryMissing": "CLI binary не найден", + "openCodeRuntimeMissing": "OpenCode runtime отсутствует", + "openCodeWindowsAccessBlocked": "Доступ OpenCode в Windows заблокирован", + "openCodeNoOutput": "Проверка OpenCode runtime не вернула вывод", + "openCodeMcpUnreachable": "OpenCode app MCP недоступен", + "workingDirectoryMissing": "Working directory отсутствует", + "cliBinaryCouldNotStart": "CLI binary не удалось запустить", + "cliPreflightIncomplete": "CLI preflight не завершился", + "authenticationRequired": "Требуется аутентификация", + "runtimeProviderNotConfigured": "Runtime provider не настроен", + "cliPreflightFailed": "CLI preflight завершился с ошибкой", + "selectedModelCompatible": "Выбранная модель совместима", + "selectedModelCompatibilityPending": "Совместимость выбранной модели ещё проверяется", + "selectedModelAvailable": "Выбранная модель доступна", + "selectedModelVerified": "Выбранная модель проверена", + "selectedModelUnavailable": "Выбранная модель недоступна", + "selectedModelTimedOut": "Проверка выбранной модели истекла по таймауту", + "selectedModelCheckFailed": "Проверка выбранной модели не удалась", + "selectedModelDeferred": "Проверка выбранной модели отложена", + "selectedModelPingNotConfirmed": "Ping выбранной модели не подтверждён", + "readyWithNotes": "Готово с заметками", + "needsAttention": "Требует внимания" + }, + "modelChecksSummary": "Проверки выбранных моделей - {{details}}", + "modelParts": { + "unavailable": "{{count}} модель недоступна", + "unavailable_one": "{{count}} модель недоступна", + "unavailable_few": "{{count}} модели недоступны", + "unavailable_many": "{{count}} моделей недоступно", + "unavailable_other": "{{count}} модели недоступны", + "checkFailed": "{{count}} проверка модели не удалась", + "checkFailed_one": "{{count}} проверка модели не удалась", + "checkFailed_few": "{{count}} проверки моделей не удались", + "checkFailed_many": "{{count}} проверок моделей не удались", + "checkFailed_other": "{{count}} проверки моделей не удались", + "timedOut": "{{count}} модель по таймауту", + "timedOut_one": "{{count}} модель по таймауту", + "timedOut_few": "{{count}} модели по таймауту", + "timedOut_many": "{{count}} моделей по таймауту", + "timedOut_other": "{{count}} модели по таймауту", + "deferred": "{{count}} проверка отложена", + "deferred_one": "{{count}} проверка отложена", + "deferred_few": "{{count}} проверки отложены", + "deferred_many": "{{count}} проверок отложено", + "deferred_other": "{{count}} проверки отложены", + "pingNotConfirmed": "{{count}} ping не подтверждён", + "pingNotConfirmed_one": "{{count}} ping не подтверждён", + "pingNotConfirmed_few": "{{count}} ping не подтверждены", + "pingNotConfirmed_many": "{{count}} ping не подтверждены", + "pingNotConfirmed_other": "{{count}} ping не подтверждены", + "compatibilityPending": "{{count}} совместима, глубокая проверка продолжается", + "compatibilityPending_one": "{{count}} совместима, глубокая проверка продолжается", + "compatibilityPending_few": "{{count}} совместимы, глубокая проверка продолжается", + "compatibilityPending_many": "{{count}} совместимы, глубокая проверка продолжается", + "compatibilityPending_other": "{{count}} совместимы, глубокая проверка продолжается", + "compatible": "{{count}} совместима", + "compatible_one": "{{count}} совместима", + "compatible_few": "{{count}} совместимы", + "compatible_many": "{{count}} совместимы", + "compatible_other": "{{count}} совместимы", + "checking": "{{count}} проверяется", + "checking_one": "{{count}} проверяется", + "checking_few": "{{count}} проверяются", + "checking_many": "{{count}} проверяются", + "checking_other": "{{count}} проверяются", + "available": "{{count}} доступна", + "available_one": "{{count}} доступна", + "available_few": "{{count}} доступны", + "available_many": "{{count}} доступны", + "available_other": "{{count}} доступны", + "verified": "{{count}} проверена", + "verified_one": "{{count}} проверена", + "verified_few": "{{count}} проверены", + "verified_many": "{{count}} проверены", + "verified_other": "{{count}} проверены" + }, + "openProviderSettings": "Открыть настройки {{provider}}", + "copied": "Скопировано", + "copyDiagnostics": "Скопировать диагностику", + "deepVerificationPending": "Глубокая проверка ещё выполняется. Бесплатные модели OpenCode могут проверяться около 20 секунд.", + "progress": { + "checkingSelectedProviders": "Проверка выбранных провайдеров параллельно...", + "checkingProvider": "Проверка провайдера {{provider}}...", + "checkingProviders": "Проверка провайдеров {{providers}}..." + }, + "failureHints": { + "openCodeAccessDenied": "Исправьте права на папку или перенесите проект в папку, доступную пользователю для записи. Запуск от администратора - только временный обходной путь.", + "openCodeBridgeNoOutput": "Перезапустите приложение и runtime OpenCode, затем повторите. Если повторится, скопируйте diagnostics.", + "workingDirectoryMissing": "Выберите существующую рабочую папку, затем откройте этот диалог заново.", + "authenticationRequired": "Авторизуйте нужного провайдера в Claude CLI, затем откройте этот диалог заново.", + "runtimeProviderNotConfigured": "Настройте выбранный provider runtime, затем откройте этот диалог заново.", + "openCodeRuntimeMissing": "Установите или повторите запуск runtime OpenCode из карточки статуса провайдера, затем откройте этот диалог заново.", + "openCodeAppMcpUnreachable": "Повторите launch, чтобы обновить OpenCode app MCP bridge. Если повторится, перезапустите приложение и runtime OpenCode.", + "cliBinaryMissing": "Убедитесь, что локальный бинарь Claude CLI существует и может запускаться, затем откройте этот диалог заново.", + "default": "Исправьте проблему выше, затем откройте этот диалог заново." + } + }, + "presentation": { + "awaitingPermission": "{{count}} участник(ов) ожидает подтверждения permission", + "nameListWithMore": "{{names}}, +{{count}} ещё", + "waitingForOpenCode": "Ожидание OpenCode: {{names}}", + "bootstrapStalled": "Bootstrap завис: {{names}}", + "bootstrapStalledWithOpenCodeWait": "{{stalled}}; ожидание OpenCode: {{names}}", + "namedPendingDiagnostic": "{{label}}: {{names}}", + "countPendingDiagnostic": "{{count}} - {{label}}", + "pendingLabels": { + "bootstrapStalled": "Bootstrap завис", + "shellOnly": "Только shell", + "waitingForBootstrap": "Ожидание bootstrap", + "bootstrapUnconfirmed": "Bootstrap не подтверждён", + "awaitingPermission": "Ожидание permission", + "waitingForRuntime": "Ожидание runtime", + "shellOnlyLower": "только shell", + "waitingForBootstrapLower": "ожидает bootstrap", + "bootstrapUnconfirmedLower": "bootstrap не подтверждён", + "awaitingPermissionLower": "ожидает permission", + "waitingForRuntimeLower": "ожидает runtime" + }, + "failed": { + "memberFailedToStart": "{{name}} не запустился", + "teammatesFailedToStart": "{{count}} участник(ов) не запустилось", + "teammatesFailedRatio": "{{count}}/{{total}} участник(ов) не запустилось" + }, + "skipped": { + "memberSkipped": "{{name}} пропущен для этого запуска", + "memberSkippedWithReason": "{{name}} пропущен для этого запуска - {{reason}}", + "memberSkippedCompact": "{{name}} пропущен", + "teammatesSkipped": "{{count}} участник(ов) пропущено", + "teammatesSkippedList": "Пропущенные участники: {{list}}", + "teammatesSkippedRatio": "{{count}}/{{total}} участник(ов) пропущено для этого запуска" + }, + "joining": { + "teammatesStillJoining": "{{count}} участник(ов) ещё подключается", + "teammatesConfirmedRatio": "{{count}}/{{total}} участников подтверждено" + }, + "ready": { + "leadOnline": "Lead online", + "allTeammatesJoined": "Все участники подключились: {{count}}", + "teamProvisionedLeadOnline": "Команда подготовлена - lead online", + "teamProvisionedAllJoined": "Команда подготовлена - все участники подключились: {{count}}", + "teamProvisionedStillJoining": "Команда подготовлена - участники ещё подключаются", + "launchFinishedWithErrors": "Запуск завершён с ошибками - {{count}}/{{total}} участник(ов) не запустилось", + "launchContinuedSkipped": "Запуск продолжен - {{count}}/{{total}} участник(ов) пропущено", + "teamLaunchedLeadOnline": "Команда запущена - lead online", + "teamLaunchedAllJoined": "Команда запущена - все участники подключились: {{count}}" + }, + "panel": { + "launchFailed": "Запуск не удался", + "launchDetails": "Детали запуска", + "launchFinishedWithErrors": "Запуск завершён с ошибками", + "launchContinuedSkipped": "Запуск продолжен с пропущенными участниками", + "coreTeamReady": "Основная команда готова", + "finishingLaunch": "Завершение запуска", + "teamLaunched": "Команда запущена", + "launchingTeam": "Запуск команды" + } + } + }, + "liveRuntimeStatus": { + "title": "Статус live runtime", + "description": "Информационный heartbeat и состояние запуска. Управление процессами находится ниже.", + "source": "источник: {{source}}", + "lane": "lane {{lane}}", + "diagnosticOnly": "Только для диагностики", + "updated": "обновлено {{value}}", + "states": { + "running": "Работает", + "starting": "Запускается", + "waiting": "Ожидает", + "degraded": "Требует внимания", + "stopped": "Остановлен", + "unknown": "Неизвестно" + } + }, + "taskLogs": { + "exact": { + "title": "Точные логи задачи", + "loading": "Загрузка точных логов задачи...", + "description": "Точные фрагменты transcript отображаются теми же компонентами execution-log, что и в логах.", + "emptyTitle": "Точных логов задачи пока нет", + "emptyDescription": "Точные transcript bundles появятся здесь, когда будут доступны явные метаданные transcript, связанные с задачей.", + "summaryOnly": "только summary" + }, + "executionSessions": { + "title": "Сессии выполнения", + "online": "Онлайн", + "updating": "Обновление...", + "description": "Просмотр и предпросмотр старых транскриптов, сгруппированных по сессиям." + }, + "stream": { + "title": "Поток логов задачи" + } + }, + "kanban": { + "taskCard": { + "cancelTask": "Отменить задачу {{taskId}}", + "cancel": "Отменить", + "moveBackToTodoConfirm": "Переместить задачу обратно в TODO и уведомить команду?", + "confirm": "Подтвердить", + "keep": "Оставить", + "changesNeedAttention": "Изменения требуют внимания", + "changes": "Изменения", + "deleteTask": "Удалить задачу", + "taskLogsActive": "Логи задачи активны", + "newTaskLogsArriving": "Поступают новые логи задачи", + "awaitingUser": "Ожидает пользователя", + "awaitingLead": "Ожидает lead", + "blockedBy": "Заблокировано", + "blocks": "Блокирует", + "start": "Начать", + "complete": "Завершить", + "approve": "Одобрить", + "requestReview": "Запросить ревью", + "manualReview": "Ручное ревью", + "requestChanges": "Запросить правки" + }, + "filter": { + "title": "Фильтр задач", + "session": "Сессия", + "allSessions": "Все сессии", + "teammate": "Участник", + "unassigned": "(не назначено)", + "column": "Колонка", + "clearAll": "Очистить всё" + }, + "board": { + "addTask": "Добавить задачу", + "noTasks": "Нет задач", + "showMore": "Показать ещё {{count}}", + "hiddenCount": "Скрыто: {{count}}", + "trash": "Корзина", + "gridView": "Вид сеткой", + "columnsView": "Вид колонками" + }, + "trash": { + "title": "Корзина", + "empty": "Удалённых задач нет", + "subject": "Тема", + "owner": "Исполнитель", + "deleted": "Удалено", + "unassigned": "Не назначено", + "restoreTask": "Восстановить задачу", + "restore": "Восстановить", + "close": "Закрыть" + }, + "sort": { + "title": "Сортировка задач", + "sortBy": "Сортировать по", + "reset": "Сбросить", + "options": { + "updatedAt": { + "label": "Последнее обновление", + "description": "Сначала недавно обновлённые" + }, + "createdAt": { + "label": "Создано", + "description": "Сначала новые" + }, + "owner": { + "label": "Исполнитель", + "description": "По имени исполнителя" + }, + "manual": { + "label": "Вручную", + "description": "Порядок drag-and-drop" + } + } + }, + "search": { + "clearSearch": "Очистить поиск", + "tasks": "Задачи", + "createdAgo": "создано {{time}}", + "updatedAgo": "обновлено {{time}}", + "placeholder": "Поиск задач... (#id или текст)" + }, + "grid": { + "addTask": "Добавить задачу", + "noTasks": "Нет задач" + }, + "title": "Канбан", + "columns": { + "todo": "TODO", + "inProgress": "В РАБОТЕ", + "review": "РЕВЬЮ", + "done": "ГОТОВО", + "approved": "ОДОБРЕНО" + } + }, + "worktreeGitReadiness": { + "checking": "Проверка Git-репозитория для worktree участников...", + "ready": "Git worktree готовы.", + "readyOnBranch": "Git worktree готовы на ветке {{branch}}.", + "needsSetup": "Для worktree isolation нужна настройка Git", + "initialCommitNotice": "Действие initial commit добавит в индекс и закоммитит все текущие файлы с сообщением", + "initializeRepository": "Инициализировать Git-репозиторий", + "createInitialCommit": "Создать initial commit", + "initialCommitMessage": "chore: initial commit" + }, + "toolApproval": { + "settings": "Настройки", + "autoAllowAllTools": "Автоматически разрешать все инструменты", + "autoAllowFileEdits": "Автоматически разрешать правки файлов (Edit, Write, NotebookEdit)", + "autoAllowSafeCommands": "Автоматически разрешать безопасные команды (git, pnpm, npm, ls...)", + "onTimeout": "При таймауте:", + "after": "через", + "secondsShort": "сек", + "timeoutActions": { + "wait": "Ждать всегда", + "allow": "Разрешить", + "deny": "Отклонить" + }, + "submit": "Отправить", + "allow": "Разрешить", + "deny": "Отклонить", + "allowAll": "Разрешить всё", + "pendingCount": "ожидает: {{count}}", + "autoActionIn": "Авто-{{action}} через {{time}}", + "diff": { + "previewChanges": "Предпросмотр изменений", + "readingFile": "Чтение файла...", + "binaryFile": "Бинарный файл - предпросмотр невозможен", + "truncated": "Файл обрезан на 2MB - diff может быть неполным", + "newFile": "Новый файл" + } + }, + "memberWorkSync": { + "details": { + "title": "Синхронизация работы участника", + "actionableItems": "Действия", + "fingerprint": "Fingerprint", + "report": "Отчёт", + "none": "нет", + "shadowWouldNudge": "Shadow отправил бы nudge", + "yes": "да", + "no": "нет", + "moreActionableItems": "Ещё действий: {{count}}", + "diagnostics": "Диагностика: {{diagnostics}}" + }, + "title": "Синхронизация работы участника", + "loadingDiagnostics": "Загрузка диагностики синхронизации работы участника.", + "diagnosticsUnavailable": "Диагностика синхронизации работы участника недоступна." + }, + "advancedCli": { + "title": "Дополнительно", + "useWorktree": "Использовать worktree", + "recent": "Недавние", + "commandPreview": "Предпросмотр команды", + "customArguments": "Пользовательские аргументы", + "validate": "Проверить", + "validation": { + "allFlagsValid": "Все флаги корректны", + "unknownFlags": "Неизвестные: {{flags}}", + "protectedFlags": "Защищённые: {{flags}}", + "failed": "Проверка не удалась" + }, + "placeholders": { + "worktreeName": "worktree-name" + } + }, + "processes": { + "ago": "{{time}} назад", + "stoppedAgo": "остановлен {{time}} назад", + "running": "Работает", + "stopped": "Остановлен", + "stopProcess": "Остановить процесс (SIGTERM)", + "kill": "Убить", + "openInBrowser": "Открыть в браузере", + "open": "Открыть", + "pid": "PID{{pid}}", + "title": "Процессы CLI" + }, + "taskActivity": { + "loadingDetails": "Загрузка деталей активности...", + "contextUnavailable": "Детальный transcript-контекст для этой активности больше недоступен.", + "loading": "Загрузка активности задачи...", + "lowSignalOnly": "Ключевая активность задачи пока не найдена. Низкоуровневые детали выполнения доступны ниже в Task Log Stream.", + "empty": "Явная активность задачи пока не найдена в доступных transcript. Более старые или эвристические логи сессий могут быть доступны ниже в Execution Sessions.", + "title": "Активность задачи", + "description": "Ключевая runtime-активность, связанная с задачей через transcript metadata." + }, + "sendMessage": { + "title": "Отправить сообщение", + "description": "Отправить прямое сообщение участнику команды.", + "recipientLabel": "Получатель", + "selectMemberPlaceholder": "Выберите участника...", + "messageLabel": "Сообщение", + "placeholder": "Напишите сообщение... (Enter для отправки)", + "send": "Отправить", + "sending": "Отправка...", + "charsLeft": "осталось символов: {{count}}", + "saved": "Сохранено", + "attachments": { + "teamOnlineRequired": "Команда должна быть онлайн, чтобы прикреплять файлы", + "recipientUnsupported": "Файлы можно отправлять лиду команды или участникам OpenCode", + "openCodeOnlineRequired": "Команда должна быть онлайн, чтобы прикреплять файлы для участников OpenCode", + "disabledHint": "Файлы поддерживаются для онлайн-лида команды и онлайн-участников OpenCode. Удалите вложения или смените получателя.", + "attachFiles": "Прикрепить файлы (вставка или drag & drop)", + "unavailable": "Вложения недоступны" + }, + "quote": { + "remove": "Удалить цитату", + "replyingTo": "Ответ для" + } + }, + "taskComments": { + "cancelReply": "Отменить ответ", + "replyingTo": "Ответ для", + "placeholder": "Добавьте комментарий... (Enter для отправки)", + "attachFile": "Прикрепить файл (или вставить)", + "voiceToText": "Голос в текст", + "comment": "Комментарий", + "charsLeft": "осталось символов: {{count}}", + "saved": "Сохранено", + "awaitingReplyFrom": "Ожидается ответ от", + "or": "или" + }, + "taskAttachments": { + "dropImageHere": "Перетащите изображение сюда", + "attachImage": "Прикрепить изображение", + "pasteOrDragDrop": "или вставьте / перетащите", + "fromOriginalMessage": "Из исходного сообщения", + "dropFilesHere": "Перетащите файлы сюда", + "loading": "Загрузка вложений..." + }, + "permissions": { + "autoApproveAllTools": "Автоодобрение всех инструментов", + "autonomousModeDescription": "Автономный режим: инструменты команды выполняются без подтверждения. Будьте осторожны с недоверенным кодом.", + "manualModeDescription": "Ручной режим: вы будете одобрять или отклонять каждый вызов инструмента в реальном времени." + }, + "memberLogStream": { + "tabs": { + "execution": "Выполнение", + "process": "Процесс" + }, + "filters": { + "all": "Все" + }, + "logs": { + "title": "Логи", + "loading": "Загрузка потока логов участника...", + "emptyTitle": "Для этого участника пока не найдено записей потока логов.", + "emptyDescription": "Транскрипт участника и runtime-логи появятся здесь, когда будут доступны." + } + }, + "reviewDialog": { + "placeholder": "Опишите, что нужно изменить... (Enter для отправки)", + "submit": "Отправить", + "charsLeft": "осталось символов: {{count}}", + "saved": "Сохранено", + "title": "Запросить изменения" + }, + "dialogs": { + "actions": { + "openDashboard": "Открыть дашборд", + "openTeam": "Открыть команду", + "cancel": "Отмена" + }, + "membersJson": { + "hide": "Скрыть JSON" + }, + "optional": { + "badge": "Опционально" + } + }, + "runningTeams": { + "title": "Активные команды" + }, + "layout": { + "maxPanesReached": "Достигнут максимум панелей: {{count}}" + }, + "codexReconnect": { + "description": "Сессия Codex выглядит устаревшей. Переподключитесь, чтобы продолжить.", + "useCode": "Использовать код" + }, + "effortLevel": { + "label": "Уровень усилий (опционально)", + "maxDescription": "Max даёт модели больше всего времени на рассуждение для сложных задач." + }, + "contextLimit": { + "limitTo200k": "Ограничить контекст до 200K токенов", + "always200k": "(для этой модели всегда 200K)", + "tooltipContent": "Ограничивает запуск окном контекста 200K токенов, когда это поддерживается.", + "tooltipTitle": "Лимит контекста" + }, + "roleSelect": { + "noRole": "Без роли", + "customRole": "Своя роль...", + "searchPlaceholder": "Поиск ролей...", + "empty": "Роли не найдены.", + "reservedRole": "Эта роль зарезервирована" + } +} diff --git a/src/features/localization/renderer/resources.d.ts b/src/features/localization/renderer/resources.d.ts new file mode 100644 index 00000000..f77efa56 --- /dev/null +++ b/src/features/localization/renderer/resources.d.ts @@ -0,0 +1,5402 @@ +// This file is automatically generated by i18next-cli. Do not edit manually. +export default interface Resources { + common: { + actions: { + cancel: 'Cancel'; + close: 'Close'; + closeDialog: 'Close dialog'; + copied: 'Copied'; + copyToClipboard: 'Copy to clipboard'; + copyUrl: 'Copy URL'; + goToDashboard: 'Go to Dashboard'; + hide: 'Hide'; + moreActions: 'More actions'; + open: 'Open'; + or: 'or'; + refresh: 'Refresh'; + reset: 'Reset'; + resetSelection: 'Reset selection'; + retry: 'Retry'; + reveal: 'Reveal'; + save: 'Save'; + showLess: 'Show less'; + showMore: 'Show more'; + }; + brand: { + claude: 'Claude'; + }; + chat: { + bottom: 'Bottom'; + compact: { + compacted: 'Compacted'; + contextCompacted: 'Context compacted'; + conversationCompacted: 'Conversation Compacted'; + freedTokens: '({{tokens}} freed)'; + phase: 'Phase {{phase}}'; + summary: 'Previous messages were summarized to save context. The full conversation history is preserved in the session file.'; + toggle: 'Toggle compacted content'; + }; + context: { + count: 'Context ({{count}})'; + count_few: 'Context ({{count}})'; + count_many: 'Context ({{count}})'; + count_one: 'Context ({{count}})'; + count_other: 'Context ({{count}})'; + remainingPercent: '({{percent}}% left)'; + }; + empty: { + description: 'This session does not contain any messages yet.'; + icon: '💬'; + title: 'No conversation history'; + }; + executionTrace: { + empty: 'No execution items'; + input: 'Input'; + nested: 'Nested: {{name}}'; + }; + items: { + empty: 'No items to display'; + }; + lastOutput: { + planReadyForApproval: 'Plan Ready for Approval'; + requestInterrupted: 'Request interrupted by user'; + }; + scrollToBottom: 'Scroll to bottom'; + subagent: { + fallbackName: 'Subagent'; + meta: { + duration: 'Duration'; + id: 'ID'; + model: 'Model'; + type: 'Type'; + }; + metrics: { + contextUsage: 'Context Usage'; + contextWindow: 'Context Window'; + mainContext: 'Main Context'; + phase: 'Phase {{phase}}'; + subagentContext: 'Subagent Context'; + totalOutput: 'Total Output'; + turns: '({{count}} turns)'; + turns_few: '({{count}} turns)'; + turns_many: '({{count}} turns)'; + turns_one: '({{count}} turn)'; + turns_other: '({{count}} turns)'; + }; + shutdownConfirmed: 'Shutdown confirmed'; + summary: { + tools: '{{count}} tools'; + tools_few: '{{count}} tools'; + tools_many: '{{count}} tools'; + tools_one: '{{count}} tool'; + tools_other: '{{count}} tools'; + }; + trace: { + title: 'Execution Trace'; + }; + }; + system: { + label: 'System'; + }; + teammateMessage: { + fallback: 'Teammate message'; + message: 'Message'; + resent: 'Resent'; + }; + tools: { + duration: 'Duration: {{duration}}'; + noResultReceived: 'No result received'; + result: 'Result'; + shutdownRequested: 'Shutdown requested ->'; + skill: { + instructions: 'Skill Instructions'; + unknown: 'Unknown Skill'; + }; + teammateSpawned: 'Teammate spawned'; + write: { + createdFile: 'Created file'; + wroteToFile: 'Wrote to file'; + }; + }; + user: { + backgroundTask: 'Background task'; + exitCode: 'exit {{code}}'; + imagesAttached: '{{count}} images attached'; + imagesAttached_few: '{{count}} images attached'; + imagesAttached_many: '{{count}} images attached'; + imagesAttached_one: '{{count}} image attached'; + imagesAttached_other: '{{count}} images attached'; + showLess: 'Show less'; + showMore: 'Show more'; + you: 'You'; + }; + }; + code: { + code: 'Code'; + line: 'line {{line}}'; + lines: 'lines {{from}}-{{to}}'; + linesParenthesized: '(lines {{from}}-{{to}})'; + markdownPreview: 'Markdown Preview'; + mermaidSyntaxError: 'Mermaid syntax error'; + moreLines: '({{count}} more lines...)'; + moreLines_few: '({{count}} more lines...)'; + moreLines_many: '({{count}} more lines...)'; + moreLines_one: '({{count}} more line...)'; + moreLines_other: '({{count}} more lines...)'; + preview: 'Preview'; + }; + codexLogin: { + copyFailed: 'Copy failed'; + copyLink: 'Copy link'; + copyLinkAndCode: 'Copy link + code'; + copyLoginLink: 'Copy ChatGPT login link'; + copyLoginLinkAndCode: 'Copy ChatGPT login link and code'; + enterCodeOnLoginPage: 'Enter this code on the ChatGPT login page'; + }; + commandPalette: { + currentProject: 'Current project'; + empty: { + minChars: 'Type at least 2 characters to search'; + noFastResults: 'No fast results in recent sessions for "{{query}}"'; + noProjects: 'No projects found'; + noProjectsForQuery: 'No projects found for "{{query}}"'; + noResults: 'No results found for "{{query}}"'; + }; + footer: { + close: 'close'; + escapeKey: 'esc'; + fastPrefix: 'fast '; + global: 'global'; + navigate: 'navigate'; + open: 'open'; + projectsCount: '{{count}} projects'; + projectsCount_few: '{{count}} projects'; + projectsCount_many: '{{count}} projects'; + projectsCount_one: '{{count}} project'; + projectsCount_other: '{{count}} projects'; + results: '{{count}} {{speed}}results'; + resultsAcrossProjects: '{{count}} {{speed}}results across all projects'; + resultsAcrossProjects_few: '{{count}} {{speed}}results across all projects'; + resultsAcrossProjects_many: '{{count}} {{speed}}results across all projects'; + resultsAcrossProjects_one: '{{count}} {{speed}}result across all projects'; + resultsAcrossProjects_other: '{{count}} {{speed}}results across all projects'; + results_few: '{{count}} {{speed}}results'; + results_many: '{{count}} {{speed}}results'; + results_one: '{{count}} {{speed}}result'; + results_other: '{{count}} {{speed}}results'; + select: 'select'; + typeToSearch: 'Type to search'; + upDownKey: '↑↓'; + }; + global: 'Global'; + mode: { + searchAcrossProjects: 'Search across all projects'; + searchInProject: 'Search in project'; + searchProjects: 'Search projects'; + }; + noRecentActivity: 'No recent activity'; + placeholders: { + conversations: 'Search conversations...'; + projects: 'Search projects...'; + }; + sessionsCount: '{{count}} sessions'; + sessionsCount_few: '{{count}} sessions'; + sessionsCount_many: '{{count}} sessions'; + sessionsCount_one: '{{count}} session'; + sessionsCount_other: '{{count}} sessions'; + }; + context: { + loadingWorkspace: 'Loading workspace'; + local: 'Local'; + switchWorkspace: 'Switch Workspace'; + switchingTo: 'Switching to {{workspace}}'; + }; + contextBadge: { + badge: 'Context'; + breakdown: { + text: 'Text'; + thinking: 'Thinking'; + }; + detailsAria: 'Context injection details'; + sectionSummary: '{{title}} ({{count}}) ~{{tokens}} tokens'; + sectionSummary_few: '{{title}} ({{count}}) ~{{tokens}} tokens'; + sectionSummary_many: '{{title}} ({{count}}) ~{{tokens}} tokens'; + sectionSummary_one: '{{title}} ({{count}}) ~{{tokens}} tokens'; + sectionSummary_other: '{{title}} ({{count}}) ~{{tokens}} tokens'; + sections: { + claudeMdFiles: 'CLAUDE.md Files'; + mentionedFiles: 'Mentioned Files'; + taskCoordination: 'Task Coordination'; + thinkingText: 'Thinking + Text'; + toolOutputs: 'Tool Outputs'; + userMessages: 'User Messages'; + }; + title: 'New Context Injected In This Turn'; + tokenCount: '~{{tokens}} tokens'; + totalNewTokens: 'Total new tokens'; + turn: 'Turn {{turn}}'; + }; + diff: { + changed: 'Changed'; + noChangesDetected: 'No changes detected'; + }; + editorFormatting: { + bold: 'Bold'; + code: 'Code'; + italic: 'Italic'; + strike: 'Strike'; + }; + errorBoundary: { + componentStack: 'Component Stack'; + copied: 'Copied'; + copyErrorDetails: 'Copy Error Details'; + description: 'An unexpected error occurred in the application. You can try reloading the page or resetting the error state.'; + diagnosticsNotice: 'GitHub bug reports and copied diagnostics include the error message, stack traces, app version, active tab, selected team, task context, and environment details.'; + reloadApp: 'Reload App'; + reportBugOnGitHub: 'Report Bug on GitHub'; + title: 'Something went wrong'; + tryAgain: 'Try Again'; + }; + export: { + session: 'Export session'; + sessionTitle: 'Export Session'; + }; + layout: { + closeTab: 'Close tab'; + collapseSidebarShortcut: 'Collapse sidebar ({{shortcut}})'; + discord: 'Discord'; + expandSidebar: 'Expand sidebar'; + github: 'GitHub'; + jumpToSection: 'Jump to section'; + loadingTab: 'Loading tab'; + menu: { + analyzeSession: 'Analyze Session'; + docs: 'Docs'; + exportJson: 'Export as JSON'; + exportMarkdown: 'Export as Markdown'; + exportPlainText: 'Export as Plain Text'; + extensions: 'Extensions'; + schedules: 'Schedules'; + search: 'Search'; + settings: 'Settings'; + teams: 'Teams'; + }; + newTab: 'New tab'; + newTabDashboard: 'New tab (Dashboard)'; + openedFromSearch: 'Opened from search'; + pinnedSession: 'Pinned session'; + refreshSession: 'Refresh session'; + refreshSessionWithShortcut: 'Refresh Session ({{shortcut}})'; + resizeSidebar: 'Resize sidebar'; + sections: { + claudeLogs: 'Claude Logs'; + kanban: 'Kanban'; + messages: 'Messages'; + sessions: 'Sessions'; + team: 'Team'; + }; + sidebarView: 'Sidebar view'; + tabMenu: { + closeAllTabs: 'Close All Tabs'; + closeOtherTabs: 'Close Other Tabs'; + closeTab: 'Close Tab'; + closeTabs: 'Close {{count}} Tabs'; + closeTabs_few: 'Close {{count}} Tabs'; + closeTabs_many: 'Close {{count}} Tabs'; + closeTabs_one: 'Close {{count}} Tab'; + closeTabs_other: 'Close {{count}} Tabs'; + hideFromSidebar: 'Hide from Sidebar'; + pinToSidebar: 'Pin to Sidebar'; + splitLeft: 'Split Left'; + splitRight: 'Split Right'; + unhideFromSidebar: 'Unhide from Sidebar'; + unpinFromSidebar: 'Unpin from Sidebar'; + }; + }; + list: { + actions: { + copyTeam: 'Copy team'; + createTeam: 'Create Team'; + deleteForever: 'Delete forever'; + deletePermanently: 'Delete permanently'; + deleteTeam: 'Delete team'; + launchTeam: 'Launch team'; + launching: 'Launching...'; + relaunchTeam: 'Relaunch team'; + restore: 'Restore'; + restoreTeam: 'Restore team'; + retry: 'Retry'; + stopTeam: 'Stop team'; + stopping: 'Stopping...'; + }; + all: 'All'; + membersCount: 'Members: {{count}}'; + membersCount_few: 'Members: {{count}}'; + membersCount_many: 'Members: {{count}}'; + membersCount_one: 'Member: {{count}}'; + membersCount_other: 'Members: {{count}}'; + moreCount: '+{{count}} more'; + moreCount_few: '+{{count}} more'; + moreCount_many: '+{{count}} more'; + moreCount_one: '+{{count}} more'; + moreCount_other: '+{{count}} more'; + noDescription: 'No description'; + partial: { + pending: 'Last launch is still reconciling.'; + skipped: 'Last launch has skipped teammates.'; + skippedWithCount: 'Last launch skipped {{count}}/{{expected}} teammate.'; + skippedWithCount_few: 'Last launch skipped {{count}}/{{expected}} teammates.'; + skippedWithCount_many: 'Last launch skipped {{count}}/{{expected}} teammates.'; + skippedWithCount_one: 'Last launch skipped {{count}}/{{expected}} teammate.'; + skippedWithCount_other: 'Last launch skipped {{count}}/{{expected}} teammates.'; + stopped: 'Last launch stopped before all teammates joined.'; + stoppedWithCount: 'Last launch stopped before {{count}}/{{expected}} teammate joined.'; + stoppedWithCount_few: 'Last launch stopped before {{count}}/{{expected}} teammates joined.'; + stoppedWithCount_many: 'Last launch stopped before {{count}}/{{expected}} teammates joined.'; + stoppedWithCount_one: 'Last launch stopped before {{count}}/{{expected}} teammate joined.'; + stoppedWithCount_other: 'Last launch stopped before {{count}}/{{expected}} teammates joined.'; + }; + solo: 'Solo'; + status: { + active: 'Active'; + deleted: 'Deleted'; + launching: 'Launching...'; + offline: 'Offline'; + partialFailure: 'Launch failed partway'; + partialPending: 'Bootstrap pending'; + partialSkipped: 'Launch skipped member'; + running: 'Running'; + }; + }; + locales: { + emptyMessage: 'No language found.'; + names: { + en: 'English'; + ru: 'Russian'; + system: 'System'; + }; + searchPlaceholder: 'Search language...'; + selectPlaceholder: 'Select app language...'; + systemWithResolved: 'System - {{locale}}'; + }; + markdown: { + imageFallback: '[Image: {{label}}]'; + largeContentNotice: 'Content is very large ({{count}} chars). Showing raw preview to keep the UI responsive.'; + largeContentNotice_few: 'Content is very large ({{count}} chars). Showing raw preview to keep the UI responsive.'; + largeContentNotice_many: 'Content is very large ({{count}} chars). Showing raw preview to keep the UI responsive.'; + largeContentNotice_one: 'Content is very large ({{count}} chars). Showing raw preview to keep the UI responsive.'; + largeContentNotice_other: 'Content is very large ({{count}} chars). Showing raw preview to keep the UI responsive.'; + largeContentTitle: 'Large content is shown as raw to prevent UI freeze'; + raw: 'Raw'; + rawPreview: 'Raw preview'; + renderMarkdown: 'Render markdown'; + showAll: 'Show all'; + showMore: 'Show more'; + showRaw: 'Show raw'; + showingChars: 'Showing {{shown}} / {{total}} chars'; + }; + members: { + emptyMessage: 'No members found.'; + searchPlaceholder: 'Search members...'; + teammateFallback: 'teammate'; + unassigned: 'Unassigned'; + }; + notifications: { + actions: { + clearAll: 'Clear all'; + clearAllNotifications: 'Clear all notifications'; + clearFiltered: 'Clear filtered'; + clearFilteredNotifications: 'Clear filtered notifications'; + clickToConfirm: 'Click to confirm'; + markAllAsRead: 'Mark all as read'; + markAllRead: 'Mark all read'; + markFilteredAsRead: 'Mark filtered as read'; + markFilteredRead: 'Mark filtered read'; + }; + counts: { + inFilter: '{{count}} in filter'; + inFilter_few: '{{count}} in filter'; + inFilter_many: '{{count}} in filter'; + inFilter_one: '{{count}} in filter'; + inFilter_other: '{{count}} in filter'; + total: '{{count}} total'; + total_few: '{{count}} total'; + total_many: '{{count}} total'; + total_one: '{{count}} total'; + total_other: '{{count}} total'; + unread: '{{count}} unread'; + unreadInFilter: '{{count}} unread in filter'; + unreadInFilter_few: '{{count}} unread in filter'; + unreadInFilter_many: '{{count}} unread in filter'; + unreadInFilter_one: '{{count}} unread in filter'; + unreadInFilter_other: '{{count}} unread in filter'; + unread_few: '{{count}} unread'; + unread_many: '{{count}} unread'; + unread_one: '{{count}} unread'; + unread_other: '{{count}} unread'; + }; + empty: { + allCaughtUp: "You're all caught up!"; + noMatching: 'No matching notifications'; + noNotifications: 'No notifications'; + tryDifferentFilter: 'Try a different filter'; + }; + filters: { + other: 'Other'; + }; + loading: 'Loading notifications...'; + row: { + delete: 'Delete'; + markAsRead: 'Mark as read'; + subagent: 'subagent'; + team: 'team'; + viewInSession: 'View in session'; + }; + title: 'Notifications'; + }; + providerModelBadges: { + checkFailed: 'Check failed'; + checking: 'Checking'; + free: 'Free'; + freeTooltip: 'Reported by OpenCode metadata. Availability and limits may change.'; + unavailable: 'Unavailable'; + }; + providerRuntime: { + codex: { + install: { + checking: 'Checking'; + downloading: 'Downloading'; + installCli: 'Install Codex CLI'; + installing: 'Installing'; + retryInstall: 'Retry install'; + }; + }; + }; + repositories: { + noneAvailable: 'No repositories available'; + remove: 'Remove repository'; + }; + runtimeBackendSelector: { + audience: { + internal: 'Internal'; + }; + auto: 'Auto'; + autoCurrently: 'Auto (currently: {{backend}})'; + cannotSelectYet: 'This backend cannot be selected yet.'; + current: 'Current'; + label: 'Runtime backend'; + recommended: 'Recommended'; + resolved: 'Resolved: {{backend}}'; + states: { + authRequired: 'Auth required'; + degraded: 'Degraded'; + disabled: 'Disabled'; + locked: 'Locked'; + runtimeMissing: 'Runtime missing'; + unavailable: 'Unavailable'; + }; + unavailable: 'Unavailable'; + }; + runtimeProvider: { + defaults: { + allProjectsHint: 'Tests use {{project}}. Default applies unless a project has an override.'; + projectHint: 'Saving overrides only {{project}}.'; + projectOverrideContext: 'Project override context'; + scopeDescriptionAllProjects: 'Default for every project that does not have its own OpenCode override.'; + scopeDescriptionProject: 'Override only the selected project. Running teams are not changed.'; + selectProjectHint: 'Select a project before testing local models or saving defaults.'; + setAllProjectsDefault: 'Set all-projects default'; + setProjectDefault: 'Set project default'; + validationContext: 'Validation context'; + }; + }; + schedules: { + actions: { + addSchedule: 'Add Schedule'; + clearFilters: 'Clear filters'; + createSchedule: 'Create Schedule'; + delete: 'Delete'; + edit: 'Edit'; + pause: 'Pause'; + resume: 'Resume'; + runNow: 'Run now'; + }; + empty: { + description: 'Create a schedule on any team to automate Claude task execution with cron expressions. Schedules from all teams will appear here.'; + noMatches: 'No schedules match the current filters'; + title: 'No scheduled tasks'; + }; + filters: { + allTeams: 'All teams'; + }; + item: { + loadingRunHistory: 'Loading run history...'; + nextRun: 'Next: {{value}}'; + noRunsYet: 'No runs yet'; + }; + loading: 'Loading schedules...'; + searchPlaceholder: 'Search schedules...'; + status: { + active: 'Active'; + all: 'All'; + disabled: 'Disabled'; + paused: 'Paused'; + }; + title: 'Schedules'; + }; + search: { + closeShortcut: 'Close (Esc)'; + findInConversation: 'Find in conversation...'; + nextResultShortcut: 'Next result (Enter)'; + noMatchingSuggestions: 'No matching suggestions'; + noResults: 'No results'; + nothingFound: 'Nothing found'; + placeholder: 'Search...'; + previousResultShortcut: 'Previous result (Shift+Enter)'; + resultCount: '{{current}} of {{total}}'; + resultCountCapped: '{{current}} of {{total}}+'; + searching: 'Searching...'; + searchingFiles: 'Searching files...'; + }; + sessionContext: { + claudeMdFiles: 'CLAUDE.md Files'; + empty: 'No context injections detected in this session'; + header: { + bySize: 'By Size'; + category: 'Category'; + closePanel: 'Close panel'; + current: 'Current'; + phase: 'Phase:'; + title: 'Context'; + view: 'View:'; + }; + help: { + availability: { + description: 'If a provider runtime does not expose prompt-side usage yet, the panel shows metrics as unavailable instead of pretending they are zero.'; + title: 'Availability'; + }; + contextUsed: { + description: "Prompt input plus output tokens currently occupying the model's context window."; + title: 'Context Used'; + }; + promptInput: { + description: 'Tokens sent to the model before generation. For Claude this includes `input_tokens + cache_creation_input_tokens + cache_read_input_tokens`.'; + title: 'Prompt Input'; + }; + visibleContext: { + description: 'The inspectable subset of prompt input: files, CLAUDE.md, tool outputs, user messages, and similar injections that you can optimize directly.'; + title: 'Visible Context'; + }; + }; + items: { + itemsCount: '{{count}} items'; + itemsCount_few: '{{count}} items'; + itemsCount_many: '{{count}} items'; + itemsCount_one: '{{count}} item'; + itemsCount_other: '{{count}} items'; + missing: 'missing'; + text: 'Text'; + thinking: 'Thinking'; + tokensApprox: '~{{tokens}} tokens'; + toolsCount: '{{count}} tools'; + toolsCount_few: '{{count}} tools'; + toolsCount_many: '{{count}} tools'; + toolsCount_one: '{{count}} tool'; + toolsCount_other: '{{count}} tools'; + turn: '@Turn {{turn}}'; + }; + mentionedFiles: 'Mentioned Files'; + metrics: { + codexTelemetryUnavailable: 'Codex prompt-side usage is not exposed by the current runtime telemetry yet, so Prompt Input and Context Used stay unavailable instead of showing a fake zero.'; + contextUsed: 'Context Used'; + details: 'details'; + ofContext: 'of context'; + ofPrompt: 'of prompt'; + parentPlus: 'parent +'; + promptInput: 'Prompt Input'; + sessionCost: 'Session Cost:'; + subagents: 'subagents'; + unavailable: 'Unavailable'; + visibleContext: 'Visible Context'; + }; + view: { + flat: 'Flat'; + grouped: 'Grouped'; + }; + }; + sessionFilters: { + project: { + selectProject: 'Select Project'; + }; + }; + sessionItem: { + compactedTo: '(compacted to {{tokens}})'; + context: 'Context: {{tokens}}'; + phase: 'Phase {{phase}}:'; + totalContext: 'Total Context: {{tokens}} tokens'; + }; + sessionReport: { + noSessionData: 'No session data available'; + title: 'Session Report'; + }; + sessions: { + actions: { + hide: 'Hide'; + pin: 'Pin'; + unhide: 'Unhide'; + }; + count: '{{count}} sessions'; + count_few: '{{count}} sessions'; + count_many: '{{count}} sessions'; + count_one: '{{count}} session'; + count_other: '{{count}} sessions'; + empty: { + noMatchingSessions: 'No matching sessions'; + noMatchingSessionsDescription: 'This project has no matching sessions yet.'; + noMatchingSessionsFiltered: 'Try another query or reset the provider filter.'; + noSessions: 'No sessions found'; + noSessionsDescription: 'This project has no sessions yet'; + selectProject: 'Select a project to view sessions'; + }; + errors: { + loading: 'Error loading sessions'; + }; + failedToLoad: 'Failed to load session'; + filter: { + title: 'Filter sessions'; + }; + inProgress: 'Session is in progress...'; + loadedMatchingMore: '{{count}} matching sessions loaded so far - scroll down to load more.'; + loadedMatchingMore_few: '{{count}} matching sessions loaded so far - scroll down to load more.'; + loadedMatchingMore_many: '{{count}} matching sessions loaded so far - scroll down to load more.'; + loadedMatchingMore_one: '{{count}} matching sessions loaded so far - scroll down to load more.'; + loadedMatchingMore_other: '{{count}} matching sessions loaded so far - scroll down to load more.'; + loading: 'Loading session...'; + loadingMore: 'Loading more sessions...'; + pinned: 'Pinned'; + scrollToLoadMore: 'Scroll to load more'; + search: { + clear: 'Clear session search'; + placeholder: 'Search sessions...'; + }; + selection: { + cancel: 'Cancel selection'; + exitMode: 'Exit selection mode'; + hideSelected: 'Hide selected sessions'; + pinSelected: 'Pin selected sessions'; + selectSessions: 'Select sessions'; + selected: '{{count}} selected'; + selected_few: '{{count}} selected'; + selected_many: '{{count}} selected'; + selected_one: '{{count}} selected'; + selected_other: '{{count}} selected'; + unhideSelected: 'Unhide selected sessions'; + }; + sort: { + byContext: 'By Context'; + byContextTooltip: 'Sort by context consumption'; + byRecentTooltip: 'Sort by recent'; + contextLoadedOnly: 'Context sorting only ranks loaded sessions.'; + }; + title: 'Sessions'; + visibility: { + hideHidden: 'Hide hidden sessions'; + showHidden: 'Show hidden sessions'; + }; + worktree: { + switch: 'Switch Worktree'; + }; + }; + states: { + error: 'Error'; + loading: 'Loading...'; + offline: 'Offline'; + online: 'Online'; + unknown: 'Unknown'; + }; + taskContextMenu: { + archive: 'Archive'; + deleteTask: 'Delete task'; + markUnread: 'Mark as unread'; + pin: 'Pin'; + rename: 'Rename'; + unarchive: 'Unarchive'; + unpin: 'Unpin'; + }; + taskFilters: { + allProjects: 'All Projects'; + allTeams: 'All teams'; + apply: 'Apply'; + clearAll: 'Clear all'; + comments: 'Comments'; + noProjects: 'No projects'; + noTeamsFound: 'No teams found'; + project: 'Project'; + read: { + all: 'All'; + read: 'Read'; + unread: 'Unread'; + }; + searchProjects: 'Search projects...'; + searchTeams: 'Search teams...'; + selectAll: 'Select all'; + status: 'Status'; + statusOptions: { + approved: 'APPROVED'; + done: 'DONE'; + inProgress: 'IN PROGRESS'; + needsFix: 'NEEDS FIXES'; + review: 'REVIEW'; + todo: 'TODO'; + }; + team: 'Team'; + }; + tasks: { + date: { + updatedPrefix: 'upd'; + updatedYesterday: 'upd yesterday'; + yesterday: 'Yesterday'; + }; + reviewState: { + needsFix: 'Needs Fixes'; + }; + unassigned: 'unassigned'; + }; + tasksPanel: { + deleteConfirm: { + cancelLabel: 'Cancel'; + confirmLabel: 'Delete'; + message: 'Move task #{{taskId}} to trash?'; + title: 'Delete task'; + }; + deleteFailed: { + confirmLabel: 'OK'; + fallbackMessage: 'An unexpected error occurred'; + title: 'Failed to delete task'; + }; + empty: { + noMatchingTasks: 'No matching tasks'; + noTasks: 'No tasks found'; + }; + groupByAria: 'Group by'; + groupByLabel: 'Group by:'; + groupModes: { + none: 'None'; + project: 'Project'; + time: 'Time'; + }; + hideArchived: 'Hide archived'; + pinned: 'Pinned'; + searchPlaceholder: 'Search tasks...'; + showArchived: 'Show archived'; + showLess: 'Show less'; + showMore: 'Show more'; + sort: { + byProject: 'By project'; + byTeam: 'By team'; + byTime: 'By time'; + byUnread: 'By unread'; + }; + teamLabel: 'Team: {{team}}'; + title: 'Tasks'; + }; + terminal: { + checkOutputForDetails: 'Check terminal output above for details'; + closingInSeconds: 'Closing in {{count}}s...'; + closingInSeconds_few: 'Closing in {{count}}s...'; + closingInSeconds_many: 'Closing in {{count}}s...'; + closingInSeconds_one: 'Closing in {{count}}s...'; + closingInSeconds_other: 'Closing in {{count}}s...'; + completedSuccessfully: 'Completed successfully'; + exitCode: '(exit code {{code}})'; + processFailed: 'Process failed'; + title: 'Terminal'; + }; + tmuxInstaller: { + actions: { + cancel: 'Cancel'; + hideSetupSteps: 'Hide setup steps'; + manualGuide: 'Manual guide'; + recheck: 'Re-check'; + showSetupSteps: 'Show setup steps ({{count}})'; + showSetupSteps_few: 'Show setup steps ({{count}})'; + showSetupSteps_many: 'Show setup steps ({{count}})'; + showSetupSteps_one: 'Show setup step ({{count}})'; + showSetupSteps_other: 'Show setup steps ({{count}})'; + }; + details: { + hide: 'Hide details'; + show: 'Show details'; + }; + detectedOs: 'Detected OS: {{os}}'; + input: { + passwordNotice: 'Password input is sent directly to the installer terminal and is not added to the log output.'; + placeholder: 'Send input to the installer'; + send: 'Send input'; + }; + installerProgress: 'Installer progress'; + phase: 'Phase: {{phase}}'; + runtimePath: 'Runtime path: {{path}}'; + summaryTitle: 'tmux is not installed'; + }; + tokens: { + accumulatedWithoutDuplication: 'Accumulated across entire session without duplication'; + approxTokens: '~{{tokens}} tokens'; + approxTokensParenthesized: '(~{{tokens}})'; + cacheRead: 'Cache Read'; + cacheWrite: 'Cache Write'; + claudeMd: 'CLAUDE.md'; + costUsd: 'Cost (USD)'; + includesClaudeMd: 'incl. CLAUDE.md ×{{count}}'; + inputTokens: 'Input Tokens'; + mentionedFiles: '@files'; + model: 'Model'; + outputTokens: 'Output Tokens'; + percentValue: '({{percent}}%)'; + phase: 'Phase {{phase}}/{{total}}'; + promptInputShare: '{{percent}}% of prompt input'; + taskCoordination: 'Task Coordination'; + thinkingText: 'Thinking + Text'; + toolOutputs: 'Tool Outputs'; + total: 'Total'; + userMessages: 'User Messages'; + visibleContext: 'Visible Context'; + }; + toolViewer: { + agent: { + action: 'action'; + runtime: 'runtime'; + startupInstructionsHidden: 'Startup instructions are hidden in the UI.'; + team: 'team'; + teammate: 'teammate'; + type: 'type'; + }; + input: 'Input'; + noInputRecorded: 'No input recorded for this tool call.'; + replaceAll: '(replace all)'; + }; + updateDialog: { + closeDialog: 'Close dialog'; + download: 'Download'; + later: 'Later'; + noReleaseNotes: 'No release notes available.'; + restartNow: 'Restart now'; + updateAvailable: 'Update available'; + updateReady: 'Update Ready'; + viewOnGitHub: 'View on GitHub'; + }; + updates: { + downloadedRestartTooltip: 'Update downloaded, restart to apply'; + newVersionAvailable: 'New version available'; + restartNow: 'Restart now'; + restartToUpdate: 'Restart to update'; + updateApp: 'Update app'; + updateReady: 'Update ready'; + updatingApp: 'Updating app'; + }; + window: { + maximize: 'Maximize'; + minimize: 'Minimize'; + restore: 'Restore'; + }; + }; + dashboard: { + actions: { + clearSearch: 'Clear search'; + or: 'or'; + selectTeam: 'Select Team'; + }; + cliStatus: { + actions: { + alreadyLoggedIn: 'Already logged in?'; + becomeSponsor: 'Become a sponsor'; + cancel: 'Cancel'; + checkNow: 'Check now'; + checkUpdates: 'Check for Updates'; + checking: 'Checking...'; + connect: 'Connect'; + extensions: 'Extensions'; + login: 'Login'; + manage: 'Manage'; + manageProviders: 'Manage Providers'; + plan: 'Plan'; + recheck: 'Re-check'; + recheckProvider: 'Re-check {{provider}}'; + retry: 'Retry'; + updateTo: 'Update to v{{version}}'; + useCode: 'Use code'; + }; + atlas: { + alt: 'Atlas Cloud'; + description: "Atlas Cloud is a full-modal AI inference platform that gives developers a single AI API to access video generation, image generation, and LLM APIs. Instead of managing multiple vendor integrations, you connect once and get unified access to 300+ curated models across all modalities. Check out Atlas Cloud's new coding plan promotion for more budget-friendly API access."; + openCodeProvider: 'OpenCode provider'; + plan: 'Atlas Cloud coding plan'; + sponsor: 'Sponsor'; + }; + errors: { + checkStatusFailed: 'Failed to check CLI status'; + installationFailed: 'Installation failed'; + refreshFailed: 'Failed to check for updates. Check your network connection and try again.'; + runtimeUpdatedRefreshFailed: 'Runtime updated, but failed to refresh provider status.'; + }; + hints: { + backgroundStatus: '{{runtime}} status will be checked in the background.'; + codexApiKeyFallback: '{{hint}} API key fallback is available if you switch auth mode.'; + codexAutoApiKey: '{{hint}} Auto will keep using the API key until ChatGPT is connected.'; + codexFinishLogin: 'Finish ChatGPT login in the browser. Enter the shown code if prompted.'; + codexNoActiveLogin: 'Usage limits appear only after Codex CLI sees an active ChatGPT account. Right now it reports no active ChatGPT login.'; + codexNoActiveManagedSession: 'Usage limits appear only after Codex CLI sees an active ChatGPT account. Local Codex account data exists, but no active managed session is selected right now.'; + codexReconnectNeeded: 'Usage limits appear only after Codex refreshes the currently selected ChatGPT session. Right now the local session needs reconnect.'; + firstCheckSlow: 'First check may take up to 30 seconds'; + loginRequiredForTeams: 'Browsing sessions and projects works without login. Login is only needed to run agent teams.'; + troubleshootTitle: "If you're sure you're logged in, try these steps:"; + }; + installer: { + checkingLatest: 'Checking latest version...'; + downloading: 'Downloading {{runtime}}...'; + installing: 'Installing {{runtime}}...'; + success: 'Successfully installed {{runtime}} v{{version}}'; + verifying: 'Verifying checksum...'; + }; + labels: { + apiKeyRequired: 'API key required'; + collapseProviderDetails: 'Collapse provider details'; + comingSoon: 'Coming soon'; + expandProviderDetails: 'Expand provider details'; + generateLink: 'Generate link'; + loadingRateLimits: 'Rate limits loading'; + loggedOut: 'Provider logged out'; + loginAuthFailed: 'Authentication failed'; + loginAuthUpdated: 'Authentication updated'; + loginComplete: 'Login complete'; + loginFailed: 'Login failed'; + loginTitle: 'Login'; + logoutFailed: 'Logout failed'; + logoutTitle: 'Logout'; + notLoggedIn: 'Not logged in'; + openLogin: 'Open login'; + providerActionRequired: 'Provider action required'; + resets: 'resets {{time}}'; + runtimeLoginTitle: '{{runtime}} Login'; + }; + loading: { + aiProviders: 'Checking AI Providers...'; + claudeCli: 'Checking Claude CLI...'; + }; + provider: { + authenticated: 'Authenticated'; + backend: 'Backend: {{backend}}'; + checkingAuthentication: 'Checking authentication...'; + checkingProviders: 'Checking providers...'; + configuredLocalCount: '{{count}} configured local'; + configuredLocalCount_few: '{{count}} configured local'; + configuredLocalCount_many: '{{count}} configured local'; + configuredLocalCount_one: '{{count}} configured local'; + configuredLocalCount_other: '{{count}} configured local'; + configuredLocalTitle: 'Local OpenCode routes imported from your OpenCode config.'; + connectedCount: 'Providers: {{connected}}/{{denominator}} connected'; + freeModels: 'Free models'; + freeModelsTitle: 'OpenCode includes free model options such as Big Pickle when available in your setup. OpenRouter through OpenCode can also expose free models, but not every OpenCode/OpenRouter model is free. Availability and limits may change.'; + loadingModels: 'Loading models...'; + modelsUnavailable: 'Models unavailable for this runtime build'; + runtime: 'Runtime: {{runtime}}'; + verifiedCount: '{{count}} verified'; + verifiedCount_few: '{{count}} verified'; + verifiedCount_many: '{{count}} verified'; + verifiedCount_one: '{{count}} verified'; + verifiedCount_other: '{{count}} verified'; + verifiedTitle: 'OpenCode routes with a successful execution proof.'; + }; + runtime: { + configuredHealthCheckFailed: 'The configured {{runtime}} failed its startup health check.'; + configuredNotFound: 'The configured {{runtime}} was not found.'; + foundButFailed: '{{runtime}} was found but failed to start'; + healthCheckFailedDescription: 'The app found the configured {{runtime}}, but its startup health check failed. Repair or reinstall it, then retry.'; + install: 'Install {{runtime}}'; + installRequiredDescription: '{{runtime}} is required for team provisioning and session management. Install it to get started.'; + isRequired: '{{runtime}} is required'; + reinstall: 'Reinstall {{runtime}}'; + }; + runtimeInstall: { + checking: 'Checking'; + codexTitle: 'Install Codex CLI into app data'; + downloading: 'Downloading'; + downloadingPercent: 'Downloading {{percent}}%'; + install: 'Install'; + installing: 'Installing'; + openCodeTitle: 'Install OpenCode runtime into app data'; + retryInstall: 'Retry install'; + }; + troubleshoot: { + again: 'again'; + authStatusCommand: 'your configured CLI auth status command'; + checkLoggedIn: '- check if it shows "Logged in"'; + click: 'Click'; + loginCommand: 'the runtime login command'; + logoutCommand: 'the runtime logout command'; + openTerminal: 'Open your terminal and run:'; + reloginPrefix: "If it says logged in but the app doesn't see it, try:"; + sameRuntime: 'Make sure the CLI in your terminal is the same runtime the app uses'; + statusCacheHint: '- sometimes the status is cached for a few seconds'; + then: 'then'; + }; + warnings: { + multipleApiKeysMissing: 'One or more providers are set to API key mode, but no API key is configured. Open Manage Providers to add keys or switch the connection mode.'; + multipleApiKeysNeedAttention: 'One or more providers are set to API key mode and need attention. Open Manage Providers to review saved keys or switch the connection mode.'; + notAuthenticated: '{{runtime}} is installed but you are not authenticated. Login is required for team provisioning and AI features.'; + singleApiKeyMissing: '{{provider}} is set to API key mode, but no API key is configured. Open Manage Providers to add a key or switch the connection mode.'; + singleApiKeyNeedsAttention: '{{provider}} is set to API key mode, but it is not connected. Open Manage Providers to review the saved key or switch the connection mode.'; + }; + }; + recentProjects: { + card: { + deleted: 'Deleted'; + projectFolderMissing: 'Project folder no longer exists'; + taskCounts: { + active: '{{count}} active'; + active_few: '{{count}} active'; + active_many: '{{count}} active'; + active_one: '{{count}} active'; + active_other: '{{count}} active'; + done: '{{count}} done'; + done_few: '{{count}} done'; + done_many: '{{count}} done'; + done_one: '{{count}} done'; + done_other: '{{count}} done'; + pending: '{{count}} pending'; + pending_few: '{{count}} pending'; + pending_many: '{{count}} pending'; + pending_one: '{{count}} pending'; + pending_other: '{{count}} pending'; + }; + }; + emptyDescription: 'Recent Claude and Codex activity will appear here.'; + failedToLoad: 'Failed to load projects'; + loadMore: 'Load more'; + noMatches: 'No matches for "{{query}}"'; + noProjects: 'No projects found'; + noRecentProjects: 'No recent projects found'; + retry: 'Retry'; + searchPlaceholder: 'Search projects...'; + searchResults: 'Search Results'; + selectFolder: 'Select Folder'; + selectFolderTitle: 'Select a project folder'; + title: 'Recent Projects'; + }; + updateBanner: { + newVersionAvailable: 'New version available'; + restartNow: 'Restart now'; + viewDetails: 'View details'; + }; + webPreview: { + description: 'The browser version is still in development. Project actions, integrations, and live status updates may be limited here. Use the desktop app to access all features reliably.'; + title: 'Open the desktop app for full functionality'; + }; + windowsAdmin: { + description: 'OpenCode runtime checks can time out when Agent Teams AI is not elevated. Restart the app with Run as administrator before launching OpenCode teams.'; + title: 'Windows Administrator mode recommended'; + }; + }; + errors: { + fallback: 'Something went wrong.'; + }; + extensions: { + apiKeys: { + actions: { + add: 'Add API Key'; + addFirst: 'Add your first key'; + edit: 'Edit'; + }; + description: 'Securely store API keys for auto-filling when installing MCP servers.'; + empty: { + description: 'Add keys to auto-fill environment variables when installing MCP servers.'; + title: 'No API keys saved'; + }; + form: { + addDescription: 'Store an API key for auto-filling in MCP server installations.'; + addTitle: 'Add API Key'; + boundTo: 'Bound to {{path}}'; + cancel: 'Cancel'; + editDescription: 'Update the key details. You must re-enter the value.'; + editTitle: 'Edit API Key'; + envVarPlaceholder: 'e.g. OPENAI_API_KEY'; + environmentVariableName: 'Environment Variable Name'; + errors: { + envVarRequired: 'Environment variable name is required'; + invalidEnvVar: 'Invalid environment variable name'; + invalidEnvVarFormat: 'Use letters, digits, underscores. Must start with a letter or underscore.'; + nameRequired: 'Name is required'; + projectScopeRequiresProject: 'Project-scoped API keys require an active project'; + saveFailed: 'Failed to save'; + valueRequired: 'Key value is required'; + }; + keychainUnavailable: 'OS keychain unavailable - keys encrypted with AES-256 locally. Install gnome-keyring for OS-level protection.'; + name: 'Name'; + namePlaceholder: 'e.g. OpenAI Production'; + projectScopeLabel: 'Project: {{project}}'; + projectUnavailable: 'Project unavailable'; + reenterValue: 'Re-enter key value'; + save: 'Save'; + saving: 'Saving...'; + scope: 'Scope'; + update: 'Update'; + userScopeLabel: 'User (global)'; + value: 'Value'; + valuePlaceholder: 'sk-...'; + }; + storage: { + localEncryption: 'OS keychain unavailable - keys are encrypted locally with AES-256. For stronger protection, install a keyring service (gnome-keyring, kwallet).'; + osKeychain: 'Keys are encrypted via {{backend}} and stored with restricted file permissions (owner-only).'; + }; + }; + customMcp: { + actions: { + add: 'Add'; + cancel: 'Cancel'; + install: 'Install'; + installing: 'Installing...'; + }; + description: 'Add a server manually without the catalog.'; + errors: { + installFailed: 'Install failed'; + invalidServerName: 'Invalid server name. Use alphanumeric characters, dashes, underscores, dots.'; + npmPackageRequired: 'npm package name is required'; + serverNameRequired: 'Server name is required'; + serverUrlRequired: 'Server URL is required'; + }; + fields: { + environmentVariables: 'Environment Variables'; + headers: 'Headers'; + npmPackage: 'npm Package'; + scope: 'Scope'; + serverName: 'Server Name'; + serverUrl: 'Server URL'; + transport: 'Transport'; + transportType: 'Transport Type'; + versionOptional: 'Version (optional)'; + }; + placeholders: { + envVarName: 'ENV_VAR_NAME'; + headerName: 'Header-Name'; + latest: 'latest'; + serverName: 'my-server'; + serverUrl: 'https://api.example.com/mcp'; + value: 'value'; + }; + title: 'Add Custom MCP Server'; + transport: { + httpSse: 'HTTP / SSE'; + stdio: 'Stdio (npm)'; + }; + }; + installButton: { + done: 'Done'; + install: 'Install'; + installing: 'Installing...'; + removing: 'Removing...'; + retry: 'Retry'; + uninstall: 'Uninstall'; + }; + mcpCard: { + auth: 'Auth'; + byAuthor: 'by {{author}}'; + envCount: '{{count}} envs'; + envCount_few: '{{count}} envs'; + envCount_many: '{{count}} envs'; + envCount_one: '{{count}} env'; + envCount_other: '{{count}} envs'; + hosting: { + both: 'Both'; + local: 'Local'; + remote: 'Remote'; + }; + repository: 'Repository'; + toolsCount: '{{count}} tools'; + toolsCount_few: '{{count}} tools'; + toolsCount_many: '{{count}} tools'; + toolsCount_one: '{{count}} tool'; + toolsCount_other: '{{count}} tools'; + website: 'Website'; + }; + mcpDetail: { + auth: { + remoteMayNeedHeaders: 'Remote MCP servers may still require custom headers or API keys even when the registry does not describe them. If connection fails after install, check the provider docs.'; + required: 'This server requires authentication'; + }; + diagnostics: { + launchTarget: 'Launch Target'; + }; + form: { + autoFilled: 'Auto-filled'; + environmentVariables: 'Environment Variables'; + headers: 'Headers'; + scope: 'Scope'; + serverName: 'Server Name'; + }; + install: { + httpTransport: 'HTTP: {{transport}}'; + install: 'Install Server'; + manage: 'Manage Installation'; + manualSetupDescription: 'This server requires manual setup. Check the repository for installation instructions.'; + manualSetupRequired: 'Manual setup required'; + npmPackage: 'npm: {{package}}'; + }; + links: { + glama: 'Glama'; + repository: 'Repository'; + website: 'Website'; + }; + metadata: { + author: 'Author'; + githubStars: 'GitHub Stars'; + hosting: 'Hosting'; + installType: 'Install Type'; + license: 'License'; + published: 'Published'; + source: 'Source'; + updated: 'Updated'; + version: 'Version'; + }; + placeholders: { + serverName: 'my-server'; + }; + scope: { + local: 'Local'; + project: 'Project'; + }; + tools: { + title: 'Tools ({{count}})'; + title_few: 'Tools ({{count}})'; + title_many: 'Tools ({{count}})'; + title_one: 'Tools ({{count}})'; + title_other: 'Tools ({{count}})'; + }; + }; + mcpPanel: { + diagnostics: { + disableReasons: { + checkingRuntimeAvailability: 'Checking runtime availability...'; + checkingRuntimeStatus: 'Checking runtime status...'; + runtimeFailedToStart: 'The configured runtime was found but failed to start. Open the Dashboard to repair or reinstall it.'; + runtimeRequired: 'The configured runtime is required. Install or repair it from the Dashboard.'; + }; + serversCount: '{{count}} servers'; + serversCount_few: '{{count}} servers'; + serversCount_many: '{{count}} servers'; + serversCount_one: '{{count}} server'; + serversCount_other: '{{count}} servers'; + title: 'Runtime MCP Diagnostics'; + waiting: 'Waiting for diagnostics results...'; + }; + empty: { + description: 'Check back later for new servers'; + searchDescription: 'Try a different search term'; + searchTitle: 'No servers found'; + title: 'No MCP servers available'; + }; + health: { + checkStatus: 'Check Status'; + checking: 'Checking...'; + checkingViaRuntime: 'Checking installed MCP servers via {{runtime}} ...'; + description: 'Run diagnostics from this page to verify installed MCP connectivity.'; + lastChecked: 'Last checked {{time}}'; + title: 'MCP Health Status'; + }; + loadMore: 'Load more'; + runtime: { + notAvailable: '{{runtime}} not available'; + notInstalled: '{{runtime}} not installed'; + requiredDescription: 'MCP health checks require {{runtime}}. Go to the Dashboard to install or repair it.'; + }; + searchPlaceholder: 'Search MCP servers...'; + sort: { + nameAsc: 'Name A→Z'; + nameDesc: 'Name Z→A'; + toolsDesc: 'Most tools'; + }; + }; + pluginCard: { + official: 'Official'; + }; + pluginDetail: { + links: { + contact: 'Contact'; + homepage: 'Homepage'; + }; + metadata: { + author: 'Author'; + capabilities: 'Capabilities'; + category: 'Category'; + installs: 'Installs'; + source: 'Source'; + version: 'Version'; + }; + readme: { + empty: 'No README available.'; + loading: 'Loading README...'; + }; + scope: { + label: 'Scope:'; + options: { + local: 'Local (gitignored)'; + project: 'Project (shared)'; + user: 'User (global)'; + }; + }; + unknown: 'Unknown'; + }; + pluginsPanel: { + activeFilters: '{{count}} active'; + activeFilters_few: '{{count}} active'; + activeFilters_many: '{{count}} active'; + activeFilters_one: '{{count}} active'; + activeFilters_other: '{{count}} active'; + browseByFit: 'Browse by fit'; + capabilities: 'Capabilities'; + categories: 'Categories'; + clearAllFilters: 'Clear all filters'; + clearFilters: 'Clear filters'; + counts: { + capabilities: '{{count}} capabilities'; + capabilities_few: '{{count}} capabilities'; + capabilities_many: '{{count}} capabilities'; + capabilities_one: '{{count}} capabilities'; + capabilities_other: '{{count}} capabilities'; + categories: '{{count}} categories'; + categories_few: '{{count}} categories'; + categories_many: '{{count}} categories'; + categories_one: '{{count}} categories'; + categories_other: '{{count}} categories'; + plugins: '{{count}} plugins'; + plugins_few: '{{count}} plugins'; + plugins_many: '{{count}} plugins'; + plugins_one: '{{count}} plugins'; + plugins_other: '{{count}} plugins'; + }; + empty: { + description: 'Check back later for new plugins'; + filteredDescription: 'Try adjusting your search or filter criteria'; + filteredTitle: 'No plugins match your filters'; + title: 'No plugins available'; + }; + filterDescription: 'Narrow the catalog by category, capability, or installed state.'; + installedOnly: 'Installed only'; + providerSupportNotice: "Plugin support is currently guaranteed for Anthropic (Claude) sessions only. We're working to support plugins across all agents."; + resultsUpdateInstantly: 'Results update instantly as you refine filters.'; + searchPlaceholder: 'Search plugins...'; + selectedCount: '{{count}} selected'; + selectedCount_few: '{{count}} selected'; + selectedCount_many: '{{count}} selected'; + selectedCount_one: '{{count}} selected'; + selectedCount_other: '{{count}} selected'; + showing: 'Showing {{shown}} of {{total}} plugins'; + sort: { + category: 'Category'; + nameAsc: 'Name A-Z'; + nameDesc: 'Name Z-A'; + popular: 'Popular'; + }; + }; + skillDetail: { + actions: { + cancel: 'Cancel'; + delete: 'Delete'; + deleteSkill: 'Delete Skill'; + deleting: 'Deleting...'; + editSkill: 'Edit Skill'; + openFolder: 'Open Folder'; + openSkillFile: 'Open SKILL.md'; + retry: 'Retry'; + }; + badges: { + assets: 'Assets'; + autoUse: 'Auto use'; + hasScripts: 'Has scripts'; + manualUse: 'Manual use'; + references: 'References'; + storedIn: 'Stored in {{root}}'; + }; + deleteDialog: { + description: 'Delete this skill and move it to Trash?'; + descriptionWithName: 'Delete "{{name}}" and move it to Trash? You can restore it later from Trash if needed.'; + title: 'Delete skill?'; + }; + descriptionFallback: 'Inspect discovered skill metadata and raw instructions.'; + errors: { + deleteFailed: 'Failed to delete skill'; + loadFailed: 'Unable to load this skill.'; + }; + files: { + advancedDetails: 'Advanced file details'; + assets: 'Assets'; + references: 'References'; + scripts: 'Scripts'; + storedAt: 'Stored at'; + }; + includes: { + assets: 'assets'; + instructionsOnly: 'Just the skill instructions'; + references: 'references'; + scripts: 'scripts'; + }; + invocation: { + auto: 'Runs automatically when it matches the task.'; + manualOnly: 'Only runs when you explicitly ask for it.'; + }; + issues: { + bundledScripts: 'This skill includes bundled scripts'; + reviewCarefully: 'Review this skill carefully before using it'; + }; + loading: 'Loading skill details...'; + scope: { + personal: 'Your personal skills'; + projectOnly: 'This project only'; + }; + summary: { + howUsed: 'How it is used'; + included: 'What comes with it'; + whoCanUse: 'Who can use it'; + }; + titleFallback: 'Skill details'; + }; + skillEditor: { + actions: { + cancel: 'Cancel'; + createSkill: 'Create Skill'; + preparing: 'Preparing...'; + reviewAndCreate: 'Review And Create'; + reviewAndSave: 'Review And Save'; + saveSkill: 'Save Skill'; + }; + advanced: { + customDescription: 'This skill uses a custom markdown format, so edit it directly here.'; + customTitle: '2. SKILL.md editor'; + description: 'Most people can skip this. Open it only if you want direct control over the raw markdown file.'; + hide: 'Hide Advanced Editor'; + resetFromStructuredFields: 'Reset From Structured Fields'; + show: 'Show Advanced Editor'; + title: '4. Advanced SKILL.md editor'; + }; + basics: { + description: 'Give this skill a clear name, choose who can use it, and decide where it should live.'; + title: '1. Basics'; + }; + description: { + create: 'Describe the workflow in plain language, review the files that will be created, then save it.'; + edit: 'Update this skill, review the resulting file changes, then save it.'; + }; + extraFiles: { + addedFiles: 'Added files:'; + assets: 'Assets'; + assetsDescription: 'Add screenshots or bundled media only if they help explain the workflow.'; + description: 'Add supporting docs, scripts, or assets only if this skill really needs them.'; + lockedForEdits: 'Root and folder are locked for edits'; + optionalDescription: 'Add starter files that will be included in the review and written together with `SKILL.md`.'; + optionalTitle: 'Optional files'; + references: 'References'; + referencesDescription: 'Add supporting docs, links, or examples the runtime can look at.'; + scripts: 'Scripts'; + scriptsDescription: 'Add helper commands or setup notes. Review carefully before sharing this skill.'; + title: '3. Extra files'; + }; + fields: { + compatibility: 'Compatibility'; + description: 'Description'; + folderName: 'Folder name'; + folderNameHint: 'We suggest this automatically from the skill name so review works right away.'; + invocation: 'How it should be used'; + license: 'License'; + name: 'Skill name'; + notes: 'Extra notes or guardrails'; + root: 'Where to store it'; + scope: 'Who can use it'; + steps: 'Main steps to follow'; + whenToUse: 'When to reach for this'; + }; + instructions: { + description: 'These sections generate the skill file for you, so you do not need to edit markdown unless you want to.'; + locked: 'Structured fields are locked because you switched to manual `SKILL.md` editing below.'; + title: '2. Instructions'; + }; + invocation: { + auto: 'Can be used automatically'; + manualOnly: 'Only when you ask for it'; + }; + placeholders: { + compatibility: 'claude-code, cursor'; + description: 'What this skill helps with'; + license: 'MIT'; + name: 'Write concise skill name'; + notes: 'Example: Call out missing tests, regressions, and risky assumptions.'; + steps: '1. Inspect the relevant files.\n2. Explain the main risk first.\n3. Suggest the safest fix.'; + whenToUse: 'Example: Use this when the task is a code review or bug triage request.'; + }; + review: { + creating: 'Creating a skill'; + hint: 'Review the file changes first, then confirm save in the next step.'; + saving: 'Saving this skill'; + }; + root: { + codexOnly: ' - Codex only'; + shared: ' - Shared'; + }; + scope: { + project: 'Project: {{project}}'; + projectUnavailable: 'Project unavailable'; + user: 'User'; + }; + title: { + create: 'Create skill'; + edit: 'Edit skill'; + }; + }; + skillImport: { + actions: { + backToImport: 'Back To Import'; + browse: 'Browse'; + cancel: 'Cancel'; + importSkill: 'Import Skill'; + preparing: 'Preparing...'; + reviewAndImport: 'Review And Import'; + }; + description: 'Pick an existing skill folder, review what will be copied, then import it into one of your supported skill locations.'; + errors: { + importFailed: 'Failed to import skill'; + invalidFolderName: 'Pick a simpler destination folder name using letters, numbers, dots, dashes, or underscores.'; + missingSkillFile: 'This folder does not look like a skill yet. It needs a SKILL.md, Skill.md, or skill.md file.'; + mustBeDirectory: 'Choose a folder to import, not a single file.'; + reviewFailed: 'Failed to review import changes'; + symbolicLinks: 'This folder contains symbolic links. Import the real files instead of links.'; + tooLarge: 'This skill folder is too large to import safely. Trim large assets and try again.'; + tooManyFiles: 'This skill folder is too large to import at once. Remove extra files and try again.'; + }; + fields: { + audience: 'Who can use it'; + destinationFolderName: 'Destination folder name'; + sourceFolder: 'Source folder'; + storage: 'Where to store it'; + }; + placeholders: { + defaultFolderName: 'Defaults to source folder name'; + }; + reviewHint: 'Review the copied files first, then confirm the import in the next step.'; + reviewLabel: 'Importing this skill'; + rootSuffix: { + codexOnly: ' - Codex only'; + shared: ' - Shared'; + }; + scope: { + project: 'Project: {{project}}'; + projectUnavailable: 'Project unavailable'; + user: 'User'; + }; + steps: { + chooseFolder: { + description: 'This should be a folder that already contains a `SKILL.md`, `Skill.md`, or `skill.md` file.'; + title: '1. Choose a skill folder'; + }; + location: { + description: 'Personal skills work everywhere. Project skills only show up for one codebase.'; + title: '2. Decide where it belongs'; + }; + }; + title: 'Import skill'; + }; + skillReview: { + binaryBadge: 'binary'; + binaryPreviewHidden: 'Binary file preview is not shown. The file will be copied as-is.'; + confirmPromptPrefix: 'Review the diff below, then use'; + confirmPromptSuffix: 'to apply these changes.'; + description: '{{reviewLabel}} previews the filesystem changes first. Nothing is written until you confirm below.'; + noChanges: 'No file changes detected yet.'; + noPreview: 'No preview available.'; + summary: { + binary: '{{count}} binary'; + fileChanges: '{{count}} file changes'; + fileChanges_few: '{{count}} file changes'; + fileChanges_many: '{{count}} file changes'; + fileChanges_one: '{{count}} file change'; + fileChanges_other: '{{count}} file changes'; + new: '{{count}} new'; + removed: '{{count}} removed'; + updated: '{{count}} updated'; + }; + title: 'Review skill changes'; + }; + skillsPanel: { + actions: { + createSkill: 'Create Skill'; + import: 'Import'; + }; + badges: { + assets: 'Assets'; + hasScripts: 'Has scripts'; + needsAttention: 'Needs attention'; + references: 'References'; + storedIn: 'Stored in {{root}}'; + }; + configuredRuntime: 'the configured runtime'; + counts: { + codexOnly: '{{count}} Codex only'; + codexOnly_few: '{{count}} Codex only'; + codexOnly_many: '{{count}} Codex only'; + codexOnly_one: '{{count}} Codex only'; + codexOnly_other: '{{count}} Codex only'; + personal: '{{count}} personal'; + personal_few: '{{count}} personal'; + personal_many: '{{count}} personal'; + personal_one: '{{count}} personal'; + personal_other: '{{count}} personal'; + project: '{{count}} project'; + project_few: '{{count}} project'; + project_many: '{{count}} project'; + project_one: '{{count}} project'; + project_other: '{{count}} project'; + shared: '{{count}} shared'; + shared_few: '{{count}} shared'; + shared_many: '{{count}} shared'; + shared_one: '{{count}} shared'; + shared_other: '{{count}} shared'; + total: '{{count}} total'; + total_few: '{{count}} total'; + total_many: '{{count}} total'; + total_one: '{{count}} total'; + total_other: '{{count}} total'; + }; + empty: { + noMatches: 'No skills match your search'; + noMatchesDescription: 'Try a different search term or switch filters.'; + noSkills: 'No skills yet'; + noSkillsDescription: 'Create your first skill to teach a repeatable workflow, or import one you already use.'; + }; + filters: { + all: 'All skills'; + codexOnly: 'Codex only'; + hasScripts: 'Has scripts'; + needsAttention: 'Needs attention'; + personal: 'Personal'; + project: 'Project'; + shared: 'Shared'; + }; + hero: { + codexAvailable: 'Use `.codex` when a skill should stay Codex-only.'; + codexUnavailable: 'Existing `.codex` skills stay editable here, but new Codex-only skills need the Codex runtime enabled.'; + description: 'Skills are reusable instructions that help the runtime handle the same kind of task more consistently.'; + guidance: 'Use personal skills for habits you want everywhere. Use project skills for workflows that only make sense inside one codebase.'; + personalContext: 'You are seeing only your personal skills right now.'; + projectContext: 'You are seeing skills for {{project}} plus your personal skills.'; + title: 'Teach repeatable work'; + }; + invocation: { + auto: 'Runs automatically when it fits'; + manualOnly: 'Only runs when you explicitly ask for it'; + }; + loading: { + loading: 'Loading skills...'; + refreshing: 'Refreshing skills...'; + }; + runtimeAudience: 'Shared skills in `.claude`, `.cursor`, and `.agents` are available to {{audience}}. Skills stored in `.codex` stay Codex-only when Codex support is available.'; + scope: { + project: 'This project'; + user: 'Personal'; + }; + searchPlaceholder: 'Search by skill name or what it helps with...'; + sections: { + personal: { + description: 'Habits and instructions you want available everywhere.'; + title: 'Personal skills'; + }; + project: { + description: 'Workflows that only make sense for this codebase.'; + title: 'Project skills'; + }; + }; + sort: { + label: 'Sort skills'; + name: 'Name'; + recent: 'Recent'; + }; + status: { + hasScripts: 'Includes scripts, so review it carefully'; + needsAttention: 'Needs attention before you rely on it'; + ready: 'Ready to use'; + }; + success: { + created: 'Skill created successfully.'; + imported: 'Skill imported successfully.'; + saved: 'Skill saved successfully.'; + }; + }; + store: { + actions: { + addCustom: 'Add Custom'; + openDashboard: 'Open Dashboard'; + refreshCatalog: 'Refresh catalog'; + }; + capabilities: { + mcp: 'MCP: {{status}}'; + plugins: 'Plugins: {{status}}'; + skills: 'Skills: {{status}}'; + }; + desktopOnly: 'Available in the desktop app only.'; + provider: { + checkingStatus: 'Checking provider status...'; + connected: 'Connected'; + loading: 'Loading...'; + needsSetup: 'Needs setup'; + readyToConfigure: 'Ready to configure'; + unsupported: 'Unsupported'; + }; + runtime: { + checkingAvailabilityDescription: 'Extensions need the configured runtime to manage plugins, MCP servers, skills, and provider connections.'; + checkingAvailabilityTitle: 'Checking extensions runtime availability'; + failedToStartDescription: 'Extensions are disabled until the runtime passes its startup health check. Open the Dashboard to repair or reinstall it.'; + failedToStartTitle: 'The configured runtime was found but failed to start'; + multimodelCapabilitiesDescription: 'Provider support can differ by section. Plugins are shown only where the runtime explicitly declares support.'; + multimodelCapabilitiesTitle: 'Multimodel runtime capabilities'; + needsSignInDescription: '{{runtime}} was found{{version}}, but plugin installs are disabled until you sign in from the Dashboard.'; + needsSignInTitle: '{{runtime}} needs sign-in'; + notAvailableDescription: 'Extensions are disabled until the runtime is installed. Open the Dashboard to install it and retry.'; + notAvailableTitle: 'The configured runtime is not available'; + readyDescription: 'Plugins can be installed from this page{{versionSuffix}}.'; + readyTitle: '{{runtime}} is ready'; + requiredForMutations: 'The configured runtime is required to install or uninstall extensions. Install or repair it from the Dashboard.'; + }; + sessionsRestartWarning: "Running sessions won't pick up extension changes until restarted."; + tabs: { + apiKeys: { + description: 'Secret keys for online services. Add them here so plugins, servers, and integrations can connect and work.'; + label: 'API Keys'; + }; + mcpServers: { + description: 'Connections to outside tools and apps. They let the runtime read data or do actions beyond this app.'; + label: 'MCP Servers'; + }; + plugins: { + description: 'Small add-ons for the runtime. In multimodel mode they currently apply to Anthropic sessions when supported. Broader provider support is in development.'; + label: 'Plugins'; + }; + skills: { + description: 'Ready-made instructions for common jobs. They help the runtime handle repeatable tasks more consistently.'; + label: 'Skills'; + }; + }; + title: 'Extensions'; + }; + }; + report: { + cost: { + breakdownTitle: 'Cost Breakdown (per 1M tokens)'; + cacheRead: 'Cache Read'; + cacheWrite: 'Cache Write'; + cost: 'Cost'; + input: 'Input'; + noCommits: 'no commits'; + noLinesChanged: 'no lines changed'; + output: 'Output'; + parent: 'Parent: {{cost}}'; + parentCost: 'Parent Cost'; + perCommit: 'Per Commit'; + perCommitFormula: 'total cost ÷ {{count}} commit'; + perCommitFormula_few: 'total cost ÷ {{count}} commits'; + perCommitFormula_many: 'total cost ÷ {{count}} commits'; + perCommitFormula_one: 'total cost ÷ {{count}} commit'; + perCommitFormula_other: 'total cost ÷ {{count}} commits'; + perLineChanged: 'Per Line Changed'; + perLineFormula: 'total cost ÷ {{count}} line'; + perLineFormula_few: 'total cost ÷ {{count}} lines'; + perLineFormula_many: 'total cost ÷ {{count}} lines'; + perLineFormula_one: 'total cost ÷ {{count}} line'; + perLineFormula_other: 'total cost ÷ {{count}} lines'; + subagent: 'Subagent: {{cost}}'; + subagentCost: 'Subagent Cost'; + title: 'Cost Analysis'; + total: 'Total'; + }; + errors: { + count: '{{count}} errors'; + count_few: '{{count}} errors'; + count_many: '{{count}} errors'; + count_one: '{{count}} error'; + count_other: '{{count}} errors'; + error: 'Error'; + input: 'Input'; + messageIndex: 'msg #{{index}}'; + permissionDenialCount: '{{count}} permission denials'; + permissionDenialCount_few: '{{count}} permission denials'; + permissionDenialCount_many: '{{count}} permission denials'; + permissionDenialCount_one: '{{count}} permission denial'; + permissionDenialCount_other: '{{count}} permission denials'; + permissionDenied: 'Permission Denied'; + title: 'Errors'; + }; + friction: { + corrections: 'Corrections'; + correctionsCount: '{{count}} corrections'; + correctionsCount_few: '{{count}} corrections'; + correctionsCount_many: '{{count}} corrections'; + correctionsCount_one: '{{count}} correction'; + correctionsCount_other: '{{count}} corrections'; + rate: 'Friction Rate: {{rate}}%'; + repeatedBashCommands: 'Repeated Bash Commands'; + reworkedFiles: 'Reworked Files (3+ edits)'; + thrashingSignals: 'Thrashing Signals'; + title: 'Friction Signals'; + }; + git: { + branchesCreated: 'Branches Created'; + commits: 'Commits'; + linesAdded: 'Lines Added'; + linesRemoved: 'Lines Removed'; + pushes: 'Pushes'; + title: 'Git Activity'; + }; + insights: { + agent: 'agent'; + agentTree: 'Agent Tree ({{count}} {{unit}})'; + agentTree_few: 'Agent Tree ({{count}} {{unit}})'; + agentTree_many: 'Agent Tree ({{count}} {{unit}})'; + agentTree_one: 'Agent Tree ({{count}} {{unit}})'; + agentTree_other: 'Agent Tree ({{count}} {{unit}})'; + agent_few: 'agents'; + agent_many: 'agents'; + agent_one: 'agent'; + agent_other: 'agents'; + background: '(background)'; + bashCommands: 'Bash Commands'; + keyTakeaways: 'Key Takeaways'; + outOfScopeFindings: 'Out-of-Scope Findings ({{count}})'; + outOfScopeFindings_few: 'Out-of-Scope Findings ({{count}})'; + outOfScopeFindings_many: 'Out-of-Scope Findings ({{count}})'; + outOfScopeFindings_one: 'Out-of-Scope Findings ({{count}})'; + outOfScopeFindings_other: 'Out-of-Scope Findings ({{count}})'; + questionsAsked: 'Questions Asked ({{count}})'; + questionsAsked_few: 'Questions Asked ({{count}})'; + questionsAsked_many: 'Questions Asked ({{count}})'; + questionsAsked_one: 'Questions Asked ({{count}})'; + questionsAsked_other: 'Questions Asked ({{count}})'; + repeated: 'Repeated'; + skillsInvoked: 'Skills Invoked ({{count}})'; + skillsInvoked_few: 'Skills Invoked ({{count}})'; + skillsInvoked_many: 'Skills Invoked ({{count}})'; + skillsInvoked_one: 'Skills Invoked ({{count}})'; + skillsInvoked_other: 'Skills Invoked ({{count}})'; + taskDispatches: 'Task Dispatches ({{count}})'; + taskDispatches_few: 'Task Dispatches ({{count}})'; + taskDispatches_many: 'Task Dispatches ({{count}})'; + taskDispatches_one: 'Task Dispatches ({{count}})'; + taskDispatches_other: 'Task Dispatches ({{count}})'; + tasksCreated: 'Tasks Created ({{count}})'; + tasksCreated_few: 'Tasks Created ({{count}})'; + tasksCreated_many: 'Tasks Created ({{count}})'; + tasksCreated_one: 'Tasks Created ({{count}})'; + tasksCreated_other: 'Tasks Created ({{count}})'; + teamMode: 'Team Mode'; + teams: 'Teams: {{teams}}'; + title: 'Session Insights'; + total: 'Total'; + unique: 'Unique'; + }; + overview: { + metrics: { + branch: 'Branch'; + compactions: 'Compactions'; + contextUsage: 'Context Usage'; + duration: 'Duration'; + messages: 'Messages'; + project: 'Project'; + sessionId: 'Session ID'; + subagents: 'Subagents'; + }; + no: 'No'; + title: 'Overview'; + yes: 'Yes'; + }; + quality: { + chars: 'chars'; + corrections: 'Corrections'; + failed: 'failed'; + fileReadRedundancy: 'File Read Redundancy'; + firstMessage: 'First Message'; + firstRun: 'First Run'; + frictionRate: 'Friction Rate'; + lastRun: 'Last Run'; + messagesBeforeWork: 'Messages Before Work'; + passed: 'passed'; + percentOfTotal: '% of Total'; + promptQuality: 'Prompt Quality'; + readsPerUniqueFile: 'Reads/Unique File'; + snapshot: 'snapshot'; + snapshot_few: 'snapshots'; + snapshot_many: 'snapshots'; + snapshot_one: 'snapshot'; + snapshot_other: 'snapshots'; + startupOverhead: 'Startup Overhead'; + testProgression: 'Test Progression'; + title: 'Quality Signals'; + tokensBeforeWork: 'Tokens Before Work'; + totalReads: 'Total Reads'; + uniqueFiles: 'Unique Files'; + userMessages: 'User Messages'; + }; + subagents: { + metrics: { + count: 'Count'; + totalCost: 'Total Cost'; + totalDuration: 'Total Duration'; + totalTokens: 'Total Tokens'; + }; + table: { + cost: 'Cost'; + description: 'Description'; + duration: 'Duration'; + tokens: 'Tokens'; + type: 'Type'; + }; + title: 'Subagents'; + }; + timeline: { + idleAnalysis: 'Idle Analysis'; + keyEvents: 'Key Events'; + messageNumber: 'msg #{{number}}'; + metrics: { + activeTime: 'Active Time'; + idleGaps: 'Idle Gaps'; + idlePercent: 'Idle %'; + totalIdle: 'Total Idle'; + }; + modelSwitches: 'Model Switches ({{count}})'; + modelSwitches_few: 'Model Switches ({{count}})'; + modelSwitches_many: 'Model Switches ({{count}})'; + modelSwitches_one: 'Model Switches ({{count}})'; + modelSwitches_other: 'Model Switches ({{count}})'; + title: 'Timeline & Activity'; + }; + tokens: { + apiCalls: 'API Calls'; + cacheCreate: 'Cache Create'; + cacheEfficiency: 'Cache Efficiency'; + cacheRead: 'Cache Read'; + cacheReadPct: 'Cache Read %'; + coldStart: 'Cold Start'; + cost: 'Cost'; + input: 'Input'; + model: 'Model'; + no: 'No'; + output: 'Output'; + readWriteRatio: 'R/W Ratio'; + title: 'Token Usage'; + total: 'Total'; + yes: 'Yes'; + }; + tools: { + columns: { + calls: 'Calls'; + errors: 'Errors'; + health: 'Health'; + successPercent: 'Success %'; + tool: 'Tool'; + }; + summary: '{{formattedCount}} total calls across {{toolCount}} tools'; + title: 'Tool Usage'; + }; + }; + settings: { + advanced: { + about: { + appIconAlt: 'App icon'; + description: 'Assemble AI agent teams that work autonomously in parallel, communicate across teams, and manage tasks on a kanban board - with built-in code review, live process monitoring, and full tool visibility.'; + standalone: 'Standalone'; + title: 'About'; + version: 'Version {{version}}'; + }; + appName: 'Agent Teams AI'; + configuration: { + editConfig: 'Edit Config'; + exportConfig: 'Export Config'; + importConfig: 'Import Config'; + openInEditor: 'Open in Editor'; + resetToDefaults: 'Reset to Defaults'; + title: 'Configuration'; + }; + updates: { + available: 'v{{version}} available'; + check: 'Check for Updates'; + checking: 'Checking...'; + ready: 'Update ready'; + unknownVersion: 'unknown'; + upToDate: 'Up to date'; + }; + }; + cliRuntime: { + actions: { + checkForUpdates: 'Check for Updates'; + checking: 'Checking...'; + extensions: 'Extensions'; + installRuntime: 'Install {{runtime}}'; + manage: 'Manage'; + recheck: 'Re-check'; + reinstallRuntime: 'Reinstall {{runtime}}'; + retry: 'Retry'; + update: 'Update'; + }; + installer: { + checkingLatest: 'Checking latest version...'; + downloading: 'Downloading...'; + failed: 'Installation failed'; + installed: 'Installed v{{version}}'; + installing: 'Installing...'; + latest: 'latest'; + verifying: 'Verifying checksum...'; + }; + labels: { + multimodel: 'Multimodel'; + }; + loading: { + aiProviders: 'Checking AI Providers...'; + claudeCli: 'Checking Claude CLI...'; + }; + provider: { + backend: 'Backend: {{backend}}'; + loadingModels: 'Loading models...'; + modelsUnavailable: 'Models unavailable for this runtime build'; + runtime: 'Runtime: {{runtime}}'; + }; + providerTerminal: { + authFailed: 'Authentication failed'; + authUpdated: 'Authentication updated'; + loggedOut: 'Provider logged out'; + login: 'Login'; + logout: 'Logout'; + logoutFailed: 'Logout failed'; + }; + status: { + configuredNotFound: 'The configured {{runtime}} was not found.'; + foundButFailed: '{{runtime}} was found but failed to start'; + healthCheckFailed: 'The configured {{runtime}} failed its startup health check.'; + notInstalled: '{{runtime}} not installed'; + }; + title: 'CLI Runtime'; + }; + cliStatus: { + versionUpgrade: 'v{{current}} -> v{{latest}}'; + }; + configEditor: { + errors: { + loadFailed: 'Failed to load config'; + saveFailed: 'Failed to save config'; + }; + footer: { + autoSave: 'Changes auto-save after editing'; + escapeKey: 'Esc'; + toClose: 'to close'; + }; + loading: 'Loading config...'; + status: { + invalidJson: 'Invalid JSON'; + saveFailed: 'Save failed'; + saved: 'Saved'; + saving: 'Saving...'; + }; + title: 'Edit Configuration'; + }; + connection: { + actions: { + connect: 'Connect'; + connecting: 'Connecting...'; + disconnect: 'Disconnect'; + testConnection: 'Test Connection'; + testing: 'Testing...'; + }; + currentMode: { + description: 'Data source for session files'; + label: 'Current Mode'; + local: 'Local ({{path}})'; + }; + description: 'Connect to a remote machine to view Claude Code sessions running there'; + form: { + authentication: 'Authentication'; + host: 'Host'; + hostPlaceholder: 'hostname or SSH config alias'; + password: 'Password'; + port: 'Port'; + privateKeyPath: 'Private Key Path'; + username: 'Username'; + usernamePlaceholder: 'user'; + }; + savedProfiles: { + title: 'Saved Profiles'; + }; + ssh: { + title: 'SSH Connection'; + }; + status: { + connectedTo: 'Connected to {{host}}'; + remoteSessions: 'Viewing remote sessions via SSH'; + }; + test: { + failed: 'Connection failed: {{error}}'; + success: 'Connection successful'; + unknownError: 'Unknown error'; + }; + title: 'Remote Connection'; + }; + general: { + agentLanguage: { + description: 'Language for agent communication'; + descriptionWithDetected: 'Language for agent communication (detected: {{detected}})'; + emptyMessage: 'No language found.'; + label: 'Language'; + searchPlaceholder: 'Search language...'; + selectPlaceholder: 'Select language...'; + title: 'Agent Language'; + }; + appLanguage: { + description: 'Language for the application interface.'; + label: 'Language'; + title: 'App Language'; + }; + appearance: { + autoExpandAIGroups: { + description: 'Automatically expand each response turn when opening a transcript or receiving a new message'; + label: 'Expand AI responses by default'; + }; + nativeTitleBar: { + description: 'Use the default system window frame instead of the custom title bar'; + label: 'Use native title bar'; + restartConfirm: { + confirmLabel: 'Restart'; + message: 'The app needs to restart to apply the title bar change. Restart now?'; + title: 'Restart required'; + }; + }; + theme: { + description: 'Choose your preferred color theme'; + label: 'Theme'; + options: { + dark: 'Dark'; + light: 'Light'; + system: 'System'; + }; + }; + title: 'Appearance'; + }; + browserAccess: { + serverMode: { + description: 'Start an HTTP server to access the UI from a browser or embed in iframes'; + label: 'Enable server mode'; + }; + title: 'Browser Access'; + }; + localClaudeRoot: { + actions: { + selectFolder: 'Select Folder'; + selectFolderManually: 'Select Folder Manually'; + useAutoDetect: 'Use Auto-Detect'; + useFolder: 'Use Folder'; + usePath: 'Use Path'; + useThisPath: 'Use This Path'; + useWsl: 'Using Linux/WSL?'; + }; + confirm: { + noProjectsDir: { + message: 'This folder does not contain a "projects" directory. Continue anyway?'; + title: 'No projects directory found'; + }; + noWslPaths: { + message: 'Could not find WSL distros with Claude data automatically. Select folder manually?'; + title: 'No WSL Claude paths found'; + }; + notClaudeDir: { + message: 'This folder is named "{{folderName}}", not ".claude". Continue anyway?'; + title: 'Selected folder is not .claude'; + }; + wslNoProjectsDir: { + message: '"{{path}}" does not contain a "projects" directory. Continue anyway?'; + title: 'WSL path missing projects directory'; + }; + }; + current: { + autoDetected: 'Auto-detected: {{path}}'; + autoDetectedPath: 'Using auto-detected path'; + customPath: 'Using custom path'; + label: 'Current Local Root'; + }; + description: 'Choose which local folder is treated as your Claude data root'; + errors: { + detectWslFailed: 'Failed to detect WSL Claude root paths'; + loadFailed: 'Failed to load local Claude root settings'; + updateFailed: 'Failed to update Claude root'; + }; + title: 'Local Claude Root'; + wslModal: { + closeAriaLabel: 'Close WSL path modal'; + description: 'Detected WSL distributions and Claude root candidates'; + noProjectsDir: 'No projects directory detected'; + title: 'Select WSL Claude Root'; + }; + }; + privacy: { + telemetry: { + description: 'Help improve the app by sending anonymous crash and performance data'; + label: 'Send crash reports'; + }; + title: 'Privacy'; + }; + server: { + runningOn: 'Running on'; + standaloneModeDescription: 'Running in standalone mode. The HTTP server is always active. System notifications are not available - notification triggers are logged in-app only.'; + title: 'Server'; + }; + startup: { + launchAtLogin: { + description: 'Automatically start the app when you log in'; + label: 'Launch at login'; + }; + showDockIcon: { + description: 'Display the app icon in the dock (macOS)'; + label: 'Show dock icon'; + }; + title: 'Startup'; + }; + }; + notificationTriggers: { + add: { + cancel: 'Cancel'; + submit: 'Add Trigger'; + title: 'Add Custom Trigger'; + }; + builtin: { + description: 'Default triggers that come with the application. You can enable or disable them and customize their patterns.'; + title: 'Built-in Triggers'; + }; + card: { + builtinBadge: 'Builtin'; + collapseAriaLabel: 'Collapse'; + deleteAriaLabel: 'Delete trigger'; + editNameAriaLabel: 'Edit name'; + expandAriaLabel: 'Expand'; + }; + color: { + customHexTitle: 'Custom hex color'; + invalidHex: 'Invalid hex'; + }; + configuration: { + alertIfGreaterThan: 'Alert if >'; + emptyPatternHint: 'Leave empty to match all content. Uses JavaScript regex syntax.'; + errorStatusDescription: 'Triggers when a tool execution reports an error (is_error: true).'; + matchPatternPlaceholder: 'e.g., error|failed|exception'; + tokensUnit: 'tokens'; + }; + custom: { + description: 'Create your own triggers to get notified for specific patterns or tool outputs.'; + empty: 'No custom triggers configured yet.'; + title: 'Custom Triggers'; + }; + errors: { + invalidRegexPattern: 'Invalid regex pattern'; + }; + fields: { + contentType: 'Content Type'; + matchField: 'Match Field'; + matchPattern: 'Match Pattern (Regex)'; + scopeToolName: 'Scope / Tool Name'; + scopeToolNameOptional: 'Scope / Tool Name (optional)'; + threshold: 'Threshold'; + tokenType: 'Token Type'; + triggerNamePlaceholder: 'e.g., Build Failure Alert'; + triggerNameRequired: 'Trigger Name *'; + }; + ignorePatterns: { + hint: 'Press Enter to add. Notification is skipped if any pattern matches.'; + placeholder: 'Add ignore regex...'; + removeAriaLabel: 'Remove ignore pattern'; + summary: 'Advanced: Exclusion Rules'; + title: 'Ignore Patterns (skip if matches)'; + }; + options: { + contentTypes: { + text: 'Text Output'; + thinking: 'Thinking'; + tool_result: 'Tool Result'; + tool_use: 'Tool Use'; + }; + matchFields: { + args: 'Arguments'; + command: 'Command'; + content: 'Content'; + description: 'Description'; + file_path: 'File Path'; + fullInput: 'Full Input (JSON)'; + glob: 'Glob Filter'; + new_string: 'New String'; + old_string: 'Old String'; + path: 'Path'; + pattern: 'Pattern'; + prompt: 'Prompt'; + query: 'Query'; + skill: 'Skill Name'; + subagent_type: 'Subagent Type'; + text: 'Text Content'; + thinking: 'Thinking Content'; + url: 'URL'; + }; + modes: { + content_match: 'Content Pattern'; + error_status: 'Execution Error'; + token_threshold: 'High Token Usage'; + }; + tokenTypes: { + input: 'Input Tokens'; + output: 'Output Tokens'; + total: 'Total Tokens'; + }; + toolNames: { + anyTool: 'Any Tool'; + }; + }; + preview: { + defaultTestTriggerName: 'Test Trigger'; + detectedSuffix: 'errors would have been detected'; + more: '...and {{count}} more'; + more_few: '...and {{count}} more'; + more_many: '...and {{count}} more'; + more_one: '...and {{count}} more'; + more_other: '...and {{count}} more'; + testTrigger: 'Test Trigger'; + testing: 'Testing...'; + title: 'Preview'; + truncatedWarning: 'Search stopped early (timeout or count limit). Actual matches may be higher.'; + viewSession: 'View Session'; + }; + repositoryScope: { + empty: 'No repositories selected - trigger applies to all repositories'; + hint: 'When repositories are selected, this trigger only fires for errors in those repositories.'; + placeholder: 'Select repository to add...'; + summary: 'Advanced: Repository Scope'; + title: 'Limit to Repositories (applies only to selected repositories)'; + }; + sections: { + configuration: 'Configuration'; + dotColor: 'Dot Color'; + generalInfo: 'General Info'; + triggerCondition: 'Trigger Condition'; + }; + }; + notifications: { + dev: { + descriptionPrefix: 'Notifications may not work in development mode. macOS identifies the app as "Electron" (bundle ID'; + descriptionSuffix: ') instead of the production app name. Check System Settings > Notifications > Electron to verify permissions.'; + title: 'Dev Mode'; + }; + ignoredRepositories: { + description: 'Notifications from these repositories will be ignored'; + empty: 'No repositories ignored'; + selectPlaceholder: 'Select repository to ignore...'; + title: 'Ignored Repositories'; + }; + settings: { + enabled: { + description: 'Show system notifications for errors and events'; + label: 'Enable System Notifications'; + }; + sound: { + description: 'Play a sound when notifications appear'; + label: 'Play sound'; + }; + subagentErrors: { + description: 'Detect and notify about errors in subagent sessions'; + label: 'Include subagent errors'; + }; + title: 'Notification Settings'; + }; + snooze: { + clear: 'Clear Snooze'; + description: 'Temporarily pause notifications'; + descriptionWithTime: 'Snoozed until {{time}}'; + label: 'Snooze notifications'; + options: { + '-1': 'Until tomorrow'; + '120': '2 hours'; + '15': '15 minutes'; + '240': '4 hours'; + '30': '30 minutes'; + '60': '1 hour'; + }; + selectDuration: 'Select duration...'; + }; + taskCompletion: { + description: 'Get native OS notifications when Claude finishes tasks - sounds, banners, and Dock/taskbar badges. Works on macOS, Linux, and Windows.'; + installPlugin: 'Install claude-notifications-go plugin'; + title: 'Task Completion Notifications'; + }; + team: { + allTasksCompleted: { + description: 'Notify when every task in a team reaches completed status'; + label: 'All tasks completed'; + }; + autoResumeOnRateLimit: { + description: 'When Claude reports a reset time, schedule a follow-up nudge for the team lead after the limit resets'; + label: 'Auto-resume after rate limit'; + }; + clarifications: { + description: 'Show native OS notifications when a task needs your input'; + label: 'Task clarification notifications'; + }; + crossTeamMessage: { + description: 'Notify when a message arrives from another team'; + label: 'Cross-team message notifications'; + }; + leadInbox: { + description: 'Notify when teammates send messages to the team lead'; + label: 'Lead inbox notifications'; + }; + statusChange: { + description: "Show native OS notifications when a task's status changes"; + label: 'Task status change notifications'; + onlySolo: { + description: 'Notify only when the team has no teammates'; + label: 'Only in Solo mode'; + }; + statuses: { + description: 'Which target statuses trigger a notification'; + label: 'Notify on these statuses'; + options: { + approved: 'Approved'; + completed: 'Completed'; + deleted: 'Deleted'; + in_progress: 'Started'; + needsFix: 'Needs Fixes'; + pending: 'Pending'; + review: 'Review'; + }; + }; + }; + taskComments: { + description: 'Show native OS notifications when agents comment on tasks'; + label: 'Task comment notifications'; + }; + taskCreated: { + description: 'Show native OS notifications when a new task is created'; + label: 'Task created notifications'; + }; + teamLaunched: { + description: 'Notify when a team finishes launching and is ready'; + label: 'Team launched notifications'; + }; + title: 'Team Notifications'; + toolApproval: { + description: 'Notify when a tool needs your approval (Allow/Deny) while the app is not focused'; + label: 'Tool approval notifications'; + }; + userInbox: { + description: 'Notify when teammates send messages to you'; + label: 'User inbox notifications'; + }; + }; + test: { + action: 'Send Test'; + description: 'Send a test notification to verify delivery'; + failedToSend: 'Failed to send test notification'; + label: 'Test notification'; + sending: 'Sending...'; + sent: 'Sent!'; + unknownError: 'Unknown error'; + }; + }; + providerRuntime: { + actions: { + cancel: 'Cancel'; + cancelLogin: 'Cancel login'; + connectChatGpt: 'Connect ChatGPT'; + delete: 'Delete'; + disable: 'Disable'; + disconnectAccount: 'Disconnect account'; + generateLink: 'Generate link'; + openLogin: 'Open login'; + reconnectAnthropic: 'Reconnect Anthropic'; + refresh: 'Refresh'; + replaceKey: 'Replace key'; + saveEndpoint: 'Save endpoint'; + saveKey: 'Save key'; + saving: 'Saving...'; + setApiKey: 'Set API key'; + updateKey: 'Update key'; + useCode: 'Use code'; + }; + alerts: { + anthropicApiKeyMissing: 'API key mode is selected, but no Anthropic API credential is available yet.'; + anthropicStoredKeyAvailable: 'A saved API key is available, but app-launched Anthropic sessions use it only after you switch to API key mode.'; + anthropicSubscriptionMissing: 'Anthropic subscription mode is selected. Sign in with Anthropic to use this provider.'; + authTokenMissing: 'Auth token is not configured. Many local Anthropic-compatible endpoints require a non-empty token.'; + chatgptLoginPending: 'Waiting for ChatGPT account login to finish...'; + chatgptLoginStarting: 'Starting ChatGPT login...'; + codexApiKeyMissing: 'API key mode is selected, but no OPENAI_API_KEY or CODEX_API_KEY credential is available yet.'; + codexLocalArtifactsNoSession: 'Codex CLI currently has no active ChatGPT account. Local Codex account data exists, but no active managed session is selected.'; + codexNeedsReconnect: 'Codex has a locally selected ChatGPT account, but the current session needs reconnect.'; + codexNoChatgptAccount: 'Codex CLI currently has no active ChatGPT account. Connect ChatGPT to use your subscription.'; + codexNoCredential: 'No ChatGPT account or API key is available yet.'; + geminiApiUnavailable: 'Gemini API is currently unavailable. Configure `GEMINI_API_KEY` here or use valid Google ADC credentials.'; + withApiKeyFallback: '{{message}} Switch to API key mode to use the detected API key.'; + }; + apiKey: { + loadingStoredCredentials: 'Loading stored credentials...'; + projectScope: 'Project'; + providers: { + anthropic: { + description: 'Use a direct Anthropic API key for API-billed access. Your Anthropic subscription session stays available when you switch back.'; + name: 'Anthropic API Key'; + placeholder: 'sk-ant-...'; + title: 'API key'; + }; + codex: { + description: 'Use an OpenAI API key as a secondary Codex auth path. If you switch Codex to API key mode, the app will mirror OPENAI_API_KEY into CODEX_API_KEY for native launches.'; + name: 'Codex API Key'; + placeholder: 'sk-proj-...'; + title: 'API key'; + }; + gemini: { + description: 'Use `GEMINI_API_KEY` for the Gemini API backend. CLI SDK and ADC do not require it.'; + name: 'Gemini API Key'; + placeholder: 'AIza...'; + title: 'API access'; + }; + }; + scope: 'Scope'; + storedIn: 'Stored in {{backend}}'; + storedInApp: 'Stored in app'; + userScope: 'User'; + }; + authModeDescriptions: { + anthropic: { + apiKey: 'Force app-launched Anthropic sessions to use an API key credential.'; + auto: 'Use the runtime default behavior. Saved API keys in this app are only used after you switch to API key mode.'; + oauth: 'Force app-launched Anthropic sessions to use the local Anthropic subscription session.'; + }; + codex: { + apiKey: 'Force native Codex launches to use OPENAI_API_KEY / CODEX_API_KEY billing.'; + auto: 'Prefer your ChatGPT account when it is available. Fall back to API key mode only when needed.'; + chatgpt: 'Force native Codex launches to use your connected ChatGPT account and subscription.'; + }; + }; + codex: { + account: { + appServer: 'App-server: {{state}}'; + connected: 'Connected'; + description: 'Manage the local Codex app-server account session that powers subscription-backed native launches.'; + hints: { + autoUsesApiKeyUntilChatgpt: '{{message}} Auto will keep using the detected API key until ChatGPT is connected.'; + detectedApiKeyNeedsApiMode: '{{message}} The detected API key is only used after you switch Codex to API key mode.'; + localArtifactsNoSession: 'Codex CLI currently reports no active ChatGPT account. Local Codex account data exists, but no active managed session is selected. Usage limits appear here only after Codex CLI sees one.'; + noActiveAccount: 'Codex CLI currently reports no active ChatGPT account. Usage limits appear here only after Codex CLI sees one.'; + reconnectBeforeUsage: 'Codex has a locally selected ChatGPT account, but the current session needs reconnect before usage limits can load here.'; + usageLimitsAfterReport: 'Usage limits appear here after Codex reports them for the connected ChatGPT account.'; + }; + loginInProgress: 'Login in progress'; + plan: 'Plan: {{plan}}'; + reconnectRequired: 'Reconnect required'; + title: 'ChatGPT account'; + }; + install: { + checking: 'Checking'; + downloading: 'Downloading'; + installCli: 'Install Codex CLI'; + installing: 'Installing'; + retryInstall: 'Retry install'; + title: 'Install Codex CLI into app data'; + }; + rateLimits: { + credits: 'Credits'; + creditsDescription: 'Credits are shown separately from window-based subscription usage and may be unavailable for plan-backed ChatGPT sessions.'; + noSecondaryWindow: 'Codex did not return a secondary window for this account snapshot.'; + notReported: 'Not reported'; + primaryReset: 'Primary reset'; + primaryUsed: 'Primary used'; + primaryWindow: 'Primary window'; + remainingLeft: '{{value}} left'; + remainingUnknown: 'Remaining unknown'; + secondaryFallback: 'secondary'; + secondaryReset: 'Secondary reset'; + secondaryUsed: 'Secondary used'; + secondaryWindow: 'Secondary window'; + secondaryWindowNote: ' Weekly limits are shown separately in the {{window}} window.'; + usageExplanationGeneric: 'Shows used quota, not remaining quota.'; + usageExplanationWindowOnly: 'Shows used quota in the current {{window}} window, not remaining quota.'; + usageExplanationWithRemaining: '{{used}} used - about {{remaining}} left in the current {{window}} window.'; + usedQuotaNote: 'These percentages show used quota, not remaining quota.'; + weeklyReset: 'Weekly reset'; + weeklyUsed: 'Weekly used'; + weeklyUsedOneWeek: 'Weekly used (1w)'; + weeklyWindow: 'Weekly window'; + }; + }; + compatibleEndpoint: { + authToken: 'Auth token'; + authTokenMissing: 'Auth token is not configured.'; + baseUrl: 'Base URL'; + description: 'Use an Anthropic-compatible local runtime endpoint.'; + keepSavedToken: 'Leave blank to keep saved token'; + status: { + endpointDisabledTokenKept: 'Endpoint disabled. Saved token was kept.'; + endpointSaved: 'Endpoint saved'; + endpointSavedTokenMissing: 'Endpoint saved. Auth token is not configured.'; + }; + title: 'Local / compatible endpoint'; + tokenStatus: 'Token {{status}}'; + validation: { + baseUrlRequired: 'Base URL is required'; + firstPartyAnthropic: 'Use Auto, Subscription, or API key for first-party Anthropic'; + httpRequired: 'Base URL must use http:// or https://'; + invalidUrl: 'Invalid URL'; + noCredentials: 'Base URL must not include credentials'; + }; + }; + connection: { + authenticationMethod: 'Authentication method'; + descriptions: { + anthropic: 'Choose how app-launched Anthropic sessions authenticate.'; + codex: 'Choose whether Codex should prefer your ChatGPT subscription or an API key when the native runtime launches.'; + gemini: 'Configure optional API access. CLI SDK and ADC are still discovered automatically.'; + opencode: 'OpenCode authentication and provider inventory are managed by the OpenCode runtime.'; + }; + method: 'Connection method'; + mode: 'Mode: {{mode}}'; + selected: 'Selected'; + switching: 'Switching...'; + title: 'Connection'; + }; + connectionCards: { + anthropic: { + apiKeyDescription: 'Use ANTHROPIC_API_KEY and Anthropic API billing.'; + autoDescription: 'Use Anthropic runtime defaults and the best local credential available.'; + hint: 'Auto keeps Anthropic on its default local credential resolution.'; + subscriptionDescription: 'Use your local Anthropic sign-in session and subscription access.'; + subscriptionTitle: 'Anthropic subscription'; + }; + apiKey: { + title: 'API key'; + }; + auto: { + title: 'Auto'; + }; + codex: { + apiKeyDescription: 'Use OPENAI_API_KEY and CODEX_API_KEY billing for native Codex launches.'; + autoDescription: 'Prefer your ChatGPT account and subscription. Use API key mode only if needed.'; + chatgptDescription: 'Use your connected ChatGPT account and Codex subscription.'; + chatgptTitle: 'ChatGPT account'; + hint: 'Codex always runs through the native runtime. Auto prefers your ChatGPT account before falling back to API-key credentials.'; + }; + }; + connectionUi: { + actions: { + connect: 'Connect'; + connectAnthropic: 'Connect Anthropic'; + connectChatGpt: 'Connect ChatGPT'; + disconnect: 'Disconnect'; + openLogin: 'Open Login'; + }; + authMethod: { + apiKey: 'API key'; + apiKeyHelper: 'API key helper'; + claudeSubscription: 'Claude subscription'; + geminiCli: 'Gemini CLI'; + googleAccount: 'Google account'; + oauth: 'OAuth'; + serviceAccount: 'service account'; + }; + authMode: { + anthropicSubscription: 'Anthropic subscription'; + apiKey: 'API key'; + auto: 'Auto'; + chatgpt: 'ChatGPT account'; + oauth: 'Subscription / OAuth'; + }; + credential: { + apiKeyAlsoConfigured: 'API key also configured in Manage'; + apiKeyConfigured: 'API key is configured'; + apiKeyConfiguredInManage: 'API key is configured in Manage'; + apiKeyFallbackInManage: 'API key also available in Manage as fallback'; + autoWillUseUntilChatGpt: '{{summary}} - Auto will use this until ChatGPT is connected'; + availableAsFallback: '{{summary}} - available as fallback'; + availableIfSwitch: '{{summary}} - available if you switch to API key mode'; + savedApiKeyAvailable: 'Saved API key available in Manage'; + savedApiKeyAvailableIfSwitch: 'Saved API key available in Manage if you switch to API key mode'; + }; + disconnect: { + anthropic: 'This removes the local Anthropic subscription session from the Claude CLI runtime.'; + anthropicTitle: 'Disconnect Anthropic subscription?'; + anthropicWithApiKey: 'This removes the local Anthropic subscription session from the Claude CLI runtime. Saved API keys in Manage stay available.'; + gemini: 'This clears the local Gemini CLI session metadata. External ADC credentials and saved API keys are not removed.'; + geminiTitle: 'Disconnect Gemini CLI?'; + }; + mode: { + preferredAuth: 'Preferred auth: {{authMode}}'; + selectedAuth: 'Selected auth: {{authMode}}'; + }; + runtime: { + codexNative: 'Codex native'; + currentRuntime: 'Current runtime'; + selectedRuntime: 'Selected runtime'; + summary: '{{prefix}}: {{runtime}}'; + }; + status: { + apiKeyConfiguredNotVerified: 'API key configured, but not verified yet'; + apiKeyModeMissingCredential: 'API key mode selected, but no API key is configured'; + apiKeyReady: 'API key ready'; + chatGptAccountReady: 'ChatGPT account ready'; + chatGptVerificationDegraded: 'ChatGPT account detected - account verification is currently degraded.'; + checked: 'Checked'; + checking: 'Checking...'; + codexLocalAccountNeedsReconnect: 'Codex has a locally selected ChatGPT account, but the current session needs reconnect.'; + codexNativeReady: 'Codex native ready'; + codexNativeUnavailable: 'Codex native unavailable'; + codexNoActiveChatGptLogin: 'Codex CLI reports no active ChatGPT login'; + codexNoActiveManagedSession: 'Codex CLI reports no active ChatGPT login. Local Codex account data exists, but no active managed session is selected.'; + connectChatGptForSubscription: 'Connect a ChatGPT account to use your Codex subscription.'; + connectedVia: 'Connected via {{method}}'; + connectedViaApiKey: 'Connected via API key'; + notConnected: 'Not connected'; + providerActivity: 'Provider Activity'; + startingChatGptLogin: 'Starting ChatGPT login...'; + unableToVerify: 'Unable to verify'; + unavailableInCurrentRuntime: 'Unavailable in current runtime'; + waitingForChatGptLogin: 'Waiting for ChatGPT account login...'; + }; + }; + description: 'Manage how each provider connects and, when supported, which backend the multimodel runtime should use.'; + errors: { + apiKeyDeletedRefreshFailed: 'API key deleted, but failed to refresh provider status.'; + apiKeyRequired: 'API key is required'; + apiKeySavedRefreshFailed: 'API key saved, but failed to refresh provider status.'; + connectionUpdatedRefreshFailed: 'Connection updated, but failed to refresh provider status.'; + deleteApiKey: 'Failed to delete API key'; + disableEndpoint: 'Failed to disable endpoint'; + endpointDisabledRefreshFailed: 'Endpoint disabled, but failed to refresh provider status.'; + endpointSavedRefreshFailed: 'Endpoint saved, but failed to refresh provider status.'; + refreshCodexAccount: 'Failed to refresh Codex account'; + saveApiKey: 'Failed to save API key'; + saveEndpoint: 'Failed to save endpoint'; + updateAnthropicFastMode: 'Failed to update Anthropic Fast mode'; + updateConnection: 'Failed to update connection'; + updateRuntimeBackend: 'Failed to update runtime backend'; + }; + fastMode: { + defaultOff: 'Default Off'; + description: 'Apply Claude Code Fast mode by default for new Anthropic team launches when the resolved model and runtime allow it.'; + disabledHint: 'New Anthropic launches stay on normal speed unless a team explicitly enables Fast mode.'; + enabledHint: 'New Anthropic launches will request Fast mode by default when the resolved model supports it.'; + notExposed: 'This Anthropic runtime does not expose Fast mode.'; + preferFast: 'Prefer Fast'; + title: 'Fast mode default'; + unavailableForRuntime: 'Fast mode is currently unavailable for this Anthropic runtime.'; + }; + progress: { + applyingConnectionChanges: 'Applying connection changes...'; + refreshingProviderStatus: 'Refreshing provider status...'; + savingCompatibleEndpoint: 'Saving compatible endpoint...'; + switchingAnthropicSubscription: 'Switching to Anthropic subscription...'; + switchingApiKey: 'Switching to API key...'; + switchingApiKeyMode: 'Switching to API key mode...'; + switchingAuto: 'Switching to Auto...'; + switchingChatgpt: 'Switching to ChatGPT account mode...'; + }; + provider: 'Provider'; + runtime: { + descriptions: { + anthropic: 'Anthropic currently has no separate runtime backend selector.'; + codex: 'Codex now runs only through the native runtime path.'; + gemini: 'Choose which Gemini runtime backend multimodel should use.'; + opencode: 'OpenCode uses its own managed runtime host. Desktop currently exposes status only.'; + }; + title: 'Runtime'; + updating: 'Updating runtime...'; + }; + runtimeSummary: 'Runtime: {{runtime}}'; + status: { + configured: 'configured'; + enabled: 'Enabled'; + notConfigured: 'Not configured'; + notSet: 'not set'; + off: 'Off'; + unknown: 'Unknown'; + }; + title: 'Provider Settings'; + usage: { + apiKey: 'Using API key'; + apiKeyRequired: 'API key required'; + compatibleEndpoint: 'Using compatible endpoint'; + notConnected: 'Not connected'; + usingMethod: 'Using {{method}}'; + }; + }; + runtimeProvider: { + actions: { + cancel: 'Cancel'; + test: 'Test'; + }; + badges: { + configured: 'configured'; + connected: 'connected'; + default: 'default'; + failed: 'failed'; + free: 'free'; + local: 'local'; + needsTest: 'needs test'; + unknown: 'unknown'; + usedInTeamPicker: 'Used in team picker'; + verified: 'verified'; + }; + compatibleEndpoint: { + baseUrlPlaceholder: 'http://localhost:1234'; + }; + defaults: { + allProjects: 'All projects'; + allProjectsHint: 'Tests use {{project}}. Default applies unless a project has an override.'; + loadingContexts: 'Loading contexts...'; + projectHint: 'Saving overrides only {{project}}.'; + projectOverrideContext: 'Project override context'; + scopeDescriptionAllProjects: 'Default for every project that does not have its own OpenCode override.'; + scopeDescriptionProject: 'Override only the selected project. Running teams are not changed.'; + selectProjectContext: 'Select project context'; + selectProjectHint: 'Select a project before testing local models or saving defaults.'; + selectValidationContext: 'Select validation context'; + setAllProjectsDefault: 'Set all-projects default'; + setProjectDefault: 'Set project default'; + thisProject: 'This project'; + title: 'OpenCode defaults'; + validationContext: 'Validation context'; + }; + diagnostics: { + copied: 'Diagnostics copied'; + copiedShort: 'Copied'; + copy: 'Copy diagnostics'; + hints: 'Hints'; + likelyCause: 'Likely cause:'; + }; + modelRoutes: { + searchPlaceholder: 'Search model routes'; + }; + models: { + alreadyDefault: 'This is already the selected OpenCode default.'; + empty: 'No models found.'; + emptyFree: 'No free models found.'; + emptyRecommended: 'No recommended models found.'; + emptyRecommendedFree: 'No recommended free models found.'; + freeOnly: 'Free only'; + launchableDescription: 'Routes you can test or use in the team picker: local config, free built-in models, and current default.'; + launchableTitle: 'Launchable OpenCode models'; + loadingRoutes: 'Loading OpenCode model routes...'; + noRoutesMatch: 'No OpenCode model routes match "{{query}}".'; + noneReported: 'No launchable OpenCode model routes were reported yet. Configure a local route in OpenCode or use the Providers tab to inspect catalog providers.'; + recommendedOnly: 'Recommended only'; + searchPlaceholder: 'Search models'; + selectProjectBeforeTesting: 'Select a project context before testing models.'; + selectProjectBeforeTestingDefaults: 'Select a project context before testing or saving OpenCode defaults.'; + useInTeamPicker: 'Use in team picker'; + }; + providers: { + catalog: 'OpenCode provider catalog'; + countFallback: 'OpenCode providers'; + description: '{{count}}. Connected and recommended providers are shown first.'; + description_few: '{{count}}. Connected and recommended providers are shown first.'; + description_many: '{{count}}. Connected and recommended providers are shown first.'; + description_one: '{{count}}. Connected and recommended providers are shown first.'; + description_other: '{{count}}. Connected and recommended providers are shown first.'; + loadMore: 'Load more providers'; + loading: 'Loading OpenCode providers'; + noMatches: 'No providers match that search.'; + noneReported: 'No OpenCode providers reported by the managed runtime.'; + recommended: 'Recommended'; + refreshCatalog: 'Refresh catalog'; + searchPlaceholder: 'Search providers'; + }; + setup: { + loading: 'Loading provider setup...'; + }; + summary: { + defaultModel: 'OpenCode default: {{model}}'; + loading: 'Loading managed OpenCode runtime, connected providers, and model defaults...'; + source: 'Source: {{source}}'; + title: 'OpenCode runtime'; + }; + tabs: { + models: 'Models'; + providers: 'Providers'; + }; + }; + tabs: { + advanced: { + description: 'Power-user options: export/import config, reset defaults, and raw configuration editing.'; + label: 'Advanced'; + }; + general: { + description: 'Core app preferences like theme, language, display density, and startup behavior.'; + label: 'General'; + }; + infoAriaLabel: 'What is {{label}}?'; + notifications: { + description: 'Control when and how you get notified about agent activity, task completions, and errors.'; + label: 'Notifications'; + }; + }; + view: { + description: 'Manage your app preferences'; + loading: 'Loading settings...'; + title: 'Settings'; + }; + workspaceProfiles: { + actions: { + addProfile: 'Add Profile'; + cancel: 'Cancel'; + deleteProfile: 'Delete profile'; + editProfile: 'Edit profile'; + save: 'Save'; + }; + authMethods: { + agent: 'SSH Agent'; + auto: 'Auto (from SSH Config)'; + password: 'Password'; + privateKey: 'Private Key'; + }; + deleteConfirm: { + confirmLabel: 'Delete'; + message: 'Are you sure you want to delete "{{name}}"? This cannot be undone.'; + title: 'Delete Profile'; + }; + description: 'Save SSH connection profiles for quick reconnection'; + empty: { + description: 'Add an SSH profile to connect quickly'; + title: 'No saved profiles'; + }; + form: { + authentication: 'Authentication'; + host: 'Host'; + hostPlaceholder: 'hostname or IP'; + name: 'Name'; + namePlaceholder: 'My Server'; + passwordPrompt: 'You will be prompted for the password when connecting.'; + port: 'Port'; + privateKeyPath: 'Private Key Path'; + username: 'Username'; + usernamePlaceholder: 'user'; + }; + loading: 'Loading profiles...'; + title: 'Workspace Profiles'; + }; + }; + team: { + activity: { + actions: { + createTaskFromMessage: 'Create task from message'; + expandMessage: 'Expand message'; + replyToMessage: 'Reply to message'; + restartTeam: 'Restart team'; + }; + activeTasks: { + inProgress: 'In progress'; + }; + authError: { + description: 'Authentication failed. Restarting the team will refresh the session and may resolve this issue. If the problem persists, check your API credentials or try again later.'; + }; + automation: { + reviewPickup: 'Asked teammate to pick up review'; + stallNudge: 'Asked teammate to continue stalled task'; + workSyncBody: 'Asked teammate to sync current work'; + }; + badges: { + automation: 'automation'; + bootstrap: 'bootstrap'; + command: 'command'; + comment: 'Comment'; + live: 'live'; + note: 'note'; + rateLimited: 'Rate Limited'; + restart: 'restart'; + result: 'result'; + session: 'session'; + stallNudge: 'stall nudge'; + start: 'start'; + workSync: 'work sync'; + }; + bootstrap: { + acknowledged: 'Bootstrap acknowledged'; + restarting: 'Restarting teammate'; + starting: 'Starting teammate'; + }; + expandDialog: { + description: 'Expanded message view'; + }; + pendingReplies: { + awaitingApproval: 'awaiting approval'; + awaitingReply: 'awaiting reply'; + crossTeamAwaitingReply: 'Cross-team message sent, awaiting reply'; + externalTeam: 'external team'; + messageSentAwaitingReply: 'Message sent, awaiting reply'; + openMember: 'Open member'; + title: 'Awaiting replies'; + user: 'user'; + }; + rawJson: 'Raw JSON'; + reply: { + action: 'Reply'; + replyingTo: 'Replying to'; + }; + thoughts: { + count: '{{count}} thoughts'; + count_few: '{{count}} thoughts'; + count_many: '{{count}} thoughts'; + count_one: '{{count}} thought'; + count_other: '{{count}} thoughts'; + expand: 'Expand thoughts'; + showLess: 'Show less'; + showMore: 'Show more'; + titleForMember: '{{name}} - thoughts'; + toolSummary: '🔧 {{summary}}'; + }; + timeline: { + emptyHint: 'Send a message to a member to see activity.'; + loadingMessages: 'Loading messages...'; + newSession: 'New session'; + noMessages: 'No messages'; + olderCount: '+{{count}} older'; + olderCount_few: '+{{count}} older'; + olderCount_many: '+{{count}} older'; + olderCount_one: '+{{count}} older'; + olderCount_other: '+{{count}} older'; + showAll: 'Show all'; + showMore: 'Show {{count}} more'; + }; + unread: 'Unread'; + }; + advancedCli: { + commandPreview: 'Command preview'; + customArguments: 'Custom arguments'; + placeholders: { + worktreeName: 'worktree-name'; + }; + recent: 'Recent'; + title: 'Advanced'; + useWorktree: 'Use worktree'; + validate: 'Validate'; + validation: { + allFlagsValid: 'All flags valid'; + failed: 'Validation failed'; + protectedFlags: 'Protected: {{flags}}'; + unknownFlags: 'Unknown: {{flags}}'; + }; + }; + agentGraph: { + activityHud: { + activity: 'Activity'; + more: '+{{count}} more'; + more_few: '+{{count}} more'; + more_many: '+{{count}} more'; + more_one: '+{{count}} more'; + more_other: '+{{count}} more'; + noRecentActivity: 'No recent activity'; + }; + blockingEdge: { + blockedHiddenTasks: 'Blocked hidden tasks'; + blockingHiddenTasks: 'Blocking hidden tasks'; + blocks: 'blocks'; + close: 'Close'; + title: 'Blocking Dependency'; + }; + logPreview: { + loading: 'Loading logs'; + logs: 'Logs'; + more: '+{{count}} more'; + more_few: '+{{count}} more'; + more_many: '+{{count}} more'; + more_one: '+{{count}} more'; + more_other: '+{{count}} more'; + }; + popover: { + externalTeam: 'External team'; + member: { + actions: { + message: 'Message'; + profile: 'Profile'; + task: 'Task'; + }; + activeTool: { + failed: 'Tool failed'; + finished: 'Tool finished'; + running: 'Running tool'; + }; + lead: 'Lead'; + recentTools: 'Recent tools'; + spawn: { + failed: 'failed'; + starting: 'starting'; + waitingToStart: 'waiting to start'; + }; + state: { + active: 'active'; + idle: 'idle'; + offline: 'offline'; + runningTool: 'running tool'; + }; + workingOn: 'working on'; + }; + overflow: { + empty: 'No hidden tasks available.'; + hiddenTasks: 'Hidden tasks'; + }; + process: { + at: 'At:'; + openUrl: 'Open URL'; + startedBy: 'Started by:'; + }; + }; + provisioning: { + launchDetails: 'Launch details'; + launchDetailsDescription: 'Detailed team launch progress, live output and CLI logs.'; + }; + }; + claudeLogs: { + clearSearch: 'Clear search'; + emptyRawLogs: '{{count}}; none are assistant/tool output yet.'; + filter: { + actions: { + reset: 'Reset'; + save: 'Save'; + }; + ariaLabel: 'Filter Claude logs'; + kinds: { + output: 'Output'; + thinking: 'Thinking'; + tool: 'Tool calls'; + }; + sections: { + content: 'Content'; + stream: 'Stream'; + }; + streams: { + stderr: 'stderr'; + stdout: 'stdout'; + }; + tooltip: 'Filter logs'; + }; + fullscreen: 'Fullscreen'; + loading: 'Loading...'; + logsTitle: 'Logs'; + newCount: '+{{count}} new'; + noLogsCaptured: 'No logs captured.'; + noLogsYet: 'No logs yet.'; + noMatchingLogs: 'No matching logs.'; + openFullscreen: 'Open fullscreen logs'; + rawLineCount: '{{formattedCount}} raw lines'; + rawLineCount_few: '{{formattedCount}} raw lines'; + rawLineCount_many: '{{formattedCount}} raw lines'; + rawLineCount_one: '{{formattedCount}} raw line'; + rawLineCount_other: '{{formattedCount}} raw lines'; + rawLinesCaptured: '{{count}} captured'; + searchPlaceholder: 'Search logs...'; + showMore: 'Show more'; + teamNotRunning: 'Team is not running.'; + viewingFullscreen: 'Viewing in fullscreen mode'; + }; + codexReconnect: { + description: 'Your Codex session appears stale. Reconnect to continue.'; + useCode: 'Use code'; + }; + contextLimit: { + always200k: '(always 200K for this model)'; + limitTo200k: 'Limit context to 200K tokens'; + tooltipContent: 'Keeps launches within a 200K-token context window when supported.'; + tooltipTitle: 'Context limit'; + }; + create: { + actions: { + create: 'Create'; + creating: 'Creating...'; + openExisting: 'Open Existing Team'; + skipPreflightAndCreate: 'Skip preflight and create'; + }; + conflict: { + description: 'Running two teams in the same directory is risky - they may conflict editing the same files. Consider using a different directory or a git worktree for isolation.'; + title: 'Another team "{{team}}" is already running for this working directory'; + workingDirectory: 'Working directory:'; + }; + description: { + copy: 'Create a new team based on an existing one.'; + create: 'Set up your team and choose how it starts.'; + }; + errors: { + createConfigFailed: 'Failed to create team config'; + loadProjectsFailed: 'Failed to load projects'; + nameExists: 'Team name already exists'; + nameLaunching: 'A team with this name is currently launching'; + }; + fields: { + color: 'Color (optional)'; + description: 'Description (optional)'; + prompt: 'Prompt for team lead (optional)'; + teamName: 'Team name'; + }; + launchAfterCreate: { + description: 'Start the team immediately via local Claude CLI.'; + label: 'Run command after create'; + }; + localOnly: 'Available only in local Electron mode.'; + onDisk: 'On disk:'; + optional: { + launchSettingsDescription: 'Prompt, safety, and CLI overrides live here when you need them.'; + launchSettingsTitle: 'Optional launch settings'; + teamDetailsDescription: 'Keep the default flow compact and only open this when you want extra context or a custom color.'; + teamDetailsTitle: 'Optional team details'; + }; + placeholders: { + description: 'Brief description of the team purpose'; + prompt: 'Instructions for the team lead during provisioning...'; + }; + prepare: { + checkingProviders: 'Checking selected providers...'; + failed: 'Failed to prepare selected providers'; + preparingEnvironment: 'Preparing environment...'; + ready: 'All selected providers are ready.'; + readyWithNotes: 'All selected providers are ready, with notes.'; + selectWorkingDirectory: 'Select a working directory to validate the launch environment.'; + selectedProvidersReady: 'Selected providers ready'; + selectedProvidersReadyWithNotes: 'Selected providers ready (with notes)'; + someProvidersNeedAttention: 'Some selected providers need attention.'; + unsupportedPreload: 'Current preload version does not support team:prepareProvisioning. Restart the dev app.'; + }; + saved: 'Saved'; + solo: { + description: 'Only the team lead (main process) will be started - no teammates will be spawned. Works like a regular agent session in your chosen runtime (Claude Code, Codex, OpenCode, Gemini) but with access to the task board for planning. Saves tokens by avoiding teammate coordination overhead. You can add members later from the team settings.'; + label: 'Solo team'; + }; + title: { + copy: 'Copy Team'; + create: 'Create Team'; + }; + validation: { + checkFormFields: 'Check form fields'; + memberNameInvalid: 'Member name must start with alphanumeric, use only [a-zA-Z0-9._-], max 128 chars'; + memberNameRequired: 'Member name cannot be empty'; + memberNamesUnique: 'Member names must be unique'; + nameMustContainLetterOrDigit: 'Name must contain at least one letter or digit'; + nameTooLong: 'Name is too long (max 128 chars)'; + openCodeLeadModelRequired: 'OpenCode lead requires a selected model.'; + openCodeTeammateRequired: 'OpenCode lead requires at least one OpenCode teammate.'; + selectWorkingDirectory: 'Select working directory (cwd)'; + teamLaunching: 'Team is currently launching'; + teamNameExists: 'Team name already exists'; + }; + }; + detail: { + actions: { + add: 'Add'; + cancel: 'Cancel'; + delete: 'Delete'; + editCode: 'Edit code'; + launch: 'Launch'; + remove: 'Remove'; + stop: 'Stop'; + task: 'Task'; + visualize: 'Visualize'; + }; + context: { + title: 'Context'; + }; + deleteTeam: { + description: 'Delete team "{{team}}"? This action is irreversible. All team data and tasks will be deleted.'; + title: 'Delete team'; + }; + draft: { + descriptionPrefix: 'This is a draft team -'; + descriptionSuffix: "has been configured with {{count}} {{member}} but hasn't been provisioned by CLI yet. Click Launch to select a model and start the team."; + descriptionSuffix_few: "has been configured with {{count}} {{member}} but hasn't been provisioned by CLI yet. Click Launch to select a model and start the team."; + descriptionSuffix_many: "has been configured with {{count}} {{member}} but hasn't been provisioned by CLI yet. Click Launch to select a model and start the team."; + descriptionSuffix_one: "has been configured with {{count}} {{member}} but hasn't been provisioned by CLI yet. Click Launch to select a model and start the team."; + descriptionSuffix_other: "has been configured with {{count}} {{member}} but hasn't been provisioned by CLI yet. Click Launch to select a model and start the team."; + member: 'members'; + member_few: 'members'; + member_many: 'members'; + member_one: 'member'; + member_other: 'members'; + title: 'Team not launched yet'; + }; + invalidTab: 'Invalid team tab'; + kanbanSafeData: 'Failed to fully load kanban. Displaying safe data.'; + loadFailed: 'Failed to load team'; + loading: 'Loading team'; + loadingSidebar: 'Loading team sidebar'; + offline: { + offline: 'Team is offline'; + partialFailed: 'Last launch failed partway'; + partialMissing: 'Last launch failed partway - {{missing}}/{{expected}} teammates did not join'; + reconciling: 'Last launch is still reconciling'; + }; + previous: 'Previous: {{paths}}'; + removeMember: { + description: 'Remove "{{member}}" from the team? Tasks and messages will be preserved, but this name cannot be reused.'; + title: 'Remove member'; + }; + sections: { + team: 'Team'; + }; + solo: 'Solo'; + status: { + active: 'Active'; + launching: 'Launching...'; + running: 'Running'; + }; + telemetry: { + cpu: 'CPU'; + memory: 'Memory'; + }; + tooltips: { + deleteTeam: 'Delete team'; + editTeam: 'Edit team'; + editUnavailableProvisioning: 'Edit team is unavailable while provisioning is still in progress'; + openBuiltInEditor: 'Open project in built-in editor'; + openTeamGraph: 'Open team graph'; + stopTeam: 'Stop team'; + }; + waitingForProvisioning: 'Team data will appear once provisioning completes'; + }; + dialogs: { + actions: { + cancel: 'Cancel'; + openDashboard: 'Open Dashboard'; + openTeam: 'Open team'; + }; + membersJson: { + hide: 'Hide JSON'; + }; + optional: { + badge: 'Optional'; + }; + }; + editTeam: { + actions: { + cancel: 'Cancel'; + save: 'Save'; + }; + addMemberLockReason: 'Use the dedicated Add member dialog to add new teammates while the team is live.'; + description: 'Change team name, description and color'; + errors: { + changesSavedRefreshFailed: 'Team changes were saved, but failed to refresh the latest view: {{message}}'; + liveRenameBlocked: 'Existing teammates cannot be renamed while the team is live. renamed: {{names}}'; + memberNameEmpty: 'Member name cannot be empty'; + memberNameInvalid: 'Member name must start with alphanumeric, use only [a-zA-Z0-9._-], max 128 chars'; + memberNameNumericSuffix: 'Member name "{{name}}" is not allowed (reserved for Claude CLI auto-suffix). Use "{{base}}" instead.'; + memberNameReserved: 'Member name "{{name}}" is reserved'; + memberNamesUnique: 'Member names must be unique before saving'; + newLiveTeammates: 'Add new teammates from the dedicated Add member dialog while the team is live. Edit Team only supports updating existing teammates.'; + provisioning: 'Team settings cannot be edited while provisioning is still in progress. Wait for launch to finish, then try again.'; + restartFailedMany: 'Team saved, but failed to restart these teammates: {{failures}}'; + restartFailedOne: 'Team saved, but failed to restart this teammate: {{failures}}'; + saveFailed: 'Failed to save'; + settingsChanged: 'Team settings changed while this dialog was open. Reopen it and review the latest state before saving.'; + settingsSavedMembersAndRefreshFailed: 'Team settings were saved, but member changes failed: {{message}}. Refresh also failed: {{refreshError}}'; + settingsSavedMembersFailed: 'Team settings were saved, but member changes failed: {{message}}'; + settingsSavedRefreshFailed: 'Team settings were saved, but failed to refresh the latest view: {{message}}'; + teamNameEmpty: 'Team name cannot be empty'; + unsupportedMixedPrimaryMutation: 'Live edits to primary-owned teammates in mixed OpenCode teams are not supported yet. Stop the team, edit the roster, then relaunch. Affected: {{names}}'; + }; + fields: { + colorOptional: 'Color (optional)'; + description: 'Description'; + name: 'Name'; + }; + memberRestartWarning: 'Saving will restart this teammate to apply role, workflow, worktree isolation, provider, model, effort, or MCP access changes.'; + notices: { + liveRenameBlocked: 'Live save is blocked because existing teammates were renamed. Revert those identity changes or stop the team first.'; + newLiveTeammates: 'New teammates cannot be added from Edit Team while the team is live. Use the Add member dialog instead.'; + provisioning: 'Team provisioning is still in progress. Editing is temporarily locked until launch finishes.'; + restartMany: 'Saving will restart or relaunch these teammates to apply role, workflow, worktree isolation, provider, model, effort, or MCP access changes: {{names}}.'; + restartOne: 'Saving will restart or relaunch this teammate to apply role, workflow, worktree isolation, provider, model, effort, or MCP access changes: {{names}}.'; + unsupportedMixedPrimaryMutation: 'Live edits/removals for primary-owned teammates in mixed OpenCode teams require stopping and relaunching the team: {{names}}.'; + }; + placeholders: { + description: 'Team description (optional)'; + teamName: 'Team name'; + }; + teamLead: { + changeRuntime: 'Change lead runtime'; + changeRuntimeDescription: 'Open Relaunch Team to change the lead provider, model, or effort.'; + modelLockReason: 'Team lead runtime is managed from Relaunch Team.'; + readOnlyHint: 'Team lead name and role stay read-only here. Open the runtime panel on the lead row to change provider, model, or effort.'; + role: 'Team Lead'; + }; + title: 'Edit Team'; + }; + editor: { + actions: { + cancel: 'Cancel'; + closeEditor: 'Close editor'; + closeTab: 'Close tab'; + closeTooltip: 'Close editor (Esc)'; + discard: 'Discard'; + discardAndClose: 'Discard & Close'; + keep: 'Keep'; + keepMine: 'Keep mine'; + keyboardShortcuts: 'Keyboard shortcuts'; + overwrite: 'Overwrite'; + refreshAria: 'Refresh (F5)'; + refreshTooltip: 'Refresh git status (F5)'; + reload: 'Reload'; + retry: 'Retry'; + save: 'Save'; + saveAllAndClose: 'Save All & Close'; + }; + ariaLabel: 'Project Editor'; + binaryPlaceholder: { + file: 'Binary file ({{size}})'; + }; + dialogs: { + conflictDescription: 'The file has been modified externally since you opened it. Overwrite with your changes?'; + conflictTitle: 'Save Conflict'; + unsavedDescription: 'You have unsaved changes. What would you like to do?'; + unsavedFileDescription: 'This file has unsaved changes. What would you like to do?'; + unsavedTitle: 'Unsaved Changes'; + }; + draftRecovered: 'Recovered unsaved changes from a previous session.'; + empty: { + selectFile: 'Select a file from the tree to edit'; + }; + errorBoundary: { + crashed: 'Editor crashed'; + unknownError: 'Unknown error'; + }; + externalChange: { + changed: 'File changed on disk.'; + deleted: 'File no longer exists on disk.'; + }; + fileTree: { + cancel: 'Cancel'; + dropForProjectRoot: 'Drop here for project root'; + empty: 'No files found'; + failedToLoadFiles: 'Failed to load files: {{error}}'; + loading: 'Loading files...'; + moveToTrash: 'Move to Trash'; + moveToTrashConfirm: 'Move "{{name}}" to Trash?'; + }; + goToLine: { + go: 'Go'; + placeholder: 'Line number, +offset, -offset, or %'; + position: '(current: {{current}}, total: {{total}})'; + title: 'Go to Line'; + }; + imagePreview: { + loading: 'Loading preview...'; + openFullSize: 'Open full-size preview'; + openSystemViewer: 'Open in System Viewer'; + }; + newFile: { + aria: { + newFileName: 'New file name'; + newFolderName: 'New folder name'; + }; + placeholders: { + fileName: 'File name...'; + folderName: 'Folder name...'; + }; + validation: { + invalidCharacters: 'Name contains invalid characters'; + invalidName: 'Invalid name'; + nameRequired: 'Name cannot be empty'; + nameTooLong: 'Name is too long'; + }; + }; + quickOpen: { + empty: 'No files found'; + loading: 'Loading files...'; + searchPlaceholder: 'Search files by name...'; + title: 'Quick Open'; + }; + saveFailed: 'Save failed: {{error}}'; + search: { + placeholder: 'Search'; + toggleReplace: 'Toggle Replace'; + }; + searchInFiles: { + closeSearch: 'Close search'; + closeSearchShortcut: 'Close search (Esc)'; + matchCase: 'Match Case'; + matchCaseToggle: 'Aa'; + noResults: 'No results found'; + resultsSummary: '{{count}} matches in {{fileCount}} files'; + resultsSummary_few: '{{count}} matches in {{fileCount}} files'; + resultsSummary_many: '{{count}} matches in {{fileCount}} files'; + resultsSummary_one: '{{count}} match in {{fileCount}} files'; + resultsSummary_other: '{{count}} matches in {{fileCount}} files'; + searchPlaceholder: 'Search...'; + title: 'Search in Files'; + truncated: '(truncated)'; + }; + searchPanel: { + all: 'All'; + close: 'Close'; + nextMatch: 'Next Match'; + previousMatch: 'Previous Match'; + replace: 'Replace'; + replaceAll: 'Replace All'; + replaceNext: 'Replace Next'; + replacePlaceholder: 'Replace'; + }; + shortcuts: { + actions: { + closeEditor: 'Close Editor'; + closeTab: 'Close Tab'; + cycleTabs: 'Cycle Tabs'; + findInFile: 'Find in File'; + fullPreview: 'Full Preview'; + goToLine: 'Go to Line'; + nextTab: 'Next Tab'; + previousTab: 'Previous Tab'; + quickOpen: 'Quick Open'; + redo: 'Redo'; + save: 'Save'; + saveAll: 'Save All'; + searchInFiles: 'Search in Files'; + selectNextMatch: 'Select Next Match'; + splitPreview: 'Split Preview'; + toggleComment: 'Toggle Comment'; + toggleSidebar: 'Toggle Sidebar'; + undo: 'Undo'; + }; + groups: { + editing: 'Editing'; + fileOperations: 'File Operations'; + general: 'General'; + markdown: 'Markdown'; + navigation: 'Navigation'; + search: 'Search'; + }; + title: 'Keyboard Shortcuts'; + }; + sidebar: { + explorer: 'Explorer'; + hide: 'Hide sidebar'; + hideWithShortcut: 'Hide sidebar ({{shortcut}})'; + show: 'Show sidebar'; + showWithShortcut: 'Show sidebar ({{shortcut}})'; + }; + statusBar: { + disableExternalWatcher: 'Disable external change watcher'; + disableWatcher: 'Disable file watcher'; + enableWatcher: 'Enable file watcher'; + encodingUtf8: 'UTF-8'; + position: 'Ln {{line}}, Col {{col}}'; + spaces: 'Spaces: {{count}}'; + watch: 'watch'; + watchExternalChanges: 'Watch for external changes'; + watching: 'watching'; + }; + toolbar: { + closePreview: 'Close preview'; + closeSplitPreview: 'Close split preview'; + disableWordWrap: 'Disable word wrap'; + enableWordWrap: 'Enable word wrap'; + }; + unsavedChanges: 'Unsaved changes'; + }; + effortLevel: { + label: 'Effort level (optional)'; + maxDescription: 'Max gives the model the most reasoning time for difficult tasks.'; + }; + kanban: { + board: { + addTask: 'Add task'; + columnsView: 'Columns view'; + gridView: 'Grid view'; + hiddenCount: '{{count}} hidden'; + noTasks: 'No tasks'; + showMore: 'Show {{count}} more'; + trash: 'Trash'; + }; + columns: { + approved: 'APPROVED'; + done: 'DONE'; + inProgress: 'IN PROGRESS'; + review: 'REVIEW'; + todo: 'TODO'; + }; + filter: { + allSessions: 'All sessions'; + clearAll: 'Clear all'; + column: 'Column'; + session: 'Session'; + teammate: 'Teammate'; + title: 'Filter tasks'; + unassigned: '(unassigned)'; + }; + grid: { + addTask: 'Add task'; + noTasks: 'No tasks'; + }; + search: { + clearSearch: 'Clear search'; + createdAgo: 'created {{time}}'; + placeholder: 'Search tasks... (#id or text)'; + tasks: 'Tasks'; + updatedAgo: 'updated {{time}}'; + }; + sort: { + options: { + createdAt: { + description: 'Newest first'; + label: 'Created'; + }; + manual: { + description: 'Drag-and-drop order'; + label: 'Manual'; + }; + owner: { + description: 'Alphabetically by assignee'; + label: 'Owner'; + }; + updatedAt: { + description: 'Recently updated first'; + label: 'Last updated'; + }; + }; + reset: 'Reset'; + sortBy: 'Sort by'; + title: 'Sort tasks'; + }; + taskCard: { + approve: 'Approve'; + awaitingLead: 'Awaiting lead'; + awaitingUser: 'Awaiting user'; + blockedBy: 'Blocked by'; + blocks: 'Blocks'; + cancel: 'Cancel'; + cancelTask: 'Cancel task {{taskId}}'; + changes: 'Changes'; + changesNeedAttention: 'Changes need attention'; + complete: 'Complete'; + confirm: 'Confirm'; + deleteTask: 'Delete task'; + keep: 'Keep'; + manualReview: 'Manual review'; + moveBackToTodoConfirm: 'Move this task back to TODO and notify the team?'; + newTaskLogsArriving: 'New task logs arriving'; + requestChanges: 'Request changes'; + requestReview: 'Request review'; + start: 'Start'; + taskLogsActive: 'Task logs active'; + }; + title: 'Kanban'; + trash: { + close: 'Close'; + deleted: 'Deleted'; + empty: 'No deleted tasks'; + owner: 'Owner'; + restore: 'Restore'; + restoreTask: 'Restore task'; + subject: 'Subject'; + title: 'Trash'; + unassigned: 'Unassigned'; + }; + }; + launch: { + actions: { + createSchedule: 'Create Schedule'; + creating: 'Creating...'; + goToDashboard: 'Go to Dashboard'; + launchTeam: 'Launch team'; + launching: 'Launching...'; + relaunchTeam: 'Relaunch team'; + relaunching: 'Relaunching...'; + saveChanges: 'Save Changes'; + saving: 'Saving...'; + }; + billing: { + prefix: 'Starting June 15, 2026, Anthropic bills'; + readArticle: 'Read Anthropic article'; + suffix: 'and Agent SDK usage from the monthly Agent SDK credit, separate from interactive Claude Code limits. The credit resets each billing cycle and unused credit does not roll over.'; + }; + conflict: { + description: 'Running two teams in the same directory is risky - they may conflict editing the same files. Consider using a different directory or a git worktree for isolation.'; + title: 'Another team "{{team}}" is already running for this working directory'; + workingDirectory: 'Working directory:'; + }; + description: { + createSchedule: 'Schedule automatic Claude task execution'; + createScheduleForTeam: 'Schedule automatic runs for team "{{team}}"'; + editSchedule: 'Editing schedule for team "{{team}}"'; + launchPrefix: 'Start team'; + launchSuffix: 'via local Claude CLI.'; + relaunchPrefix: 'Stop the current run for'; + relaunchSuffix: 'and start it again via local Claude CLI.'; + }; + errors: { + launchFailed: 'Failed to launch team'; + loadProjectsFailed: 'Failed to load projects'; + relaunchFailed: 'Failed to relaunch team'; + saveScheduleFailed: 'Failed to save schedule'; + }; + optionalSettings: { + description: 'Keep the launch flow focused on the project path and only expand this when you want extra control.'; + relaunchDescription: 'Review the roster and lead runtime before restarting the team.'; + relaunchTitle: 'Relaunch settings'; + title: 'Optional launch settings'; + }; + prepare: { + action: { + launch: 'launch'; + relaunch: 'relaunch'; + }; + blocked: 'Runtime environment is not available - {{action}} is blocked'; + checkingProviders: 'Checking selected providers...'; + failed: 'Failed to prepare selected providers'; + preflight: 'Pre-flight check to catch errors before {{action}}'; + preparingEnvironment: 'Preparing environment...'; + ready: 'All selected providers are ready.'; + readyWithNotes: 'All selected providers are ready, with notes.'; + selectWorkingDirectory: 'Select a working directory to validate the launch environment.'; + someProvidersNeedAttention: 'Some selected providers need attention.'; + unsupportedPreload: 'Current preload version does not support team:prepareProvisioning. Restart the dev app.'; + }; + prompt: { + label: 'Prompt'; + oneShotPrefix: 'This prompt will be passed to'; + oneShotSuffix: 'for one-shot execution'; + saved: 'Saved'; + schedulePlaceholder: 'Instructions for Claude to execute on schedule...'; + teamLeadOptional: 'Prompt for team lead (optional)'; + teamLeadPlaceholder: 'Instructions for team lead...'; + }; + providerChanged: 'Provider changed from {{from}} to {{to}}. The previous lead session will not be resumed, and the lead will start with fresh context so the new runtime is applied correctly.'; + relaunchFreshSession: 'Team relaunch starts a fresh lead session. Durable team state, task board, and member configuration are rehydrated into the launch prompt.'; + relaunchWarning: { + description: 'Saving these settings will stop the current team process, persist the updated roster, and launch the team again with the new runtime.'; + title: 'Relaunch will restart the current team run'; + }; + schedule: { + labelOptional: 'Label (optional)'; + labelPlaceholder: 'e.g., Daily code review, Nightly tests...'; + maxBudgetUsd: 'Max budget (USD)'; + maxTurns: 'Max turns'; + noLimit: 'No limit'; + noMatches: 'No teams match your search.'; + noTeams: 'No teams available. Create a team first.'; + searchTeams: 'Search teams...'; + selectTeam: 'Select a team...'; + team: 'Team'; + title: 'Schedule'; + }; + title: { + createSchedule: 'Create Schedule'; + editSchedule: 'Edit Schedule'; + launch: 'Launch Team'; + relaunch: 'Relaunch Team'; + }; + validation: { + fixMemberNames: 'Fix member names before launch'; + memberNamesUnique: 'Member names must be unique before launch'; + openCodeLeadModelRequired: 'OpenCode lead requires a selected model.'; + openCodeTeammateRequired: 'OpenCode lead requires at least one OpenCode teammate.'; + selectWorkingDirectory: 'Select working directory (cwd)'; + }; + }; + layout: { + maxPanesReached: 'Maximum of {{count}} panes reached'; + }; + list: { + actions: { + copyTeam: 'Copy team'; + createTeam: 'Create Team'; + deleteForever: 'Delete forever'; + deletePermanently: 'Delete permanently'; + deleteTeam: 'Delete team'; + launchTeam: 'Launch team'; + launching: 'Launching...'; + relaunchTeam: 'Relaunch team'; + restore: 'Restore'; + restoreTeam: 'Restore team'; + retry: 'Retry'; + stopTeam: 'Stop team'; + stopping: 'Stopping...'; + }; + deleteDraft: { + cancelLabel: 'Cancel'; + confirmLabel: 'Delete'; + message: 'Delete draft team "{{teamName}}"? This cannot be undone.'; + title: 'Delete draft'; + }; + deleteForever: { + cancelLabel: 'Cancel'; + confirmLabel: 'Delete forever'; + message: 'Delete team "{{teamName}}" permanently? All data will be lost.'; + title: 'Delete permanently'; + }; + electronOnly: { + description: 'In browser mode, access to local `~/.claude/teams` directories is not available.'; + title: 'Teams is only available in Electron mode'; + }; + empty: { + description: 'Create a team here to get started. It will show up in the list automatically.'; + localOnly: 'Team creation is only available in local Electron mode.'; + title: 'No teams found'; + }; + filter: { + clearAll: 'Clear all'; + label: 'Filter teams'; + projectPriority: 'Project priority'; + status: 'Status'; + }; + loadFailed: 'Failed to load teams'; + loading: 'Loading teams...'; + localOnly: 'Only available in local Electron mode.'; + membersCount: 'Members: {{count}}'; + membersCount_few: 'Members: {{count}}'; + membersCount_many: 'Members: {{count}}'; + membersCount_one: 'Member: {{count}}'; + membersCount_other: 'Members: {{count}}'; + moveToTrash: { + cancelLabel: 'Cancel'; + confirmLabel: 'Move to trash'; + message: 'Move team "{{teamName}}" to trash? You can restore it later.'; + title: 'Move to trash'; + }; + noDescription: 'No description'; + noMatches: 'No teams matching current filters'; + partial: { + pending: 'Last launch is still reconciling.'; + skipped: 'Last launch has skipped teammates.'; + skippedWithCount: 'Last launch skipped {{count}}/{{expected}} teammate.'; + skippedWithCount_few: 'Last launch skipped {{count}}/{{expected}} teammates.'; + skippedWithCount_many: 'Last launch skipped {{count}}/{{expected}} teammates.'; + skippedWithCount_one: 'Last launch skipped {{count}}/{{expected}} teammate.'; + skippedWithCount_other: 'Last launch skipped {{count}}/{{expected}} teammates.'; + stopped: 'Last launch stopped before all teammates joined.'; + stoppedWithCount: 'Last launch stopped before {{count}}/{{expected}} teammate joined.'; + stoppedWithCount_few: 'Last launch stopped before {{count}}/{{expected}} teammates joined.'; + stoppedWithCount_many: 'Last launch stopped before {{count}}/{{expected}} teammates joined.'; + stoppedWithCount_one: 'Last launch stopped before {{count}}/{{expected}} teammate joined.'; + stoppedWithCount_other: 'Last launch stopped before {{count}}/{{expected}} teammates joined.'; + }; + searchPlaceholder: 'Search teams...'; + sections: { + otherTeams: 'Other teams'; + projectTeams: 'Teams for {{project}}'; + selectedProject: 'selected project'; + }; + solo: 'Solo'; + status: { + active: 'Active'; + deleted: 'Deleted'; + launching: 'Launching...'; + offline: 'Offline'; + partialFailure: 'Launch failed partway'; + partialPending: 'Bootstrap pending'; + partialSkipped: 'Launch skipped member'; + running: 'Running'; + }; + title: 'Select Team'; + trash: 'Trash ({{count}})'; + trash_few: 'Trash ({{count}})'; + trash_many: 'Trash ({{count}})'; + trash_one: 'Trash ({{count}})'; + trash_other: 'Trash ({{count}})'; + }; + liveRuntimeStatus: { + description: 'Display-only heartbeat and launch state. Process controls remain below.'; + diagnosticOnly: 'Diagnostic only'; + lane: '{{lane}} lane'; + source: 'source: {{source}}'; + states: { + degraded: 'Needs attention'; + running: 'Running'; + starting: 'Starting'; + stopped: 'Stopped'; + unknown: 'Unknown'; + waiting: 'Waiting'; + }; + title: 'Live runtime status'; + updated: 'updated {{value}}'; + }; + memberDraft: { + actions: { + remove: 'Remove member'; + removeAria: 'Remove {{name}}'; + restore: 'Restore member'; + restoreAria: 'Restore {{name}}'; + }; + addMembers: { + description: 'Add new members to {{teamName}}'; + title: 'Add Members'; + }; + anthropicContext: { + defaultSetting: 'default context setting'; + description: "Anthropic context is team-wide for this launch: {{mode}}. Use the lead runtime panel's Limit context checkbox to change it."; + limitEnabled: '200K limit enabled'; + }; + mcp: { + agentTeamsMcp: 'Agent Teams MCP'; + buttonInherit: 'MCP inherit'; + buttonScopes: 'MCP scopes'; + chooseScopes: 'Choose scopes'; + inheritLead: 'Inherit lead'; + lockedInfo: 'Agent Teams MCP only is enabled for all teammates. This teammate will launch with only the Agent Teams server.'; + mode: 'MCP mode'; + scopes: { + local: 'local'; + project: 'project'; + user: 'user'; + }; + serverNames: 'Server names'; + settingInfo: 'Agent Teams MCP launches this teammate with only the Agent Teams server. Scope and allowlist modes apply only to this teammate launch.'; + strictAllowlist: 'Strict allowlist'; + tooltip: "{{label}}: Control this member's MCP inheritance policy"; + }; + model: { + ariaLabel: '{{provider}} provider, {{model}}'; + currentLeadRuntime: 'Current lead runtime'; + default: 'Default'; + inheritedTooltip: 'Provider, model, and effort are inherited from the lead while sync is enabled.'; + leadSuffix: '{{label}} (lead)'; + liveDisabled: 'Provider, model, and effort changes are disabled while the team is live. Reconnect the team to apply them safely.'; + lockedActionFallback: 'Lead runtime changes open Relaunch Team, where provider, model, and effort can be updated.'; + restartWholeTeam: 'Saving those runtime changes restarts the whole team.'; + }; + nameAria: 'Member {{index}} name'; + nameFallback: 'member {{index}}'; + noRole: 'No role'; + placeholders: { + mcpServers: 'github, sentry'; + name: 'member-name'; + }; + removed: 'Removed'; + workflow: { + addTooltip: 'Add teammate workflow'; + editTooltip: 'Edit teammate workflow'; + label: 'Workflow (optional)'; + placeholder: 'How this agent should behave, interact with others...'; + saved: 'Saved'; + }; + worktree: { + description: 'Run this teammate in a separate git worktree. Apply/reject changes targets that worktree, not the lead workspace.'; + label: 'Worktree'; + }; + }; + memberLogStream: { + filters: { + all: 'All'; + }; + logs: { + emptyDescription: 'Member-scoped transcript or runtime logs will appear here when available.'; + emptyTitle: 'No log stream entries were found for this member yet.'; + loading: 'Loading member log stream...'; + title: 'Logs'; + }; + tabs: { + execution: 'Execution'; + process: 'Process'; + }; + }; + memberWorkSync: { + details: { + actionableItems: 'Actionable items'; + diagnostics: 'Diagnostics: {{diagnostics}}'; + fingerprint: 'Fingerprint'; + moreActionableItems: '{{count}} more actionable item(s)'; + no: 'no'; + none: 'none'; + report: 'Report'; + shadowWouldNudge: 'Shadow would nudge'; + title: 'Member work sync'; + yes: 'yes'; + }; + diagnosticsUnavailable: 'Member work sync diagnostics are unavailable.'; + loadingDiagnostics: 'Loading member work sync diagnostics.'; + title: 'Member work sync'; + }; + members: { + actions: { + assignTask: 'Assign task'; + editRole: 'Edit role'; + openProfile: 'Open profile'; + sendMessage: 'Send message'; + }; + badges: { + worktree: 'worktree'; + }; + detail: { + assignTask: 'Assign Task'; + copyDiagnostics: 'Copy diagnostics'; + failedToRestartMember: 'Failed to restart member'; + legacyLogsFallback: 'Legacy Logs Fallback'; + pid: 'PID {{pid}}'; + relaunchOpenCode: 'Relaunch OpenCode'; + remove: 'Remove'; + removedAt: 'Removed {{date}}'; + restart: 'Restart'; + sendMessage: 'Send Message'; + }; + editor: { + addMember: 'Add member'; + agentTeamsMcpOnly: 'Agent Teams MCP only'; + editAsJson: 'Edit as JSON'; + memberNamesUnique: 'Member names must be unique'; + removedCount: 'Removed ({{count}})'; + removedModelLockReason: 'Removed members are kept for soft delete history. Restore them to edit settings.'; + runInSeparateWorktrees: 'Run teammates in separate worktrees'; + title: 'Members'; + }; + executionLog: { + agentInstructions: 'Agent instructions'; + agentTurn: 'Agent turn'; + empty: 'Nothing to display'; + emptyUserMessage: '{{time}} - (empty)'; + memberTurn: '{{member}} turn'; + turn: 'turn'; + }; + leadModel: { + anthropicContextLimit: 'The 200K context limit is team-wide for Anthropic runtimes in this launch, including custom Anthropic teammates.'; + anthropicTeamWide: 'Anthropic team-wide'; + defaultModel: 'Default'; + leadShort: 'lead'; + providerModelAria: '{{provider}} provider, {{model}}'; + runtimeInheritance: 'Lead runtime applies to teammates unless they set their own provider or model.'; + syncWithTeammates: 'Sync model with teammates'; + teamLead: 'Team Lead'; + }; + list: { + loading: 'Loading team members'; + removedCount: 'Removed ({{count}})'; + soloLeadOnly: 'Solo team - lead only'; + unavailable: 'Member roster unavailable'; + unavailableDescription: '{{count}} teammates are known from team metadata, but roster details are missing.'; + unavailableDescription_few: '{{count}} teammates are known from team metadata, but roster details are missing.'; + unavailableDescription_many: '{{count}} teammates are known from team metadata, but roster details are missing.'; + unavailableDescription_one: '{{count}} teammate is known from team metadata, but roster details are missing.'; + unavailableDescription_other: '{{count}} teammates are known from team metadata, but roster details are missing.'; + }; + logs: { + active: 'active'; + empty: 'No logs found'; + failedToLoadDetails: 'Failed to load details'; + hideDetails: 'Hide details'; + leadSessionTooltip: 'Full team lead session logs - useful for global orchestration context, not specific to this agent'; + loadingDetails: 'Loading details...'; + memberSessionTooltip: 'Full persistent teammate session logs - useful when work runs in a root member session instead of a subagent file'; + noMemberActivity: 'This member has no recorded session activity yet'; + noTaskActivity: 'No session activity for this task yet'; + searching: 'Searching logs...'; + showDetails: 'Show details'; + startedAt: 'started {{time}}'; + waitingForTaskActivity: 'Task is in progress - waiting for session activity (auto-refreshing)...'; + }; + messages: { + empty: { + loading: 'Loading activity...'; + noActivity: 'No activity with this member'; + noComments: 'No comments for this member'; + noLoadedActivity: 'No loaded activity for this member yet'; + noLoadedMessages: 'No loaded messages for this member yet'; + noMessages: 'No messages with this member'; + }; + filters: { + all: 'All'; + comments: 'Comments'; + messages: 'Messages'; + }; + loadOlder: 'Load older messages'; + }; + recentMessages: { + collapse: 'Collapse'; + expand: 'Expand'; + latest: 'Latest messages'; + latestForMember: 'Latest messages - {{member}}'; + loadMore: 'Load more'; + }; + roleSelect: { + customRolePlaceholder: 'Enter custom role...'; + }; + runtimeLogs: { + autoRefresh: 'Auto-refresh'; + empty: 'No process log file captured for this member yet.'; + loadingTail: 'Loading process log tail...'; + wrapLines: 'Wrap lines'; + }; + runtimeTelemetry: { + cpu: 'CPU'; + description: 'Parent and child processes only. Remote LLM inference is not included.'; + memory: 'Memory'; + processTreeCapped: 'Process tree was capped for this sample.'; + rssHint: 'RSS can include shared pages, so it is best read as a load signal, not exclusive memory.'; + sharedHost: 'Shared OpenCode host metric. It is not exclusive to this member.'; + summedRss: 'summed RSS'; + title: 'Local runtime load'; + }; + stats: { + computing: 'Computing stats...'; + empty: 'No stats available'; + files: 'Files'; + filesTouched: 'Files Touched ({{count}})'; + footer: '{{count}} sessions · computed {{computedAgo}}'; + footer_few: '{{count}} sessions · computed {{computedAgo}}'; + footer_many: '{{count}} sessions · computed {{computedAgo}}'; + footer_one: '{{count}} session · computed {{computedAgo}}'; + footer_other: '{{count}} sessions · computed {{computedAgo}}'; + lines: 'Lines'; + linesInfo: 'Approximate. Accurate for Edit and Write tools. Bash file writes are estimated from command patterns (heredoc, echo, sed) and may be underreported.'; + moreFiles: '+{{count}} more'; + showLess: 'Show less'; + tokens: 'Tokens'; + toolCalls: 'Tool Calls'; + toolUsage: 'Tool Usage'; + viewAllChanges: 'View All Changes'; + }; + tasks: { + empty: 'No tasks assigned to this member'; + }; + }; + messageComposer: { + actions: { + send: 'Send'; + sendingUnavailableLaunching: 'Sending unavailable while team is launching'; + voiceToText: 'Voice to text'; + }; + attachments: { + attachFiles: 'Attach files (paste or drag & drop)'; + disabledHint: 'File attachments are supported for the online team lead and online OpenCode teammates. Remove attachments or switch recipient.'; + restrictions: { + crossTeam: 'File attachments are not supported for cross-team messages'; + leadOnly: 'Files can only be sent to the team lead'; + maximumReached: 'Maximum attachments reached'; + openCodeOffline: 'Team must be online to attach files for OpenCode teammates'; + sending: 'Wait for current message to finish sending before adding files'; + teamOffline: 'Team must be online to attach files'; + unsupportedRecipient: 'Files can be sent to the team lead or OpenCode teammates'; + }; + unavailable: 'Attachments are unavailable'; + }; + crossTeam: { + hint: 'Tip: Cross-team messages go to the target team lead. If you want the reply to come back to your team lead instead of you, say that explicitly in the message.'; + }; + input: { + charsLeft: '{{count}} chars left'; + charsLeft_few: '{{count}} chars left'; + charsLeft_many: '{{count}} chars left'; + charsLeft_one: '{{count}} char left'; + charsLeft_other: '{{count}} chars left'; + crossTeamPlaceholder: 'Cross-team message to {{team}}...'; + placeholder: 'Write a message... (Enter to send, Shift+Enter for new line)'; + slashTip: 'Tip: You can use "/" to run any Claude commands.'; + teamFallback: 'team'; + teamLaunchingPlaceholder: 'Team is launching... message will be queued for inbox delivery.'; + }; + recipient: { + noResults: 'No results'; + searchPlaceholder: 'Search...'; + select: 'Select...'; + }; + slash: { + restrictions: { + attachments: 'Slash commands require a live team lead and cannot be sent with attachments'; + crossTeam: 'Slash commands can only be run on the current team lead'; + leadOffline: 'Slash commands require the team lead to be online'; + notLead: 'Slash commands can only be sent to the team lead'; + }; + }; + status: { + reusedCrossTeamRequest: 'Reused recent cross-team request'; + teamOffline: 'Team offline'; + }; + teamSelector: { + current: 'current'; + offline: 'offline'; + offlineTitle: 'Offline'; + online: 'online'; + onlineTitle: 'Online'; + thisTeam: 'This team'; + }; + }; + messages: { + actionMode: { + label: 'Action mode'; + }; + actions: { + bottomSheetActions: 'Message bottom sheet actions'; + collapseAll: 'Collapse all messages'; + collapseSheet: 'Collapse sheet'; + expandAll: 'Expand all messages'; + expandSheet: 'Expand sheet'; + floatComposer: 'Float composer'; + floatMessagesComposer: 'Float messages composer'; + hideSearch: 'Hide search'; + loadOlder: 'Load older messages'; + markAllRead: 'Mark all as read'; + messageActions: 'Message actions'; + moveMessagesToBottomSheet: 'Move messages to bottom sheet'; + moveMessagesToSidebar: 'Move messages to sidebar'; + moveToBottomSheet: 'Move to bottom sheet'; + moveToInline: 'Move to inline'; + moveToSidebar: 'Move to sidebar'; + panelActions: 'Message panel actions'; + searchMessages: 'Search messages'; + }; + delivery: { + copied: 'Copied'; + copyDebugDetails: 'Copy debug details'; + details: 'Details'; + fields: { + acceptanceUnknown: 'acceptanceUnknown'; + delivered: 'delivered'; + diagnostics: 'diagnostics'; + ledgerStatus: 'ledgerStatus'; + messageId: 'messageId'; + providerId: 'providerId'; + queuedBehindMessageId: 'queuedBehindMessageId'; + reason: 'reason'; + responsePending: 'responsePending'; + responseState: 'responseState'; + statusMessageId: 'statusMessageId'; + userVisibleMessage: 'userVisibleMessage'; + userVisibleNextReviewAt: 'userVisibleNextReviewAt'; + userVisibleReasonCode: 'userVisibleReasonCode'; + userVisibleState: 'userVisibleState'; + visibleReplyCorrelation: 'visibleReplyCorrelation'; + visibleReplyMessageId: 'visibleReplyMessageId'; + }; + }; + filter: { + actions: { + reset: 'Reset'; + save: 'Save'; + }; + ariaLabel: 'Filter messages'; + from: 'From'; + noData: 'No data'; + showStatusUpdates: 'Show status updates (idle/shutdown)'; + to: 'To'; + tooltip: 'Filter messages'; + }; + panelMode: 'Message panel mode'; + search: { + placeholder: 'Search...'; + }; + status: { + title: 'Status'; + }; + title: 'Messages'; + unread: { + new: '{{count}} new'; + new_few: '{{count}} new'; + new_many: '{{count}} new'; + new_one: '{{count}} new'; + new_other: '{{count}} new'; + unread: '{{count}} unread'; + unread_few: '{{count}} unread'; + unread_many: '{{count}} unread'; + unread_one: '{{count}} unread'; + unread_other: '{{count}} unread'; + }; + }; + modelSelector: { + advisory: { + note: 'Note'; + pingNotConfirmed: 'Ping not confirmed'; + }; + anthropicExtraUsage: { + pricingDocs: 'Read Anthropic pricing docs'; + }; + badges: { + configured: 'Configured'; + connected: 'Connected'; + failed: 'Failed'; + free: 'Free'; + issue: 'Issue'; + local: 'Local'; + needsTest: 'Needs test'; + unavailable: 'Unavailable'; + verified: 'Verified'; + }; + customModelId: 'Custom model id'; + defaultModel: 'Default'; + defaultTooltip: { + anthropic: 'Uses the Claude team default model.\nResolves to {{longContextModel}} with 1M context, or {{limitedContextModel}} with 200K context when Limit context is enabled.'; + anthropicCompatible: 'Uses the Anthropic-compatible endpoint default model.'; + anthropicCompatibleWithResolved: 'Uses the Anthropic-compatible endpoint default model.\nCurrently resolves to {{model}}.'; + openCode: 'Uses the OpenCode runtime default model.'; + openCodeWithResolved: 'Uses the OpenCode default model.\nCurrently resolves to {{model}}.'; + runtime: 'Uses the runtime default for the selected provider.'; + }; + empty: { + freeOpenCode: 'No free OpenCode models are available in the current runtime list.'; + noModels: 'No models are available in the current runtime list.'; + noSearchMatches: 'No models match this search.'; + recommendedFreeOpenCode: 'No recommended free OpenCode models are available in the current runtime list.'; + recommendedOpenCode: 'No recommended OpenCode models are available in the current runtime list.'; + }; + fastMode: { + codexLabel: 'Fast mode (2x credits)'; + defaultFast: 'Default (Fast)'; + defaultOff: 'Default (Off)'; + defaultResolvesTo: 'Default currently resolves to {{mode}}.'; + fast: 'Fast'; + off: 'Off'; + optionalLabel: 'Fast mode (optional)'; + runtimeBackedHint: 'Fast mode is runtime-backed and only unlocks when the resolved Anthropic launch model supports it.'; + }; + label: 'Model (optional)'; + multimodelOff: 'Multimodel off'; + multimodelRequired: 'Codex and Gemini require Multimodel mode.'; + openCode: { + allSources: 'All OpenCode sources'; + filterSource: 'Filter {{source}}'; + filterSources: 'Filter OpenCode sources'; + freeOnly: 'Free only'; + freeTooltip: 'OpenCode marks this model as free.'; + loadingModels: 'Loading OpenCode models...'; + noSourcesFound: 'No sources found.'; + recommendedOnly: 'Recommended only'; + searchSources: 'Search sources'; + sourcesCount: '{{count}} OpenCode sources'; + sourcesCount_few: '{{count}} OpenCode sources'; + sourcesCount_many: '{{count}} OpenCode sources'; + sourcesCount_one: '{{count}} OpenCode sources'; + sourcesCount_other: '{{count}} OpenCode sources'; + }; + openCodeStatus: { + badges: { + check: 'Check'; + free: 'Free'; + install: 'Install'; + setup: 'Setup'; + }; + freeModelsAvailableTitle: 'OpenCode free models are available'; + loadingRuntime: 'OpenCode runtime status is still loading.'; + messages: { + checking: 'The app is still checking the OpenCode runtime. Wait for provider status to finish, then try again.'; + freeAvailable: 'OpenCode is detected. You can use free OpenCode models such as Big Pickle without connecting a provider. Connect a provider only when you want provider-backed models.'; + launchBlocked: 'OpenCode is installed and authenticated, but Agent Teams launch readiness is blocked.'; + noFreeListed: 'OpenCode is detected, but no free OpenCode model is listed yet. Refresh provider status, or connect a provider in OpenCode for provider-backed models.'; + ready: 'OpenCode is ready for team launch.'; + unsupported: 'OpenCode is not installed, not found, or the detected runtime is not supported. Install or update OpenCode, then refresh provider status. You can also use the Install button on the home page.'; + }; + notReadyTitle: 'OpenCode is not ready for team launch'; + providerNotConnectedTitle: 'OpenCode provider is not connected'; + readyMessage: 'OpenCode passed provider readiness. Select it to use OpenCode models for this team.'; + readyTitle: 'OpenCode is ready'; + summary: { + checking: 'OpenCode status: checking runtime'; + status: 'OpenCode status: {{parts}}'; + }; + summaryParts: { + freeWithoutAuth: 'free models available without auth'; + providerConnected: 'provider connected'; + providerModelsNeedSetup: 'provider-backed models need setup'; + providerNotConnected: 'provider not connected'; + providerOptional: 'provider connection optional'; + runtimeDetected: 'runtime detected'; + runtimeMissing: 'runtime missing'; + teamLaunchBlocked: 'team launch blocked'; + teamLaunchReady: 'team launch ready'; + }; + useOpenCode: 'Use OpenCode'; + }; + placeholders: { + customModelId: 'openai/gpt-oss-20b'; + }; + pricing: { + cacheReadTitle: 'Cache read: {{rate}} per 1M tokens'; + cacheWriteTitle: 'Cache write: {{rate}} per 1M tokens'; + free: 'Free'; + inputShort: 'in {{rate}}'; + inputTitle: 'Input: {{rate}} per 1M tokens'; + outputShort: 'out {{rate}}'; + outputTitle: 'Output: {{rate}} per 1M tokens'; + perMillionSummary: '{{summary}} / 1M'; + }; + reason: 'Reason: {{reason}}'; + routeGroups: { + builtinFree: 'Free built-in'; + connectedProviders: 'Connected providers'; + openCodeConfig: 'OpenCode config'; + otherCatalog: 'Other OpenCode catalog'; + }; + runtimeModelsSyncing: 'Explicit models load from the current runtime. Default remains available while the list is syncing.'; + searchModels: 'Search models'; + unavailableInRuntime: 'Unavailable in current runtime'; + }; + openCodeContextConfigHint: { + and: 'and'; + compactionConfig: 'Compaction config'; + description: 'Add matching limits to the OpenCode config for the provider and model used by this teammate. This helps OpenCode compact and prune before local models overflow their context window.'; + promptInstructionsSuffix: 'are weaker because the request is assembled before the model reads them.'; + providerLimits: 'Provider limits'; + replacePrefix: 'Replace'; + replaceSuffix: 'with the provider and model IDs from your OpenCode setup. Prompt instructions like'; + summary: 'OpenCode local models can use an OpenCode context budget instead of prompt-only limits.'; + }; + permissions: { + autoApproveAllTools: 'Auto-approve all tools'; + autonomousModeDescription: 'Autonomous mode: team tools execute without confirmation. Be cautious with untrusted code.'; + manualModeDescription: "Manual mode: you'll approve or deny each tool call in real time."; + }; + processes: { + ago: '{{time}} ago'; + kill: 'Kill'; + open: 'Open'; + openInBrowser: 'Open in browser'; + pid: 'PID{{pid}}'; + running: 'Running'; + stopProcess: 'Stop process (SIGTERM)'; + stopped: 'Stopped'; + stoppedAgo: 'stopped {{time}} ago'; + title: 'CLI Processes'; + }; + projectPath: { + browse: 'Browse'; + createAutomatically: 'If the directory does not exist, it will be created automatically.'; + customWorkingDirectory: 'Custom working directory'; + deleted: { + label: 'Deleted'; + title: 'Project folder no longer exists'; + }; + empty: 'Nothing found'; + label: 'Project'; + loadingProjects: 'Loading projects...'; + mode: { + customPath: 'Custom path'; + projectList: 'From project list'; + }; + noProjects: 'No projects found, switch to custom path.'; + searchPlaceholder: 'Search project by name or path'; + selectFromList: 'Select a project from the list'; + selectProject: 'Select a project...'; + source: { + claude: 'Found by Claude'; + codex: 'Found by Codex'; + mixed: 'Found by Claude and Codex'; + }; + }; + provisioning: { + cancel: 'Cancel'; + cliLogs: 'CLI logs'; + copied: 'Copied'; + copyDiagnostics: 'Copy diagnostics'; + diagnostics: 'Diagnostics'; + diagnosticsCopied: 'Diagnostics copied'; + liveOutput: 'Live output'; + moreWarningsHidden: '{{count}} more warnings hidden'; + noOutput: 'No output captured yet.'; + pid: 'PID {{pid}}'; + presentation: { + awaitingPermission: '{{count}} teammate awaiting permission approval'; + bootstrapStalled: 'Bootstrap stalled: {{names}}'; + bootstrapStalledWithOpenCodeWait: '{{stalled}}; Waiting for OpenCode: {{names}}'; + countPendingDiagnostic: '{{count}} {{label}}'; + failed: { + memberFailedToStart: '{{name}} failed to start'; + teammatesFailedRatio: '{{count}}/{{total}} teammates failed to start'; + teammatesFailedToStart: '{{count}} teammates failed to start'; + }; + joining: { + teammatesConfirmedRatio: '{{count}}/{{total}} teammates confirmed'; + teammatesStillJoining: '{{count}} teammates still joining'; + }; + nameListWithMore: '{{names}}, +{{count}} more'; + namedPendingDiagnostic: '{{label}}: {{names}}'; + panel: { + coreTeamReady: 'Core team ready'; + finishingLaunch: 'Finishing launch'; + launchContinuedSkipped: 'Launch continued with skipped teammates'; + launchDetails: 'Launch details'; + launchFailed: 'Launch failed'; + launchFinishedWithErrors: 'Launch finished with errors'; + launchingTeam: 'Launching team'; + teamLaunched: 'Team launched'; + }; + pendingLabels: { + awaitingPermission: 'Awaiting permission'; + awaitingPermissionLower: 'awaiting permission'; + bootstrapStalled: 'Bootstrap stalled'; + bootstrapUnconfirmed: 'Bootstrap unconfirmed'; + bootstrapUnconfirmedLower: 'bootstrap unconfirmed'; + shellOnly: 'Shell-only'; + shellOnlyLower: 'shell-only'; + waitingForBootstrap: 'Waiting for bootstrap'; + waitingForBootstrapLower: 'waiting for bootstrap'; + waitingForRuntime: 'Waiting for runtime'; + waitingForRuntimeLower: 'waiting for runtime'; + }; + ready: { + allTeammatesJoined: 'All {{count}} teammates joined'; + launchContinuedSkipped: 'Launch continued - {{count}}/{{total}} teammates skipped'; + launchFinishedWithErrors: 'Launch finished with errors - {{count}}/{{total}} teammates failed to start'; + leadOnline: 'Lead online'; + teamLaunchedAllJoined: 'Team launched - all {{count}} teammates joined'; + teamLaunchedLeadOnline: 'Team launched - lead online'; + teamProvisionedAllJoined: 'Team provisioned - all {{count}} teammates joined'; + teamProvisionedLeadOnline: 'Team provisioned - lead online'; + teamProvisionedStillJoining: 'Team provisioned - teammates are still joining'; + }; + skipped: { + memberSkipped: '{{name}} skipped for this launch'; + memberSkippedCompact: '{{name}} skipped'; + memberSkippedWithReason: '{{name}} skipped for this launch - {{reason}}'; + teammatesSkipped: '{{count}} teammates skipped'; + teammatesSkippedList: 'Skipped teammates: {{list}}'; + teammatesSkippedRatio: '{{count}}/{{total}} teammates skipped for this launch'; + }; + waitingForOpenCode: 'Waiting for OpenCode: {{names}}'; + }; + providerStatus: { + copied: 'Copied'; + copyDiagnostics: 'Copy diagnostics'; + deepVerificationPending: 'Deep verification is still running. OpenCode free models may take around 20 seconds.'; + detailSummary: { + authenticationRequired: 'Authentication required'; + cliBinaryCouldNotStart: 'CLI binary could not be started'; + cliBinaryMissing: 'CLI binary missing'; + cliPreflightFailed: 'CLI preflight failed'; + cliPreflightIncomplete: 'CLI preflight did not complete'; + needsAttention: 'Needs attention'; + openCodeMcpUnreachable: 'OpenCode app MCP unreachable'; + openCodeNoOutput: 'OpenCode runtime check returned no output'; + openCodeRuntimeMissing: 'OpenCode runtime missing'; + openCodeWindowsAccessBlocked: 'OpenCode Windows access blocked'; + readyWithNotes: 'Ready with notes'; + runtimeProviderNotConfigured: 'Runtime provider is not configured'; + selectedModelAvailable: 'Selected model available'; + selectedModelCheckFailed: 'Selected model check failed'; + selectedModelCompatibilityPending: 'Selected model compatibility pending'; + selectedModelCompatible: 'Selected model compatible'; + selectedModelDeferred: 'Selected model verification deferred'; + selectedModelPingNotConfirmed: 'Selected model ping not confirmed'; + selectedModelTimedOut: 'Selected model verification timed out'; + selectedModelUnavailable: 'Selected model unavailable'; + selectedModelVerified: 'Selected model verified'; + workingDirectoryMissing: 'Working directory missing'; + }; + failureHints: { + authenticationRequired: 'Authenticate the required provider in Claude CLI, then reopen this dialog.'; + cliBinaryMissing: 'Make sure the local Claude CLI binary exists and can be started, then reopen this dialog.'; + default: 'Resolve the issue above, then reopen this dialog.'; + openCodeAccessDenied: 'Fix folder permissions or move the project to a user-writable folder. Running as administrator is only a temporary workaround.'; + openCodeAppMcpUnreachable: 'Retry launch to refresh the OpenCode app MCP bridge. If it repeats, restart the app and OpenCode runtime.'; + openCodeBridgeNoOutput: 'Restart the app and OpenCode runtime, then retry. If it repeats, copy diagnostics.'; + openCodeRuntimeMissing: 'Install or retry OpenCode runtime from the provider status card, then reopen this dialog.'; + runtimeProviderNotConfigured: 'Configure the selected provider runtime, then reopen this dialog.'; + workingDirectoryMissing: 'Choose an existing working directory, then reopen this dialog.'; + }; + modelChecksSummary: 'Selected model checks - {{details}}'; + modelParts: { + available: '{{count}} available'; + available_few: '{{count}} available'; + available_many: '{{count}} available'; + available_one: '{{count}} available'; + available_other: '{{count}} available'; + checkFailed: '{{count}} model check failed'; + checkFailed_few: '{{count}} models check failed'; + checkFailed_many: '{{count}} models check failed'; + checkFailed_one: '{{count}} model check failed'; + checkFailed_other: '{{count}} models check failed'; + checking: '{{count}} checking'; + checking_few: '{{count}} checking'; + checking_many: '{{count}} checking'; + checking_one: '{{count}} checking'; + checking_other: '{{count}} checking'; + compatibilityPending: '{{count}} compatible, deep verification pending'; + compatibilityPending_few: '{{count}} compatible, deep verification pending'; + compatibilityPending_many: '{{count}} compatible, deep verification pending'; + compatibilityPending_one: '{{count}} compatible, deep verification pending'; + compatibilityPending_other: '{{count}} compatible, deep verification pending'; + compatible: '{{count}} compatible'; + compatible_few: '{{count}} compatible'; + compatible_many: '{{count}} compatible'; + compatible_one: '{{count}} compatible'; + compatible_other: '{{count}} compatible'; + deferred: '{{count}} verification deferred'; + deferred_few: '{{count}} verification deferred'; + deferred_many: '{{count}} verification deferred'; + deferred_one: '{{count}} verification deferred'; + deferred_other: '{{count}} verification deferred'; + pingNotConfirmed: '{{count}} ping not confirmed'; + pingNotConfirmed_few: '{{count}} ping not confirmed'; + pingNotConfirmed_many: '{{count}} ping not confirmed'; + pingNotConfirmed_one: '{{count}} ping not confirmed'; + pingNotConfirmed_other: '{{count}} ping not confirmed'; + timedOut: '{{count}} model timed out'; + timedOut_few: '{{count}} models timed out'; + timedOut_many: '{{count}} models timed out'; + timedOut_one: '{{count}} model timed out'; + timedOut_other: '{{count}} models timed out'; + unavailable: '{{count}} model unavailable'; + unavailable_few: '{{count}} models unavailable'; + unavailable_many: '{{count}} models unavailable'; + unavailable_one: '{{count}} model unavailable'; + unavailable_other: '{{count}} models unavailable'; + verified: '{{count}} verified'; + verified_few: '{{count}} verified'; + verified_many: '{{count}} verified'; + verified_one: '{{count}} verified'; + verified_other: '{{count}} verified'; + }; + openProviderSettings: 'Open {{provider}} settings'; + progress: { + checkingProvider: 'Checking {{provider}} provider...'; + checkingProviders: 'Checking {{providers}} providers...'; + checkingSelectedProviders: 'Checking selected providers in parallel...'; + }; + status: { + checking: 'checking...'; + failed: 'ERR'; + notes: 'OK (notes)'; + pending: 'waiting'; + ready: 'OK'; + }; + }; + steps: { + assembling: 'Members joining'; + configuring: 'Team setup'; + finalizing: 'Finalizing'; + starting: 'Starting'; + }; + }; + review: { + conflict: { + cancel: 'Cancel'; + description: "This file has been modified since the agent's changes"; + editManually: 'Edit Manually'; + keepCurrent: 'Keep Current'; + saveResolution: 'Save Resolution'; + title: 'Conflict Detected'; + useOriginal: 'Use Original'; + }; + continuousScroll: { + empty: 'No reviewable file changes'; + }; + diffControls: { + acceptChange: 'Accept change (⌘Y)'; + acceptShortcut: '⌘Y'; + keep: 'Keep'; + nextChunk: 'Next chunk'; + previousChunk: 'Previous chunk'; + rejectChange: 'Reject change (⌘N)'; + rejectShortcut: '⌘N'; + undo: 'Undo'; + }; + diffError: { + actions: { + retry: 'Retry'; + }; + raw: { + charsTotal: '... ({{count}} chars total)'; + charsTotal_few: '... ({{count}} chars total)'; + charsTotal_many: '... ({{count}} chars total)'; + charsTotal_one: '... ({{count}} char total)'; + charsTotal_other: '... ({{count}} chars total)'; + file: 'File: {{file}}'; + modified: '+++ Modified'; + original: '--- Original'; + show: 'Show raw diff data'; + }; + title: 'Failed to render diff view'; + unexpected: 'An unexpected error occurred while rendering the diff.'; + }; + empty: { + noFileChangesRecorded: 'No file changes recorded'; + noFileEvents: 'The task ledger has no file events for this task.'; + noFileEventsYet: 'The task ledger has no file events for this task yet.'; + noSafeDiff: 'No safe diff available'; + noSafeDiffDescription: 'The task ledger did not expose a safe file diff for this task.'; + noSafeDiffDiagnosticsDescription: 'The task ledger did not expose a safe file diff for this task. The diagnostics below explain why.'; + }; + fileHeader: { + actions: { + accept: 'Accept'; + discard: 'Discard'; + discardTooltip: 'Discard all edits for this file'; + keepMyDraft: 'Keep my draft'; + reject: 'Reject'; + reloadFromDisk: 'Reload from disk'; + restore: 'Restore'; + restoreTooltip: 'Create/restore this file on disk from the preview'; + saveFile: 'Save File'; + saveFileTooltip: 'Save file to disk'; + }; + badges: { + deleted: 'DELETED'; + manualReview: 'MANUAL REVIEW'; + new: 'NEW'; + worktree: 'WORKTREE'; + }; + contentSource: { + 'disk-current': 'Current Disk'; + 'file-history': 'File History'; + 'git-fallback': 'Git Fallback'; + 'ledger-exact': 'Task Ledger'; + 'ledger-snapshot': 'Ledger Snapshot'; + 'snippet-reconstruction': 'Reconstructed'; + unavailable: 'Content unavailable'; + }; + contentUnavailable: { + badge: 'Content unavailable'; + description: 'The ledger recorded metadata for this change, but full text content is not available. This usually means binary, large, or hash-only content.'; + safety: 'Automatic accept/reject is disabled for this file to avoid unsafe disk writes.'; + title: 'Text content is unavailable'; + }; + disabled: { + acceptRejectContentUnavailable: 'Accept/Reject is disabled because full text content is unavailable.'; + acceptRejectMissingOnDisk: 'Accept/Reject is disabled while the file is missing on disk.'; + rejectBaselineUnavailable: 'Reject is disabled because the original baseline is unavailable.'; + rejectContentUnavailable: 'Reject is disabled because full text content is unavailable.'; + rejectManualLedgerReview: 'Reject is disabled because this ledger change has binary, large, or unavailable content.'; + }; + externalChange: { + changedOnDisk: 'Changed on disk'; + deletedOnDisk: 'Deleted on disk'; + recreatedOnDisk: 'Recreated on disk'; + }; + missingOnDisk: { + badge: 'Missing on disk'; + description: 'We can still show a preview from agent logs, but your filesystem is out of sync.'; + restorePrefix: 'Use'; + restoreSuffix: 'to write the preview content back to disk.'; + restoreUnavailable: 'Full file content is not available to restore automatically.'; + title: 'File is missing on disk'; + }; + pathChange: { + from: 'From {{path}}'; + to: 'To {{path}}'; + }; + worktree: { + isolated: 'Isolated worktree'; + }; + }; + fileMissingPrefix: 'File is missing on disk. This diff may be only a preview from agent logs. Use'; + fileMissingSuffix: 'to create the file on disk.'; + filePlaceholder: { + description: 'Preparing a full editor diff for this file.'; + loading: 'Loading'; + }; + fileTree: { + badges: { + deleted: 'deleted'; + new: 'new'; + }; + collapseFolder: 'Collapse {{name}}'; + empty: { + noChangedFiles: 'No changed files'; + noMatchingFiles: 'No matching files'; + }; + expandFolder: 'Expand {{name}}'; + filters: { + clear: 'Clear'; + new: 'New'; + rejected: 'Rejected'; + unresolved: 'Unresolved'; + }; + searchPlaceholder: 'Search files…'; + viewed: 'Viewed'; + }; + fullDiffLoading: { + editorViewLoading: 'Editor view loading'; + filesInProgress: '{{count}} files in progress'; + filesInProgress_few: '{{count}} files in progress'; + filesInProgress_many: '{{count}} files in progress'; + filesInProgress_one: '{{count}} file in progress'; + filesInProgress_other: '{{count}} files in progress'; + filesReady: '{{ready}}/{{total}} files ready'; + previewsReady: '{{count}} previews ready'; + previewsReady_few: '{{count}} previews ready'; + previewsReady_many: '{{count}} previews ready'; + previewsReady_one: '{{count}} preview ready'; + previewsReady_other: '{{count}} previews ready'; + progressDescription: '{{ready}} ready, {{loading}} still loading. Preview diffs stay visible below while the remaining baselines are resolved.'; + singleDescription: 'Preview diffs stay visible below while the exact baseline is resolved.'; + subtitleCurrentFile: 'Finalizing the exact editor diff for the current file.'; + subtitleForFile: 'Finalizing the exact editor diff for {{file}}.'; + subtitleMany: 'Resolving exact before/after baselines for the files currently loading.'; + titleMany: 'Preparing {{count}} Full Diffs'; + titleOne: 'Preparing Full Diff'; + }; + loading: { + diff: 'DIFF'; + ledgerObjectsProcessed: '{{count}} ledger objects processed'; + ledgerObjectsProcessed_few: '{{count}} ledger objects processed'; + ledgerObjectsProcessed_many: '{{count}} ledger objects processed'; + ledgerObjectsProcessed_one: '{{count}} ledger object processed'; + ledgerObjectsProcessed_other: '{{count}} ledger objects processed'; + phases: { + checkingWorktree: 'Checking worktree context...'; + preparingDiffs: 'Preparing review diffs...'; + readingLedger: 'Reading task ledger...'; + resolvingFiles: 'Resolving file states...'; + }; + }; + progress: { + viewed: '{{viewed}}/{{total}} viewed'; + }; + restore: 'Restore'; + scope: { + confidence: { + bestEffort: 'Best effort'; + high: 'High confidence'; + low: 'Low confidence'; + medium: 'Medium confidence'; + }; + ledger: { + exact: { + badge: 'Ledger exact'; + detail: 'The orchestrator captured these file changes while the agent was working on this task.'; + title: 'Changes captured by task ledger'; + }; + limited: { + detail: 'The orchestrator captured these file changes for this task, but at least one change was captured from a snapshot or metadata-only source. Review exact text diffs where available; binary or unavailable content may require manual review.'; + mixedBadge: 'Mixed reviewability'; + needsReviewBadge: 'Needs review'; + title: 'Changes captured with limited reviewability'; + }; + }; + readMore: 'Read more'; + tiers: { + allSession: { + detail: 'No task markers found in the session log. Cannot isolate this task - all file changes from the entire session are shown, including changes from other tasks. This can happen with older CLI versions or non-standard workflows.'; + title: 'Showing all session changes'; + }; + endEstimated: { + detail: 'Only the start marker was found - the task has no completion marker yet. Changes shown from task start to end of session. If other tasks ran after this one in the same session, their changes may also be included.'; + title: 'End boundary estimated'; + }; + exact: { + detail: 'Both start and completion markers found in the session log. The diff includes only changes made during this specific task - other tasks that modified the same files are excluded.'; + title: 'Task scope determined precisely'; + }; + startEstimated: { + detail: 'Only the completion marker was found - the start of work was not captured. If other tasks ran before this one in the same session, their changes to the same files may also be included.'; + title: 'Start boundary estimated'; + }; + }; + workInterval: { + badge: 'Interval scoped'; + detail: 'The task start marker was not available in the session log, so the diff is scoped by the task work interval stored on the board.'; + title: 'Scoped by persisted work interval'; + }; + }; + shortcuts: { + actions: { + acceptChange: 'Accept change'; + closeDialog: 'Close dialog'; + nextChange: 'Next change'; + nextFile: 'Next file'; + previousChange: 'Previous change'; + previousFile: 'Previous file'; + redo: 'Redo'; + rejectChange: 'Reject change'; + saveFile: 'Save file'; + toggleShortcuts: 'Toggle shortcuts'; + undo: 'Undo'; + }; + title: 'Keyboard Shortcuts'; + }; + timeline: { + empty: 'No edit events'; + titleWithCount: 'Edit Timeline ({{count}})'; + }; + toolbar: { + actions: { + acceptAll: 'Accept All'; + applyRejections: 'Apply Rejections'; + applying: 'Applying...'; + auto: 'Auto'; + rejectAll: 'Reject All'; + undo: 'Undo'; + }; + stats: { + accepted: '{{count}} accepted'; + accepted_few: '{{count}} accepted'; + accepted_many: '{{count}} accepted'; + accepted_one: '{{count}} accepted'; + accepted_other: '{{count}} accepted'; + acrossFiles: 'across {{count}} files'; + acrossFiles_few: 'across {{count}} files'; + acrossFiles_many: 'across {{count}} files'; + acrossFiles_one: 'across {{count}} file'; + acrossFiles_other: 'across {{count}} files'; + edited: '{{count}} edited'; + edited_few: '{{count}} edited'; + edited_many: '{{count}} edited'; + edited_one: '{{count}} edited'; + edited_other: '{{count}} edited'; + pending: '{{count}} pending'; + pending_few: '{{count}} pending'; + pending_many: '{{count}} pending'; + pending_one: '{{count}} pending'; + pending_other: '{{count}} pending'; + rejected: '{{count}} rejected'; + rejected_few: '{{count}} rejected'; + rejected_many: '{{count}} rejected'; + rejected_one: '{{count}} rejected'; + rejected_other: '{{count}} rejected'; + }; + tooltips: { + acceptAll: 'Accept all changes across all files'; + applyRejections: 'Apply rejected hunks to disk; accepted changes are kept as-is'; + autoOff: 'Auto-mark files as viewed when scrolled to end (OFF)'; + autoOn: 'Auto-mark files as viewed when scrolled to end (ON)'; + rejectAll: 'Reject all safely rejectable changes across all files'; + rejectAllDisabled: 'No pending files have a safe original baseline to reject.'; + undo: 'Undo last review operation (Ctrl+Z)'; + }; + }; + }; + reviewDialog: { + charsLeft: '{{count}} chars left'; + placeholder: 'Describe what needs to change... (Enter to submit)'; + saved: 'Saved'; + submit: 'Submit'; + title: 'Request Changes'; + }; + roleSelect: { + customRole: 'Custom role...'; + empty: 'No roles found.'; + noRole: 'No role'; + reservedRole: 'This role is reserved'; + searchPlaceholder: 'Search roles...'; + }; + runningTeams: { + title: 'Running Teams'; + }; + schedule: { + actions: { + addSchedule: 'Add Schedule'; + delete: 'Delete'; + edit: 'Edit'; + pause: 'Pause'; + resume: 'Resume'; + runNow: 'Run now'; + }; + count: '{{count}} schedules'; + count_few: '{{count}} schedules'; + count_many: '{{count}} schedules'; + count_one: '{{count}} schedule'; + count_other: '{{count}} schedules'; + cron: { + errors: { + enterExpression: 'Enter a cron expression'; + invalidExpression: 'Invalid cron expression'; + }; + expression: 'Cron expression'; + highFrequencyWarning: 'High frequency schedule (less than 5 min interval)'; + nextRuns: 'Next runs:'; + presets: { + dailyAtNine: 'Daily at 9am'; + everyHour: 'Every hour'; + everySixHours: 'Every 6 hours'; + everyThirtyMinutes: 'Every 30 min'; + mondayAtNine: 'Monday at 9am'; + weekdaysAtNine: 'Weekdays at 9am'; + }; + selectTimezone: 'Select timezone'; + timezone: 'Timezone'; + warmUpDescription: 'Prepares selected providers before scheduled execution'; + warmUpOptions: { + fifteenMinutes: '15 min'; + fiveMinutes: '5 min'; + none: 'No warm-up'; + tenMinutes: '10 min'; + thirtyMinutes: '30 min'; + }; + warmUpTime: 'Warm-up time'; + }; + empty: { + description: 'Create a schedule to run Claude tasks automatically on a cron schedule.'; + title: 'No schedules yet'; + }; + nextRun: 'Next: {{next}}'; + runHistory: { + empty: 'No runs yet'; + loading: 'Loading run history...'; + }; + runLog: { + close: 'Close'; + errors: 'Errors'; + exitCode: 'exit {{code}}'; + loadingLogs: 'Loading logs...'; + retryCount: 'retry {{count}}/{{max}}'; + stillRunning: 'Task is still running...'; + title: 'Run Log'; + }; + runStatus: { + cancelled: 'Cancelled'; + completed: 'Completed'; + failed: 'Failed'; + interrupted: 'Interrupted'; + pending: 'Pending'; + running: 'Running'; + warm: 'Warm'; + warmingUp: 'Warming up'; + }; + status: { + active: 'Active'; + disabled: 'Disabled'; + paused: 'Paused'; + }; + title: 'Schedules'; + }; + sendMessage: { + attachments: { + attachFiles: 'Attach files (paste or drag & drop)'; + disabledHint: 'File attachments are supported for the online team lead and online OpenCode teammates. Remove attachments or switch recipient.'; + openCodeOnlineRequired: 'Team must be online to attach files for OpenCode teammates'; + recipientUnsupported: 'Files can be sent to the team lead or OpenCode teammates'; + teamOnlineRequired: 'Team must be online to attach files'; + unavailable: 'Attachments are unavailable'; + }; + charsLeft: '{{count}} chars left'; + description: 'Send a direct message to a team member.'; + messageLabel: 'Message'; + placeholder: 'Write your message... (Enter to send)'; + quote: { + remove: 'Remove quote'; + replyingTo: 'Replying to'; + }; + recipientLabel: 'Recipient'; + saved: 'Saved'; + selectMemberPlaceholder: 'Select member...'; + send: 'Send'; + sending: 'Sending...'; + title: 'Send Message'; + }; + sessions: { + empty: 'No sessions found'; + filterBySession: 'Filter by this session'; + lead: 'lead'; + loading: 'Loading sessions...'; + noProjectPath: 'No project path linked'; + openSession: 'Open session'; + projectNotFound: 'Project not found'; + provisioningHint: 'Sessions will appear after team provisioning'; + removeFilter: 'Remove filter'; + showAllSessions: 'Show for all sessions'; + title: 'Sessions'; + }; + taskActivity: { + contextUnavailable: 'Detailed transcript context is no longer available for this activity.'; + description: 'Key explicit runtime activity linked to this task from transcript metadata.'; + empty: 'No explicit task activity was found in the available transcripts yet. Older or heuristic session logs may still be available below in Execution Sessions.'; + loading: 'Loading task activity...'; + loadingDetails: 'Loading activity details...'; + lowSignalOnly: 'No key task activity was found yet. Low-level execution details are available below in Task Log Stream.'; + title: 'Task Activity'; + }; + taskAttachments: { + attachImage: 'Attach image'; + dropFilesHere: 'Drop files here'; + dropImageHere: 'Drop image here'; + fromOriginalMessage: 'From original message'; + loading: 'Loading attachments...'; + pasteOrDragDrop: 'or paste / drag-drop'; + }; + taskComments: { + attachFile: 'Attach file (or paste)'; + awaitingReplyFrom: 'Awaiting reply from'; + cancelReply: 'Cancel reply'; + charsLeft: '{{count}} chars left'; + comment: 'Comment'; + or: 'or'; + placeholder: 'Add a comment... (Enter to send)'; + replyingTo: 'Replying to'; + saved: 'Saved'; + voiceToText: 'Voice to text'; + }; + taskDetail: { + actions: { + cancel: 'Cancel'; + delete: 'Delete'; + markResolved: 'Mark resolved'; + save: 'Save'; + }; + attachments: { + commentAttachment: 'Comment attachment'; + fromComments: 'From comments'; + preview: 'Preview {{filename}}'; + }; + changes: { + badges: { + attention: 'attention'; + noSafeDiff: 'no safe diff'; + }; + empty: { + noFileChangesRecorded: 'No file changes recorded'; + noFileChangesRecordedYet: 'No file changes recorded yet'; + noReviewableChangesRecovered: 'No reviewable file changes recovered'; + noSafeDiffAvailable: 'No safe diff available'; + }; + fileCount: '{{count}} files'; + fileCount_few: '{{count}} files'; + fileCount_many: '{{count}} files'; + fileCount_one: '{{count}} files'; + fileCount_other: '{{count}} files'; + fileRowsHidden: '{{count}} file rows hidden'; + fileRowsHidden_few: '{{count}} file rows hidden'; + fileRowsHidden_many: '{{count}} file rows hidden'; + fileRowsHidden_one: '{{count}} file rows hidden'; + fileRowsHidden_other: '{{count}} file rows hidden'; + loadFailed: 'Failed to load task changes summary'; + loading: 'Loading changes...'; + moreDiagnostics: '{{count}} more diagnostics'; + moreDiagnostics_few: '{{count}} more diagnostics'; + moreDiagnostics_many: '{{count}} more diagnostics'; + moreDiagnostics_one: '{{count}} more diagnostics'; + moreDiagnostics_other: '{{count}} more diagnostics'; + moreFiles: '{{count}} more files'; + moreFiles_few: '{{count}} more files'; + moreFiles_many: '{{count}} more files'; + moreFiles_one: '{{count}} more files'; + moreFiles_other: '{{count}} more files'; + openInEditor: 'Open in editor'; + openTask: 'Open task {{subject}}'; + refresh: 'Refresh changes'; + refreshFailed: 'Refresh failed: {{error}}'; + refreshShort: 'Refresh'; + refreshTeamChanges: 'Refresh team changes'; + refreshing: 'Refreshing'; + refreshingChanges: 'Refreshing changes...'; + reviewDiff: 'Review diff'; + reviewTaskDiff: 'Review task diff'; + scannedCandidateTasks: 'Scanned {{requested}} of {{eligible}} candidate tasks'; + tasksDeferred: '{{count}} tasks deferred this pass'; + tasksDeferred_few: '{{count}} tasks deferred this pass'; + tasksDeferred_many: '{{count}} tasks deferred this pass'; + tasksDeferred_one: '{{count}} tasks deferred this pass'; + tasksDeferred_other: '{{count}} tasks deferred this pass'; + title: 'Changes'; + }; + clarification: { + awaitingLead: 'Awaiting clarification from team lead'; + awaitingUser: 'Awaiting clarification from you'; + }; + comments: { + actions: { + cancelReply: 'Cancel reply'; + comment: 'Comment'; + reply: 'Reply'; + replyToComment: 'Reply to comment'; + showMore: 'Show more comments ({{visible}}/{{total}})'; + }; + attachments: { + downloadFailed: 'Download failed'; + previewAlt: 'Attachment preview'; + }; + badges: { + approved: 'Approved'; + reviewRequested: 'Review requested'; + }; + input: { + charsLeft: '{{count}} chars left'; + charsLeft_few: '{{count}} chars left'; + charsLeft_many: '{{count}} chars left'; + charsLeft_one: '{{count}} char left'; + charsLeft_other: '{{count}} chars left'; + placeholder: 'Add a comment... (Enter to send)'; + }; + renderLimit: 'Showing the most recent {{formattedCount}} comments to keep the UI responsive.'; + replyingTo: 'Replying to'; + unknownTime: 'unknown time'; + }; + description: { + add: 'Click to add description...'; + edit: 'Edit description'; + placeholder: 'Task description (supports markdown)'; + }; + loading: { + fetchingTeamData: 'Fetching team data'; + title: 'Loading task...'; + }; + logs: { + newArriving: 'New task logs arriving'; + }; + notFound: 'Task not found'; + related: { + blockedBy: 'Blocked by'; + blocks: 'Blocks'; + linkedFrom: 'Linked from'; + links: 'Links'; + title: 'Related tasks'; + }; + review: { + reviewer: 'Reviewer: {{reviewer}}'; + }; + reviewStates: { + approved: 'Approved'; + inReview: 'In review'; + needsFix: 'Needs fix'; + }; + sections: { + attachments: 'Attachments'; + changes: 'Changes'; + comments: 'Comments'; + description: 'Description'; + taskLogs: 'Task Logs'; + workflowHistory: 'Workflow History'; + }; + unassigned: 'Unassigned'; + workflow: { + implementationTimeTitle: 'Implementation time from persisted work intervals'; + inProgressTime: 'In progress time {{duration}}'; + }; + workflowTimeline: { + approved: 'Approved'; + assignedTo: 'Assigned to'; + by: 'by'; + changesRequested: 'Changes requested'; + createdAs: 'Created as'; + currentImplementationInterval: 'Current implementation interval'; + empty: 'No workflow history recorded'; + implementationIntervalEnded: 'Implementation interval ended at this transition'; + ownerChanged: 'Owner changed'; + reassigned: 'Reassigned'; + reviewRequested: 'Review requested'; + reviewStarted: 'Review started'; + runningPrefix: 'running '; + unassignedFrom: 'Unassigned from'; + unknownEvent: 'Unknown event'; + }; + }; + taskLogs: { + exact: { + description: 'Exact transcript slices rendered with the same execution-log components used in Logs.'; + emptyDescription: 'Exact transcript bundles will appear here when explicit task-linked transcript metadata is available.'; + emptyTitle: 'No exact task logs yet'; + loading: 'Loading exact task logs...'; + summaryOnly: 'summary only'; + title: 'Exact Task Logs'; + }; + executionSessions: { + description: 'Legacy session-centric transcript browsing and previews.'; + online: 'Online'; + title: 'Execution Sessions'; + updating: 'Updating...'; + }; + stream: { + title: 'Task Log Stream'; + }; + }; + tasks: { + createTask: { + assignee: 'Assignee'; + assigneeOptional: 'Assignee (optional)'; + blockedByOptional: 'Blocked by tasks (optional)'; + blockedBySummary: 'Task will be blocked by: {{tasks}}'; + cancel: 'Cancel'; + create: 'Create'; + creating: 'Creating...'; + description: "The task will be created in the team's tasks/ directory and appear on the Kanban board."; + descriptionOptional: 'Description (optional)'; + detailsPlaceholder: 'Task details (supports markdown)'; + hideOptionalFields: 'Hide optional fields'; + offlineNotice: { + after: '- launch the team to start execution.'; + before: 'Team is offline. The task will be added to'; + }; + promptOptional: 'Prompt for assignee (optional)'; + promptPlaceholder: 'Custom instructions for the team member...'; + relatedOptional: 'Related tasks (optional)'; + relatedSummary: 'Related: {{tasks}}'; + saved: 'Saved'; + searchTasks: 'Search tasks...'; + selectMember: 'Select a member'; + selectMemberOptional: 'Select member...'; + showOptionalFields: 'Show optional fields'; + startImmediately: 'Start immediately'; + startOfflineHint: 'Team is offline. Launch the team first to start tasks immediately.'; + subject: 'Subject'; + subjectPlaceholder: 'What needs to be done?'; + title: 'Create Task'; + todo: 'TODO'; + }; + deleteConfirm: { + cancelLabel: 'Cancel'; + confirmLabel: 'Delete'; + message: 'Move task #{{taskId}} to trash?'; + title: 'Delete task'; + }; + list: { + columns: { + blockedBy: 'Blocked By'; + blocks: 'Blocks'; + id: 'ID'; + owner: 'Owner'; + status: 'Status'; + subject: 'Subject'; + }; + empty: 'No tasks in this team'; + filters: { + allOwners: 'All owners'; + allStatuses: 'All statuses'; + ownerAria: 'Filter tasks by owner'; + statusAria: 'Filter tasks by status'; + }; + showing: 'Showing {{shown}} of {{total}}'; + }; + openTask: 'Open task'; + status: { + completed: 'completed'; + deleted: 'deleted'; + inProgress: 'in_progress'; + pending: 'pending'; + }; + statusSummary: { + completed: '{{count}} completed'; + completed_few: '{{count}} completed'; + completed_many: '{{count}} completed'; + completed_one: '{{count}} completed'; + completed_other: '{{count}} completed'; + inProgress: '{{count}} in_progress'; + inProgress_few: '{{count}} in_progress'; + inProgress_many: '{{count}} in_progress'; + inProgress_one: '{{count}} in_progress'; + inProgress_other: '{{count}} in_progress'; + pending: '{{count}} pending'; + pending_few: '{{count}} pending'; + pending_many: '{{count}} pending'; + pending_one: '{{count}} pending'; + pending_other: '{{count}} pending'; + progressAria: 'Tasks {{completed}}/{{total}} completed'; + }; + teamPrefix: 'Team:'; + unassigned: 'Unassigned'; + }; + toolApproval: { + after: 'after'; + allow: 'Allow'; + allowAll: 'Allow all'; + autoActionIn: 'Auto-{{action}} in {{time}}'; + autoAllowAllTools: 'Auto-allow all tools'; + autoAllowFileEdits: 'Auto-allow file edits (Edit, Write, NotebookEdit)'; + autoAllowSafeCommands: 'Auto-allow safe commands (git, pnpm, npm, ls...)'; + deny: 'Deny'; + diff: { + binaryFile: 'Binary file - cannot preview'; + newFile: 'New file'; + previewChanges: 'Preview changes'; + readingFile: 'Reading file...'; + truncated: 'File truncated at 2MB - diff may be incomplete'; + }; + onTimeout: 'On timeout:'; + pendingCount: '{{count}} pending'; + secondsShort: 'sec'; + settings: 'Settings'; + submit: 'Submit'; + timeoutActions: { + allow: 'Allow'; + deny: 'Deny'; + wait: 'Wait forever'; + }; + }; + worktreeGitReadiness: { + checking: 'Checking Git repository status for teammate worktrees...'; + createInitialCommit: 'Create initial commit'; + initialCommitMessage: 'chore: initial commit'; + initialCommitNotice: 'The initial commit action stages and commits all current files with message'; + initializeRepository: 'Initialize Git repository'; + needsSetup: 'Worktree isolation needs Git setup'; + ready: 'Git worktrees are ready.'; + readyOnBranch: 'Git worktrees are ready on branch {{branch}}.'; + }; + }; +} diff --git a/src/features/localization/renderer/ui/AppLanguageSelect.tsx b/src/features/localization/renderer/ui/AppLanguageSelect.tsx new file mode 100644 index 00000000..e8e28a43 --- /dev/null +++ b/src/features/localization/renderer/ui/AppLanguageSelect.tsx @@ -0,0 +1,64 @@ +import { useCallback, useMemo } from 'react'; + +import { Combobox } from '@renderer/components/ui/combobox'; +import { Check } from 'lucide-react'; + +import { APP_LOCALE_PREFERENCES } from '../../contracts'; +import { resolveAppLocale } from '../../core/domain/localePolicy'; +import { getBrowserSystemLocale } from '../adapters/browserSystemLocaleAdapter'; +import { useAppTranslation } from '../hooks/useAppTranslation'; + +import type { AppLocalePreference } from '../../contracts'; + +interface AppLanguageSelectProps { + readonly value: AppLocalePreference; + readonly disabled?: boolean; + readonly onValueChange: (value: AppLocalePreference) => void; +} + +export const AppLanguageSelect = ({ + value, + disabled = false, + onValueChange, +}: AppLanguageSelectProps): React.JSX.Element => { + const { t } = useAppTranslation('common'); + const systemLocale = getBrowserSystemLocale(); + const resolvedSystemLocale = resolveAppLocale({ preference: 'system', systemLocale }); + const options = useMemo( + () => + APP_LOCALE_PREFERENCES.map((preference) => ({ + label: + preference === 'system' + ? t('locales.systemWithResolved', { + locale: t(`locales.names.${resolvedSystemLocale}`), + }) + : t(`locales.names.${preference}`), + value: preference, + })), + [resolvedSystemLocale, t] + ); + + const renderOption = useCallback( + (option: { value: string; label: string }, isSelected: boolean) => ( + <> + + {option.label} + + ), + [] + ); + + return ( + onValueChange(nextValue as AppLocalePreference)} + placeholder={t('locales.selectPlaceholder')} + searchPlaceholder={t('locales.searchPlaceholder')} + emptyMessage={t('locales.emptyMessage')} + disabled={disabled} + className="min-w-[180px]" + renderOption={renderOption} + /> + ); +}; diff --git a/src/features/localization/renderer/ui/LocalizationProvider.tsx b/src/features/localization/renderer/ui/LocalizationProvider.tsx new file mode 100644 index 00000000..54146451 --- /dev/null +++ b/src/features/localization/renderer/ui/LocalizationProvider.tsx @@ -0,0 +1,40 @@ +import { useEffect, useMemo } from 'react'; +import { I18nextProvider } from 'react-i18next'; + +import { resolveRuntimeLocale } from '../../core/application/resolveRuntimeLocale'; +import { normalizeAppLocalePreference } from '../../core/domain/localePolicy'; +import { getBrowserSystemLocale } from '../adapters/browserSystemLocaleAdapter'; +import { appI18n } from '../composition/createI18nextInstance'; + +import type { AppConfig } from '@shared/types'; + +interface LocalizationProviderProps { + readonly appConfig: AppConfig | null; + readonly children: React.ReactNode; +} + +export const LocalizationProvider = ({ + appConfig, + children, +}: LocalizationProviderProps): React.JSX.Element => { + const resolvedLocale = useMemo( + () => + resolveRuntimeLocale({ + preference: normalizeAppLocalePreference(appConfig?.general.appLocale), + systemLocale: getBrowserSystemLocale(), + }), + [appConfig?.general.appLocale] + ); + + useEffect(() => { + if (appI18n.language !== resolvedLocale) { + void appI18n.changeLanguage(resolvedLocale); + } + }, [resolvedLocale]); + + useEffect(() => { + document.documentElement.lang = resolvedLocale; + }, [resolvedLocale]); + + return {children}; +}; diff --git a/src/features/member-log-stream/renderer/adapters/MemberLogStreamSection.tsx b/src/features/member-log-stream/renderer/adapters/MemberLogStreamSection.tsx index 6f973e58..b38401a6 100644 --- a/src/features/member-log-stream/renderer/adapters/MemberLogStreamSection.tsx +++ b/src/features/member-log-stream/renderer/adapters/MemberLogStreamSection.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { api } from '@renderer/api'; import { useStore } from '@renderer/store'; import { selectResolvedMembersForTeamName } from '@renderer/store/slices/teamSlice'; @@ -43,6 +44,7 @@ export function MemberLogStreamSection({ enabled = true, onInitialLoadErrorChange, }: Readonly): React.JSX.Element { + const { t } = useAppTranslation('team'); const [selectedLogView, setSelectedLogView] = useState<'execution' | 'process'>('execution'); const teamMembers = useStore((s) => selectResolvedMembersForTeamName(s, teamName)); const { stream, loading, error } = useMemberLogStream({ teamName, member, enabled }); @@ -79,7 +81,7 @@ export function MemberLogStreamSection({ }`} onClick={() => setSelectedLogView('execution')} > - Execution + {t('memberLogStream.tabs.execution')}
{selectedLogView === 'execution' ? ( ({ buildSegmentRenderKey, getSegmentMetaLabel, }: Readonly>): React.JSX.Element { + const { t } = useAppTranslation('team'); const [selectedParticipantKey, setSelectedParticipantKey] = useState('all'); const appliedSelectionResetKeyRef = useRef(null); const participants = stream?.participants ?? []; @@ -329,7 +331,7 @@ export function ExecutionLogStreamView({ }`} onClick={() => setSelectedParticipantKey('all')} > - All + {t('memberLogStream.filters.all')} {participants.map((participant) => ( ): React.JSX.Element { + const { t } = useAppTranslation('team'); + const { t: tCommon } = useAppTranslation('common'); const [kind, setKind] = useState('stdout'); const [log, setLog] = useState(null); const [loading, setLoading] = useState(false); @@ -222,7 +225,7 @@ export function MemberRuntimeProcessLogsPanel({ checked={autoRefresh} onChange={(event) => setAutoRefresh(event.target.checked)} /> - Auto-refresh + {t('members.runtimeLogs.autoRefresh')}
) : hasContent ? ( ) : (
- {statusText ?? 'No process log file captured for this member yet.'} + {statusText ?? t('members.runtimeLogs.empty')}
)}
diff --git a/src/features/member-work-sync/renderer/ui/MemberWorkSyncDetails.tsx b/src/features/member-work-sync/renderer/ui/MemberWorkSyncDetails.tsx index 5f21e65f..829227ba 100644 --- a/src/features/member-work-sync/renderer/ui/MemberWorkSyncDetails.tsx +++ b/src/features/member-work-sync/renderer/ui/MemberWorkSyncDetails.tsx @@ -1,3 +1,5 @@ +import { useAppTranslation } from '@features/localization/renderer'; + import { toMemberWorkSyncStatusViewModel } from '../adapters/memberWorkSyncStatusViewModel'; import { MemberWorkSyncBadge } from './MemberWorkSyncBadge'; @@ -22,6 +24,7 @@ export function MemberWorkSyncDetails({ status, showDiagnostics = false, }: MemberWorkSyncDetailsProps): React.ReactElement { + const { t } = useAppTranslation('team'); const viewModel = toMemberWorkSyncStatusViewModel(status); const agendaItems = status?.agenda.items ?? []; @@ -29,7 +32,9 @@ export function MemberWorkSyncDetails({
-

Member work sync

+

+ {t('memberWorkSync.details.title')} +

{viewModel.tooltip}

@@ -37,25 +42,33 @@ export function MemberWorkSyncDetails({
-
Actionable items
+
+ {t('memberWorkSync.details.actionableItems')} +
{viewModel.actionableCount}
-
Fingerprint
+
+ {t('memberWorkSync.details.fingerprint')} +
{shortFingerprint(viewModel.fingerprint)}
-
Report
+
{t('memberWorkSync.details.report')}
- {viewModel.reportState ?? 'none'} + {viewModel.reportState ?? t('memberWorkSync.details.none')}
-
Shadow would nudge
+
+ {t('memberWorkSync.details.shadowWouldNudge')} +
- {viewModel.wouldNudge ? 'yes' : 'no'} + {viewModel.wouldNudge + ? t('memberWorkSync.details.yes') + : t('memberWorkSync.details.no')}
@@ -69,7 +82,7 @@ export function MemberWorkSyncDetails({ ))} {agendaItems.length > 3 ? (
  • - {agendaItems.length - 3} more actionable item(s) + {t('memberWorkSync.details.moreActionableItems', { count: agendaItems.length - 3 })}
  • ) : null} @@ -77,7 +90,7 @@ export function MemberWorkSyncDetails({ {showDiagnostics && status?.diagnostics.length ? (

    - Diagnostics: {status.diagnostics.join(', ')} + {t('memberWorkSync.details.diagnostics', { diagnostics: status.diagnostics.join(', ') })}

    ) : null}
    diff --git a/src/features/member-work-sync/renderer/ui/MemberWorkSyncStatusPanel.tsx b/src/features/member-work-sync/renderer/ui/MemberWorkSyncStatusPanel.tsx index 919fcfa3..b9f697c6 100644 --- a/src/features/member-work-sync/renderer/ui/MemberWorkSyncStatusPanel.tsx +++ b/src/features/member-work-sync/renderer/ui/MemberWorkSyncStatusPanel.tsx @@ -1,3 +1,5 @@ +import { useAppTranslation } from '@features/localization/renderer'; + import { useMemberWorkSyncStatus } from '../hooks/useMemberWorkSyncStatus'; import { MemberWorkSyncBadge } from './MemberWorkSyncBadge'; @@ -18,6 +20,7 @@ export function MemberWorkSyncStatusPanel({ enabled = true, showDiagnostics = false, }: MemberWorkSyncStatusPanelProps): React.ReactElement | null { + const { t } = useAppTranslation('team'); const { status, viewModel, loading, error } = useMemberWorkSyncStatus({ teamName, memberName, @@ -36,12 +39,14 @@ export function MemberWorkSyncStatusPanel({
    -

    Member work sync

    +

    + {t('memberWorkSync.title')} +

    {loading - ? 'Loading member work sync diagnostics.' + ? t('memberWorkSync.loadingDiagnostics') : error - ? 'Member work sync diagnostics are unavailable.' + ? t('memberWorkSync.diagnosticsUnavailable') : viewModel.tooltip}

    diff --git a/src/features/recent-projects/renderer/ui/RecentProjectCard.tsx b/src/features/recent-projects/renderer/ui/RecentProjectCard.tsx index cdc03227..e754ea49 100644 --- a/src/features/recent-projects/renderer/ui/RecentProjectCard.tsx +++ b/src/features/recent-projects/renderer/ui/RecentProjectCard.tsx @@ -1,5 +1,6 @@ import { useMemo } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo'; import { ActivePulseIndicator } from '@renderer/components/ui/ActivePulseIndicator'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; @@ -20,6 +21,8 @@ export const RecentProjectCard = ({ onClick, onOpenPath, }: Readonly): React.JSX.Element => { + const { t } = useAppTranslation('dashboard'); + const { t: tCommon } = useAppTranslation('common'); const color = useMemo(() => projectColor(card.name), [card.name]); const isDeleted = card.filesystemState === 'deleted'; const FolderIcon = isDeleted ? FolderX : FolderGit2; @@ -53,10 +56,12 @@ export const RecentProjectCard = ({ - Deleted + {t('recentProjects.card.deleted')} - Project folder no longer exists + + {t('recentProjects.card.projectFolderMissing')} + )} {card.pathSummary && ( @@ -134,7 +139,7 @@ export const RecentProjectCard = ({
    - {isDeleted ? 'Project folder no longer exists' : 'Open'} + {isDeleted ? t('recentProjects.card.projectFolderMissing') : tCommon('actions.open')} @@ -164,17 +169,23 @@ export const RecentProjectCard = ({ <> {card.taskCounts.inProgress > 0 && ( - {card.taskCounts.inProgress} active + {t('recentProjects.card.taskCounts.active', { + count: card.taskCounts.inProgress, + })} )} {card.taskCounts.pending > 0 && ( - {card.taskCounts.pending} pending + {t('recentProjects.card.taskCounts.pending', { + count: card.taskCounts.pending, + })} )} {card.taskCounts.completed > 0 && ( - {card.taskCounts.completed} done + {t('recentProjects.card.taskCounts.done', { + count: card.taskCounts.completed, + })} )} · diff --git a/src/features/recent-projects/renderer/ui/RecentProjectsSection.tsx b/src/features/recent-projects/renderer/ui/RecentProjectsSection.tsx index b8e27d50..7131a840 100644 --- a/src/features/recent-projects/renderer/ui/RecentProjectsSection.tsx +++ b/src/features/recent-projects/renderer/ui/RecentProjectsSection.tsx @@ -1,3 +1,4 @@ +import { useAppTranslation } from '@features/localization/renderer'; import { Button } from '@renderer/components/ui/button'; import { FolderGit2, FolderOpen, Search } from 'lucide-react'; @@ -17,17 +18,18 @@ function SelectProjectFolderCard({ }: Readonly<{ onClick: () => void; }>): React.JSX.Element { + const { t } = useAppTranslation('dashboard'); return ( ); @@ -36,6 +38,7 @@ function SelectProjectFolderCard({ export const RecentProjectsSection = ({ searchQuery, }: Readonly): React.JSX.Element => { + const { t } = useAppTranslation('dashboard'); const { cards, loading, @@ -102,14 +105,14 @@ export const RecentProjectsSection = ({
    -

    Failed to load projects

    +

    {t('recentProjects.failedToLoad')}

    {error}

    ); @@ -121,8 +124,10 @@ export const RecentProjectsSection = ({
    -

    No projects found

    -

    No matches for "{searchQuery}"

    +

    {t('recentProjects.noProjects')}

    +

    + {t('recentProjects.noMatches', { query: searchQuery })} +

    ); } @@ -133,10 +138,8 @@ export const RecentProjectsSection = ({
    -

    No recent projects found

    -

    - Recent Claude and Codex activity will appear here. -

    +

    {t('recentProjects.noRecentProjects')}

    +

    {t('recentProjects.emptyDescription')}

    ); } @@ -162,7 +165,7 @@ export const RecentProjectsSection = ({ {canLoadMore && (
    )} diff --git a/src/features/running-teams/renderer/ui/RunningTeamsSection.tsx b/src/features/running-teams/renderer/ui/RunningTeamsSection.tsx index cecf9581..e78c04e4 100644 --- a/src/features/running-teams/renderer/ui/RunningTeamsSection.tsx +++ b/src/features/running-teams/renderer/ui/RunningTeamsSection.tsx @@ -1,3 +1,4 @@ +import { useAppTranslation } from '@features/localization/renderer'; import { TeamTaskStatusSummary } from '@renderer/components/team/TeamTaskStatusSummary'; import { ActivePulseIndicator } from '@renderer/components/ui/ActivePulseIndicator'; import { FolderOpen, UsersRound } from 'lucide-react'; @@ -18,6 +19,7 @@ function getRowTitle(row: RunningTeamRowModel): string { export function RunningTeamsSection({ searchQuery, }: Readonly): React.JSX.Element | null { + const { t } = useAppTranslation('team'); const { rows, hidden, openRunningTeam } = useRunningTeamsSection(searchQuery); if (hidden) { @@ -28,7 +30,7 @@ export function RunningTeamsSection({

    - Running Teams + {t('runningTeams.title')} {rows.length} diff --git a/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx b/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx index abdab89c..7c475646 100644 --- a/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx +++ b/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { Badge } from '@renderer/components/ui/badge'; import { Button } from '@renderer/components/ui/button'; import { Checkbox } from '@renderer/components/ui/checkbox'; @@ -95,6 +96,7 @@ interface RuntimeProviderErrorAlertProps { } type OpenCodeSettingsSection = 'models' | 'providers'; +type SettingsT = ReturnType['t']; const NO_PROJECT_CONTEXT_VALUE = '__runtime-provider-no-project-context__'; @@ -149,31 +151,36 @@ function getProjectContextName(projectPath: string | null | undefined): string | return name || normalized; } -function getDefaultScopeDescription(scope: RuntimeProviderDefaultScopeDto): string { +function getDefaultScopeDescription(scope: RuntimeProviderDefaultScopeDto, t: SettingsT): string { return scope === 'all_projects' - ? 'Default for every project that does not have its own OpenCode override.' - : 'Override only the selected project. Running teams are not changed.'; + ? t('runtimeProvider.defaults.scopeDescriptionAllProjects') + : t('runtimeProvider.defaults.scopeDescriptionProject'); } -function getDefaultScopeButtonLabel(scope: RuntimeProviderDefaultScopeDto): string { - return scope === 'all_projects' ? 'Set all-projects default' : 'Set project default'; +function getDefaultScopeButtonLabel(scope: RuntimeProviderDefaultScopeDto, t: SettingsT): string { + return scope === 'all_projects' + ? t('runtimeProvider.defaults.setAllProjectsDefault') + : t('runtimeProvider.defaults.setProjectDefault'); } -function getContextControlLabel(scope: RuntimeProviderDefaultScopeDto): string { - return scope === 'all_projects' ? 'Validation context' : 'Project override context'; +function getContextControlLabel(scope: RuntimeProviderDefaultScopeDto, t: SettingsT): string { + return scope === 'all_projects' + ? t('runtimeProvider.defaults.validationContext') + : t('runtimeProvider.defaults.projectOverrideContext'); } function getContextControlHint( scope: RuntimeProviderDefaultScopeDto, - projectPath: string | null | undefined + projectPath: string | null | undefined, + t: SettingsT ): string { const projectName = getProjectContextName(projectPath) ?? projectPath?.trim(); if (!projectName) { - return 'Select a project before testing local models or saving defaults.'; + return t('runtimeProvider.defaults.selectProjectHint'); } return scope === 'all_projects' - ? `Tests use ${projectName}. Default applies unless a project has an override.` - : `Saving overrides only ${projectName}.`; + ? t('runtimeProvider.defaults.allProjectsHint', { project: projectName }) + : t('runtimeProvider.defaults.projectHint', { project: projectName }); } function getDefaultModelSourceLabel( @@ -345,6 +352,7 @@ function ProviderSetupFormPanel({ readonly disabled: boolean; readonly actions: RuntimeProviderManagementActions; }): JSX.Element { + const { t } = useAppTranslation('settings'); const form = state.setupForm?.providerId === provider.providerId ? state.setupForm : null; const loading = state.setupFormLoading && state.activeFormProviderId === provider.providerId; const error = state.setupFormError; @@ -364,7 +372,7 @@ function ProviderSetupFormPanel({ {loading ? (
    - Loading provider setup... + {t('runtimeProvider.setup.loading')}
    ) : null} @@ -477,7 +485,7 @@ function ProviderSetupFormPanel({ disabled={busy} onClick={actions.cancelConnect} > - Cancel + {t('runtimeProvider.actions.cancel')}

    {diagnostics ? (
    {diagnostics.likelyCause ? (
    - Likely cause: + + {t('runtimeProvider.diagnostics.likelyCause')}{' '} + {diagnostics.likelyCause}
    ) : null} @@ -855,7 +878,9 @@ const RuntimeProviderErrorAlert = ({ ) : null} {hints.length > 0 ? (
    -
    Hints
    +
    + {t('runtimeProvider.diagnostics.hints')} +
      {hints.map((hint, index) => (
    • {provider.displayName} - {provider.recommended ? Recommended : null} + {provider.recommended ? ( + {t('runtimeProvider.providers.recommended')} + ) : null} {provider.defaultModelId ? ( - OpenCode default: {provider.defaultModelId} + {t('runtimeProvider.summary.defaultModel', { model: provider.defaultModelId })} ) : null} {provider.ownership.map((owner) => ( @@ -1171,6 +1199,7 @@ function DirectoryProviderRow({ readonly hasProjectContext: boolean; readonly actions: RuntimeProviderManagementActions; }): JSX.Element { + const { t } = useAppTranslation('settings'); const connect = getDirectoryAction(provider, 'connect'); const configure = getDirectoryAction(provider, 'configure'); const forget = getDirectoryAction(provider, 'forget'); @@ -1227,7 +1256,9 @@ function DirectoryProviderRow({ {provider.displayName} - {provider.recommended ? Recommended : null} + {provider.recommended ? ( + {t('runtimeProvider.providers.recommended')} + ) : null} @@ -1338,6 +1369,7 @@ function ModelBadges({ readonly model: RuntimeProviderModelDto; readonly usedForNewTeams: boolean; }): JSX.Element | null { + const { t } = useAppTranslation('settings'); const modelRecommendation = getOpenCodeTeamModelRecommendation(model.modelId); const localRoute = model.routeKind === 'configured_local'; const connectedRoute = model.routeKind === 'connected_provider'; @@ -1403,39 +1435,53 @@ function ModelBadges({ {usedForNewTeams ? ( - Used in team picker + {t('runtimeProvider.badges.usedInTeamPicker')} ) : null} {freeModel ? ( - free + + {t('runtimeProvider.badges.free')} + ) : null} {localRoute ? ( <> - local - configured + + {t('runtimeProvider.badges.local')} + + + {t('runtimeProvider.badges.configured')} + ) : null} {connectedRoute ? ( - connected + {t('runtimeProvider.badges.connected')} ) : null} {verified ? ( - verified + {t('runtimeProvider.badges.verified')} ) : null} {needsTest && !verified ? ( - needs test + + {t('runtimeProvider.badges.needsTest')} + ) : null} {failed ? ( - failed + + {t('runtimeProvider.badges.failed')} + ) : null} {unknown ? ( - unknown + + {t('runtimeProvider.badges.unknown')} + ) : null} {model.default ? ( - default + + {t('runtimeProvider.badges.default')} + ) : null}
    ); @@ -1546,6 +1592,7 @@ function ModelRow({ readonly result: RuntimeProviderModelTestResultDto | undefined; readonly actions: RuntimeProviderManagementActions; }): JSX.Element { + const { t } = useAppTranslation('settings'); const chooseModel = (): void => { if (!disabled) { actions.useModelForNewTeams(model.modelId); @@ -1607,7 +1654,7 @@ function ModelRow({ className="h-8 min-w-20 justify-center" disabled={disabled || !hasProjectContext || testing} title={ - hasProjectContext ? undefined : 'Select a project context before testing models.' + hasProjectContext ? undefined : t('runtimeProvider.models.selectProjectBeforeTesting') } onClick={(event) => { event.stopPropagation(); @@ -1620,7 +1667,7 @@ function ModelRow({ ) : ( )} - Test + {t('runtimeProvider.actions.test')}
    @@ -1646,6 +1693,7 @@ function OpenCodeModelScopeControls({ readonly error: string | null; readonly onProjectContextChange?: (projectPath: string | null) => void; }): JSX.Element { + const { t } = useAppTranslation('settings'); const selectedValue = projectPath?.trim() || NO_PROJECT_CONTEXT_VALUE; const projectOptions = useMemo(() => { const seen = new Set(); @@ -1671,10 +1719,10 @@ function OpenCodeModelScopeControls({ return options; }, [projectPath, projects]); const contextPlaceholder = loading - ? 'Loading contexts...' + ? t('runtimeProvider.defaults.loadingContexts') : defaultScope === 'all_projects' - ? 'Select validation context' - : 'Select project context'; + ? t('runtimeProvider.defaults.selectValidationContext') + : t('runtimeProvider.defaults.selectProjectContext'); return (
    -
    OpenCode defaults
    +
    + {t('runtimeProvider.defaults.title')} +
    - {getDefaultScopeDescription(defaultScope)} + {getDefaultScopeDescription(defaultScope, t)}
    @@ -1703,7 +1753,9 @@ function OpenCodeModelScopeControls({ }`} onClick={() => onDefaultScopeChange(scope)} > - {scope === 'all_projects' ? 'All projects' : 'This project'} + {scope === 'all_projects' + ? t('runtimeProvider.defaults.allProjects') + : t('runtimeProvider.defaults.thisProject')} ))}
    @@ -1712,7 +1764,7 @@ function OpenCodeModelScopeControls({
    setQuery(event.target.value)} - placeholder="Search model routes" + placeholder={t('runtimeProvider.modelRoutes.searchPlaceholder')} className="h-9 pl-10 pr-3 text-sm leading-5" style={{ paddingLeft: 40 }} /> @@ -1813,7 +1865,7 @@ function ConfiguredOpenCodeModelsPanel({
    {visibleModels.length === 0 ? (
    - No OpenCode model routes match “{query.trim()}”. + {t('runtimeProvider.models.noRoutesMatch', { query: query.trim() })}
    ) : null} {visibleModels.map((model) => { @@ -1824,7 +1876,7 @@ function ConfiguredOpenCodeModelsPanel({ const unavailableTitle = getOpenCodeRouteUnavailableTitle(model); const contextRequiredTitle = hasProjectContext ? undefined - : 'Select a project context before testing or saving OpenCode defaults.'; + : t('runtimeProvider.models.selectProjectBeforeTestingDefaults'); const alreadyDefaultForScope = isDefaultForScope(model, state, defaultScope); const canTest = !disabled && hasProjectContext && !testing && canTestOpenCodeModelRoute(model); @@ -1877,7 +1929,7 @@ function ConfiguredOpenCodeModelsPanel({ ) : ( )} - Test + {t('runtimeProvider.actions.test')}
    @@ -1939,6 +1991,7 @@ function ProviderModelList({ readonly disabled: boolean; readonly hasProjectContext: boolean; }): JSX.Element { + const { t } = useAppTranslation('settings'); const pickerOpen = state.modelPickerProviderId === provider.providerId; const [recommendedOnly, setRecommendedOnly] = useState(false); const [freeOnly, setFreeOnly] = useState(false); @@ -1981,11 +2034,11 @@ function ProviderModelList({ ); const emptyModelListMessage = recommendedOnly ? freeOnly - ? 'No recommended free models found.' - : 'No recommended models found.' + ? t('runtimeProvider.models.emptyRecommendedFree') + : t('runtimeProvider.models.emptyRecommended') : freeOnly - ? 'No free models found.' - : 'No models found.'; + ? t('runtimeProvider.models.emptyFree') + : t('runtimeProvider.models.empty'); return (
    @@ -1999,7 +2052,7 @@ function ProviderModelList({ onChange={(event) => actions.setModelQuery(event.target.value)} onClick={(event) => event.stopPropagation()} onKeyDown={(event) => event.stopPropagation()} - placeholder="Search models" + placeholder={t('runtimeProvider.models.searchPlaceholder')} className="h-10 pl-10 pr-3 text-sm leading-5" style={{ paddingLeft: 42 }} /> @@ -2021,7 +2074,7 @@ function ProviderModelList({ htmlFor={`runtime-provider-${provider.providerId}-recommended-only`} className="cursor-pointer text-xs font-normal text-[var(--color-text-secondary)]" > - Recommended only + {t('runtimeProvider.models.recommendedOnly')}
    ) : null} @@ -2042,7 +2095,7 @@ function ProviderModelList({ htmlFor={`runtime-provider-${provider.providerId}-free-only`} className="cursor-pointer text-xs font-normal text-[var(--color-text-secondary)]" > - Free only + {t('runtimeProvider.models.freeOnly')}
    ) : null} @@ -2095,6 +2148,7 @@ export function RuntimeProviderManagementPanelView({ projectContextError = null, onProjectContextChange, }: RuntimeProviderManagementPanelViewProps): JSX.Element { + const { t } = useAppTranslation('settings'); const [selectedSection, setSelectedSection] = useState(null); const [defaultScope, setDefaultScope] = useState('all_projects'); const providerQuery = state.providerQuery.trim().toLowerCase(); @@ -2123,8 +2177,8 @@ export function RuntimeProviderManagementPanelView({ state.directoryTotalCount !== null ? formatOpenCodeProviderCount(state.directoryTotalCount) : state.directorySupported - ? 'OpenCode provider catalog' - : 'OpenCode providers'; + ? t('runtimeProvider.providers.catalog') + : t('runtimeProvider.providers.countFallback'); const launchableModelCount = state.view?.configuredModels?.length ?? 0; const modelsLoading = state.loading && launchableModelCount === 0; const activeSection = @@ -2167,7 +2221,7 @@ export function RuntimeProviderManagementPanelView({ value="models" className="rounded-b-none data-[state=active]:bg-[var(--color-surface)]" > - Models + {t('runtimeProvider.tabs.models')} {launchableModelCount > 0 ? ( {launchableModelCount} @@ -2178,7 +2232,7 @@ export function RuntimeProviderManagementPanelView({ value="providers" className="rounded-b-none data-[state=active]:bg-[var(--color-surface)]" > - Providers + {t('runtimeProvider.tabs.providers')} {state.directoryTotalCount !== null ? ( {state.directoryTotalCount} @@ -2215,15 +2269,14 @@ export function RuntimeProviderManagementPanelView({ >
    - Loading OpenCode model routes... + {t('runtimeProvider.models.loadingRoutes')}
    ) : null} {!modelsLoading && launchableModelCount === 0 ? (
    - No launchable OpenCode model routes were reported yet. Configure a local route in - OpenCode or use the Providers tab to inspect catalog providers. + {t('runtimeProvider.models.noneReported')}
    ) : null} @@ -2231,9 +2284,11 @@ export function RuntimeProviderManagementPanelView({
    -
    Providers
    +
    + {t('runtimeProvider.tabs.providers')} +
    - {providerCountLabel}. Connected and recommended providers are shown first. + {t('runtimeProvider.providers.description', { count: providerCountLabel })}
    {state.directorySupported ? ( @@ -2249,7 +2304,7 @@ export function RuntimeProviderManagementPanelView({ ) : ( )} - Refresh catalog + {t('runtimeProvider.providers.refreshCatalog')} ) : null}
    @@ -2267,7 +2322,7 @@ export function RuntimeProviderManagementPanelView({ actions.searchAllProviders(state.providerQuery.trim()); } }} - placeholder="Search providers" + placeholder={t('runtimeProvider.providers.searchPlaceholder')} className="h-9 pr-3 text-sm" style={{ paddingLeft: 40 }} /> @@ -2313,7 +2368,7 @@ export function RuntimeProviderManagementPanelView({ {state.directoryRefreshing ? ( ) : null} - Load more providers + {t('runtimeProvider.providers.loadMore')}
    ) : null} @@ -2351,7 +2406,7 @@ export function RuntimeProviderManagementPanelView({ color: 'var(--color-text-secondary)', }} > - No providers match that search. + {t('runtimeProvider.providers.noMatches')}
    ) : null} @@ -2366,7 +2421,7 @@ export function RuntimeProviderManagementPanelView({ color: 'var(--color-text-secondary)', }} > - No providers match that search. + {t('runtimeProvider.providers.noMatches')} ) : null} @@ -2378,7 +2433,7 @@ export function RuntimeProviderManagementPanelView({ color: 'var(--color-text-secondary)', }} > - No OpenCode providers reported by the managed runtime. + {t('runtimeProvider.providers.noneReported')} ) : null} diff --git a/src/features/tmux-installer/renderer/ui/TmuxInstallerBannerView.tsx b/src/features/tmux-installer/renderer/ui/TmuxInstallerBannerView.tsx index 4a57e3ae..e2f4309d 100644 --- a/src/features/tmux-installer/renderer/ui/TmuxInstallerBannerView.tsx +++ b/src/features/tmux-installer/renderer/ui/TmuxInstallerBannerView.tsx @@ -1,5 +1,6 @@ import React from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { AlertTriangle, ChevronDown, @@ -12,7 +13,6 @@ import { import { useTmuxInstallerBanner } from '../hooks/useTmuxInstallerBanner'; -const SUMMARY_TITLE = 'tmux is not installed'; const BANNER_MIN_H = 'min-h-[4.25rem]'; const SourceLink = ({ @@ -36,6 +36,7 @@ const SourceLink = ({ ); export function TmuxInstallerBannerView(): React.JSX.Element | null { + const { t } = useAppTranslation('common'); const { viewModel, install, cancel, submitInput, refresh, toggleDetails, openExternal } = useTmuxInstallerBanner(); const [expanded, setExpanded] = React.useState(false); @@ -78,6 +79,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null { viewModel.manualHints.length > 0 && (!viewModel.manualHintsCollapsible || manualHintsExpanded); const primaryGuideUrl = viewModel.primaryGuideUrl; const bannerPaddingClass = expanded ? `py-3 ${BANNER_MIN_H}` : 'py-2.5'; + const summaryTitle = t('tmuxInstaller.summaryTitle'); return (
    - {SUMMARY_TITLE} + {summaryTitle} {!expanded && viewModel.benefitsBody && (
    - {viewModel.title !== SUMMARY_TITLE && ( + {viewModel.title !== summaryTitle && (
    - Detected OS: {viewModel.platformLabel} + {t('tmuxInstaller.detectedOs', { os: viewModel.platformLabel })} )} {viewModel.locationLabel && ( @@ -187,7 +189,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null { backgroundColor: 'rgba(255, 255, 255, 0.04)', }} > - Runtime path: {viewModel.locationLabel} + {t('tmuxInstaller.runtimePath', { path: viewModel.locationLabel })} )} {viewModel.runtimeReadyLabel && ( @@ -220,7 +222,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null { backgroundColor: 'rgba(255, 255, 255, 0.04)', }} > - Phase: {viewModel.phase} + {t('tmuxInstaller.phase', { phase: viewModel.phase })} )}
    @@ -258,7 +260,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null { style={{ borderColor: 'var(--color-border)' }} > - Cancel + {t('tmuxInstaller.actions.cancel')} )} {primaryGuideUrl && ( @@ -269,7 +271,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null { style={{ borderColor: 'var(--color-border)' }} > - Manual guide + {t('tmuxInstaller.actions.manualGuide')} )} {viewModel.manualHintsCollapsible && ( @@ -285,8 +287,10 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null { )} {manualHintsExpanded - ? 'Hide setup steps' - : `Show setup steps (${viewModel.manualHints.length})`} + ? t('tmuxInstaller.actions.hideSetupSteps') + : t('tmuxInstaller.actions.showSetupSteps', { + count: viewModel.manualHints.length, + })} )} {viewModel.showRefreshButton && ( @@ -297,7 +301,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null { style={{ borderColor: 'var(--color-border)' }} > - Re-check + {t('tmuxInstaller.actions.recheck')} )}
    @@ -305,7 +309,9 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null { {viewModel.progressPercent !== null && (
    - Installer progress + + {t('tmuxInstaller.installerProgress')} + {viewModel.progressPercent}% @@ -343,7 +349,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null { type={viewModel.inputSecret ? 'password' : 'text'} value={inputValue} onChange={(event) => setInputValue(event.target.value)} - placeholder={viewModel.inputPrompt ?? 'Send input to the installer'} + placeholder={viewModel.inputPrompt ?? t('tmuxInstaller.input.placeholder')} className="min-w-0 flex-1 rounded-md border px-3 py-2 text-sm" style={{ borderColor: 'var(--color-border)', @@ -357,13 +363,12 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null { className="inline-flex items-center justify-center rounded-md border px-3 py-2 text-sm transition-colors hover:bg-white/5 disabled:cursor-not-allowed disabled:opacity-60" style={{ borderColor: 'var(--color-border)' }} > - Send input + {t('tmuxInstaller.input.send')} {viewModel.inputSecret && (
    - Password input is sent directly to the installer terminal and is not added to the - log output. + {t('tmuxInstaller.input.passwordNotice')}
    )}
    @@ -409,7 +414,9 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null { className="text-xs underline-offset-4 hover:underline" style={{ color: 'var(--color-text-secondary)' }} > - {viewModel.detailsOpen ? 'Hide details' : 'Show details'} + {viewModel.detailsOpen + ? t('tmuxInstaller.details.hide') + : t('tmuxInstaller.details.show')} {viewModel.detailsOpen && (
     | V
         'multimodelEnabled',
         'claudeRootPath',
         'agentLanguage',
    +    'appLocale',
         'autoExpandAIGroups',
         'useNativeTitleBar',
         'telemetryEnabled',
    @@ -407,6 +409,12 @@ function validateGeneralSection(data: unknown): ValidationSuccess<'general'> | V
             }
             result.agentLanguage = value.trim();
             break;
    +      case 'appLocale':
    +        if (!isAppLocalePreference(value)) {
    +          return { valid: false, error: 'general.appLocale must be a supported app locale' };
    +        }
    +        result.appLocale = value;
    +        break;
           case 'autoExpandAIGroups':
             if (typeof value !== 'boolean') {
               return { valid: false, error: `general.${key} must be a boolean` };
    diff --git a/src/main/services/infrastructure/ConfigManager.ts b/src/main/services/infrastructure/ConfigManager.ts
    index a1d8972d..dba96c79 100644
    --- a/src/main/services/infrastructure/ConfigManager.ts
    +++ b/src/main/services/infrastructure/ConfigManager.ts
    @@ -9,6 +9,7 @@
      * - Handle JSON parse errors gracefully
      */
     
    +import { normalizeAppLocalePreference } from '@features/localization';
     import { getClaudeBasePath, setClaudeBasePathOverride } from '@main/utils/pathDecoder';
     import { validateRegexPattern } from '@main/utils/regexValidation';
     import { createLogger } from '@shared/utils/logger';
    @@ -258,6 +259,7 @@ export interface GeneralConfig {
       multimodelEnabled: boolean;
       claudeRootPath: string | null;
       agentLanguage: string;
    +  appLocale: string;
       autoExpandAIGroups: boolean;
       useNativeTitleBar: boolean;
       /** Paths manually added via "Select Folder" that persist across app restarts */
    @@ -373,6 +375,7 @@ const DEFAULT_CONFIG: AppConfig = {
         multimodelEnabled: true,
         claudeRootPath: null,
         agentLanguage: 'system',
    +    appLocale: 'system',
         autoExpandAIGroups: false,
         useNativeTitleBar: false,
         customProjectPaths: [],
    @@ -598,6 +601,7 @@ export class ConfigManager {
         };
         mergedGeneral.multimodelEnabled = true;
         mergedGeneral.claudeRootPath = normalizeConfiguredClaudeRootPath(mergedGeneral.claudeRootPath);
    +    mergedGeneral.appLocale = normalizeAppLocalePreference(mergedGeneral.appLocale);
     
         // Merge triggers: preserve existing triggers, add missing builtin ones
         const mergedTriggers = TriggerManager.mergeTriggers(loadedTriggers, DEFAULT_TRIGGERS);
    diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx
    index 52bd7695..a3089b46 100644
    --- a/src/renderer/App.tsx
    +++ b/src/renderer/App.tsx
    @@ -1,5 +1,6 @@
     import React, { useEffect } from 'react';
     
    +import { LocalizationProvider } from '@features/localization/renderer';
     import { TooltipProvider } from '@renderer/components/ui/tooltip';
     
     import { ConfirmDialog } from './components/common/ConfirmDialog';
    @@ -33,6 +34,7 @@ const SPLASH_REDUCED_AVATAR_READY_MAX_WAIT_MS = 160;
     export const App = (): React.JSX.Element => {
       // Initialize theme on app load
       useTheme();
    +  const appConfig = useStore((s) => s.appConfig);
     
       // Upgrade the static preload splash, then dismiss it after the scene is visible.
       useEffect(() => {
    @@ -104,13 +106,15 @@ export const App = (): React.JSX.Element => {
       }, []);
     
       return (
    -    
    -      
    -        
    -        
    -        
    -        
    -      
    -    
    +    
    +      
    +        
    +          
    +          
    +          
    +          
    +        
    +      
    +    
       );
     };
    diff --git a/src/renderer/components/chat/AIChatGroup.tsx b/src/renderer/components/chat/AIChatGroup.tsx
    index 25209ee3..1bd51996 100644
    --- a/src/renderer/components/chat/AIChatGroup.tsx
    +++ b/src/renderer/components/chat/AIChatGroup.tsx
    @@ -1,5 +1,6 @@
     import React, { useCallback, useEffect, useMemo, useRef } from 'react';
     
    +import { useAppTranslation } from '@features/localization/renderer';
     import { COLOR_TEXT_MUTED, COLOR_TEXT_SECONDARY } from '@renderer/constants/cssVariables';
     import { useTabUI } from '@renderer/hooks/useTabUI';
     import { useStore } from '@renderer/store';
    @@ -125,6 +126,7 @@ const AIChatGroupInner = ({
       highlightColor,
       registerToolRef,
     }: Readonly): React.JSX.Element => {
    +  const { t } = useAppTranslation('common');
       // Per-tab UI state for expansion (completely isolated per tab)
       const {
         tabId,
    @@ -396,7 +398,7 @@ const AIChatGroupInner = ({
                   className="shrink-0 text-xs font-semibold"
                   style={{ color: COLOR_TEXT_SECONDARY }}
                 >
    -              Claude
    +              {t('brand.claude')}
                 
     
                 {/* Main agent model */}
    diff --git a/src/renderer/components/chat/ChatHistory.tsx b/src/renderer/components/chat/ChatHistory.tsx
    index ea7d0e5e..71e6b531 100644
    --- a/src/renderer/components/chat/ChatHistory.tsx
    +++ b/src/renderer/components/chat/ChatHistory.tsx
    @@ -1,5 +1,6 @@
     import { type JSX, useCallback, useEffect, useMemo, useRef, useState } from 'react';
     
    +import { useAppTranslation } from '@features/localization/renderer';
     import { isNearBottom, useAutoScrollBottom } from '@renderer/hooks/useAutoScrollBottom';
     import { useTabNavigationController } from '@renderer/hooks/useTabNavigationController';
     import { useTabUI } from '@renderer/hooks/useTabUI';
    @@ -39,6 +40,7 @@ interface ChatHistoryProps {
     }
     
     export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => {
    +  const { t } = useAppTranslation('common');
       const VIRTUALIZATION_THRESHOLD = 120;
       const ESTIMATED_CHAT_ITEM_HEIGHT = 260;
     
    @@ -914,12 +916,14 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => {
                             }}
                           >
                             {' '}
    -                        ({remainingContext.remainingPct.toFixed(0)}% left)
    +                        {t('chat.context.remainingPercent', {
    +                          percent: remainingContext.remainingPct.toFixed(0),
    +                        })}
                           
                         )}
                       
                     ) : (
    -                  `Context (${allContextInjections.length})`
    +                  t('chat.context.count', { count: allContextInjections.length })
                     )}
                   
                 
    @@ -1031,10 +1035,10 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => { color: 'var(--color-text-secondary)', border: '1px solid var(--color-border-emphasis)', }} - title="Scroll to bottom" + title={t('chat.scrollToBottom')} > - Bottom + {t('chat.bottom')} )}
    diff --git a/src/renderer/components/chat/ChatHistoryEmptyState.tsx b/src/renderer/components/chat/ChatHistoryEmptyState.tsx index 82e959f0..3a3c4810 100644 --- a/src/renderer/components/chat/ChatHistoryEmptyState.tsx +++ b/src/renderer/components/chat/ChatHistoryEmptyState.tsx @@ -1,14 +1,19 @@ import type { JSX } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; + /** * Empty state for ChatHistory when no conversation exists. */ export const ChatHistoryEmptyState = (): JSX.Element => { + const { t } = useAppTranslation('common'); return (
    -
    💬
    -
    No conversation history
    -
    This session does not contain any messages yet.
    + +
    {t('chat.empty.title')}
    +
    {t('chat.empty.description')}
    ); diff --git a/src/renderer/components/chat/CompactBoundary.tsx b/src/renderer/components/chat/CompactBoundary.tsx index 88f44216..0a195ff2 100644 --- a/src/renderer/components/chat/CompactBoundary.tsx +++ b/src/renderer/components/chat/CompactBoundary.tsx @@ -1,6 +1,7 @@ import React, { memo, useState } from 'react'; import ReactMarkdown from 'react-markdown'; +import { useAppTranslation } from '@features/localization/renderer'; import { CODE_BG, CODE_BORDER, @@ -31,6 +32,7 @@ interface CompactBoundaryProps { export const CompactBoundary = memo(function CompactBoundary({ compactGroup, }: Readonly): React.JSX.Element { + const { t } = useAppTranslation('common'); const { timestamp, message } = compactGroup; const [isExpanded, setIsExpanded] = useState(false); @@ -62,7 +64,7 @@ export const CompactBoundary = memo(function CompactBoundary({ onClick={() => setIsExpanded(!isExpanded)} className="group flex w-full cursor-pointer items-center transition-opacity hover:opacity-90" aria-expanded={isExpanded} - aria-label="Toggle compacted content" + aria-label={t('chat.compact.toggle')} > {/* Left line */}
    @@ -82,7 +84,7 @@ export const CompactBoundary = memo(function CompactBoundary({ className="whitespace-nowrap text-[11px] font-medium" style={{ color: TOOL_CALL_TEXT }} > - Context compacted + {t('chat.compact.contextCompacted')} {/* Token delta */} @@ -95,7 +97,9 @@ export const CompactBoundary = memo(function CompactBoundary({ {formatTokens(compactGroup.tokenDelta.postCompactionTokens)} {' '} - ({formatTokens(Math.abs(compactGroup.tokenDelta.delta))} freed) + {t('chat.compact.freedTokens', { + tokens: formatTokens(Math.abs(compactGroup.tokenDelta.delta)), + })} )} @@ -109,7 +113,7 @@ export const CompactBoundary = memo(function CompactBoundary({ color: 'var(--compact-phase-text)', }} > - Phase {compactGroup.startingPhaseNumber} + {t('chat.compact.phase', { phase: compactGroup.startingPhaseNumber })} )} @@ -152,12 +156,9 @@ export const CompactBoundary = memo(function CompactBoundary({

    - Conversation Compacted -

    -

    - Previous messages were summarized to save context. The full conversation history - is preserved in the session file. + {t('chat.compact.conversationCompacted')}

    +

    {t('chat.compact.summary')}

    )} diff --git a/src/renderer/components/chat/ContextBadge.tsx b/src/renderer/components/chat/ContextBadge.tsx index 2b4d5991..66e36c49 100644 --- a/src/renderer/components/chat/ContextBadge.tsx +++ b/src/renderer/components/chat/ContextBadge.tsx @@ -7,6 +7,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; +import { useAppTranslation } from '@features/localization/renderer'; import { COLOR_BORDER, COLOR_BORDER_SUBTLE, @@ -95,6 +96,7 @@ const PopoverSection = ({ children: React.ReactNode; defaultExpanded?: boolean; }>): React.ReactElement => { + const { t } = useAppTranslation('common'); const [expanded, setExpanded] = useState(defaultExpanded); return ( @@ -121,7 +123,11 @@ const PopoverSection = ({ className={`size-3 shrink-0 transition-transform ${expanded ? 'rotate-90' : ''}`} /> - {title} ({count}) ~{formatTokens(tokenCount)} tokens + {t('contextBadge.sectionSummary', { + title, + count, + tokens: formatTokens(tokenCount), + })} {/* Section content */} @@ -134,6 +140,7 @@ export const ContextBadge = ({ stats, projectRoot, }: Readonly): React.ReactElement | null => { + const { t } = useAppTranslation('common'); const [showPopover, setShowPopover] = useState(false); const [popoverStyle, setPopoverStyle] = useState({}); const [arrowStyle, setArrowStyle] = useState({}); @@ -361,7 +368,7 @@ export const ContextBadge = ({ className="inline-flex cursor-pointer items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium" style={badgeStyle} > - Context + {t('contextBadge.badge')} +{totalNew} @@ -373,7 +380,7 @@ export const ContextBadge = ({ ref={popoverRef} role="dialog" aria-modal="false" - aria-label="Context injection details" + aria-label={t('contextBadge.detailsAria')} className="rounded-lg p-3 shadow-xl" style={{ ...popoverStyle, @@ -395,7 +402,7 @@ export const ContextBadge = ({ borderBottom: `1px solid ${COLOR_BORDER_SUBTLE}`, }} > - New Context Injected In This Turn + {t('contextBadge.title')} {/* Sections */} @@ -403,7 +410,7 @@ export const ContextBadge = ({ {/* User Messages section */} {newUserMessageInjections.length > 0 && ( @@ -411,10 +418,12 @@ export const ContextBadge = ({
    - Turn {injection.turnIndex + 1} + {t('contextBadge.turn', { turn: injection.turnIndex + 1 })} - ~{formatTokens(injection.estimatedTokens)} tokens + {t('contextBadge.tokenCount', { + tokens: formatTokens(injection.estimatedTokens), + })}
    {injection.textPreview && ( @@ -433,7 +442,7 @@ export const ContextBadge = ({ {/* CLAUDE.md Files section */} {newClaudeMdInjections.length > 0 && ( @@ -450,7 +459,9 @@ export const ContextBadge = ({ style={{ color: COLOR_TEXT_SECONDARY }} />
    - ~{formatTokens(injection.estimatedTokens)} tokens + {t('contextBadge.tokenCount', { + tokens: formatTokens(injection.estimatedTokens), + })}
    ); @@ -461,7 +472,7 @@ export const ContextBadge = ({ {/* Mentioned Files section */} {newMentionedFileInjections.length > 0 && ( @@ -477,7 +488,9 @@ export const ContextBadge = ({ style={{ color: COLOR_TEXT_SECONDARY }} />
    - ~{formatTokens(injection.estimatedTokens)} tokens + {t('contextBadge.tokenCount', { + tokens: formatTokens(injection.estimatedTokens), + })}
    ); @@ -488,7 +501,7 @@ export const ContextBadge = ({ {/* Tool Outputs section */} {newToolOutputInjections.length > 0 && ( @@ -500,7 +513,9 @@ export const ContextBadge = ({ > {tool.toolName} - ~{formatTokens(tool.tokenCount)} tokens + {t('contextBadge.tokenCount', { + tokens: formatTokens(tool.tokenCount), + })} )) @@ -511,7 +526,7 @@ export const ContextBadge = ({ {/* Task Coordination section */} {newTaskCoordinationInjections.length > 0 && ( @@ -523,7 +538,9 @@ export const ContextBadge = ({ > {item.label} - ~{formatTokens(item.tokenCount)} tokens + {t('contextBadge.tokenCount', { + tokens: formatTokens(item.tokenCount), + })} )) @@ -534,14 +551,14 @@ export const ContextBadge = ({ {/* Thinking + Text section */} {newThinkingTextInjections.length > 0 && ( {newThinkingTextInjections.map((injection) => (
    - Turn {injection.turnIndex + 1} + {t('contextBadge.turn', { turn: injection.turnIndex + 1 })}
    {injection.breakdown.map((item, idx) => ( @@ -550,10 +567,14 @@ export const ContextBadge = ({ className="flex items-center justify-between text-xs" > - {item.type === 'thinking' ? 'Thinking' : 'Text'} + {item.type === 'thinking' + ? t('contextBadge.breakdown.thinking') + : t('contextBadge.breakdown.text')} - ~{formatTokens(item.tokenCount)} tokens + {t('contextBadge.tokenCount', { + tokens: formatTokens(item.tokenCount), + })}
    ))} @@ -569,9 +590,9 @@ export const ContextBadge = ({ className="mt-2 flex items-center justify-between pt-2 text-xs" style={{ borderTop: `1px solid ${COLOR_BORDER_SUBTLE}` }} > - Total new tokens + {t('contextBadge.totalNewTokens')} - ~{formatTokens(totalNewTokens)} tokens + {t('contextBadge.tokenCount', { tokens: formatTokens(totalNewTokens) })}
    , diff --git a/src/renderer/components/chat/DisplayItemList.tsx b/src/renderer/components/chat/DisplayItemList.tsx index 8ae4e8c9..dc230401 100644 --- a/src/renderer/components/chat/DisplayItemList.tsx +++ b/src/renderer/components/chat/DisplayItemList.tsx @@ -1,5 +1,6 @@ import React, { memo, useCallback, useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { CODE_BG, CODE_BORDER, @@ -138,6 +139,7 @@ const DisplayItemRow = memo(function DisplayItemRow({ timestampFormat, showItemMetaTooltip = false, }: DisplayItemRowProps): React.JSX.Element | null { + const { t } = useAppTranslation('common'); const handleClick = useCallback(() => onItemClick(itemKey), [onItemClick, itemKey]); let element: React.ReactNode = null; @@ -343,7 +345,7 @@ const DisplayItemRow = memo(function DisplayItemRow({ - Compacted + {t('chat.compact.compacted')} {item.tokenDelta && ( {' '} - ({formatTokensCompact(Math.abs(item.tokenDelta.delta))} freed) + {t('chat.compact.freedTokens', { + tokens: formatTokensCompact(Math.abs(item.tokenDelta.delta)), + })} )} @@ -365,7 +369,7 @@ const DisplayItemRow = memo(function DisplayItemRow({ color: '#818cf8', }} > - Phase {item.phaseNumber} + {t('chat.compact.phase', { phase: item.phaseNumber })} {format(new Date(item.timestamp), 'h:mm:ss a')} @@ -438,6 +442,7 @@ export const DisplayItemList = React.memo(function DisplayItemList({ timestampFormat, showItemMetaTooltip = false, }: Readonly): React.JSX.Element { + const { t } = useAppTranslation('common'); const [replyLinkToolId, setReplyLinkToolId] = useState(null); const handleReplyHover = useCallback((toolId: string | null) => { @@ -447,7 +452,7 @@ export const DisplayItemList = React.memo(function DisplayItemList({ if (!items || items.length === 0) { return (
    - No items to display + {t('chat.items.empty')}
    ); } diff --git a/src/renderer/components/chat/LastOutputDisplay.tsx b/src/renderer/components/chat/LastOutputDisplay.tsx index 3df2c96b..5074cfe9 100644 --- a/src/renderer/components/chat/LastOutputDisplay.tsx +++ b/src/renderer/components/chat/LastOutputDisplay.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactMarkdown from 'react-markdown'; +import { useAppTranslation } from '@features/localization/renderer'; import { useStore } from '@renderer/store'; import { REHYPE_PLUGINS } from '@renderer/utils/markdownPlugins'; import { AlertTriangle, CheckCircle, FileCheck, XCircle } from 'lucide-react'; @@ -41,6 +42,7 @@ export const LastOutputDisplay = ({ isLastGroup = false, isSessionOngoing = false, }: Readonly): React.JSX.Element | null => { + const { t } = useAppTranslation('common'); // Only re-render if THIS AI group has search matches const { searchQuery, searchMatches, currentSearchIndex } = useStore( useShallow((s) => { @@ -152,7 +154,7 @@ export const LastOutputDisplay = ({ className="text-xs font-medium" style={{ color: 'var(--tool-result-error-text)' }} > - Error + {t('states.error')}
    )} @@ -185,7 +187,7 @@ export const LastOutputDisplay = ({ style={{ color: 'var(--warning-text, #f59e0b)' }} /> - Request interrupted by user + {t('chat.lastOutput.requestInterrupted')} ); @@ -234,7 +236,7 @@ export const LastOutputDisplay = ({
    - Plan Ready for Approval + {t('chat.lastOutput.planReadyForApproval')}
    diff --git a/src/renderer/components/chat/SessionContextPanel/DirectoryTree/DirectoryTreeNode.tsx b/src/renderer/components/chat/SessionContextPanel/DirectoryTree/DirectoryTreeNode.tsx index 28f06a05..5f94f08a 100644 --- a/src/renderer/components/chat/SessionContextPanel/DirectoryTree/DirectoryTreeNode.tsx +++ b/src/renderer/components/chat/SessionContextPanel/DirectoryTree/DirectoryTreeNode.tsx @@ -4,6 +4,7 @@ import React, { useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { CopyablePath } from '@renderer/components/common/CopyablePath'; import { COLOR_TEXT_MUTED, COLOR_TEXT_SECONDARY } from '@renderer/constants/cssVariables'; import { ChevronRight } from 'lucide-react'; @@ -24,6 +25,7 @@ export const DirectoryTreeNode = ({ depth = 0, onNavigateToTurn, }: Readonly): React.ReactElement | null => { + const { t } = useAppTranslation('common'); const [expanded, setExpanded] = useState(true); const indent = depth * 12; @@ -48,7 +50,9 @@ export const DirectoryTreeNode = ({ className="text-xs" style={{ color: COLOR_TEXT_SECONDARY }} /> - (~{formatTokens(node.tokens ?? 0)}) + + {t('tokens.approxTokensParenthesized', { tokens: formatTokens(node.tokens ?? 0) })} + {node.firstSeenInGroup && (isClickable ? ( diff --git a/src/renderer/components/chat/SessionContextPanel/components/FlatInjectionList.tsx b/src/renderer/components/chat/SessionContextPanel/components/FlatInjectionList.tsx index 107c9c3b..9a849435 100644 --- a/src/renderer/components/chat/SessionContextPanel/components/FlatInjectionList.tsx +++ b/src/renderer/components/chat/SessionContextPanel/components/FlatInjectionList.tsx @@ -6,6 +6,7 @@ import React, { useMemo } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { CopyButton } from '@renderer/components/common/CopyButton'; import { COLOR_TEXT_MUTED, COLOR_TEXT_SECONDARY } from '@renderer/constants/cssVariables'; @@ -169,6 +170,7 @@ export const FlatInjectionList = ({ onNavigateToTool, onNavigateToUserGroup, }: Readonly): React.ReactElement => { + const { t } = useAppTranslation('common'); const rows = useMemo(() => flattenInjections(injections), [injections]); return ( @@ -223,7 +225,7 @@ export const FlatInjectionList = ({ fontSize: '10px', }} > - error + {t('states.error')} )} {/* Token count */} diff --git a/src/renderer/components/chat/SessionContextPanel/components/MentionedFilesSection.tsx b/src/renderer/components/chat/SessionContextPanel/components/MentionedFilesSection.tsx index 75dd31a1..b497c71c 100644 --- a/src/renderer/components/chat/SessionContextPanel/components/MentionedFilesSection.tsx +++ b/src/renderer/components/chat/SessionContextPanel/components/MentionedFilesSection.tsx @@ -4,6 +4,7 @@ import React from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { MentionedFileItem } from '../items/MentionedFileItem'; import { CollapsibleSection } from './CollapsibleSection'; @@ -27,11 +28,13 @@ export const MentionedFilesSection = ({ projectRoot, onNavigateToTurn, }: Readonly): React.ReactElement | null => { + const { t } = useAppTranslation('common'); + if (injections.length === 0) return null; return ( void; onNavigateToTool?: (turnIndex: number, toolUseId: string) => void; }>): React.ReactElement => { + const { t } = useAppTranslation('common'); const [expanded, setExpanded] = useState(false); const hasBreakdown = injection.toolBreakdown.length > 0; const categoryInfo = CATEGORY_COLORS['tool-output']; @@ -183,7 +185,7 @@ const ToolOutputRankedItem = ({ fontSize: '10px', }} > - error + {t('states.error')} )} diff --git a/src/renderer/components/chat/SessionContextPanel/components/SessionContextHeader.tsx b/src/renderer/components/chat/SessionContextPanel/components/SessionContextHeader.tsx index b23b085e..a002cf1d 100644 --- a/src/renderer/components/chat/SessionContextPanel/components/SessionContextHeader.tsx +++ b/src/renderer/components/chat/SessionContextPanel/components/SessionContextHeader.tsx @@ -4,6 +4,7 @@ import React from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { COLOR_BORDER, COLOR_BORDER_SUBTLE, @@ -53,6 +54,8 @@ export const SessionContextHeader = ({ viewMode, onViewModeChange, }: Readonly): React.ReactElement => { + const { t } = useAppTranslation('common'); + const formatPercentLabel = (percent: number | null, suffix: string): string | null => { if (percent === null) { return null; @@ -77,7 +80,7 @@ export const SessionContextHeader = ({
    {tokens === null - ? (options?.unavailableLabel ?? 'Unavailable') + ? (options?.unavailableLabel ?? t('sessionContext.metrics.unavailable')) : `${options?.approximate ? '~' : ''}${formatTokens(tokens)}`}
    {percentLabel && ( @@ -99,7 +102,7 @@ export const SessionContextHeader = ({

    - Context + {t('sessionContext.header.title')}

    @@ -132,27 +135,27 @@ export const SessionContextHeader = ({ style={{ borderTop: `1px solid ${COLOR_BORDER_SUBTLE}` }} > {renderMetricValue( - 'Context Used', + t('sessionContext.metrics.contextUsed'), contextMetrics?.contextUsedTokens ?? null, formatPercentLabel( contextMetrics?.contextUsedPercentOfContextWindow ?? null, - 'of context' + t('sessionContext.metrics.ofContext') ) )} {renderMetricValue( - 'Prompt Input', + t('sessionContext.metrics.promptInput'), contextMetrics?.promptInputTokens ?? null, formatPercentLabel( contextMetrics?.promptInputPercentOfContextWindow ?? null, - 'of context' + t('sessionContext.metrics.ofContext') ) )} {renderMetricValue( - 'Visible Context', + t('sessionContext.metrics.visibleContext'), totalTokens, formatPercentLabel( contextMetrics?.visibleContextPercentOfPromptInput ?? null, - 'of prompt' + t('sessionContext.metrics.ofPrompt') ), { approximate: true } )} @@ -166,8 +169,7 @@ export const SessionContextHeader = ({ color: COLOR_TEXT_MUTED, }} > - Codex prompt-side usage is not exposed by the current runtime telemetry yet, so Prompt - Input and Context Used stay unavailable instead of showing a fake zero. + {t('sessionContext.metrics.codexTelemetryUnavailable')}
    )} @@ -180,7 +182,9 @@ export const SessionContextHeader = ({ {/* Cost */} {sessionMetrics.costUsd !== undefined && sessionMetrics.costUsd > 0 && (
    - Session Cost: + + {t('sessionContext.metrics.sessionCost')}{' '} + {formatCostUsd(sessionMetrics.costUsd + (subagentCostUsd ?? 0))} @@ -188,9 +192,9 @@ export const SessionContextHeader = ({ {' ('} {formatCostUsd(sessionMetrics.costUsd)} - {' parent + '} + {` ${t('sessionContext.metrics.parentPlus')} `} {formatCostUsd(subagentCostUsd)} - {' subagents'} + {` ${t('sessionContext.metrics.subagents')}`} {onViewReport && ( <> {' · '} @@ -199,7 +203,7 @@ export const SessionContextHeader = ({ className="underline" style={{ color: COLOR_TEXT_SECONDARY }} > - details + {t('sessionContext.metrics.details')} )} @@ -218,7 +222,7 @@ export const SessionContextHeader = ({ style={{ borderTop: `1px solid ${COLOR_BORDER_SUBTLE}` }} > - Phase: + {t('sessionContext.header.phase')} {phaseInfo.phases.map((phase) => (
    )} @@ -258,7 +262,7 @@ export const SessionContextHeader = ({ style={{ borderTop: `1px solid ${COLOR_BORDER_SUBTLE}` }} > - View: + {t('sessionContext.header.view')}
    diff --git a/src/renderer/components/chat/SessionContextPanel/components/SessionContextHelpTooltip.tsx b/src/renderer/components/chat/SessionContextPanel/components/SessionContextHelpTooltip.tsx index 666a3f23..511c5e25 100644 --- a/src/renderer/components/chat/SessionContextPanel/components/SessionContextHelpTooltip.tsx +++ b/src/renderer/components/chat/SessionContextPanel/components/SessionContextHelpTooltip.tsx @@ -5,9 +5,11 @@ import React, { useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; +import { useAppTranslation } from '@features/localization/renderer'; import { HelpCircle } from 'lucide-react'; export const SessionContextHelpTooltip = (): React.ReactElement => { + const { t } = useAppTranslation('common'); const [showTooltip, setShowTooltip] = useState(false); const [tooltipStyle, setTooltipStyle] = useState({}); const [arrowStyle, setArrowStyle] = useState({}); @@ -119,41 +121,37 @@ export const SessionContextHelpTooltip = (): React.ReactElement => { {/* Metric definitions */}
    - Context Used + {t('sessionContext.help.contextUsed.title')}

    - Prompt input plus output tokens currently occupying the model's context - window. + {t('sessionContext.help.contextUsed.description')}

    - Prompt Input + {t('sessionContext.help.promptInput.title')}

    - Tokens sent to the model before generation. For Claude this includes `input_tokens - + cache_creation_input_tokens + cache_read_input_tokens`. + {t('sessionContext.help.promptInput.description')}

    - Visible Context + {t('sessionContext.help.visibleContext.title')}

    - The inspectable subset of prompt input: files, CLAUDE.md, tool outputs, user - messages, and similar injections that you can optimize directly. + {t('sessionContext.help.visibleContext.description')}

    - Availability + {t('sessionContext.help.availability.title')}

    - If a provider runtime does not expose prompt-side usage yet, the panel shows - metrics as unavailable instead of pretending they are zero. + {t('sessionContext.help.availability.description')}

    diff --git a/src/renderer/components/chat/SessionContextPanel/components/TaskCoordinationSection.tsx b/src/renderer/components/chat/SessionContextPanel/components/TaskCoordinationSection.tsx index 508eccdb..b71928f1 100644 --- a/src/renderer/components/chat/SessionContextPanel/components/TaskCoordinationSection.tsx +++ b/src/renderer/components/chat/SessionContextPanel/components/TaskCoordinationSection.tsx @@ -4,6 +4,7 @@ import React from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { TaskCoordinationItem } from '../items/TaskCoordinationItem'; import { CollapsibleSection } from './CollapsibleSection'; @@ -25,11 +26,13 @@ export const TaskCoordinationSection = ({ onToggle, onNavigateToTurn, }: Readonly): React.ReactElement | null => { + const { t } = useAppTranslation('common'); + if (injections.length === 0) return null; return ( ): React.ReactElement | null => { + const { t } = useAppTranslation('common'); + if (injections.length === 0) return null; return ( ): React.ReactElement | null => { + const { t } = useAppTranslation('common'); + if (injections.length === 0) return null; return ( ): React.ReactElement | null => { + const { t } = useAppTranslation('common'); + if (injections.length === 0) return null; return ( ): React.ReactElement => { + const { t } = useAppTranslation('common'); // View mode: category sections or ranked list const [viewMode, setViewMode] = useState('category'); // Flat sub-toggle within "By Size" view @@ -212,7 +214,7 @@ export const SessionContextPanel = ({ className="flex h-full items-center justify-center text-sm" style={{ color: COLOR_TEXT_MUTED }} > - No context injections detected in this session + {t('sessionContext.empty')} ) : viewMode === 'category' ? ( <> @@ -278,7 +280,7 @@ export const SessionContextPanel = ({ color: !flatMode ? '#818cf8' : COLOR_TEXT_MUTED, }} > - Grouped + {t('sessionContext.view.grouped')} {flatMode ? ( diff --git a/src/renderer/components/chat/SessionContextPanel/items/ClaudeMdItem.tsx b/src/renderer/components/chat/SessionContextPanel/items/ClaudeMdItem.tsx index 6b25072b..b36aa765 100644 --- a/src/renderer/components/chat/SessionContextPanel/items/ClaudeMdItem.tsx +++ b/src/renderer/components/chat/SessionContextPanel/items/ClaudeMdItem.tsx @@ -4,6 +4,7 @@ import React from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { CopyablePath } from '@renderer/components/common/CopyablePath'; import { resolveAbsolutePath, shortenDisplayPath } from '@renderer/utils/pathDisplay'; @@ -23,6 +24,7 @@ export const ClaudeMdItem = ({ projectRoot, onNavigateToTurn, }: Readonly): React.ReactElement => { + const { t } = useAppTranslation('common'); const turnIndex = parseTurnIndex(injection.firstSeenInGroup); const isClickable = onNavigateToTurn && turnIndex >= 0; const displayPath = shortenDisplayPath(injection.path, projectRoot); @@ -38,7 +40,7 @@ export const ClaudeMdItem = ({ />
    - ~{formatTokens(injection.estimatedTokens)} tokens + {t('tokens.approxTokens', { tokens: formatTokens(injection.estimatedTokens) })} {isClickable ? (
    - ~{formatTokens(injection.estimatedTokens)} tokens + {t('sessionContext.items.tokensApprox', { + tokens: formatTokens(injection.estimatedTokens), + })} {isClickable ? ( - @Turn {turnIndex + 1} + {t('sessionContext.items.turn', { turn: turnIndex + 1 })} ) : ( - @Turn {turnIndex + 1} + {t('sessionContext.items.turn', { turn: turnIndex + 1 })} )}
    diff --git a/src/renderer/components/chat/SessionContextPanel/items/TaskCoordinationItem.tsx b/src/renderer/components/chat/SessionContextPanel/items/TaskCoordinationItem.tsx index 62832646..51fd720d 100644 --- a/src/renderer/components/chat/SessionContextPanel/items/TaskCoordinationItem.tsx +++ b/src/renderer/components/chat/SessionContextPanel/items/TaskCoordinationItem.tsx @@ -4,6 +4,7 @@ import React, { useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { COLOR_TEXT_MUTED, COLOR_TEXT_SECONDARY } from '@renderer/constants/cssVariables'; import { ChevronRight, Users } from 'lucide-react'; @@ -20,6 +21,7 @@ export const TaskCoordinationItem = ({ injection, onNavigateToTurn, }: Readonly): React.ReactElement => { + const { t } = useAppTranslation('common'); const [expanded, setExpanded] = useState(false); const turnIndex = injection.turnIndex; const isClickable = onNavigateToTurn && turnIndex >= 0; @@ -56,15 +58,17 @@ export const TaskCoordinationItem = ({ } }} > - @Turn {turnIndex + 1} + {t('sessionContext.items.turn', { turn: turnIndex + 1 })} ) : ( - @Turn {turnIndex + 1} + {t('sessionContext.items.turn', { turn: turnIndex + 1 })} )} - ~{formatTokens(injection.estimatedTokens)} tokens + {t('sessionContext.items.tokensApprox', { + tokens: formatTokens(injection.estimatedTokens), + })} - {injection.breakdown.length} item{injection.breakdown.length !== 1 ? 's' : ''} + {t('sessionContext.items.itemsCount', { count: injection.breakdown.length })} ); diff --git a/src/renderer/components/chat/SessionContextPanel/items/ThinkingTextItem.tsx b/src/renderer/components/chat/SessionContextPanel/items/ThinkingTextItem.tsx index 92be27b0..491cd322 100644 --- a/src/renderer/components/chat/SessionContextPanel/items/ThinkingTextItem.tsx +++ b/src/renderer/components/chat/SessionContextPanel/items/ThinkingTextItem.tsx @@ -4,6 +4,7 @@ import React, { useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { COLOR_TEXT_MUTED, COLOR_TEXT_SECONDARY } from '@renderer/constants/cssVariables'; import { Brain, ChevronRight } from 'lucide-react'; @@ -20,6 +21,7 @@ export const ThinkingTextItem = ({ injection, onNavigateToTurn, }: Readonly): React.ReactElement => { + const { t } = useAppTranslation('common'); const [expanded, setExpanded] = useState(false); const turnIndex = injection.turnIndex; const isClickable = onNavigateToTurn && turnIndex >= 0; @@ -65,15 +67,17 @@ export const ThinkingTextItem = ({ } }} > - @Turn {turnIndex + 1} + {t('sessionContext.items.turn', { turn: turnIndex + 1 })} ) : ( - @Turn {turnIndex + 1} + {t('sessionContext.items.turn', { turn: turnIndex + 1 })} )} - ~{formatTokens(injection.estimatedTokens)} tokens + {t('sessionContext.items.tokensApprox', { + tokens: formatTokens(injection.estimatedTokens), + })} @@ -82,7 +86,9 @@ export const ThinkingTextItem = ({ {injection.breakdown.map((item, idx) => (
    - {item.type === 'thinking' ? 'Thinking' : 'Text'} + {item.type === 'thinking' + ? t('sessionContext.items.thinking') + : t('sessionContext.items.text')} ~{formatTokens(item.tokenCount)} diff --git a/src/renderer/components/chat/SessionContextPanel/items/ToolBreakdownItem.tsx b/src/renderer/components/chat/SessionContextPanel/items/ToolBreakdownItem.tsx index dec23d2c..beb7594f 100644 --- a/src/renderer/components/chat/SessionContextPanel/items/ToolBreakdownItem.tsx +++ b/src/renderer/components/chat/SessionContextPanel/items/ToolBreakdownItem.tsx @@ -4,6 +4,7 @@ import React from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { formatTokens } from '../utils/formatting'; import type { ToolTokenBreakdown } from '@renderer/types/contextInjection'; @@ -15,6 +16,8 @@ interface ToolBreakdownItemProps { export const ToolBreakdownItem = ({ tool, }: Readonly): React.ReactElement => { + const { t } = useAppTranslation('common'); + return (
    {tool.toolName} @@ -30,7 +33,7 @@ export const ToolBreakdownItem = ({ fontSize: '10px', }} > - error + {t('states.error')} )}
    diff --git a/src/renderer/components/chat/SessionContextPanel/items/ToolOutputItem.tsx b/src/renderer/components/chat/SessionContextPanel/items/ToolOutputItem.tsx index fe46a522..e5b2ddf8 100644 --- a/src/renderer/components/chat/SessionContextPanel/items/ToolOutputItem.tsx +++ b/src/renderer/components/chat/SessionContextPanel/items/ToolOutputItem.tsx @@ -4,6 +4,7 @@ import React, { useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { COLOR_TEXT_MUTED, COLOR_TEXT_SECONDARY } from '@renderer/constants/cssVariables'; import { ChevronRight, Wrench } from 'lucide-react'; @@ -22,6 +23,7 @@ export const ToolOutputItem = ({ injection, onNavigateToTurn, }: Readonly): React.ReactElement => { + const { t } = useAppTranslation('common'); const [expanded, setExpanded] = useState(false); const turnIndex = injection.turnIndex; const isClickable = onNavigateToTurn && turnIndex >= 0; @@ -58,15 +60,17 @@ export const ToolOutputItem = ({ } }} > - @Turn {turnIndex + 1} + {t('sessionContext.items.turn', { turn: turnIndex + 1 })}
    ) : ( - @Turn {turnIndex + 1} + {t('sessionContext.items.turn', { turn: turnIndex + 1 })} )} - ~{formatTokens(injection.estimatedTokens)} tokens + {t('sessionContext.items.tokensApprox', { + tokens: formatTokens(injection.estimatedTokens), + })} - {injection.toolCount} tool{injection.toolCount !== 1 ? 's' : ''} + {t('sessionContext.items.toolsCount', { count: injection.toolCount })} ); diff --git a/src/renderer/components/chat/SessionContextPanel/items/UserMessageItem.tsx b/src/renderer/components/chat/SessionContextPanel/items/UserMessageItem.tsx index ce3219fa..111f95a5 100644 --- a/src/renderer/components/chat/SessionContextPanel/items/UserMessageItem.tsx +++ b/src/renderer/components/chat/SessionContextPanel/items/UserMessageItem.tsx @@ -4,6 +4,7 @@ import React from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { COLOR_TEXT_MUTED, COLOR_TEXT_SECONDARY } from '@renderer/constants/cssVariables'; import { MessageSquare } from 'lucide-react'; @@ -20,6 +21,7 @@ export const UserMessageItem = ({ injection, onNavigateToTurn, }: Readonly): React.ReactElement => { + const { t } = useAppTranslation('common'); const turnIndex = injection.turnIndex; const isClickable = onNavigateToTurn && turnIndex >= 0; @@ -45,15 +47,17 @@ export const UserMessageItem = ({ } }} > - @Turn {turnIndex + 1} + {t('sessionContext.items.turn', { turn: turnIndex + 1 })} ) : ( - @Turn {turnIndex + 1} + {t('sessionContext.items.turn', { turn: turnIndex + 1 })} )} - ~{formatTokens(injection.estimatedTokens)} tokens + {t('sessionContext.items.tokensApprox', { + tokens: formatTokens(injection.estimatedTokens), + })}
    {injection.textPreview && ( diff --git a/src/renderer/components/chat/SystemChatGroup.tsx b/src/renderer/components/chat/SystemChatGroup.tsx index 92f03e01..8dd337b6 100644 --- a/src/renderer/components/chat/SystemChatGroup.tsx +++ b/src/renderer/components/chat/SystemChatGroup.tsx @@ -1,5 +1,6 @@ import React from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { format } from 'date-fns'; import { Terminal } from 'lucide-react'; @@ -19,6 +20,7 @@ interface SystemChatGroupProps { const SystemChatGroupInner = ({ systemGroup, }: Readonly): React.JSX.Element => { + const { t } = useAppTranslation('common'); const { commandOutput, timestamp } = systemGroup; // Clean ANSI escape codes from output @@ -34,7 +36,7 @@ const SystemChatGroupInner = ({ > - System + {t('chat.system.label')} · {format(timestamp, 'h:mm:ss a')} diff --git a/src/renderer/components/chat/UserChatGroup.tsx b/src/renderer/components/chat/UserChatGroup.tsx index ef7fa9a5..7d6db1a7 100644 --- a/src/renderer/components/chat/UserChatGroup.tsx +++ b/src/renderer/components/chat/UserChatGroup.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import ReactMarkdown, { type Components, defaultUrlTransform } from 'react-markdown'; +import { useAppTranslation } from '@features/localization/renderer'; import { api } from '@renderer/api'; import { MemberHoverCard } from '@renderer/components/team/members/MemberHoverCard'; import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors'; @@ -380,6 +381,7 @@ function createUserMarkdownComponents( * - Shows image count indicator */ const UserChatGroupInner = ({ userGroup }: Readonly): React.JSX.Element => { + const { t } = useAppTranslation('common'); const { content, timestamp, id: groupId } = userGroup; const [isManuallyExpanded, setIsManuallyExpanded] = useState(false); const [validatedPaths, setValidatedPaths] = useState>({}); @@ -544,7 +546,7 @@ const UserChatGroupInner = ({ userGroup }: Readonly): React. {format(timestamp, 'h:mm:ss a')} - You + {t('chat.user.you')} @@ -578,7 +580,7 @@ const UserChatGroupInner = ({ userGroup }: Readonly): React. style={{ color: 'var(--color-text-muted)' }} > - Show more + {t('chat.user.showMore')} )} @@ -596,7 +598,7 @@ const UserChatGroupInner = ({ userGroup }: Readonly): React. }} > - Show less + {t('chat.user.showLess')} ) : null} @@ -613,7 +615,7 @@ const UserChatGroupInner = ({ userGroup }: Readonly): React. : 'var(--color-text-muted)'; const commandMatch = /"([^"]+)"/.exec(notification.summary); const commandName = - commandMatch?.[1] ?? notification.summary.trim() ?? 'Background task'; + commandMatch?.[1] ?? notification.summary.trim() ?? t('chat.user.backgroundTask'); const exitCodeMatch = /\(exit code (\d+)\)/.exec(notification.summary); const outputFileName = notification.outputFile ? (notification.outputFile.split(/[\\/]/).pop() ?? notification.outputFile) @@ -634,14 +636,16 @@ const UserChatGroupInner = ({ userGroup }: Readonly): React. className="text-xs font-medium leading-snug" style={{ color: 'var(--color-text-secondary)' }} > - {commandName || 'Background task'} + {commandName || t('chat.user.backgroundTask')}
    {notification.status || 'unknown'} - {exitCodeMatch?.[1] ? exit {exitCodeMatch[1]} : null} + {exitCodeMatch?.[1] ? ( + {t('chat.user.exitCode', { code: exitCodeMatch[1] })} + ) : null} {outputFileName ? ( @@ -657,7 +661,7 @@ const UserChatGroupInner = ({ userGroup }: Readonly): React. {/* Images indicator */} {hasImages && (
    - {content.images.length} image{content.images.length > 1 ? 's' : ''} attached + {t('chat.user.imagesAttached', { count: content.images.length })}
    )}
    diff --git a/src/renderer/components/chat/items/ExecutionTrace.tsx b/src/renderer/components/chat/items/ExecutionTrace.tsx index aaf39889..872f385f 100644 --- a/src/renderer/components/chat/items/ExecutionTrace.tsx +++ b/src/renderer/components/chat/items/ExecutionTrace.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { CARD_ICON_MUTED, CODE_BG, @@ -56,6 +57,7 @@ export const ExecutionTrace: React.FC = React.memo( searchExpandedItemId, registerToolRef, }): React.JSX.Element => { + const { t } = useAppTranslation('common'); const [manualExpandedItemId, setManualExpandedItemId] = useState(null); // Use searchExpandedItemId if set, otherwise use manually expanded item @@ -68,7 +70,7 @@ export const ExecutionTrace: React.FC = React.memo( if (!items || items.length === 0) { return (
    - No execution items + {t('chat.executionTrace.empty')}
    ); } @@ -157,7 +159,9 @@ export const ExecutionTrace: React.FC = React.memo( className="px-2 py-1 text-xs" style={{ color: CARD_ICON_MUTED }} > - Nested: {item.subagent.description ?? item.subagent.id} + {t('chat.executionTrace.nested', { + name: item.subagent.description ?? item.subagent.id, + })} ); @@ -168,7 +172,7 @@ export const ExecutionTrace: React.FC = React.memo( } - label="Input" + label={t('chat.executionTrace.input')} summary={truncateText(item.content, 80)} tokenCount={item.tokenCount} timestamp={item.timestamp} @@ -222,7 +226,7 @@ export const ExecutionTrace: React.FC = React.memo( className="shrink-0 text-xs font-medium" style={{ color: TOOL_CALL_TEXT }} > - Compacted + {t('chat.compact.compacted')} {item.tokenDelta && ( = React.memo( {formatTokensCompact(item.tokenDelta.postCompactionTokens)} {' '} - ({formatTokensCompact(Math.abs(item.tokenDelta.delta))} freed) + {t('chat.compact.freedTokens', { + tokens: formatTokensCompact(Math.abs(item.tokenDelta.delta)), + })} )} @@ -244,7 +250,7 @@ export const ExecutionTrace: React.FC = React.memo( color: '#818cf8', }} > - Phase {item.phaseNumber} + {t('chat.compact.phase', { phase: item.phaseNumber })} { + const { t } = useAppTranslation('common'); const status = getToolStatus(linkedTool); const { isLight } = useTheme(); const summary = getToolSummary(linkedTool.name, linkedTool.input); @@ -107,7 +109,7 @@ export const LinkedToolItem = memo( const isTeammateSpawned = linkedTool.result?.toolUseResult?.status === 'teammate_spawned'; if (isTeammateSpawned) { const teamResult = linkedTool.result!.toolUseResult!; - const name = (teamResult.name as string) || 'teammate'; + const name = (teamResult.name as string) || t('members.teammateFallback'); const color = (teamResult.color as string) || ''; const colors = getTeamColorSet(color); return ( @@ -120,7 +122,7 @@ export const LinkedToolItem = memo( {name} - Teammate spawned + {t('chat.tools.teammateSpawned')} ); @@ -130,12 +132,12 @@ export const LinkedToolItem = memo( const isShutdownRequest = linkedTool.name === 'SendMessage' && linkedTool.input?.type === 'shutdown_request'; if (isShutdownRequest) { - const target = (linkedTool.input?.recipient as string) || 'teammate'; + const target = (linkedTool.input?.recipient as string) || t('members.teammateFallback'); return (
    - Shutdown requested →{' '} + {t('chat.tools.shutdownRequested')}{' '} {target}
    @@ -223,13 +225,13 @@ export const LinkedToolItem = memo( style={{ color: 'var(--tool-item-muted)' }} > - No result received + {t('chat.tools.noResultReceived')} )} {/* Timing */}
    - Duration: {formatDuration(linkedTool.durationMs)} + {t('chat.tools.duration', { duration: formatDuration(linkedTool.durationMs) })}
    diff --git a/src/renderer/components/chat/items/MetricsPill.tsx b/src/renderer/components/chat/items/MetricsPill.tsx index e6a31c61..b4e3652f 100644 --- a/src/renderer/components/chat/items/MetricsPill.tsx +++ b/src/renderer/components/chat/items/MetricsPill.tsx @@ -1,6 +1,7 @@ import React, { memo, useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; +import { useAppTranslation } from '@features/localization/renderer'; import { CARD_ICON_MUTED, CARD_SEPARATOR, @@ -49,6 +50,7 @@ export const MetricsPill = memo( isolatedOverride, phaseBreakdown, }: Readonly): React.ReactElement | null => { + const { t } = useAppTranslation('common'); const [showTooltip, setShowTooltip] = useState(false); const [tooltipStyle, setTooltipStyle] = useState({}); const containerRef = useRef(null); @@ -160,7 +162,9 @@ export const MetricsPill = memo(
    {hasMainImpact && (
    - Main Context + + {t('chat.subagent.metrics.mainContext')} + {mainSessionImpact.totalTokens.toLocaleString()} @@ -181,7 +185,7 @@ export const MetricsPill = memo( className="flex items-center justify-between gap-3 pl-2" > - Phase {phase.phaseNumber} + {t('chat.subagent.metrics.phase', { phase: phase.phaseNumber })} = React.memo( notificationColorMap, registerToolRef, }) => { - const description = subagent.description ?? step.content.subagentDescription ?? 'Subagent'; + const { t } = useAppTranslation('common'); + const description = + subagent.description ?? step.content.subagentDescription ?? t('chat.subagent.fallbackName'); const subagentType = subagent.subagentType ?? 'Task'; const truncatedDesc = description.length > 60 ? description.slice(0, 60) + '...' : description; @@ -142,10 +145,10 @@ export const SubagentItem: React.FC = React.memo( Array.isArray(m.content) && m.content.some((b) => b.type === 'tool_use') ).length ?? 0; - return toolCount > 0 ? `${toolCount} tools` : ''; + return toolCount > 0 ? t('chat.subagent.summary.tools', { count: toolCount }) : ''; } return buildSummary(displayItems); - }, [isExpanded, containsHighlightedError, displayItems, subagent.messages]); + }, [isExpanded, containsHighlightedError, displayItems, subagent.messages, t]); // Model info const modelInfo = useMemo(() => { @@ -250,7 +253,7 @@ export const SubagentItem: React.FC = React.memo( {subagent.team.memberName} - Shutdown confirmed + {t('chat.subagent.shutdownConfirmed')} = React.memo( 0 ? phaseData.totalConsumption : undefined } @@ -391,14 +394,14 @@ export const SubagentItem: React.FC = React.memo( style={{ color: COLOR_TEXT_MUTED }} > - Type{' '} + {t('chat.subagent.meta.type')}{' '} {subagentType} - Duration{' '} + {t('chat.subagent.meta.duration')}{' '} {formatDuration(subagent.durationMs)} @@ -407,7 +410,7 @@ export const SubagentItem: React.FC = React.memo( <> - Model{' '} + {t('chat.subagent.meta.model')}{' '} {modelInfo.name} @@ -416,7 +419,7 @@ export const SubagentItem: React.FC = React.memo( )} - ID{' '} + {t('chat.subagent.meta.id')}{' '} = React.memo( className="mb-2 text-[10px] font-semibold uppercase tracking-wider" style={{ color: CARD_ICON_MUTED }} > - Context Usage + {t('chat.subagent.metrics.contextUsage')}
    {/* Token rows - floating alignment */} @@ -448,7 +451,7 @@ export const SubagentItem: React.FC = React.memo( style={{ color: 'rgba(251, 191, 36, 0.7)' }} /> - Main Context + {t('chat.subagent.metrics.mainContext')}
    = React.memo(
    - Total Output + {t('chat.subagent.metrics.totalOutput')}
    = React.memo( {cumulativeMetrics.outputTokens.toLocaleString()} {' '} - ({cumulativeMetrics.turnCount} turns) + {t('chat.subagent.metrics.turns', { + count: cumulativeMetrics.turnCount, + })} @@ -489,7 +494,9 @@ export const SubagentItem: React.FC = React.memo( style={{ color: 'rgba(56, 189, 248, 0.7)' }} /> - {subagent.team ? 'Context Window' : 'Subagent Context'} + {subagent.team + ? t('chat.subagent.metrics.contextWindow') + : t('chat.subagent.metrics.subagentContext')} = React.memo( className="flex items-center justify-between pl-5" > - Phase {phase.phaseNumber} + {t('chat.subagent.metrics.phase', { phase: phase.phaseNumber })} = React.memo( /> - Execution Trace + {t('chat.subagent.trace.title')} · {itemsSummary} diff --git a/src/renderer/components/chat/items/TeammateMessageItem.tsx b/src/renderer/components/chat/items/TeammateMessageItem.tsx index 4bc8d309..a8c7b236 100644 --- a/src/renderer/components/chat/items/TeammateMessageItem.tsx +++ b/src/renderer/components/chat/items/TeammateMessageItem.tsx @@ -1,5 +1,6 @@ import React, { memo, useMemo } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { CARD_BG, CARD_BORDER_STYLE, @@ -84,6 +85,7 @@ export const TeammateMessageItem = memo( highlightClasses = '', highlightStyle, }: TeammateMessageItemProps): React.JSX.Element => { + const { t } = useAppTranslation('common'); const colors = getTeamColorSet(teammateMessage.color); const { isLight } = useTheme(); @@ -200,7 +202,7 @@ export const TeammateMessageItem = memo( {/* "Message" type label — parallels SubagentItem's model info */} - Message + {t('chat.teammateMessage.message')} {/* Reply indicator — shows which SendMessage triggered this response */} @@ -226,13 +228,13 @@ export const TeammateMessageItem = memo( style={{ color: CARD_ICON_MUTED }} > - Resent + {t('chat.teammateMessage.resent')} )} {/* Summary */} - {truncatedSummary || 'Teammate message'} + {truncatedSummary || t('chat.teammateMessage.fallback')} {/* Context impact — tokens injected into main session */} @@ -241,7 +243,9 @@ export const TeammateMessageItem = memo( className="shrink-0 font-mono text-[11px] tabular-nums" style={{ color: CARD_ICON_MUTED }} > - ~{formatTokensCompact(teammateMessage.tokenCount)} tokens + {t('tokens.approxTokens', { + tokens: formatTokensCompact(teammateMessage.tokenCount), + })} )} diff --git a/src/renderer/components/chat/items/linkedTool/DefaultToolViewer.tsx b/src/renderer/components/chat/items/linkedTool/DefaultToolViewer.tsx index 8c4ba316..60918d14 100644 --- a/src/renderer/components/chat/items/linkedTool/DefaultToolViewer.tsx +++ b/src/renderer/components/chat/items/linkedTool/DefaultToolViewer.tsx @@ -6,6 +6,8 @@ import React, { memo } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; + import { type ItemStatus } from '../BaseItem'; import { CollapsibleOutputSection } from './CollapsibleOutputSection'; @@ -27,6 +29,7 @@ export const DefaultToolViewer = memo(function DefaultToolViewer({ linkedTool, status, }: DefaultToolViewerProps) { + const { t } = useAppTranslation('common'); const displayOutputContent = linkedTool.result ? formatToolOutputForDisplay(linkedTool.name, linkedTool.result.content) : null; @@ -42,7 +45,7 @@ export const DefaultToolViewer = memo(function DefaultToolViewer({ {/* Input Section */}
    - Input + {t('toolViewer.input')}
    - {renderInput(linkedTool.name, linkedTool.input)} + {renderInput(linkedTool.name, linkedTool.input, { + replaceAll: t('toolViewer.replaceAll'), + agentAction: t('toolViewer.agent.action'), + agentTeammate: t('toolViewer.agent.teammate'), + agentTeam: t('toolViewer.agent.team'), + agentRuntime: t('toolViewer.agent.runtime'), + agentType: t('toolViewer.agent.type'), + startupInstructionsHidden: t('toolViewer.agent.startupInstructionsHidden'), + noInputRecorded: t('toolViewer.noInputRecorded'), + })}
    diff --git a/src/renderer/components/chat/items/linkedTool/EditToolViewer.tsx b/src/renderer/components/chat/items/linkedTool/EditToolViewer.tsx index c537eb04..b2059cdb 100644 --- a/src/renderer/components/chat/items/linkedTool/EditToolViewer.tsx +++ b/src/renderer/components/chat/items/linkedTool/EditToolViewer.tsx @@ -6,6 +6,7 @@ import React, { memo } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { DiffViewer } from '@renderer/components/chat/viewers'; import { type ItemStatus, StatusDot } from '../BaseItem'; @@ -24,6 +25,7 @@ export const EditToolViewer = memo(function EditToolViewer({ linkedTool, status, }: EditToolViewerProps) { + const { t } = useAppTranslation('common'); const toolUseResult = linkedTool.result?.toolUseResult as Record | undefined; const filePath = (toolUseResult?.filePath as string) || (linkedTool.input.file_path as string); @@ -49,11 +51,11 @@ export const EditToolViewer = memo(function EditToolViewer({ className="mb-1 flex items-center gap-2 text-xs" style={{ color: 'var(--tool-item-muted)' }} > - Result + {t('chat.tools.result')} {linkedTool.result?.tokenCount !== undefined && linkedTool.result.tokenCount > 0 && ( - ~{formatTokens(linkedTool.result.tokenCount)} tokens + {t('tokens.approxTokens', { tokens: formatTokens(linkedTool.result.tokenCount) })} )} diff --git a/src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx b/src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx index c2d14b6b..e1fd4124 100644 --- a/src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx +++ b/src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx @@ -6,6 +6,7 @@ import React, { memo } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { CodeBlockViewer, MarkdownViewer } from '@renderer/components/chat/viewers'; import type { LinkedToolItem } from '@renderer/types/groups'; @@ -15,6 +16,7 @@ interface ReadToolViewerProps { } export const ReadToolViewer = memo(function ReadToolViewer({ linkedTool }: ReadToolViewerProps) { + const { t } = useAppTranslation('common'); const filePath = linkedTool.input.file_path as string; // Prefer enriched toolUseResult data @@ -73,7 +75,7 @@ export const ReadToolViewer = memo(function ReadToolViewer({ linkedTool }: ReadT border: '1px solid var(--tag-border)', }} > - Code + {t('code.code')} )} {isMarkdownFile && viewMode === 'preview' ? ( - + ) : (
    - Result + {t('chat.tools.result')}
    - Skill Instructions + {t('chat.tools.skill.instructions')}
    - Error + {t('states.error')}
    | undefined; const filePath = @@ -37,7 +39,7 @@ export const WriteToolViewer = memo(function WriteToolViewer({ linkedTool }: Wri return (
    - {isCreate ? 'Created file' : 'Wrote to file'} + {isCreate ? t('chat.tools.write.createdFile') : t('chat.tools.write.wroteToFile')}
    {isMarkdownFile && (
    @@ -51,7 +53,7 @@ export const WriteToolViewer = memo(function WriteToolViewer({ linkedTool }: Wri border: '1px solid var(--tag-border)', }} > - Code + {t('code.code')}
    )} {isMarkdownFile && viewMode === 'preview' ? ( - + ) : ( )} diff --git a/src/renderer/components/chat/items/linkedTool/renderHelpers.tsx b/src/renderer/components/chat/items/linkedTool/renderHelpers.tsx index 924e5725..47c5b1d9 100644 --- a/src/renderer/components/chat/items/linkedTool/renderHelpers.tsx +++ b/src/renderer/components/chat/items/linkedTool/renderHelpers.tsx @@ -15,10 +15,25 @@ import { import { highlightLines } from '@renderer/utils/syntaxHighlighter'; import { getAgentToolDisplayDetails } from '@shared/utils/toolSummary'; +export interface RenderInputLabels { + replaceAll: string; + agentAction: string; + agentTeammate: string; + agentTeam: string; + agentRuntime: string; + agentType: string; + startupInstructionsHidden: string; + noInputRecorded: string; +} + /** * Renders the input section based on tool type with theme-aware styling. */ -export function renderInput(toolName: string, input: Record): React.ReactElement { +export function renderInput( + toolName: string, + input: Record, + labels: RenderInputLabels +): React.ReactElement { const normalizedToolName = toolName.toLowerCase(); // Special rendering for Edit tool - show diff-like format if (normalizedToolName === 'edit') { @@ -34,7 +49,7 @@ export function renderInput(toolName: string, input: Record): R {filePath} {replaceAll && ( - (replace all) + {labels.replaceAll} )}
    @@ -110,7 +125,7 @@ export function renderInput(toolName: string, input: Record): R
    - action + {labels.agentAction}
    {details.action}
    @@ -118,7 +133,7 @@ export function renderInput(toolName: string, input: Record): R {details.teammateName && (
    - teammate + {labels.agentTeammate}
    {details.teammateName}
    @@ -127,7 +142,7 @@ export function renderInput(toolName: string, input: Record): R {details.teamName && (
    - team + {labels.agentTeam}
    {details.teamName}
    @@ -136,7 +151,7 @@ export function renderInput(toolName: string, input: Record): R {details.runtime && (
    - runtime + {labels.agentRuntime}
    {details.runtime}
    @@ -145,7 +160,7 @@ export function renderInput(toolName: string, input: Record): R {details.subagentType && (
    - type + {labels.agentType}
    {details.subagentType}
    @@ -160,7 +175,7 @@ export function renderInput(toolName: string, input: Record): R color: COLOR_TEXT_MUTED, }} > - Startup instructions are hidden in the UI. + {labels.startupInstructionsHidden}
    ); @@ -180,7 +195,7 @@ export function renderInput(toolName: string, input: Record): R )) ) : (
    - No input recorded for this tool call. + {labels.noInputRecorded}
    )} diff --git a/src/renderer/components/chat/session-panel.ts b/src/renderer/components/chat/session-panel.ts new file mode 100644 index 00000000..5e636ab6 --- /dev/null +++ b/src/renderer/components/chat/session-panel.ts @@ -0,0 +1 @@ +export { SessionContextPanel as SessionPanel } from './SessionContextPanel/index'; diff --git a/src/renderer/components/chat/viewers/CodeBlockViewer.tsx b/src/renderer/components/chat/viewers/CodeBlockViewer.tsx index 9edb7a64..997dcc41 100644 --- a/src/renderer/components/chat/viewers/CodeBlockViewer.tsx +++ b/src/renderer/components/chat/viewers/CodeBlockViewer.tsx @@ -1,5 +1,6 @@ import React, { memo, useMemo, useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { getBaseName } from '@renderer/utils/pathUtils'; import { createLogger } from '@shared/utils/logger'; import { Check, Copy, FileCode } from 'lucide-react'; @@ -125,6 +126,7 @@ export const CodeBlockViewer = memo(function CodeBlockViewer({ endLine, maxHeight = 'max-h-96', }: CodeBlockViewerProps): React.JSX.Element { + const { t } = useAppTranslation('common'); const [isCopied, setIsCopied] = useState(false); // Infer language from file extension if not provided @@ -178,7 +180,7 @@ export const CodeBlockViewer = memo(function CodeBlockViewer({
    {(startLine > 1 || endLine) && ( - (lines {startLine}-{actualEndLine}) + {t('code.linesParenthesized', { from: startLine, to: actualEndLine })} )} {isCopied ? ( diff --git a/src/renderer/components/chat/viewers/DiffViewer.tsx b/src/renderer/components/chat/viewers/DiffViewer.tsx index efb74cd6..63611209 100644 --- a/src/renderer/components/chat/viewers/DiffViewer.tsx +++ b/src/renderer/components/chat/viewers/DiffViewer.tsx @@ -1,5 +1,6 @@ import React, { memo, useMemo } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { CODE_BG, CODE_BORDER, @@ -357,6 +358,7 @@ export const DiffViewer = memo(function DiffViewer({ tokenCount, syntaxHighlight = false, }: DiffViewerProps): React.JSX.Element { + const { t } = useAppTranslation('common'); // Compute diff const oldLines = oldString.split(/\r?\n/); const newLines = newString.split(/\r?\n/); @@ -431,12 +433,12 @@ export const DiffViewer = memo(function DiffViewer({ )} {stats.removed > 0 && -{stats.removed}} {stats.added === 0 && stats.removed === 0 && ( - Changed + {t('diff.changed')} )} {tokenCount !== undefined && tokenCount > 0 && ( - ~{formatTokens(tokenCount)} tokens + {t('tokens.approxTokens', { tokens: formatTokens(tokenCount) })} )} @@ -449,7 +451,7 @@ export const DiffViewer = memo(function DiffViewer({ ))} {diffLines.length === 0 && (
    - No changes detected + {t('diff.noChangesDetected')}
    )} diff --git a/src/renderer/components/chat/viewers/MarkdownViewer.tsx b/src/renderer/components/chat/viewers/MarkdownViewer.tsx index cca878d4..61d3a7f2 100644 --- a/src/renderer/components/chat/viewers/MarkdownViewer.tsx +++ b/src/renderer/components/chat/viewers/MarkdownViewer.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactMarkdown, { type Components, defaultUrlTransform } from 'react-markdown'; +import { useAppTranslation } from '@features/localization/renderer'; import { api } from '@renderer/api'; import { CopyButton } from '@renderer/components/common/CopyButton'; import { MemberHoverCard } from '@renderer/components/team/members/MemberHoverCard'; @@ -338,6 +339,7 @@ const LocalImage = React.memo(function LocalImage({ alt, baseDir, }: LocalImageProps): React.ReactElement { + const { t } = useAppTranslation('common'); const [dataUrl, setDataUrl] = React.useState(null); const [error, setError] = React.useState(false); @@ -366,7 +368,7 @@ const LocalImage = React.memo(function LocalImage({ if (error) { return ( - [Image: {alt || src}] + {t('markdown.imageFallback', { label: alt || src })} ); } @@ -959,6 +961,7 @@ export const MarkdownViewer: React.FC = React.memo(function teamColorByName: providedTeamColorByName, onTeamClick: providedOnTeamClick, }) { + const { t } = useAppTranslation('common'); const [showRaw, setShowRaw] = React.useState(false); const [rawLimit, setRawLimit] = React.useState(LARGE_PREVIEW_CHARS); const { isLight } = useTheme(); @@ -1016,7 +1019,7 @@ export const MarkdownViewer: React.FC = React.memo(function {label}
    - Raw + {t('markdown.raw')} {copyable && } @@ -1042,28 +1041,23 @@ export const MarkdownViewer: React.FC = React.memo(function className="flex items-center justify-between px-3 py-2 text-xs" style={{ color: COLOR_TEXT_MUTED }} > - Raw preview + {t('markdown.rawPreview')} )} {isTooLarge && (
    - Content is very large ({content.length.toLocaleString()} chars). Showing raw preview to - keep the UI responsive. + {t('markdown.largeContentNotice', { count: content.length.toLocaleString() })}
    )} @@ -1077,7 +1071,10 @@ export const MarkdownViewer: React.FC = React.memo(function {isTruncated && (
    - Showing {shown.length.toLocaleString()} / {content.length.toLocaleString()} chars + {t('markdown.showingChars', { + shown: shown.length.toLocaleString(), + total: content.length.toLocaleString(), + })}
    @@ -1175,9 +1172,9 @@ export const MarkdownViewer: React.FC = React.memo(function className="text-xs underline" style={{ color: PROSE_LINK }} onClick={() => setShowRaw(true)} - title="Show raw" + title={t('markdown.showRaw')} > - Show raw + {t('markdown.showRaw')} {copyable && } @@ -1195,9 +1192,9 @@ export const MarkdownViewer: React.FC = React.memo(function className="underline" style={{ color: PROSE_LINK }} onClick={() => setShowRaw(true)} - title="Show raw" + title={t('markdown.showRaw')} > - Show raw + {t('markdown.showRaw')} )} diff --git a/src/renderer/components/chat/viewers/MermaidDiagram.tsx b/src/renderer/components/chat/viewers/MermaidDiagram.tsx index 41833bdc..c69b6557 100644 --- a/src/renderer/components/chat/viewers/MermaidDiagram.tsx +++ b/src/renderer/components/chat/viewers/MermaidDiagram.tsx @@ -9,6 +9,7 @@ import React, { useEffect, useRef, useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { PROSE_PRE_BG, PROSE_PRE_BORDER } from '@renderer/constants/cssVariables'; import DOMPurify from 'dompurify'; import mermaid from 'mermaid'; @@ -52,6 +53,7 @@ interface MermaidDiagramProps { export const MermaidDiagram = React.memo(function MermaidDiagram({ code, }: MermaidDiagramProps): React.ReactElement { + const { t } = useAppTranslation('common'); const containerRef = useRef(null); const [error, setError] = useState(null); @@ -97,7 +99,7 @@ export const MermaidDiagram = React.memo(function MermaidDiagram({ border: `1px solid ${PROSE_PRE_BORDER}`, }} > -
    Mermaid syntax error
    +
    {t('code.mermaidSyntaxError')}
    {code}
    ); diff --git a/src/renderer/components/common/CliInstallWarningBanner.tsx b/src/renderer/components/common/CliInstallWarningBanner.tsx index 3a9cb1f1..eca46c3e 100644 --- a/src/renderer/components/common/CliInstallWarningBanner.tsx +++ b/src/renderer/components/common/CliInstallWarningBanner.tsx @@ -6,12 +6,14 @@ * Only rendered in Electron mode. */ +import { useAppTranslation } from '@features/localization/renderer'; import { isElectronMode } from '@renderer/api'; import { useStore } from '@renderer/store'; import { AlertTriangle } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; export const CliInstallWarningBanner = (): React.JSX.Element | null => { + const { t } = useAppTranslation('common'); const cliStatus = useStore(useShallow((s) => s.cliStatus)); const cliStatusLoading = useStore((s) => s.cliStatusLoading); const openDashboard = useStore((s) => s.openDashboard); @@ -58,7 +60,7 @@ export const CliInstallWarningBanner = (): React.JSX.Element | null => { color: 'var(--warning-text)', }} > - Go to Dashboard + {t('actions.goToDashboard')} ); diff --git a/src/renderer/components/common/ConfirmDialog.tsx b/src/renderer/components/common/ConfirmDialog.tsx index 0633e5f1..1f2d3aec 100644 --- a/src/renderer/components/common/ConfirmDialog.tsx +++ b/src/renderer/components/common/ConfirmDialog.tsx @@ -7,6 +7,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { AlertTriangle } from 'lucide-react'; interface ConfirmDialogState { @@ -67,6 +68,7 @@ export async function confirm(opts: { * ConfirmDialog component. Mount once at the app root (e.g. in App.tsx). */ export const ConfirmDialog = (): React.JSX.Element | null => { + const { t } = useAppTranslation('common'); const [state, setState] = useState(initialState); const dialogRef = useRef(null); @@ -115,7 +117,7 @@ export const ConfirmDialog = (): React.JSX.Element | null => { className="absolute inset-0 cursor-default" style={{ backgroundColor: 'rgba(0, 0, 0, 0.6)' }} onClick={() => close(false)} - aria-label="Close dialog" + aria-label={t('actions.closeDialog')} tabIndex={-1} />
    { + const { t } = useAppTranslation('common'); const isContextSwitching = useStore((state) => state.isContextSwitching); const targetContextId = useStore((state) => state.targetContextId); @@ -19,7 +21,9 @@ export const ContextSwitchOverlay: React.FC = () => { // Format context label for display const contextLabel = - targetContextId === 'local' ? 'Local' : (targetContextId?.replace(/^ssh-/, '') ?? 'Unknown'); + targetContextId === 'local' + ? t('context.local') + : (targetContextId?.replace(/^ssh-/, '') ?? t('states.unknown')); return (
    @@ -29,8 +33,8 @@ export const ContextSwitchOverlay: React.FC = () => { {/* Text */}
    -

    Switching to {contextLabel}...

    -

    Loading workspace

    +

    {t('context.switchingTo', { workspace: contextLabel })}

    +

    {t('context.loadingWorkspace')}

    diff --git a/src/renderer/components/common/CopyButton.tsx b/src/renderer/components/common/CopyButton.tsx index 005a95ca..cee3ccea 100644 --- a/src/renderer/components/common/CopyButton.tsx +++ b/src/renderer/components/common/CopyButton.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { Check, Copy } from 'lucide-react'; interface CopyButtonProps { @@ -26,6 +27,7 @@ export const CopyButton: React.FC = ({ bgColor = 'var(--code-bg)', inline = false, }) => { + const { t } = useAppTranslation('common'); const [isCopied, setIsCopied] = useState(false); const handleCopy = async (): Promise => { @@ -49,7 +51,7 @@ export const CopyButton: React.FC = ({ @@ -75,7 +77,7 @@ export const CopyButton: React.FC = ({ diff --git a/src/renderer/components/common/ErrorBoundary.tsx b/src/renderer/components/common/ErrorBoundary.tsx index 9c4b69cc..93f99b48 100644 --- a/src/renderer/components/common/ErrorBoundary.tsx +++ b/src/renderer/components/common/ErrorBoundary.tsx @@ -1,5 +1,6 @@ import React, { Component, type ErrorInfo, type ReactNode } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { captureRendererException, isSentryRendererActive } from '@renderer/sentry'; import { useStore } from '@renderer/store'; import { @@ -15,6 +16,19 @@ const logger = createLogger('Component:ErrorBoundary'); interface Props { children: ReactNode; fallback?: ReactNode; + labels?: ErrorBoundaryLabels; +} + +interface ErrorBoundaryLabels { + title: string; + description: string; + componentStack: string; + tryAgain: string; + copied: string; + copyErrorDetails: string; + reportBugOnGitHub: string; + reloadApp: string; + diagnosticsNotice: string; } interface State { @@ -24,7 +38,7 @@ interface State { errorInfo: ErrorInfo | null; } -export class ErrorBoundary extends Component { +class ErrorBoundaryInner extends Component { private copyResetTimeout: ReturnType | null = null; constructor(props: Props) { @@ -136,7 +150,7 @@ export class ErrorBoundary extends Component { // eslint-disable-next-line sonarjs/function-return-type -- Error boundaries inherently return different content based on error state render(): ReactNode { const { hasError, copiedReport, error, errorInfo } = this.state; - const { children, fallback } = this.props; + const { children, fallback, labels } = this.props; if (hasError) { if (fallback) { @@ -147,12 +161,11 @@ export class ErrorBoundary extends Component {
    -

    Something went wrong

    +

    {labels?.title}

    - An unexpected error occurred in the application. You can try reloading the page or - resetting the error state. + {labels?.description}

    {error && ( @@ -161,7 +174,7 @@ export class ErrorBoundary extends Component { {errorInfo?.componentStack && (
    - Component Stack + {labels?.componentStack}
                         {errorInfo.componentStack}
    @@ -176,7 +189,7 @@ export class ErrorBoundary extends Component {
                   onClick={this.handleReset}
                   className="flex items-center gap-2 rounded-lg border border-claude-dark-border bg-claude-dark-surface px-4 py-2 transition-colors hover:bg-claude-dark-border"
                 >
    -              Try Again
    +              {labels?.tryAgain}
                 
                 
                 
                 
               

    - GitHub bug reports and copied diagnostics include the error message, stack traces, app - version, active tab, selected team, task context, and environment details. + {labels?.diagnosticsNotice}

    ); @@ -215,3 +227,24 @@ export class ErrorBoundary extends Component { return children; } } + +export function ErrorBoundary(props: Omit): React.JSX.Element { + const { t } = useAppTranslation('common'); + + return ( + + ); +} diff --git a/src/renderer/components/common/ExportDropdown.tsx b/src/renderer/components/common/ExportDropdown.tsx index c35f71d9..def86290 100644 --- a/src/renderer/components/common/ExportDropdown.tsx +++ b/src/renderer/components/common/ExportDropdown.tsx @@ -7,6 +7,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { triggerDownload } from '@renderer/utils/sessionExporter'; import { Braces, Download, FileText, Type } from 'lucide-react'; @@ -33,6 +34,7 @@ const FORMAT_OPTIONS: FormatOption[] = [ export const ExportDropdown = ({ sessionDetail, }: Readonly): React.JSX.Element => { + const { t } = useAppTranslation('common'); const [isOpen, setIsOpen] = useState(false); const [buttonHover, setButtonHover] = useState(false); const [hoveredFormat, setHoveredFormat] = useState(null); @@ -86,7 +88,7 @@ export const ExportDropdown = ({ color: buttonHover || isOpen ? 'var(--color-text)' : 'var(--color-text-muted)', backgroundColor: buttonHover || isOpen ? 'var(--color-surface-raised)' : 'transparent', }} - title="Export session" + title={t('export.session')} > @@ -108,7 +110,7 @@ export const ExportDropdown = ({ borderBottom: '1px solid var(--color-border)', }} > - Export Session + {t('export.sessionTitle')} {/* Format options */} diff --git a/src/renderer/components/common/OngoingIndicator.tsx b/src/renderer/components/common/OngoingIndicator.tsx index 86008604..a00112cd 100644 --- a/src/renderer/components/common/OngoingIndicator.tsx +++ b/src/renderer/components/common/OngoingIndicator.tsx @@ -5,6 +5,7 @@ import React from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { Loader2 } from 'lucide-react'; interface OngoingIndicatorProps { @@ -50,6 +51,8 @@ export const OngoingIndicator = ({ * Shows animated spinner and text. */ export const OngoingBanner = (): React.JSX.Element => { + const { t } = useAppTranslation('common'); + return (
    { > - Session is in progress... + {t('sessions.inProgress')}
    ); diff --git a/src/renderer/components/common/ProviderActivityStatusStrip.tsx b/src/renderer/components/common/ProviderActivityStatusStrip.tsx index 1c576dd7..c73ac632 100644 --- a/src/renderer/components/common/ProviderActivityStatusStrip.tsx +++ b/src/renderer/components/common/ProviderActivityStatusStrip.tsx @@ -1,5 +1,6 @@ import { useEffect, useMemo, useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { isElectronMode } from '@renderer/api'; import { formatProviderStatusText } from '@renderer/components/runtime/providerConnectionUi'; import { createLoadingMultimodelCliStatus } from '@renderer/store/slices/cliInstallerSlice'; @@ -236,8 +237,10 @@ export const ProviderActivityStatusStrip = ({ codexSnapshotPending = false, providerIds, className = '', - label = 'Provider Activity', + label, }: ProviderActivityStatusStripProps): React.JSX.Element | null => { + const { t } = useAppTranslation('settings'); + const effectiveLabel = label ?? t('providerRuntime.connectionUi.status.providerActivity'); const { displayProviderIds, providerStateMap, shouldRender } = useProviderActivityDisplay({ cliStatus, sourceCliStatus, @@ -254,12 +257,12 @@ export const ProviderActivityStatusStrip = ({ return (
    - {label ? ( + {effectiveLabel ? ( - {label} + {effectiveLabel} ) : null}
    @@ -277,10 +280,10 @@ export const ProviderActivityStatusStrip = ({ const styles = getActivityToneStyles(tone); const statusText = tone === 'loading' - ? 'Checking...' + ? t('providerRuntime.connectionUi.status.checking') : tone === 'error' - ? formatProviderStatusText(providerState.provider) - : 'Checked'; + ? formatProviderStatusText(providerState.provider, t) + : t('providerRuntime.connectionUi.status.checked'); return (
    ): React.JSX.Element => { + const { t } = useAppTranslation('common'); const [isOpen, setIsOpen] = useState(false); const containerRef = useRef(null); @@ -115,7 +117,7 @@ export const RepositoryDropdown = ({ > - {isEmpty ? 'No repositories available' : placeholder} + {isEmpty ? t('repositories.noneAvailable') : placeholder} void; }>): React.JSX.Element => { + const { t } = useAppTranslation('common'); return (
    {item.path} @@ -190,6 +193,7 @@ const SelectedRepositoryItemInner = ({ onRemove: () => void; disabled?: boolean; }>): React.JSX.Element => { + const { t } = useAppTranslation('common'); return (
    @@ -212,7 +216,7 @@ const SelectedRepositoryItemInner = ({ onClick={onRemove} disabled={disabled} className={`shrink-0 rounded p-1 text-text-muted transition-colors hover:bg-red-500/10 hover:text-red-400 ${disabled ? 'cursor-not-allowed opacity-50' : ''} `} - aria-label="Remove repository" + aria-label={t('repositories.remove')} > ): React.JSX.Element => { + const { t } = useAppTranslation('common'); const [expanded, setExpanded] = useState(false); const { tokensByCategory } = contextStats; @@ -139,13 +141,14 @@ const SessionContextSection = ({ - Visible Context + {t('tokens.visibleContext')}
    - {formatTokens(contextStats.totalEstimatedTokens)} ({contextPercent}% of prompt input) + {formatTokens(contextStats.totalEstimatedTokens)} ( + {t('tokens.promptInputShare', { percent: contextPercent })})
    @@ -156,11 +159,13 @@ const SessionContextSection = ({ {tokensByCategory.claudeMd > 0 && (
    - CLAUDE.md ×{claudeMdCount} + {t('tokens.claudeMd')} ×{claudeMdCount} {formatTokens(tokensByCategory.claudeMd)}{' '} - ({claudeMdPercent}%) + + {t('tokens.percentValue', { percent: claudeMdPercent })} +
    )} @@ -169,11 +174,14 @@ const SessionContextSection = ({ {tokensByCategory.mentionedFiles > 0 && (
    - @files ×{mentionedFilesCount} + {t('tokens.mentionedFiles')}{' '} + ×{mentionedFilesCount} {formatTokens(tokensByCategory.mentionedFiles)}{' '} - ({mentionedFilesPercent}%) + + {t('tokens.percentValue', { percent: mentionedFilesPercent })} +
    )} @@ -182,11 +190,13 @@ const SessionContextSection = ({ {tokensByCategory.toolOutputs > 0 && (
    - Tool Outputs ×{toolOutputsCount} + {t('tokens.toolOutputs')} ×{toolOutputsCount} {formatTokens(tokensByCategory.toolOutputs)}{' '} - ({toolOutputsPercent}%) + + {t('tokens.percentValue', { percent: toolOutputsPercent })} +
    )} @@ -195,11 +205,14 @@ const SessionContextSection = ({ {tokensByCategory.taskCoordination > 0 && (
    - Task Coordination ×{taskCoordinationCount} + {t('tokens.taskCoordination')}{' '} + ×{taskCoordinationCount} {formatTokens(tokensByCategory.taskCoordination)}{' '} - ({taskCoordinationPercent}%) + + {t('tokens.percentValue', { percent: taskCoordinationPercent })} +
    )} @@ -208,11 +221,13 @@ const SessionContextSection = ({ {tokensByCategory.userMessages > 0 && (
    - User Messages ×{userMessagesCount} + {t('tokens.userMessages')} ×{userMessagesCount} {formatTokens(tokensByCategory.userMessages)}{' '} - ({userMessagesPercent}%) + + {t('tokens.percentValue', { percent: userMessagesPercent })} +
    )} @@ -220,10 +235,12 @@ const SessionContextSection = ({ {/* Thinking + Text */} {tokensByCategory.thinkingText > 0 && (
    - Thinking + Text + {t('tokens.thinkingText')} {formatTokens(tokensByCategory.thinkingText)}{' '} - ({thinkingTextPercent}%) + + {t('tokens.percentValue', { percent: thinkingTextPercent })} +
    )} @@ -233,7 +250,7 @@ const SessionContextSection = ({ className="pt-0.5 text-[9px] italic" style={{ color: COLOR_TEXT_MUTED, opacity: 0.7 }} > - Accumulated across entire session without duplication + {t('tokens.accumulatedWithoutDuplication')}
    )} @@ -255,6 +272,7 @@ export const TokenUsageDisplay = ({ totalPhases, costUsd, }: Readonly): React.JSX.Element => { + const { t } = useAppTranslation('common'); const totalTokens = inputTokens + cacheReadTokens + cacheCreationTokens + outputTokens; // Total prompt-side tokens only (without output) - used as denominator for visible context % const totalInputTokens = inputTokens + cacheReadTokens + cacheCreationTokens; @@ -391,7 +409,7 @@ export const TokenUsageDisplay = ({ className="rounded px-1 py-0.5 text-[10px]" style={{ backgroundColor: 'rgba(99, 102, 241, 0.15)', color: '#818cf8' }} > - Phase {phaseNumber}/{totalPhases} + {t('tokens.phase', { phase: phaseNumber, total: totalPhases })}
    )}
    {/* Input Tokens */}
    - Input Tokens + {t('tokens.inputTokens')} - Cache Read + {t('tokens.cacheRead')} - Cache Write + {t('tokens.cacheWrite')} - Output Tokens + {t('tokens.outputTokens')} - Total + {t('tokens.total')} 0 && (
    - Cost (USD) + {t('tokens.costUsd')} - incl. CLAUDE.md ×{claudeMdStats.accumulatedCount} + {t('tokens.includesClaudeMd', { count: claudeMdStats.accumulatedCount })} {totalInputTokens > 0 @@ -561,7 +579,7 @@ export const TokenUsageDisplay = ({ style={{ borderTop: '1px solid var(--color-border-subtle)' }} />
    - Model + {t('tokens.model')} { + const { t } = useAppTranslation('common'); const { showUpdateBanner, updateStatus, @@ -57,7 +59,7 @@ export const UpdateBanner = (): React.JSX.Element | null => { style={{ color: 'var(--color-text-secondary)' }} > - Updating app + {t('updates.updatingApp')} {clampedPercent}% @@ -76,7 +78,7 @@ export const UpdateBanner = (): React.JSX.Element | null => {
    - Update ready + {t('updates.updateReady')} {availableVersion ? ( v{availableVersion} @@ -94,7 +96,7 @@ export const UpdateBanner = (): React.JSX.Element | null => { } as React.CSSProperties } > - Restart now + {t('updates.restartNow')}
    )} diff --git a/src/renderer/components/common/UpdateDialog.tsx b/src/renderer/components/common/UpdateDialog.tsx index 72fa533d..82e5c846 100644 --- a/src/renderer/components/common/UpdateDialog.tsx +++ b/src/renderer/components/common/UpdateDialog.tsx @@ -9,6 +9,7 @@ import { useEffect, useRef } from 'react'; import ReactMarkdown from 'react-markdown'; +import { useAppTranslation } from '@features/localization/renderer'; import { isElectronMode } from '@renderer/api'; import { markdownComponents } from '@renderer/components/chat/markdownComponents'; import { useStore } from '@renderer/store'; @@ -19,6 +20,7 @@ import remarkGfm from 'remark-gfm'; import { useShallow } from 'zustand/react/shallow'; export const UpdateDialog = (): React.JSX.Element | null => { + const { t } = useAppTranslation('common'); const { showUpdateDialog, updateStatus, @@ -117,7 +119,7 @@ export const UpdateDialog = (): React.JSX.Element | null => { className="absolute inset-0 cursor-default" style={{ backgroundColor: 'rgba(0, 0, 0, 0.6)' }} onClick={dismissUpdateDialog} - aria-label="Close dialog" + aria-label={t('updateDialog.closeDialog')} tabIndex={-1} />
    { className="relative mx-4 w-full max-w-2xl rounded-md border p-5 shadow-lg" role="dialog" aria-modal="true" - aria-label="Update available" + aria-label={t('updateDialog.updateAvailable')} style={{ backgroundColor: 'var(--color-surface-overlay)', borderColor: 'var(--color-border-emphasis)', @@ -142,7 +144,7 @@ export const UpdateDialog = (): React.JSX.Element | null => {

    - {isDownloaded ? 'Update Ready' : 'Update Available'} + {isDownloaded ? t('updateDialog.updateReady') : t('updateDialog.updateAvailable')}

    {availableVersion && (
    { ) : (

    - No release notes available. + {t('updateDialog.noReleaseNotes')}

    )}
    @@ -192,7 +194,7 @@ export const UpdateDialog = (): React.JSX.Element | null => { style={{ color: 'var(--color-text-muted)' }} > - View on GitHub + {t('updateDialog.viewOnGitHub')} )}
    @@ -204,21 +206,21 @@ export const UpdateDialog = (): React.JSX.Element | null => { color: 'var(--color-text-secondary)', }} > - Later + {t('updateDialog.later')} {isDownloaded ? ( ) : ( )}
    diff --git a/src/renderer/components/common/WorkspaceIndicator.tsx b/src/renderer/components/common/WorkspaceIndicator.tsx index fbd70624..b5355eb8 100644 --- a/src/renderer/components/common/WorkspaceIndicator.tsx +++ b/src/renderer/components/common/WorkspaceIndicator.tsx @@ -8,6 +8,7 @@ import { useEffect, useRef, useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { useStore } from '@renderer/store'; import { Check, ChevronDown } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; @@ -15,6 +16,7 @@ import { useShallow } from 'zustand/react/shallow'; import { ConnectionStatusBadge } from './ConnectionStatusBadge'; export const WorkspaceIndicator = (): React.JSX.Element | null => { + const { t } = useAppTranslation('common'); const { activeContextId, isContextSwitching, availableContexts, switchContext } = useStore( useShallow((s) => ({ activeContextId: s.activeContextId, @@ -109,7 +111,7 @@ export const WorkspaceIndicator = (): React.JSX.Element | null => { className="px-3 py-2 text-[10px] font-semibold uppercase tracking-wider" style={{ color: 'var(--color-text-muted)' }} > - Switch Workspace + {t('context.switchWorkspace')}
    {/* Context list */} diff --git a/src/renderer/components/dashboard/CliStatusBanner.tsx b/src/renderer/components/dashboard/CliStatusBanner.tsx index 4f3a4134..ed06f95b 100644 --- a/src/renderer/components/dashboard/CliStatusBanner.tsx +++ b/src/renderer/components/dashboard/CliStatusBanner.tsx @@ -15,6 +15,7 @@ import { mergeCodexProviderStatusWithSnapshot, useCodexAccountSnapshot, } from '@features/codex-account/renderer'; +import { useAppTranslation } from '@features/localization/renderer'; import { api, isElectronMode } from '@renderer/api'; import atlasCloudLogo from '@renderer/assets/atlascloud-logo.svg'; import { confirm } from '@renderer/components/common/ConfirmDialog'; @@ -110,9 +111,6 @@ const ANTHROPIC_LIMIT_REFRESH_INTERVAL_MS = 60 * 1000; const SHOW_ATLAS_CLOUD_OPENCODE_BANNER = false; const ATLAS_CLOUD_OPENCODE_PROVIDER_ID = 'atlascloud'; const ATLAS_CLOUD_CODING_PLAN_URL = 'https://www.atlascloud.ai/console/coding-plan'; -const ATLAS_CLOUD_DESCRIPTION = - "Atlas Cloud is a full-modal AI inference platform that gives developers a single AI API to access video generation, image generation, and LLM APIs. Instead of managing multiple vendor integrations, you connect once and get unified access to 300+ curated models across all modalities. Check out Atlas Cloud's new coding plan promotion for more budget-friendly API access."; - const ProviderRuntimeSettingsDialog = lazy(() => import('@renderer/components/runtime/ProviderRuntimeSettingsDialog').then((module) => ({ default: module.ProviderRuntimeSettingsDialog, @@ -135,78 +133,92 @@ const DashboardRateLimitChips = ({ }: { providerId: CliProviderId; items: DashboardRateLimitItem[]; -}): React.JSX.Element => ( -
    - {items.map((item) => ( -
    -
    - - {item.label} - - - {item.remaining} - - - • resets {item.resetsAt} - +}): React.JSX.Element => { + const { t } = useAppTranslation('dashboard'); + + return ( +
    + {items.map((item) => ( +
    +
    + + {item.label} + + + {item.remaining} + + + • {t('cliStatus.labels.resets', { time: item.resetsAt })} + +
    -
    - ))} -
    -); + ))} +
    + ); +}; const RATE_LIMIT_SKELETON_LABELS = ['5h left', 'Weekly left'] as const; -const DashboardRateLimitSkeletonChips = (): React.JSX.Element => ( -
    - {RATE_LIMIT_SKELETON_LABELS.map((label, index) => ( -
    -
    - - {label} - - - -
    -
    - ))} -
    -); +const DashboardRateLimitSkeletonChips = (): React.JSX.Element => { + const { t } = useAppTranslation('dashboard'); -function getCodexDashboardHint(provider: CliProviderStatus): string | null { + return ( +
    + {RATE_LIMIT_SKELETON_LABELS.map((label, index) => ( +
    +
    + + {label} + + + +
    +
    + ))} +
    + ); +}; + +function getCodexDashboardHint( + provider: CliProviderStatus, + t: ReturnType['t'] +): string | null { if (provider.providerId !== 'codex') { return null; } @@ -217,25 +229,23 @@ function getCodexDashboardHint(provider: CliProviderStatus): string | null { } if (codex.login.status === 'starting' || codex.login.status === 'pending') { - return codex.login.authUrl - ? 'Finish ChatGPT login in the browser. Enter the shown code if prompted.' - : null; + return codex.login.authUrl ? t('cliStatus.hints.codexFinishLogin') : null; } const usageHint = codex.localActiveChatgptAccountPresent - ? 'Usage limits appear only after Codex refreshes the currently selected ChatGPT session. Right now the local session needs reconnect.' + ? t('cliStatus.hints.codexReconnectNeeded') : codex.localAccountArtifactsPresent - ? 'Usage limits appear only after Codex CLI sees an active ChatGPT account. Local Codex account data exists, but no active managed session is selected right now.' - : 'Usage limits appear only after Codex CLI sees an active ChatGPT account. Right now it reports no active ChatGPT login.'; + ? t('cliStatus.hints.codexNoActiveManagedSession') + : t('cliStatus.hints.codexNoActiveLogin'); if ( provider.connection?.configuredAuthMode === 'chatgpt' && provider.connection.apiKeyConfigured ) { - return `${usageHint} API key fallback is available if you switch auth mode.`; + return t('cliStatus.hints.codexApiKeyFallback', { hint: usageHint }); } if (provider.connection?.configuredAuthMode === 'auto' && provider.connection.apiKeyConfigured) { - return `${usageHint} Auto will keep using the API key until ChatGPT is connected.`; + return t('cliStatus.hints.codexAutoApiKey', { hint: usageHint }); } return provider.connection?.configuredAuthMode === 'chatgpt' ? usageHint : null; @@ -261,20 +271,27 @@ const InstallCompletedNotice = ({ }: { version: string | null; runtimeDisplayName: string; -}): React.JSX.Element => ( -
    - - - Successfully installed {runtimeDisplayName} v{version ?? 'latest'} - -
    -); +}): React.JSX.Element => { + const { t } = useAppTranslation('dashboard'); + + return ( +
    + + + {t('cliStatus.installer.success', { + runtime: runtimeDisplayName, + version: version ?? 'latest', + })} + +
    + ); +}; /** Error display with multi-line support */ const ErrorDisplay = ({ @@ -284,6 +301,7 @@ const ErrorDisplay = ({ error: string; onRetry: () => void; }): React.JSX.Element => { + const { t } = useAppTranslation('dashboard'); const lines = error.split('\n'); const title = lines[0]; const details = lines.slice(1).filter(Boolean); @@ -321,7 +339,7 @@ const ErrorDisplay = ({ style={{ borderColor: 'var(--color-border)', color: 'var(--color-text-secondary)' }} > - Retry + {t('cliStatus.actions.retry')}
    @@ -341,6 +359,7 @@ const CliCheckingSpinner = ({ styles: { border: string; bg: string }; label: string; }): React.JSX.Element => { + const { t } = useAppTranslation('dashboard'); const [showHint, setShowHint] = useState(false); useEffect(() => { @@ -363,7 +382,7 @@ const CliCheckingSpinner = ({
    {showHint && (

    - First check may take up to 30 seconds + {t('cliStatus.hints.firstCheckSlow')}

    )}
    @@ -506,7 +525,8 @@ function isPendingMultimodelProviderStatus(provider: CliProviderStatus): boolean function formatRuntimeAuthSummary( cliStatus: NonNullable['cliStatus']>, - visibleProviders: readonly CliProviderStatus[] + visibleProviders: readonly CliProviderStatus[], + t: ReturnType['t'] ): string | null { if (isMultimodelRuntimeStatus(cliStatus)) { if (visibleProviders.length === 0) { @@ -514,20 +534,20 @@ function formatRuntimeAuthSummary( } if (visibleProviders.every(isPendingMultimodelProviderStatus)) { - return 'Checking providers...'; + return t('cliStatus.provider.checkingProviders'); } const denominator = visibleProviders.length; const connected = visibleProviders.filter((provider) => provider.authenticated).length; - return `Providers: ${connected}/${denominator} connected`; + return t('cliStatus.provider.connectedCount', { connected, denominator }); } if (cliStatus.authStatusChecking) { - return 'Checking authentication...'; + return t('cliStatus.provider.checkingAuthentication'); } if (cliStatus.authLoggedIn) { - return 'Authenticated'; + return t('cliStatus.provider.authenticated'); } return null; @@ -625,32 +645,35 @@ function isRuntimeInstalling( ); } -function getRuntimeInstallLabel(status: OpenCodeRuntimeStatus | CodexRuntimeStatus | null): string { +function getRuntimeInstallLabel( + status: OpenCodeRuntimeStatus | CodexRuntimeStatus | null, + t: ReturnType['t'] +): string { if (status?.state === 'downloading') { const percent = status.progress?.percent; - return typeof percent === 'number' ? `Downloading ${percent}%` : 'Downloading'; + return typeof percent === 'number' + ? t('cliStatus.runtimeInstall.downloadingPercent', { percent }) + : t('cliStatus.runtimeInstall.downloading'); } if (status?.state === 'installing') { - return 'Installing'; + return t('cliStatus.runtimeInstall.installing'); } if (status?.state === 'checking') { - return 'Checking'; + return t('cliStatus.runtimeInstall.checking'); } if (status?.state === 'failed') { - return 'Retry install'; + return t('cliStatus.runtimeInstall.retryInstall'); } - return 'Install'; + return t('cliStatus.runtimeInstall.install'); } -const OPENCODE_PROVIDER_FREE_BADGE_TITLE = - 'OpenCode includes free model options such as Big Pickle when available in your setup. OpenRouter through OpenCode can also expose free models, but not every OpenCode/OpenRouter model is free. Availability and limits may change.'; - function shouldShowOpenCodeProviderFreeBadge(provider: CliProviderStatus): boolean { return provider.providerId === 'opencode'; } function getOpenCodeDashboardChips( - provider: CliProviderStatus + provider: CliProviderStatus, + t: ReturnType['t'] ): { label: string; title?: string }[] { if (!shouldShowOpenCodeProviderFreeBadge(provider)) { return []; @@ -670,22 +693,24 @@ function getOpenCodeDashboardChips( return [ { - label: 'Free models', - title: OPENCODE_PROVIDER_FREE_BADGE_TITLE, + label: t('cliStatus.provider.freeModels'), + title: t('cliStatus.provider.freeModelsTitle'), }, ...(configuredLocalCount > 0 ? [ { - label: `${configuredLocalCount} configured local`, - title: 'Local OpenCode routes imported from your OpenCode config.', + label: t('cliStatus.provider.configuredLocalCount', { + count: configuredLocalCount, + }), + title: t('cliStatus.provider.configuredLocalTitle'), }, ] : []), ...(verifiedCount > 0 ? [ { - label: `${verifiedCount} verified`, - title: 'OpenCode routes with a successful execution proof.', + label: t('cliStatus.provider.verifiedCount', { count: verifiedCount }), + title: t('cliStatus.provider.verifiedTitle'), }, ] : []), @@ -698,93 +723,97 @@ const OpenCodeAtlasCloudBanner = ({ }: { disabled: boolean; onConnect: () => void; -}): React.JSX.Element => ( -
    -
    -
    - Atlas Cloud - - Atlas Cloud coding plan - - - Sponsor - - - OpenCode provider - -
    -
    - - - +}): React.JSX.Element => { + const { t } = useAppTranslation('dashboard'); + + return ( +
    +
    +
    + {t('cliStatus.atlas.alt')} + + {t('cliStatus.atlas.plan')} + + + {t('cliStatus.atlas.sponsor')} + + + {t('cliStatus.atlas.openCodeProvider')} + +
    +
    + + + +
    +

    + {t('cliStatus.atlas.description')} +

    -

    - {ATLAS_CLOUD_DESCRIPTION} -

    -
    -); + ); +}; const InstalledBanner = ({ cliStatus, @@ -817,6 +846,8 @@ const InstalledBanner = ({ codexReconnectBusy, variant, }: InstalledBannerProps): React.JSX.Element => { + const { t } = useAppTranslation('dashboard'); + const { t: settingsT } = useAppTranslation('settings'); const openExtensionsTab = useStore((s) => s.openExtensionsTab); const styles = VARIANT_STYLES[variant]; const visibleProviders = useMemo( @@ -825,7 +856,7 @@ const InstalledBanner = ({ ); const canOpenExtensions = cliStatus.installed; const runtimeLabel = formatRuntimeLabel(cliStatus); - const runtimeAuthSummary = formatRuntimeAuthSummary(cliStatus, visibleProviders); + const runtimeAuthSummary = formatRuntimeAuthSummary(cliStatus, visibleProviders, t); const showCollapseControl = visibleProviders.length > 0; const showExpandedContent = !providersCollapsed; @@ -845,10 +876,16 @@ const InstalledBanner = ({ className="flex items-center justify-center rounded-md p-1 transition-colors hover:bg-white/5" style={{ color: 'var(--color-text-muted)' }} aria-label={ - providersCollapsed ? 'Expand provider details' : 'Collapse provider details' + providersCollapsed + ? t('cliStatus.labels.expandProviderDetails') + : t('cliStatus.labels.collapseProviderDetails') } aria-expanded={!providersCollapsed} - title={providersCollapsed ? 'Expand provider details' : 'Collapse provider details'} + title={ + providersCollapsed + ? t('cliStatus.labels.expandProviderDetails') + : t('cliStatus.labels.collapseProviderDetails') + } > {providersCollapsed ? ( @@ -875,7 +912,7 @@ const InstalledBanner = ({ style={{ backgroundColor: '#3b82f6' }} > - Update to v{cliStatus.latestVersion} + {t('cliStatus.actions.updateTo', { version: cliStatus.latestVersion })} ) : cliStatus.supportsSelfUpdate ? ( ) : null} @@ -917,14 +956,14 @@ const InstalledBanner = ({ style={{ borderColor: 'var(--color-border)', color: 'var(--color-text-secondary)' }} > - Extensions + {t('cliStatus.actions.extensions')} )}
    {showExpandedContent && cliStatusError && !cliStatusLoading && (

    - Failed to check for updates. Check your network connection and try again. + {t('cliStatus.errors.refreshFailed')}

    )} {showExpandedContent && visibleProviders.length > 0 && ( @@ -935,10 +974,10 @@ const InstalledBanner = ({ {visibleProviders.map((provider) => { const actionDisabled = isBusy || !cliStatus.binaryPath; const runtimeSummary = isConnectionManagedRuntimeProvider(provider) - ? getProviderCurrentRuntimeSummary(provider) + ? getProviderCurrentRuntimeSummary(provider, settingsT) : getProviderRuntimeBackendSummary(provider); - const connectionModeSummary = getProviderConnectionModeSummary(provider); - const credentialSummary = getProviderCredentialSummary(provider); + const connectionModeSummary = getProviderConnectionModeSummary(provider, settingsT); + const credentialSummary = getProviderCredentialSummary(provider, settingsT); const dashboardRateLimits = getDashboardRateLimitsForProvider(provider); const hasDashboardRateLimits = Boolean(dashboardRateLimits?.length); const isSubscriptionRateLimitMode = isDashboardRateLimitSubscriptionMode({ @@ -946,7 +985,7 @@ const InstalledBanner = ({ sourceProvider: sourceProviderMap.get(provider.providerId) ?? null, configuredAuthModes: providerConnectionAuthModes, }); - const codexDashboardHint = getCodexDashboardHint(provider); + const codexDashboardHint = getCodexDashboardHint(provider, t); const codexNeedsReconnect = provider.providerId === 'codex' && Boolean(provider.connection?.codex?.localActiveChatgptAccountPresent) && @@ -956,7 +995,7 @@ const InstalledBanner = ({ const codexLoginAuthUrl = provider.connection?.codex?.login.authUrl ?? null; const codexLoginUserCode = provider.connection?.codex?.login.userCode ?? null; const showCodexLoginActions = codexNeedsReconnect || Boolean(codexLoginAuthUrl); - const disconnectAction = getProviderDisconnectAction(provider); + const disconnectAction = getProviderDisconnectAction(provider, settingsT); const providerLoading = cliProviderStatusLoading[provider.providerId] === true; const sourceProvider = sourceProviderMap.get(provider.providerId) ?? null; const maskNegativeBootstrapState = shouldMaskCodexNegativeBootstrapState( @@ -982,7 +1021,9 @@ const InstalledBanner = ({ hasRateLimits: hasDashboardRateLimits, loading: rateLimitsLoading, }); - const statusText = showSkeleton ? 'Checking...' : formatProviderStatusText(provider); + const statusText = showSkeleton + ? t('cliStatus.actions.checking') + : formatProviderStatusText(provider, settingsT); const modelCatalogLoading = provider.modelCatalogRefreshState === 'loading' || isOpenCodeCatalogHydrating(provider); @@ -991,7 +1032,7 @@ const InstalledBanner = ({ ? getVisibleTeamProviderModels(provider.providerId, provider.models, provider) .length > 0 : provider.models.length > 0; - const openCodeDashboardChips = getOpenCodeDashboardChips(provider); + const openCodeDashboardChips = getOpenCodeDashboardChips(provider, t); const hasDetailContent = Boolean( (provider.backend?.label && !runtimeSummary) || runtimeSummary || @@ -1050,20 +1091,24 @@ const InstalledBanner = ({ style={{ color: 'var(--color-text-muted)' }} > {provider.backend?.label && !runtimeSummary && ( - Backend: {provider.backend.label} + + {t('cliStatus.provider.backend', { backend: provider.backend.label })} + )} {runtimeSummary ? ( {isConnectionManagedRuntimeProvider(provider) ? runtimeSummary - : `Runtime: ${runtimeSummary}`} + : t('cliStatus.provider.runtime', { runtime: runtimeSummary })} ) : null} {connectionModeSummary ? {connectionModeSummary} : null} {credentialSummary ? {credentialSummary} : null} - {modelCatalogLoading ? Loading models... : null} + {modelCatalogLoading ? ( + {t('cliStatus.provider.loadingModels')} + ) : null} {!hasProviderModels && !modelCatalogLoading && ( - Models unavailable for this runtime build + {t('cliStatus.provider.modelsUnavailable')} )}
    ) : null} @@ -1099,7 +1144,7 @@ const InstalledBanner = ({ color: '#fbbf24', }} > - Use code + {t('cliStatus.actions.useCode')} ) : null} ) : null} @@ -1144,7 +1191,7 @@ const InstalledBanner = ({ title={ codexRuntimeStatus?.error ?? codexRuntimeStatus?.progress?.detail ?? - 'Install Codex CLI into app data' + t('cliStatus.runtimeInstall.codexTitle') } > {isRuntimeInstalling(codexRuntimeStatus, codexRuntimeStatusLoading) ? ( @@ -1152,7 +1199,7 @@ const InstalledBanner = ({ ) : ( )} - {getRuntimeInstallLabel(codexRuntimeStatus)} + {getRuntimeInstallLabel(codexRuntimeStatus, t)} ) : null} {shouldShowOpenCodeInstallAction( @@ -1175,7 +1222,7 @@ const InstalledBanner = ({ title={ openCodeRuntimeStatus?.error ?? openCodeRuntimeStatus?.progress?.detail ?? - 'Install OpenCode runtime into app data' + t('cliStatus.runtimeInstall.openCodeTitle') } > {isRuntimeInstalling( @@ -1186,7 +1233,7 @@ const InstalledBanner = ({ ) : ( )} - {getRuntimeInstallLabel(openCodeRuntimeStatus)} + {getRuntimeInstallLabel(openCodeRuntimeStatus, t)} ) : null} {disconnectAction ? ( ) : null}
    @@ -1761,7 +1816,7 @@ export const CliStatusBanner = (): React.JSX.Element | null => { style={{ borderColor: styles.border, backgroundColor: styles.bg }} > - {runtimeDisplayName} status will be checked in the background. + {t('cliStatus.hints.backgroundStatus', { runtime: runtimeDisplayName })} ); @@ -1816,7 +1871,9 @@ export const CliStatusBanner = (): React.JSX.Element | null => { return ( ); } @@ -1832,7 +1889,7 @@ export const CliStatusBanner = (): React.JSX.Element | null => {
    - Downloading {runtimeDisplayName}... + {t('cliStatus.installer.downloading', { runtime: runtimeDisplayName })}
    @@ -1864,7 +1921,9 @@ export const CliStatusBanner = (): React.JSX.Element | null => { // ── Checking / Verifying ─────────────────────────────────────────────── if (installerState === 'checking' || installerState === 'verifying') { const label = - installerState === 'checking' ? 'Checking latest version...' : 'Verifying checksum...'; + installerState === 'checking' + ? t('cliStatus.installer.checkingLatest') + : t('cliStatus.installer.verifying'); return (
    {
    - Installing {runtimeDisplayName}... + {t('cliStatus.installer.installing', { runtime: runtimeDisplayName })}
    @@ -1919,7 +1978,10 @@ export const CliStatusBanner = (): React.JSX.Element | null => { className={`mb-6 rounded-lg border-l-4 px-4 py-3 ${BANNER_MIN_H}`} style={{ borderColor: styles.border, backgroundColor: styles.bg }} > - +
    ); } @@ -1943,13 +2005,17 @@ export const CliStatusBanner = (): React.JSX.Element | null => {

    {cliLaunchIssue - ? `${runtimeDisplayName} was found but failed to start` - : `${runtimeDisplayName} is required`} + ? t('cliStatus.runtime.foundButFailed', { runtime: runtimeDisplayName }) + : t('cliStatus.runtime.isRequired', { runtime: runtimeDisplayName })}

    {cliLaunchIssue - ? `The app found the configured ${runtimeDisplayName}, but its startup health check failed. Repair or reinstall it, then retry.` - : `${runtimeDisplayName} is required for team provisioning and session management. Install it to get started.`} + ? t('cliStatus.runtime.healthCheckFailedDescription', { + runtime: runtimeDisplayName, + }) + : t('cliStatus.runtime.installRequiredDescription', { + runtime: runtimeDisplayName, + })}

    {renderCliStatus.showBinaryPath && renderCliStatus.binaryPath && (

    { style={{ borderColor: 'var(--color-border)', color: 'var(--color-text-secondary)' }} > - Re-check + {t('cliStatus.actions.recheck')} {renderCliStatus.supportsSelfUpdate ? ( ) : (

    {cliLaunchIssue - ? `The configured ${runtimeDisplayName} failed its startup health check.` - : `The configured ${runtimeDisplayName} was not found.`} + ? t('cliStatus.runtime.configuredHealthCheckFailed', { + runtime: runtimeDisplayName, + }) + : t('cliStatus.runtime.configuredNotFound', { runtime: runtimeDisplayName })}

    )}
    @@ -2071,18 +2139,22 @@ export const CliStatusBanner = (): React.JSX.Element | null => { hasApiKeyModeIssue && apiKeyMissingProviders.length === apiKeyActionRequiredProviders.length; const warningTitle = hasApiKeyModeIssue ? allApiKeyIssuesAreMissingKeys - ? 'API key required' - : 'Provider action required' - : 'Not logged in'; + ? t('cliStatus.labels.apiKeyRequired') + : t('cliStatus.labels.providerActionRequired') + : t('cliStatus.labels.notLoggedIn'); const warningMessage = hasApiKeyModeIssue ? allApiKeyIssuesAreMissingKeys ? apiKeyActionRequiredProviders.length === 1 && primaryApiKeyProvider - ? `${primaryApiKeyProvider.displayName} is set to API key mode, but no API key is configured. Open Manage Providers to add a key or switch the connection mode.` - : 'One or more providers are set to API key mode, but no API key is configured. Open Manage Providers to add keys or switch the connection mode.' + ? t('cliStatus.warnings.singleApiKeyMissing', { + provider: primaryApiKeyProvider.displayName, + }) + : t('cliStatus.warnings.multipleApiKeysMissing') : apiKeyActionRequiredProviders.length === 1 && primaryApiKeyProvider - ? `${primaryApiKeyProvider.displayName} is set to API key mode, but it is not connected. Open Manage Providers to review the saved key or switch the connection mode.` - : 'One or more providers are set to API key mode and need attention. Open Manage Providers to review saved keys or switch the connection mode.' - : `${runtimeDisplayName} is installed but you are not authenticated. Login is required for team provisioning and AI features.`; + ? t('cliStatus.warnings.singleApiKeyNeedsAttention', { + provider: primaryApiKeyProvider.displayName, + }) + : t('cliStatus.warnings.multipleApiKeysNeedAttention') + : t('cliStatus.warnings.notAuthenticated', { runtime: runtimeDisplayName }); return ( <> @@ -2146,7 +2218,7 @@ export const CliStatusBanner = (): React.JSX.Element | null => { style={{ backgroundColor: '#f59e0b' }} > - Manage Providers + {t('cliStatus.actions.manageProviders')} ) : ( <> @@ -2159,7 +2231,7 @@ export const CliStatusBanner = (): React.JSX.Element | null => { }} > - Already logged in? + {t('cliStatus.actions.alreadyLoggedIn')} {showTroubleshoot ? ( ) : ( @@ -2172,7 +2244,7 @@ export const CliStatusBanner = (): React.JSX.Element | null => { style={{ backgroundColor: '#f59e0b' }} > - Login + {t('cliStatus.actions.login')} )} @@ -2191,14 +2263,14 @@ export const CliStatusBanner = (): React.JSX.Element | null => { className="mb-2 text-xs font-medium" style={{ color: 'var(--color-text-secondary)' }} > - If you're sure you're logged in, try these steps: + {t('cliStatus.hints.troubleshootTitle')}

    1. - Click{' '} + {t('cliStatus.troubleshoot.click')}{' '} {' '} - — sometimes the status is cached for a few seconds + {t('cliStatus.troubleshoot.statusCacheHint')}
    2. - Open your terminal and run:{' '} + {t('cliStatus.troubleshoot.openTerminal')}{' '} {renderCliStatus.showBinaryPath && renderCliStatus.binaryPath ? `"${renderCliStatus.binaryPath}" auth status` - : 'your configured CLI auth status command'} + : t('cliStatus.troubleshoot.authStatusCommand')} {' '} - — check if it shows "Logged in" + {t('cliStatus.troubleshoot.checkLoggedIn')}
    3. - If it says logged in but the app doesn't see it, try:{' '} + {t('cliStatus.troubleshoot.reloginPrefix')}{' '} {renderCliStatus.showBinaryPath && renderCliStatus.binaryPath ? `"${renderCliStatus.binaryPath}" auth logout` - : 'the runtime logout command'} + : t('cliStatus.troubleshoot.logoutCommand')} {' '} - then{' '} + {t('cliStatus.troubleshoot.then')}{' '} {renderCliStatus.showBinaryPath && renderCliStatus.binaryPath ? `"${renderCliStatus.binaryPath}" auth login` - : 'the runtime login command'} + : t('cliStatus.troubleshoot.loginCommand')} {' '} - again + {t('cliStatus.troubleshoot.again')}
    4. - Make sure the CLI in your terminal is the same runtime the app uses + {t('cliStatus.troubleshoot.sameRuntime')} {renderCliStatus.showBinaryPath && renderCliStatus.binaryPath && ( :{' '} @@ -2261,8 +2333,7 @@ export const CliStatusBanner = (): React.JSX.Element | null => {

    - Browsing sessions and projects works without login. Login is only needed to run - agent teams. + {t('cliStatus.hints.loginRequiredForTeams')}

    )} @@ -2271,7 +2342,9 @@ export const CliStatusBanner = (): React.JSX.Element | null => { {showLoginTerminal && renderCliStatus.binaryPath && ( { @@ -2306,8 +2379,8 @@ export const CliStatusBanner = (): React.JSX.Element | null => { })(); }} autoCloseOnSuccessMs={4000} - successMessage="Login complete" - failureMessage="Login failed" + successMessage={t('cliStatus.labels.loginComplete')} + failureMessage={t('cliStatus.labels.loginFailed')} /> )} diff --git a/src/renderer/components/dashboard/DashboardUpdateBanner.tsx b/src/renderer/components/dashboard/DashboardUpdateBanner.tsx index 598049fd..7479f2b8 100644 --- a/src/renderer/components/dashboard/DashboardUpdateBanner.tsx +++ b/src/renderer/components/dashboard/DashboardUpdateBanner.tsx @@ -8,6 +8,7 @@ import { useEffect, useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { useStore } from '@renderer/store'; import { ArrowUpCircle, X } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; @@ -15,6 +16,7 @@ import { useShallow } from 'zustand/react/shallow'; const DISMISSED_KEY = 'update:dashboard-dismissed-version'; export const DashboardUpdateBanner = (): React.JSX.Element | null => { + const { t } = useAppTranslation('dashboard'); const { updateStatus, availableVersion, openUpdateDialog, installUpdate } = useStore( useShallow((s) => ({ updateStatus: s.updateStatus, @@ -57,7 +59,7 @@ export const DashboardUpdateBanner = (): React.JSX.Element | null => { > - New version available{' '} + {t('updateBanner.newVersionAvailable')}{' '} {availableVersion && ( v{availableVersion} )} @@ -70,7 +72,7 @@ export const DashboardUpdateBanner = (): React.JSX.Element | null => { color: '#4ade80', }} > - {isDownloaded ? 'Restart now' : 'View details'} + {isDownloaded ? t('updateBanner.restartNow') : t('updateBanner.viewDetails')} - or + {t('actions.or')}
    @@ -138,14 +141,14 @@ export const DashboardView = (): React.JSX.Element => {

    - {searchQuery.trim() ? 'Search Results' : 'Recent Projects'} + {searchQuery.trim() ? t('recentProjects.searchResults') : t('recentProjects.title')}

    {searchQuery.trim() && ( )}
    diff --git a/src/renderer/components/dashboard/WebPreviewBanner.tsx b/src/renderer/components/dashboard/WebPreviewBanner.tsx index 1c7a7cdd..1d63ffc5 100644 --- a/src/renderer/components/dashboard/WebPreviewBanner.tsx +++ b/src/renderer/components/dashboard/WebPreviewBanner.tsx @@ -1,7 +1,9 @@ +import { useAppTranslation } from '@features/localization/renderer'; import { isElectronMode } from '@renderer/api'; import { FlaskConical } from 'lucide-react'; export const WebPreviewBanner = (): React.JSX.Element | null => { + const { t } = useAppTranslation('dashboard'); if (isElectronMode()) { return null; } @@ -16,13 +18,8 @@ export const WebPreviewBanner = (): React.JSX.Element | null => { >
    -

    - Open the desktop app for full functionality -

    -

    - The browser version is still in development. Project actions, integrations, and live - status updates may be limited here. Use the desktop app to access all features reliably. -

    +

    {t('webPreview.title')}

    +

    {t('webPreview.description')}

    ); diff --git a/src/renderer/components/dashboard/WindowsAdministratorBanner.tsx b/src/renderer/components/dashboard/WindowsAdministratorBanner.tsx index 633b708a..eda3b776 100644 --- a/src/renderer/components/dashboard/WindowsAdministratorBanner.tsx +++ b/src/renderer/components/dashboard/WindowsAdministratorBanner.tsx @@ -1,11 +1,13 @@ import { useEffect, useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { api, isElectronMode } from '@renderer/api'; import { AlertTriangle } from 'lucide-react'; import type { WindowsElevationStatus } from '@shared/types/api'; export const WindowsAdministratorBanner = (): React.JSX.Element | null => { + const { t } = useAppTranslation('dashboard'); const [status, setStatus] = useState(null); useEffect(() => { @@ -51,12 +53,9 @@ export const WindowsAdministratorBanner = (): React.JSX.Element | null => { >
    -
    - Windows Administrator mode recommended -
    +
    {t('windowsAdmin.title')}

    - OpenCode runtime checks can time out when Agent Teams AI is not elevated. Restart the app - with Run as administrator before launching OpenCode teams. + {t('windowsAdmin.description')}

    diff --git a/src/renderer/components/extensions/ExtensionStoreView.tsx b/src/renderer/components/extensions/ExtensionStoreView.tsx index d635f6bf..df7e310c 100644 --- a/src/renderer/components/extensions/ExtensionStoreView.tsx +++ b/src/renderer/components/extensions/ExtensionStoreView.tsx @@ -10,6 +10,7 @@ import { mergeCodexProviderStatusWithSnapshot, useCodexAccountSnapshot, } from '@features/codex-account/renderer'; +import { useAppTranslation } from '@features/localization/renderer'; import { api, isElectronMode } from '@renderer/api'; import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo'; import { Badge } from '@renderer/components/ui/badge'; @@ -63,33 +64,36 @@ const ProviderCapabilityCardSkeleton = ({ }: { providerId: 'anthropic' | 'codex' | 'gemini' | 'opencode'; displayName: string; -}): React.JSX.Element => ( -
    -
    -
    -

    - - {displayName} -

    -
    - - Checking provider status... +}): React.JSX.Element => { + const { t } = useAppTranslation('extensions'); + return ( +
    +
    +
    +

    + + {displayName} +

    +
    + + {t('store.provider.checkingStatus')} +
    + + {t('store.provider.loading')} + +
    +
    + {Array.from({ length: 3 }, (_, index) => ( + + ))}
    - - Loading... -
    -
    - {Array.from({ length: 3 }, (_, index) => ( - - ))} -
    -
    -); + ); +}; function isProviderCapabilityCardLoading( provider: CliProviderStatus, @@ -112,6 +116,7 @@ function isCodexSnapshotPending( } export const ExtensionStoreView = (): React.JSX.Element => { + const { t } = useAppTranslation('extensions'); const isElectron = useMemo(() => isElectronMode(), []); const tabId = useTabIdOptional(); const { @@ -222,34 +227,30 @@ export const ExtensionStoreView = (): React.JSX.Element => { () => [ { value: 'plugins' as const, - label: 'Plugins', + label: t('store.tabs.plugins.label'), icon: Puzzle, - description: - 'Small add-ons for the runtime. In multimodel mode they currently apply to Anthropic sessions when supported. Broader provider support is in development.', + description: t('store.tabs.plugins.description'), }, { value: 'mcp-servers' as const, - label: 'MCP Servers', + label: t('store.tabs.mcpServers.label'), icon: Server, - description: - 'Connections to outside tools and apps. They let the runtime read data or do actions beyond this app.', + description: t('store.tabs.mcpServers.description'), }, { value: 'skills' as const, - label: 'Skills', + label: t('store.tabs.skills.label'), icon: BookOpen, - description: - 'Ready-made instructions for common jobs. They help the runtime handle repeatable tasks more consistently.', + description: t('store.tabs.skills.description'), }, { value: 'api-keys' as const, - label: 'API Keys', + label: t('store.tabs.apiKeys.label'), icon: Key, - description: - 'Secret keys for online services. Add them here so plugins, servers, and integrations can connect and work.', + description: t('store.tabs.apiKeys.description'), }, ], - [] + [t] ); // Fetch plugin catalog on mount @@ -343,11 +344,10 @@ export const ExtensionStoreView = (): React.JSX.Element => {

    - Checking extensions runtime availability + {t('store.runtime.checkingAvailabilityTitle')}

    - Extensions need the configured runtime to manage plugins, MCP servers, skills, and - provider connections. + {t('store.runtime.checkingAvailabilityDescription')}

    @@ -364,13 +364,13 @@ export const ExtensionStoreView = (): React.JSX.Element => {

    {cliLaunchIssue - ? 'The configured runtime was found but failed to start' - : 'The configured runtime is not available'} + ? t('store.runtime.failedToStartTitle') + : t('store.runtime.notAvailableTitle')}

    {cliLaunchIssue - ? 'Extensions are disabled until the runtime passes its startup health check. Open the Dashboard to repair or reinstall it.' - : 'Extensions are disabled until the runtime is installed. Open the Dashboard to install it and retry.'} + ? t('store.runtime.failedToStartDescription') + : t('store.runtime.notAvailableDescription')}

    {cliLaunchIssue && effectiveCliStatus.launchError && (

    @@ -379,7 +379,7 @@ export const ExtensionStoreView = (): React.JSX.Element => { )}

    ); @@ -390,17 +390,20 @@ export const ExtensionStoreView = (): React.JSX.Element => {
    -

    {runtimeDisplayName} needs sign-in

    +

    + {t('store.runtime.needsSignInTitle', { runtime: runtimeDisplayName })} +

    - {runtimeDisplayName} was found - {effectiveCliStatus.installedVersion - ? ` (${effectiveCliStatus.installedVersion})` - : ''} - , but plugin installs are disabled until you sign in from the Dashboard. + {t('store.runtime.needsSignInDescription', { + runtime: runtimeDisplayName, + version: effectiveCliStatus.installedVersion + ? ` (${effectiveCliStatus.installedVersion})` + : '', + })}

    ); @@ -412,10 +415,11 @@ export const ExtensionStoreView = (): React.JSX.Element => {
    -

    Multimodel runtime capabilities

    +

    + {t('store.runtime.multimodelCapabilitiesTitle')} +

    - Provider support can differ by section. Plugins are shown only where the runtime - explicitly declares support. + {t('store.runtime.multimodelCapabilitiesDescription')}

    @@ -444,8 +448,11 @@ export const ExtensionStoreView = (): React.JSX.Element => { const statusLabel = provider.authenticated ? 'Connected' : provider.supported - ? 'Needs setup' - : 'Unsupported'; + ? t('store.provider.needsSetup') + : t('store.provider.unsupported'); + const finalStatusLabel = provider.authenticated + ? t('store.provider.connected') + : statusLabel; const extensionCapabilities = getCliProviderExtensionCapabilities(provider); const pluginStatus = extensionCapabilities.plugins.status; @@ -466,11 +473,11 @@ export const ExtensionStoreView = (): React.JSX.Element => {

    {provider.statusMessage ?? provider.backend?.label ?? - 'Ready to configure'} + t('store.provider.readyToConfigure')}

    - {statusLabel} + {finalStatusLabel}
    @@ -482,13 +489,21 @@ export const ExtensionStoreView = (): React.JSX.Element => { : undefined } > - Plugins: {formatCliExtensionCapabilityStatus(pluginStatus)} + {t('store.capabilities.plugins', { + status: formatCliExtensionCapabilityStatus(pluginStatus), + })} - MCP: {formatCliExtensionCapabilityStatus(extensionCapabilities.mcp.status)} + {t('store.capabilities.mcp', { + status: formatCliExtensionCapabilityStatus( + extensionCapabilities.mcp.status + ), + })} - Skills: {extensionCapabilities.skills.ownership} + {t('store.capabilities.skills', { + status: extensionCapabilities.skills.ownership, + })}
    @@ -504,13 +519,16 @@ export const ExtensionStoreView = (): React.JSX.Element => {
    -

    {runtimeDisplayName} is ready

    +

    + {t('store.runtime.readyTitle', { runtime: runtimeDisplayName })} +

    - Plugins can be installed from this page - {effectiveCliStatus.installedVersion - ? ` using ${runtimeDisplayName} ${effectiveCliStatus.installedVersion}` - : ''} - . + {t('store.runtime.readyDescription', { + runtime: runtimeDisplayName, + versionSuffix: effectiveCliStatus.installedVersion + ? ` using ${runtimeDisplayName} ${effectiveCliStatus.installedVersion}` + : '', + })}

    @@ -522,6 +540,7 @@ export const ExtensionStoreView = (): React.JSX.Element => { effectiveCliStatusLoading, openDashboard, runtimeDisplayName, + t, ]); // Browser mode guard @@ -530,8 +549,8 @@ export const ExtensionStoreView = (): React.JSX.Element => {
    -

    Extensions

    -

    Available in the desktop app only.

    +

    {t('store.title')}

    +

    {t('store.desktopOnly')}

    ); @@ -546,7 +565,7 @@ export const ExtensionStoreView = (): React.JSX.Element => {
    -

    Extensions

    +

    {t('store.title')}

    @@ -554,7 +573,7 @@ export const ExtensionStoreView = (): React.JSX.Element => { - Refresh catalog + {t('store.actions.refreshCatalog')}
    @@ -564,15 +583,14 @@ export const ExtensionStoreView = (): React.JSX.Element => { {!cliInstalled && (
    - The configured runtime is required to install or uninstall extensions. Install or - repair it from the Dashboard. + {t('store.runtime.requiredForMutations')}
    )} {/* Active sessions warning */} {hasOngoingSessions && (
    - Running sessions won't pick up extension changes until restarted. + {t('store.sessionsRestartWarning')}
    )} { disabled={Boolean(mcpMutationDisableReason)} > - Add Custom + {t('store.actions.addCustom')}
    diff --git a/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx b/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx index a946adec..80940929 100644 --- a/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx +++ b/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx @@ -4,6 +4,7 @@ import { useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { Badge } from '@renderer/components/ui/badge'; import { Button } from '@renderer/components/ui/button'; import { @@ -23,6 +24,7 @@ interface ApiKeyCardProps { } export const ApiKeyCard = ({ apiKey, onEdit }: ApiKeyCardProps): React.JSX.Element => { + const { t } = useAppTranslation('extensions'); const deleteApiKey = useStore((s) => s.deleteApiKey); const [copied, setCopied] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false); @@ -117,7 +119,7 @@ export const ApiKeyCard = ({ apiKey, onEdit }: ApiKeyCardProps): React.JSX.Eleme - Edit + {t('apiKeys.actions.edit')} diff --git a/src/renderer/components/extensions/apikeys/ApiKeyFormDialog.tsx b/src/renderer/components/extensions/apikeys/ApiKeyFormDialog.tsx index f2b53396..45035a46 100644 --- a/src/renderer/components/extensions/apikeys/ApiKeyFormDialog.tsx +++ b/src/renderer/components/extensions/apikeys/ApiKeyFormDialog.tsx @@ -5,6 +5,7 @@ import { useEffect, useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { Button } from '@renderer/components/ui/button'; import { Dialog, @@ -39,11 +40,6 @@ interface ApiKeyFormDialogProps { type Scope = 'user' | 'project'; -const SCOPE_OPTIONS: { value: Scope; label: string }[] = [ - { value: 'user', label: 'User (global)' }, - { value: 'project', label: 'Project' }, -]; - export const ApiKeyFormDialog = ({ open, editingKey, @@ -51,6 +47,7 @@ export const ApiKeyFormDialog = ({ currentProjectLabel, onClose, }: ApiKeyFormDialogProps): React.JSX.Element => { + const { t } = useAppTranslation('extensions'); const saveApiKey = useStore((s) => s.saveApiKey); const apiKeySaving = useStore((s) => s.apiKeySaving); const storageStatus = useStore((s) => s.apiKeyStorageStatus); @@ -101,7 +98,7 @@ export const ApiKeyFormDialog = ({ return; } if (!ENV_KEY_RE.test(v)) { - setEnvVarError('Use letters, digits, underscores. Must start with a letter or underscore.'); + setEnvVarError(t('apiKeys.form.errors.invalidEnvVarFormat')); } else { setEnvVarError(null); } @@ -112,23 +109,23 @@ export const ApiKeyFormDialog = ({ setError(null); if (!name.trim()) { - setError('Name is required'); + setError(t('apiKeys.form.errors.nameRequired')); return; } if (!envVarName.trim()) { - setError('Environment variable name is required'); + setError(t('apiKeys.form.errors.envVarRequired')); return; } if (!ENV_KEY_RE.test(envVarName)) { - setError('Invalid environment variable name'); + setError(t('apiKeys.form.errors.invalidEnvVar')); return; } if (!value) { - setError('Key value is required'); + setError(t('apiKeys.form.errors.valueRequired')); return; } if (scope === 'project' && !effectiveProjectPath) { - setError('Project-scoped API keys require an active project'); + setError(t('apiKeys.form.errors.projectScopeRequiresProject')); return; } @@ -143,7 +140,7 @@ export const ApiKeyFormDialog = ({ }); onClose(); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to save'); + setError(err instanceof Error ? err.message : t('apiKeys.form.errors.saveFailed')); } }; @@ -165,11 +162,11 @@ export const ApiKeyFormDialog = ({
    - {isEdit ? 'Edit API Key' : 'Add API Key'} + + {isEdit ? t('apiKeys.form.editTitle') : t('apiKeys.form.addTitle')} + - {isEdit - ? 'Update the key details. You must re-enter the value.' - : 'Store an API key for auto-filling in MCP server installations.'} + {isEdit ? t('apiKeys.form.editDescription') : t('apiKeys.form.addDescription')}
    @@ -178,8 +175,7 @@ export const ApiKeyFormDialog = ({ {storageStatus && storageStatus.encryptionMethod !== 'os-keychain' && (
    - OS keychain unavailable — keys encrypted with AES-256 locally. Install gnome-keyring for - OS-level protection. + {t('apiKeys.form.keychainUnavailable')}
    )} @@ -187,13 +183,13 @@ export const ApiKeyFormDialog = ({ {/* Name */}
    setName(e.target.value)} - placeholder="e.g. OpenAI Production" + placeholder={t('apiKeys.form.namePlaceholder')} className="h-8 text-sm" autoFocus /> @@ -202,7 +198,7 @@ export const ApiKeyFormDialog = ({ {/* Env var name */}
    {envVarError &&

    {envVarError}

    } @@ -220,43 +216,47 @@ export const ApiKeyFormDialog = ({ {/* Value */}
    setValue(e.target.value)} - placeholder={isEdit ? 'Re-enter key value' : 'sk-...'} + placeholder={ + isEdit ? t('apiKeys.form.reenterValue') : t('apiKeys.form.valuePlaceholder') + } className="h-8 text-sm" />
    {/* Scope */}
    - + {scope === 'project' && effectiveProjectPath && ( -

    Bound to {effectiveProjectPath}

    +

    + {t('apiKeys.form.boundTo', { path: effectiveProjectPath })} +

    )}
    @@ -270,10 +270,14 @@ export const ApiKeyFormDialog = ({ {/* Actions */}
    diff --git a/src/renderer/components/extensions/apikeys/ApiKeysPanel.tsx b/src/renderer/components/extensions/apikeys/ApiKeysPanel.tsx index 4d70cc9d..8b663758 100644 --- a/src/renderer/components/extensions/apikeys/ApiKeysPanel.tsx +++ b/src/renderer/components/extensions/apikeys/ApiKeysPanel.tsx @@ -8,6 +8,7 @@ import { mergeCodexProviderStatusWithSnapshot, useCodexAccountSnapshot, } from '@features/codex-account/renderer'; +import { useAppTranslation } from '@features/localization/renderer'; import { isElectronMode } from '@renderer/api'; import { Button } from '@renderer/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; @@ -30,6 +31,7 @@ export const ApiKeysPanel = ({ projectPath, projectLabel, }: ApiKeysPanelProps): React.JSX.Element => { + const { t } = useAppTranslation('extensions'); const isElectron = useMemo(() => isElectronMode(), []); const { apiKeys, @@ -188,7 +190,7 @@ export const ApiKeysPanel = ({ {/* Header row */}

    - Securely store API keys for auto-filling when installing MCP servers. + {t('apiKeys.description')} {storageStatus && ( @@ -200,15 +202,9 @@ export const ApiKeysPanel = ({ {isOsKeychain ? ( -

    - Keys are encrypted via {storageStatus.backend} and stored with restricted file - permissions (owner-only). -

    +

    {t('apiKeys.storage.osKeychain', { backend: storageStatus.backend })}

    ) : ( -

    - OS keychain unavailable — keys are encrypted locally with AES-256. For stronger - protection, install a keyring service (gnome-keyring, kwallet). -

    +

    {t('apiKeys.storage.localEncryption')}

    )} @@ -216,7 +212,7 @@ export const ApiKeysPanel = ({

    @@ -250,13 +246,11 @@ export const ApiKeysPanel = ({
    -

    No API keys saved

    -

    - Add keys to auto-fill environment variables when installing MCP servers. -

    +

    {t('apiKeys.empty.title')}

    +

    {t('apiKeys.empty.description')}

    )} diff --git a/src/renderer/components/extensions/common/InstallButton.tsx b/src/renderer/components/extensions/common/InstallButton.tsx index 653b9ef2..e509c439 100644 --- a/src/renderer/components/extensions/common/InstallButton.tsx +++ b/src/renderer/components/extensions/common/InstallButton.tsx @@ -5,6 +5,7 @@ import { useEffect, useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { Button } from '@renderer/components/ui/button'; import { Tooltip, @@ -48,6 +49,7 @@ export const InstallButton = ({ cliStatus: cliStatusOverride, cliStatusLoading: cliStatusLoadingOverride, }: InstallButtonProps) => { + const { t } = useAppTranslation('extensions'); const { cliStatus: storedCliStatus, cliStatusLoading: storedCliStatusLoading } = useStore( useShallow((s) => ({ cliStatus: s.cliStatus, @@ -77,7 +79,9 @@ export const InstallButton = ({ ); @@ -87,7 +91,7 @@ export const InstallButton = ({ return ( ); } @@ -111,7 +115,7 @@ export const InstallButton = ({ }} disabled={isDisabled} > - Retry + {t('installButton.retry')} ); @@ -152,7 +156,7 @@ export const InstallButton = ({ disabled={isDisabled} > - Uninstall + {t('installButton.uninstall')} ) : ( ); diff --git a/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx b/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx index 114f9cb8..1f029337 100644 --- a/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx +++ b/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx @@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { api } from '@renderer/api'; import { Button } from '@renderer/components/ui/button'; import { @@ -75,6 +76,7 @@ export const CustomMcpServerDialog = ({ cliStatus: cliStatusOverride, cliStatusLoading: cliStatusLoadingOverride, }: CustomMcpServerDialogProps): React.JSX.Element => { + const { t } = useAppTranslation('extensions'); const installCustomMcpServer = useStore((s) => s.installCustomMcpServer); const storedCliStatus = useStore((s) => s.cliStatus); const storedCliStatusLoading = useStore((s) => s.cliStatusLoading); @@ -231,11 +233,11 @@ export const CustomMcpServerDialog = ({ } if (!serverName.trim()) { - setError('Server name is required'); + setError(t('customMcp.errors.serverNameRequired')); return; } if (!SERVER_NAME_RE.test(serverName)) { - setError('Invalid server name. Use alphanumeric characters, dashes, underscores, dots.'); + setError(t('customMcp.errors.invalidServerName')); return; } @@ -243,7 +245,7 @@ export const CustomMcpServerDialog = ({ if (transportMode === 'stdio') { if (!npmPackage.trim()) { - setError('npm package name is required'); + setError(t('customMcp.errors.npmPackageRequired')); return; } installSpec = { @@ -253,7 +255,7 @@ export const CustomMcpServerDialog = ({ }; } else { if (!httpUrl.trim()) { - setError('Server URL is required'); + setError(t('customMcp.errors.serverUrlRequired')); return; } installSpec = { @@ -284,7 +286,7 @@ export const CustomMcpServerDialog = ({ await installCustomMcpServer(request); onClose(); } catch (err) { - setError(err instanceof Error ? err.message : 'Install failed'); + setError(err instanceof Error ? err.message : t('customMcp.errors.installFailed')); } finally { setInstalling(false); } @@ -316,8 +318,8 @@ export const CustomMcpServerDialog = ({
    - Add Custom MCP Server - Add a server manually without the catalog. + {t('customMcp.title')} + {t('customMcp.description')}
    @@ -326,13 +328,13 @@ export const CustomMcpServerDialog = ({ {/* Server name */}
    setServerName(e.target.value)} - placeholder="my-server" + placeholder={t('customMcp.placeholders.serverName')} className="h-8 text-sm" autoFocus /> @@ -340,7 +342,7 @@ export const CustomMcpServerDialog = ({ {/* Transport toggle */}
    - +
    @@ -366,7 +368,7 @@ export const CustomMcpServerDialog = ({
    setNpmVersion(e.target.value)} - placeholder="latest" + placeholder={t('customMcp.placeholders.latest')} className="h-8 text-sm" />
    @@ -396,18 +398,18 @@ export const CustomMcpServerDialog = ({
    setHttpUrl(e.target.value)} - placeholder="https://api.example.com/mcp" + placeholder={t('customMcp.placeholders.serverUrl')} className="h-8 text-sm" />
    - + updateHeader(i, 'value', e.target.value)} className="h-7 flex-1 text-xs" - placeholder="value" + placeholder={t('customMcp.placeholders.value')} />
    diff --git a/src/renderer/components/extensions/mcp/McpServerCard.tsx b/src/renderer/components/extensions/mcp/McpServerCard.tsx index 3e6d0f6e..0a60fec0 100644 --- a/src/renderer/components/extensions/mcp/McpServerCard.tsx +++ b/src/renderer/components/extensions/mcp/McpServerCard.tsx @@ -5,6 +5,7 @@ import { useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { api } from '@renderer/api'; import { Badge } from '@renderer/components/ui/badge'; import { Button } from '@renderer/components/ui/button'; @@ -56,6 +57,7 @@ export const McpServerCard = ({ cliStatus: cliStatusOverride, cliStatusLoading, }: McpServerCardProps): React.JSX.Element => { + const { t } = useAppTranslation('extensions'); const storedCliStatus = useStore((s) => s.cliStatus); const cliStatus = cliStatusOverride ?? storedCliStatus; const sharedScope = getDefaultMcpSharedScope(cliStatus?.flavor); @@ -179,19 +181,19 @@ export const McpServerCard = ({ {server.tools.length > 0 && ( - {server.tools.length} {server.tools.length === 1 ? 'tool' : 'tools'} + {t('mcpCard.toolsCount', { count: server.tools.length })} )} {server.envVars.length > 0 && ( - {server.envVars.length} {server.envVars.length === 1 ? 'env' : 'envs'} + {t('mcpCard.envCount', { count: server.envVars.length })} )} {server.requiresAuth && ( - Auth + {t('mcpCard.auth')} )} {server.version && ( @@ -206,23 +208,25 @@ export const McpServerCard = ({ {formatRelativeTime(server.updatedAt)} )} - {server.author && by {server.author}} + {server.author && ( + {t('mcpCard.byAuthor', { author: server.author })} + )} {server.hostingType === 'remote' && ( - Remote + {t('mcpCard.hosting.remote')} )} {server.hostingType === 'local' && ( - Local + {t('mcpCard.hosting.local')} )} {server.hostingType === 'both' && ( - Both + {t('mcpCard.hosting.both')} )} {/* External links + stars */} @@ -245,7 +249,7 @@ export const McpServerCard = ({ )} - Repository + {t('mcpCard.repository')} )} {server.websiteUrl && ( @@ -261,7 +265,7 @@ export const McpServerCard = ({ - Website + {t('mcpCard.website')} )}
    diff --git a/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx b/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx index 0cd87512..59bfeb3e 100644 --- a/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +++ b/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx @@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { api } from '@renderer/api'; import { Badge } from '@renderer/components/ui/badge'; import { Button } from '@renderer/components/ui/button'; @@ -82,6 +83,7 @@ export const McpServerDetailDialog = ({ cliStatus: cliStatusOverride, cliStatusLoading, }: McpServerDetailDialogProps): React.JSX.Element => { + const { t } = useAppTranslation('extensions'); const storedCliStatus = useStore((s) => s.cliStatus); const cliStatus = cliStatusOverride ?? storedCliStatus; const defaultSharedScope = getDefaultMcpSharedScope(cliStatus?.flavor); @@ -115,8 +117,8 @@ export const McpServerDetailDialog = ({ normalizedInstalledEntries.some((entry) => entry.scope === 'user') ? [{ value: 'user' as const, label: getMcpScopeLabel('user', cliStatus?.flavor) }] : []), - { value: 'project', label: 'Project' }, - { value: 'local', label: 'Local' }, + { value: 'project', label: t('mcpDetail.scope.project') }, + { value: 'local', label: t('mcpDetail.scope.local') }, ]; const preferredInstalledEntry = getPreferredMcpInstallationEntry(normalizedInstalledEntries); const selectedInstalledEntry = @@ -336,12 +338,12 @@ export const McpServerDetailDialog = ({ {/* Metadata grid */}
    - Source + {t('mcpDetail.metadata.source')}

    {server.source}

    {stars != null && (
    - GitHub Stars + {t('mcpDetail.metadata.githubStars')}

    {stars.toLocaleString()} @@ -350,55 +352,57 @@ export const McpServerDetailDialog = ({ )} {server.version && (

    - Version + {t('mcpDetail.metadata.version')}

    {server.version}

    )} {server.license && (
    - License + {t('mcpDetail.metadata.license')}

    {server.license}

    )}
    - Install Type + {t('mcpDetail.metadata.installType')} {server.installSpec?.type === 'stdio' ? ( ) : (

    {server.installSpec - ? `HTTP: ${server.installSpec.transportType}` - : 'Manual setup required'} + ? t('mcpDetail.install.httpTransport', { + transport: server.installSpec.transportType, + }) + : t('mcpDetail.install.manualSetupRequired')}

    )}
    {server.author && (
    - Author + {t('mcpDetail.metadata.author')}

    {server.author}

    )} {server.hostingType && (
    - Hosting + {t('mcpDetail.metadata.hosting')}

    {server.hostingType}

    )} {server.publishedAt && (
    - Published + {t('mcpDetail.metadata.published')}

    {new Date(server.publishedAt).toLocaleDateString()}

    )} {server.updatedAt && (
    - Updated + {t('mcpDetail.metadata.updated')}

    {new Date(server.updatedAt).toLocaleDateString()}

    )} @@ -408,13 +412,12 @@ export const McpServerDetailDialog = ({ {server.requiresAuth && (
    - This server requires authentication + {t('mcpDetail.auth.required')}
    )} {isHttp && !server.requiresAuth && (server.authHeaders?.length ?? 0) === 0 && (
    - Remote MCP servers may still require custom headers or API keys even when the registry - does not describe them. If connection fails after install, check the provider docs. + {t('mcpDetail.auth.remoteMayNeedHeaders')}
    )} {isInstalledForScope && ( @@ -443,7 +446,9 @@ export const McpServerDetailDialog = ({
    {diagnostic?.target && (
    -

    Launch Target

    +

    + {t('mcpDetail.diagnostics.launchTarget')} +

    {diagnostic.target} @@ -456,19 +461,19 @@ export const McpServerDetailDialog = ({ {canAutoInstall && (

    - {isInstalledForScope ? 'Manage Installation' : 'Install Server'} + {isInstalledForScope ? t('mcpDetail.install.manage') : t('mcpDetail.install.install')}

    {/* Server name */}
    setServerName(e.target.value)} - placeholder="my-server" + placeholder={t('mcpDetail.placeholders.serverName')} className="h-8 text-sm" disabled={isInstalledForScope} /> @@ -476,7 +481,7 @@ export const McpServerDetailDialog = ({ {/* Scope */}
    - + - This server requires manual setup. Check the repository for installation instructions. + {t('mcpDetail.install.manualSetupDescription')}
    )} @@ -619,7 +626,7 @@ export const McpServerDetailDialog = ({

    - Tools ({server.tools.length}) + {t('mcpDetail.tools.title', { count: server.tools.length })}

    {server.tools.map((tool) => ( @@ -641,7 +648,7 @@ export const McpServerDetailDialog = ({ onClick={() => void api.openExternal(server.repositoryUrl!)} > - Repository + {t('mcpDetail.links.repository')} )} {server.glamaUrl && ( @@ -651,7 +658,7 @@ export const McpServerDetailDialog = ({ onClick={() => void api.openExternal(server.glamaUrl!)} > - Glama + {t('mcpDetail.links.glama')} )} {server.websiteUrl && ( @@ -661,7 +668,7 @@ export const McpServerDetailDialog = ({ onClick={() => void api.openExternal(server.websiteUrl!)} > - Website + {t('mcpDetail.links.website')} )}
    diff --git a/src/renderer/components/extensions/mcp/McpServersPanel.tsx b/src/renderer/components/extensions/mcp/McpServersPanel.tsx index 39eb99ee..6146109a 100644 --- a/src/renderer/components/extensions/mcp/McpServersPanel.tsx +++ b/src/renderer/components/extensions/mcp/McpServersPanel.tsx @@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { Badge } from '@renderer/components/ui/badge'; import { Button } from '@renderer/components/ui/button'; import { @@ -46,6 +47,20 @@ const MCP_SORT_OPTIONS: { value: McpSortValue; label: string }[] = [ { value: 'tools-desc', label: 'Most tools' }, ]; +function getMcpSortLabel( + value: McpSortValue, + t: ReturnType['t'] +): string { + switch (value) { + case 'name-asc': + return t('mcpPanel.sort.nameAsc'); + case 'name-desc': + return t('mcpPanel.sort.nameDesc'); + case 'tools-desc': + return t('mcpPanel.sort.toolsDesc'); + } +} + function sortMcpServers(servers: McpCatalogItem[], sort: McpSortValue): McpCatalogItem[] { return [...servers].sort((a, b) => { switch (sort) { @@ -95,6 +110,7 @@ export const McpServersPanel = ({ cliStatus: cliStatusOverride, cliStatusLoading: cliStatusLoadingOverride, }: McpServersPanelProps): React.JSX.Element => { + const { t } = useAppTranslation('extensions'); const projectStateKey = getMcpProjectStateKey(projectPath); const { browseCatalog, @@ -163,18 +179,20 @@ export const McpServersPanel = ({ const diagnosticsDisableReason = useMemo(() => { if (cliStatus === null || typeof cliStatus === 'undefined') { - return cliStatusLoading ? 'Checking runtime status...' : 'Checking runtime availability...'; + return cliStatusLoading + ? t('mcpPanel.diagnostics.disableReasons.checkingRuntimeStatus') + : t('mcpPanel.diagnostics.disableReasons.checkingRuntimeAvailability'); } if (cliStatus?.installed === false) { if (cliStatus.binaryPath && cliStatus.launchError) { - return 'The configured runtime was found but failed to start. Open the Dashboard to repair or reinstall it.'; + return t('mcpPanel.diagnostics.disableReasons.runtimeFailedToStart'); } - return 'The configured runtime is required. Install or repair it from the Dashboard.'; + return t('mcpPanel.diagnostics.disableReasons.runtimeRequired'); } return null; - }, [cliStatus, cliStatusLoading]); + }, [cliStatus, cliStatusLoading, t]); useEffect(() => { if (diagnosticsDisableReason) { @@ -270,17 +288,19 @@ export const McpServersPanel = ({
    -

    MCP Health Status

    +

    {t('mcpPanel.health.title')}

    - {mcpDiagnosticsLoading ? ( - <>Checking installed MCP servers via {runtimeLabel} ... - ) : diagnosticsDisableReason ? ( - diagnosticsDisableReason - ) : mcpDiagnosticsLastCheckedAt ? ( - `Last checked ${formatRelativeTime(new Date(mcpDiagnosticsLastCheckedAt).toISOString())}` - ) : ( - <>Run diagnostics from this page to verify installed MCP connectivity. - )} + {mcpDiagnosticsLoading + ? t('mcpPanel.health.checkingViaRuntime', { runtime: runtimeLabel }) + : diagnosticsDisableReason + ? diagnosticsDisableReason + : mcpDiagnosticsLastCheckedAt + ? t('mcpPanel.health.lastChecked', { + time: formatRelativeTime( + new Date(mcpDiagnosticsLastCheckedAt).toISOString() + ), + }) + : t('mcpPanel.health.description')}

    {(mcpDiagnosticsLoading || allDiagnostics.length > 0) && (
    -

    Runtime MCP Diagnostics

    +

    {t('mcpPanel.diagnostics.title')}

    {allDiagnostics.length > 0 && ( - {allDiagnostics.length} servers + + {t('mcpPanel.diagnostics.serversCount', { count: allDiagnostics.length })} + )}
    {allDiagnostics.length > 0 ? ( @@ -335,7 +359,7 @@ export const McpServersPanel = ({ ))}
    ) : ( -

    Waiting for diagnostics results...

    +

    {t('mcpPanel.diagnostics.waiting')}

    )}
    )} @@ -347,7 +371,7 @@ export const McpServersPanel = ({
    setScope(v as InstallScope)}> - {SCOPE_OPTIONS.map((opt) => ( + {SCOPE_OPTIONS.map((scopeOption) => ( - {opt.label} + {getScopeOptionLabel(scopeOption, t)} ))} @@ -236,7 +250,7 @@ export const PluginDetailDialog = ({ onClick={() => void api.openExternal(plugin.homepage!)} > - Homepage + {t('pluginDetail.links.homepage')} )} {plugin.author?.email && ( @@ -246,7 +260,7 @@ export const PluginDetailDialog = ({ onClick={() => void api.openExternal(`mailto:${plugin.author!.email}`)} > - Contact + {t('pluginDetail.links.contact')} )}
    @@ -256,14 +270,14 @@ export const PluginDetailDialog = ({ {isReadmeLoading && (
    - Loading README... + {t('pluginDetail.readme.loading')}
    )} {!isReadmeLoading && readme && ( )} {!isReadmeLoading && !readme && ( -

    No README available.

    +

    {t('pluginDetail.readme.empty')}

    )}
    diff --git a/src/renderer/components/extensions/plugins/PluginsPanel.tsx b/src/renderer/components/extensions/plugins/PluginsPanel.tsx index bb5bbf1f..69633e9e 100644 --- a/src/renderer/components/extensions/plugins/PluginsPanel.tsx +++ b/src/renderer/components/extensions/plugins/PluginsPanel.tsx @@ -4,6 +4,7 @@ import { useEffect, useMemo } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { Badge } from '@renderer/components/ui/badge'; import { Button } from '@renderer/components/ui/button'; import { Checkbox } from '@renderer/components/ui/checkbox'; @@ -56,12 +57,7 @@ interface PluginsPanelProps { cliStatusLoading?: boolean; } -const SORT_OPTIONS: { value: string; label: string }[] = [ - { value: 'popularity:desc', label: 'Popular' }, - { value: 'name:asc', label: 'Name A-Z' }, - { value: 'name:desc', label: 'Name Z-A' }, - { value: 'category:asc', label: 'Category' }, -]; +const SORT_OPTIONS = ['popularity:desc', 'name:asc', 'name:desc', 'category:asc'] as const; /** Pure function: filter + sort the catalog */ function selectFilteredPlugins( @@ -134,6 +130,7 @@ export const PluginsPanel = ({ cliStatus: cliStatusOverride, cliStatusLoading, }: PluginsPanelProps): React.JSX.Element => { + const { t } = useAppTranslation('extensions'); const { catalog, loading, @@ -191,6 +188,20 @@ export const PluginsPanel = ({ } return counts.size; }, [catalog]); + + const getSortLabel = (value: (typeof SORT_OPTIONS)[number]): string => { + switch (value) { + case 'popularity:desc': + return t('pluginsPanel.sort.popular'); + case 'name:asc': + return t('pluginsPanel.sort.nameAsc'); + case 'name:desc': + return t('pluginsPanel.sort.nameDesc'); + case 'category:asc': + return t('pluginsPanel.sort.category'); + } + }; + return (
    {cliStatus?.flavor === 'agent_teams_orchestrator' && @@ -206,8 +217,7 @@ export const PluginsPanel = ({ return (
    - Plugin support is currently guaranteed for Anthropic (Claude) sessions only. - We're working to support plugins across all agents. + {t('pluginsPanel.providerSupportNotice')}
    ); })()} @@ -217,7 +227,7 @@ export const PluginsPanel = ({
    @@ -233,9 +243,9 @@ export const PluginsPanel = ({ - {SORT_OPTIONS.map((opt) => ( - - {opt.label} + {SORT_OPTIONS.map((value) => ( + + {getSortLabel(value)} ))} @@ -249,7 +259,7 @@ export const PluginsPanel = ({ checked={pluginFilters.installedOnly} onCheckedChange={toggleInstalledOnly} /> - Installed only + {t('pluginsPanel.installedOnly')}
    @@ -265,25 +275,25 @@ export const PluginsPanel = ({
    -

    Browse by fit

    +

    + {t('pluginsPanel.browseByFit')} +

    - {activeFilterCount} active + {t('pluginsPanel.activeFilters', { count: activeFilterCount })}
    -

    - Narrow the catalog by category, capability, or installed state. -

    +

    {t('pluginsPanel.filterDescription')}

    - {catalog.length} plugins + {t('pluginsPanel.counts.plugins', { count: catalog.length })} - {totalCategoryCount} categories + {t('pluginsPanel.counts.categories', { count: totalCategoryCount })} - {totalCapabilityCount} capabilities + {t('pluginsPanel.counts.capabilities', { count: totalCapabilityCount })}
    @@ -294,7 +304,7 @@ export const PluginsPanel = ({ onClick={clearFilters} className="justify-start rounded-lg border border-border px-3 text-xs text-text-secondary hover:text-text lg:justify-center" > - Clear all filters + {t('pluginsPanel.clearAllFilters')} )} @@ -304,10 +314,12 @@ export const PluginsPanel = ({
    - Categories + {t('pluginsPanel.categories')} - {pluginFilters.categories.length} selected + {t('pluginsPanel.selectedCount', { + count: pluginFilters.categories.length, + })}
    - Capabilities + {t('pluginsPanel.capabilities')} - {pluginFilters.capabilities.length} selected + {t('pluginsPanel.selectedCount', { + count: pluginFilters.capabilities.length, + })}
    0 && (

    - Showing {filtered.length} of {catalog.length} plugin{catalog.length !== 1 ? 's' : ''} + {t('pluginsPanel.showing', { shown: filtered.length, total: catalog.length })}

    {hasActiveFilters && ( -

    - Results update instantly as you refine filters. -

    +

    {t('pluginsPanel.resultsUpdateInstantly')}

    )}
    )} @@ -397,16 +409,18 @@ export const PluginsPanel = ({ )}

    - {hasActiveFilters ? 'No plugins match your filters' : 'No plugins available'} + {hasActiveFilters + ? t('pluginsPanel.empty.filteredTitle') + : t('pluginsPanel.empty.title')}

    {hasActiveFilters - ? 'Try adjusting your search or filter criteria' - : 'Check back later for new plugins'} + ? t('pluginsPanel.empty.filteredDescription') + : t('pluginsPanel.empty.description')}

    {hasActiveFilters && ( )} diff --git a/src/renderer/components/extensions/skills/SkillDetailDialog.tsx b/src/renderer/components/extensions/skills/SkillDetailDialog.tsx index 40df90fd..751ad77f 100644 --- a/src/renderer/components/extensions/skills/SkillDetailDialog.tsx +++ b/src/renderer/components/extensions/skills/SkillDetailDialog.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { api } from '@renderer/api'; import { CodeBlockViewer } from '@renderer/components/chat/viewers/CodeBlockViewer'; import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer'; @@ -48,6 +49,7 @@ export const SkillDetailDialog = ({ onEdit, onDeleted, }: SkillDetailDialogProps): React.JSX.Element => { + const { t } = useAppTranslation('extensions'); const fetchSkillDetail = useStore((s) => s.fetchSkillDetail); const deleteSkill = useStore((s) => s.deleteSkill); const detail = useStore(useShallow((s) => (skillId ? s.skillsDetailsById[skillId] : undefined))); @@ -86,13 +88,15 @@ export const SkillDetailDialog = ({ const issuesTone = item?.issues.length ? getIssuesTone(item.issues) : null; function formatScopeLabel(scope: 'user' | 'project'): string { - return scope === 'project' ? 'This project only' : 'Your personal skills'; + return scope === 'project' + ? t('skillDetail.scope.projectOnly') + : t('skillDetail.scope.personal'); } function formatInvocationLabel(invocationMode: 'auto' | 'manual-only'): string { return invocationMode === 'manual-only' - ? 'Only runs when you explicitly ask for it.' - : 'Runs automatically when it matches the task.'; + ? t('skillDetail.invocation.manualOnly') + : t('skillDetail.invocation.auto'); } function getIssuesTone(issues: SkillValidationIssue[]): { @@ -104,14 +108,14 @@ export const SkillDetailDialog = ({ if (informationalOnly) { return { className: 'border-blue-500/30 bg-blue-500/5', - title: 'This skill includes bundled scripts', + title: t('skillDetail.issues.bundledScripts'), Icon: Info, }; } return { className: 'border-amber-500/30 bg-amber-500/5', - title: 'Review this skill carefully before using it', + title: t('skillDetail.issues.reviewCarefully'), Icon: AlertTriangle, }; } @@ -128,7 +132,7 @@ export const SkillDetailDialog = ({ setDeleteConfirmOpen(false); onDeleted(); } catch (error) { - setDeleteError(error instanceof Error ? error.message : 'Failed to delete skill'); + setDeleteError(error instanceof Error ? error.message : t('skillDetail.errors.deleteFailed')); } finally { setDeleteLoading(false); } @@ -138,14 +142,14 @@ export const SkillDetailDialog = ({ !next && onClose()}> - {item?.name ?? 'Skill details'} + {item?.name ?? t('skillDetail.titleFallback')} - {item?.description ?? 'Inspect discovered skill metadata and raw instructions.'} + {item?.description ?? t('skillDetail.descriptionFallback')} {(loading || (open && skillId && detail === undefined)) && ( -

    Loading skill details...

    +

    {t('skillDetail.loading')}

    )} {!loading && detailError && ( @@ -159,7 +163,7 @@ export const SkillDetailDialog = ({ void fetchSkillDetail(skillId, effectiveProjectPath).catch(() => undefined); }} > - Retry + {t('skillDetail.actions.retry')} )} @@ -167,7 +171,7 @@ export const SkillDetailDialog = ({ {!loading && !detailError && detail === null && (
    - Unable to load this skill. + {t('skillDetail.errors.loadFailed')}
    )} @@ -180,14 +184,24 @@ export const SkillDetailDialog = ({ )}
    {formatScopeLabel(item.scope)} - Stored in {formatSkillRootKind(item.rootKind)} + + {t('skillDetail.badges.storedIn', { root: formatSkillRootKind(item.rootKind) })} + {getSkillAudienceLabel(item.rootKind)} - {item.invocationMode === 'manual-only' ? 'Manual use' : 'Auto use'} + {item.invocationMode === 'manual-only' + ? t('skillDetail.badges.manualUse') + : t('skillDetail.badges.autoUse')} - {item.flags.hasScripts && Has scripts} - {item.flags.hasReferences && References} - {item.flags.hasAssets && Assets} + {item.flags.hasScripts && ( + {t('skillDetail.badges.hasScripts')} + )} + {item.flags.hasReferences && ( + {t('skillDetail.badges.references')} + )} + {item.flags.hasAssets && ( + {t('skillDetail.badges.assets')} + )}
    {item.issues.length > 0 && ( @@ -224,28 +238,28 @@ export const SkillDetailDialog = ({

    - Who can use it + {t('skillDetail.summary.whoCanUse')}

    {formatScopeLabel(item.scope)}

    - How it is used + {t('skillDetail.summary.howUsed')}

    {formatInvocationLabel(item.invocationMode)}

    - What comes with it + {t('skillDetail.summary.included')}

    {[ - item.flags.hasReferences ? 'references' : null, - item.flags.hasScripts ? 'scripts' : null, - item.flags.hasAssets ? 'assets' : null, + item.flags.hasReferences ? t('skillDetail.includes.references') : null, + item.flags.hasScripts ? t('skillDetail.includes.scripts') : null, + item.flags.hasAssets ? t('skillDetail.includes.assets') : null, ] .filter(Boolean) - .join(', ') || 'Just the skill instructions'} + .join(', ') || t('skillDetail.includes.instructionsOnly')}

    @@ -253,7 +267,7 @@ export const SkillDetailDialog = ({
    @@ -279,13 +295,13 @@ export const SkillDetailDialog = ({
    -

    Stored at

    +

    {t('skillDetail.files.storedAt')}

    {item.skillDir}

    {detail.scriptFiles.length > 0 && (
    -

    Scripts

    +

    {t('skillDetail.files.scripts')}

    {detail.scriptFiles.map((file) => (

    {file} @@ -296,7 +312,7 @@ export const SkillDetailDialog = ({ {detail.referencesFiles.length > 0 && (

    -

    References

    +

    {t('skillDetail.files.references')}

    {detail.referencesFiles.map((file) => (

    {file} @@ -307,7 +323,7 @@ export const SkillDetailDialog = ({ {detail.assetFiles.length > 0 && (

    -

    Assets

    +

    {t('skillDetail.files.assets')}

    {detail.assetFiles.map((file) => (

    {file} @@ -319,7 +335,7 @@ export const SkillDetailDialog = ({

    - Advanced file details + {t('skillDetail.files.advancedDetails')}
    @@ -329,7 +345,7 @@ export const SkillDetailDialog = ({ onClick={() => void api.showInFolder(item.skillFile)} > - Open Folder + {t('skillDetail.actions.openFolder')}
    - Delete skill? + {t('skillDetail.deleteDialog.title')} {item - ? `Delete "${item.name}" and move it to Trash? You can restore it later from Trash if needed.` - : 'Delete this skill and move it to Trash?'} + ? t('skillDetail.deleteDialog.descriptionWithName', { name: item.name }) + : t('skillDetail.deleteDialog.description')} - Cancel + + {t('skillDetail.actions.cancel')} + void handleDelete()} disabled={deleteLoading}> - {deleteLoading ? 'Deleting...' : 'Delete Skill'} + {deleteLoading + ? t('skillDetail.actions.deleting') + : t('skillDetail.actions.deleteSkill')} diff --git a/src/renderer/components/extensions/skills/SkillEditorDialog.tsx b/src/renderer/components/extensions/skills/SkillEditorDialog.tsx index 65885733..fd59eb80 100644 --- a/src/renderer/components/extensions/skills/SkillEditorDialog.tsx +++ b/src/renderer/components/extensions/skills/SkillEditorDialog.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { MarkdownPreviewPane } from '@renderer/components/team/editor/MarkdownPreviewPane'; import { Badge } from '@renderer/components/ui/badge'; import { Button } from '@renderer/components/ui/button'; @@ -45,6 +46,8 @@ import type { SkillRootKind, } from '@shared/types/extensions'; +const SKILL_MARKDOWN_FILENAME = ['SKILL', 'md'].join('.'); + type EditorMode = 'create' | 'edit'; interface SkillEditorDialogProps { @@ -76,6 +79,7 @@ export const SkillEditorDialog = ({ onClose, onSaved, }: SkillEditorDialogProps): React.JSX.Element => { + const { t } = useAppTranslation('extensions'); const containerRef = useRef(null); const editorScrollRef = useRef(null); const rawContentRef = useRef(''); @@ -294,7 +298,7 @@ export const SkillEditorDialog = ({ [request.files] ); const auxiliaryDraftFilePaths = useMemo( - () => draftFilePaths.filter((filePath) => filePath !== 'SKILL.md'), + () => draftFilePaths.filter((filePath) => filePath !== SKILL_MARKDOWN_FILENAME), [draftFilePaths] ); @@ -308,11 +312,9 @@ export const SkillEditorDialog = ({ [allowCodexRootKind, detail?.item.rootKind] ); const instructionsLocked = manualRawEdit || customMarkdownDetected; - const title = mode === 'create' ? 'Create skill' : 'Edit skill'; + const title = mode === 'create' ? t('skillEditor.title.create') : t('skillEditor.title.edit'); const descriptionText = - mode === 'create' - ? 'Describe the workflow in plain language, review the files that will be created, then save it.' - : 'Update this skill, review the resulting file changes, then save it.'; + mode === 'create' ? t('skillEditor.description.create') : t('skillEditor.description.edit'); function validateBeforeReview(): string | null { if (!name.trim()) { @@ -412,16 +414,15 @@ export const SkillEditorDialog = ({
    -

    1. Basics

    -

    - Give this skill a clear name, choose who can use it, and decide where it should - live. -

    +

    + {t('skillEditor.basics.title')} +

    +

    {t('skillEditor.basics.description')}

    - +
    - + {mode === 'create' && (

    - We suggest this automatically from the skill name so review works right - away. + {t('skillEditor.fields.folderNameHint')}

    )}
    - +
    @@ -504,7 +510,7 @@ export const SkillEditorDialog = ({
    - +
    - +
    - +
    - +
    @@ -566,16 +574,19 @@ export const SkillEditorDialog = ({ {!customMarkdownDetected && ( <>
    -

    2. Instructions

    +

    + {t('skillEditor.instructions.title')} +

    - These sections generate the skill file for you, so you do not need to edit - markdown unless you want to. + {t('skillEditor.instructions.description')}

    - +