feat(i18n): add localization foundation

Refs https://github.com/777genius/agent-teams-ai/issues/139
This commit is contained in:
777genius 2026-05-24 15:33:51 +03:00
parent c88a8836df
commit 6855d63ec6
355 changed files with 26205 additions and 4964 deletions

29
i18next.config.ts Normal file
View file

@ -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',
},
});

View file

@ -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",

File diff suppressed because it is too large Load diff

145
scripts/i18n/validate.ts Normal file
View file

@ -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<TranslationCatalogsByLocale> {
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<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}

View file

@ -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<HTMLDivElement | null>(null);
const shellRefs = useRef(new Map<string, HTMLDivElement | null>());
const connectorRefs = useRef(new Map<string, SVGSVGElement | null>());
@ -552,12 +554,12 @@ export const GraphActivityHud = ({
>
<div className="flex h-full min-w-0 max-w-full flex-col overflow-hidden">
<div className="mb-1 px-1 text-[10px] font-semibold tracking-[0.2em] text-slate-400/70">
Activity
{t('agentGraph.activityHud.activity')}
</div>
<div className="flex min-h-0 flex-1 flex-col gap-2 overflow-hidden">
{lane.entries.length === 0 && lane.overflowCount === 0 ? (
<div className="flex h-[72px] min-h-[72px] items-center rounded-md border border-dashed border-white/10 bg-[rgba(8,14,28,0.28)] px-3 text-[11px] text-slate-400/60">
No recent activity
{t('agentGraph.activityHud.noRecentActivity')}
</div>
) : 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 })}
</button>
) : null}
</div>

View file

@ -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 = ({
<div className="min-w-[260px] max-w-[340px] rounded-lg border border-red-500/20 bg-[var(--color-surface-raised)] p-3 shadow-xl">
<div className="flex items-center justify-between gap-2">
<div className="font-mono text-[10px] uppercase tracking-[0.14em] text-red-400/90">
Blocking Dependency
{t('agentGraph.blockingEdge.title')}
</div>
{relationCount > 1 && (
<Badge
@ -118,17 +120,19 @@ export const GraphBlockingEdgePopover = ({
<div className="font-medium text-red-100">{sourceLabel}</div>
{sourceHiddenTasks.length > 0 && (
<HiddenTaskPreview
title="Blocking hidden tasks"
title={t('agentGraph.blockingEdge.blockingHiddenTasks')}
tasks={sourceHiddenTasks}
onOpenTaskDetail={onOpenTaskDetail}
onClose={onClose}
/>
)}
<div className="mt-1 text-[11px] text-red-300/85">blocks</div>
<div className="mt-1 text-[11px] text-red-300/85">
{t('agentGraph.blockingEdge.blocks')}
</div>
<div className="mt-1 font-medium text-red-100">{targetLabel}</div>
{targetHiddenTasks.length > 0 && (
<HiddenTaskPreview
title="Blocked hidden tasks"
title={t('agentGraph.blockingEdge.blockedHiddenTasks')}
tasks={targetHiddenTasks}
onOpenTaskDetail={onOpenTaskDetail}
onClose={onClose}
@ -148,7 +152,7 @@ export const GraphBlockingEdgePopover = ({
</Button>
)}
<Button type="button" size="sm" variant="ghost" onClick={onClose}>
Close
{t('agentGraph.blockingEdge.close')}
</Button>
</div>
</div>

View file

@ -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<HTMLDivElement | null>(null);
const shellRefs = useRef(new Map<string, HTMLDivElement | null>());
const visibleKeyRef = useRef('');
@ -607,7 +609,7 @@ export const GraphMemberLogPreviewHud = ({
<div className="flex h-full min-w-0 max-w-full flex-col overflow-hidden">
<div className="mb-1 flex h-4 min-h-4 items-center gap-1 px-1 text-[9px] font-semibold tracking-[0.18em] text-slate-400/70">
<Wrench className="size-2.5 text-slate-500" />
Logs
{t('agentGraph.logPreview.logs')}
</div>
<div className="flex min-h-0 flex-1 flex-col gap-2 overflow-hidden">
{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)}
>
<span className="sr-only">Loading logs</span>
<span className="sr-only">{t('agentGraph.logPreview.loading')}</span>
{renderLoadingSkeleton()}
</button>
) : (
@ -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 })}
</button>
) : null}
</div>

View file

@ -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 (
<MemberPopoverContent
@ -168,7 +170,9 @@ export const GraphNodePopover = ({
<span className="text-sm text-purple-400">{'\u{2194}'}</span>
<span className="font-mono text-xs font-bold text-purple-300">{extTeamName}</span>
</div>
<div className="mt-1 text-[10px] text-[var(--color-text-muted)]">External team</div>
<div className="mt-1 text-[10px] text-[var(--color-text-muted)]">
{t('agentGraph.popover.externalTeam')}
</div>
</div>
);
}
@ -185,11 +189,15 @@ export const GraphNodePopover = ({
<div className="mt-2 space-y-0.5 text-[10px] text-[var(--color-text-muted)]">
{node.processRegisteredBy && (
<div>
Started by: <span className="text-[var(--color-text)]">{node.processRegisteredBy}</span>
{t('agentGraph.popover.process.startedBy')}{' '}
<span className="text-[var(--color-text)]">{node.processRegisteredBy}</span>
</div>
)}
{node.processRegisteredAt && (
<div>At: {new Date(node.processRegisteredAt).toLocaleTimeString()}</div>
<div>
{t('agentGraph.popover.process.at')}{' '}
{new Date(node.processRegisteredAt).toLocaleTimeString()}
</div>
)}
{node.exceptionLabel && (
<Badge
@ -211,7 +219,7 @@ export const GraphNodePopover = ({
rel="noreferrer"
className="mt-2 flex items-center gap-1 text-xs text-blue-400 hover:underline"
>
<ExternalLink size={12} /> Open URL
<ExternalLink size={12} /> {t('agentGraph.popover.process.openUrl')}
</a>
)}
</div>
@ -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 (
<div className="min-w-[240px] max-w-[320px] rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-raised)] p-3 shadow-xl">
<div className="flex items-center justify-between gap-2">
<div className="text-sm font-semibold text-[var(--color-text)]">Hidden tasks</div>
<div className="text-sm font-semibold text-[var(--color-text)]">
{t('agentGraph.popover.overflow.hiddenTasks')}
</div>
<Badge variant="outline" className="px-1.5 py-0 text-[10px]">
{node.overflowCount ?? hiddenTasks.length}
</Badge>
</div>
<div className="mt-2 max-h-[260px] space-y-1 overflow-y-auto pr-1">
{hiddenTasks.length === 0 ? (
<div className="text-xs text-[var(--color-text-muted)]">No hidden tasks available.</div>
<div className="text-xs text-[var(--color-text-muted)]">
{t('agentGraph.popover.overflow.empty')}
</div>
) : (
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')}
</Badge>
)}
{(launchPresentation?.spawnBadgeLabel ?? fallbackSpawnStatusLabel) &&
@ -499,7 +514,9 @@ const MemberPopoverContent = ({
className="size-3 shrink-0 animate-spin"
style={{ color: node.color ?? '#66ccff' }}
/>
<span className="shrink-0 text-[var(--color-text-muted)]">working on</span>
<span className="shrink-0 text-[var(--color-text-muted)]">
{t('agentGraph.popover.member.workingOn')}
</span>
<button
type="button"
className="min-w-0 truncate rounded px-1.5 py-0.5 font-medium text-[var(--color-text)] transition-opacity hover:opacity-90"
@ -533,10 +550,10 @@ const MemberPopoverContent = ({
/>
<span className="font-medium text-[var(--color-text)]">
{node.activeTool.state === 'running'
? 'Running tool'
? t('agentGraph.popover.member.activeTool.running')
: node.activeTool.state === 'error'
? 'Tool failed'
: 'Tool finished'}
? t('agentGraph.popover.member.activeTool.failed')
: t('agentGraph.popover.member.activeTool.finished')}
</span>
</div>
<div className="mt-1 font-mono text-[var(--color-text-muted)]">
@ -555,7 +572,7 @@ const MemberPopoverContent = ({
{node.recentTools && node.recentTools.length > 0 && (
<div className="mt-2">
<div className="mb-1 text-[10px] font-medium text-[var(--color-text-muted)]">
Recent tools
{t('agentGraph.popover.member.recentTools')}
</div>
<div className="space-y-1">
{node.recentTools.slice(0, 5).map((tool) => {
@ -594,7 +611,7 @@ const MemberPopoverContent = ({
onClose();
}}
>
<MessageSquare size={12} /> Message
<MessageSquare size={12} /> {t('agentGraph.popover.member.actions.message')}
</Button>
<Button
variant="outline"
@ -605,7 +622,7 @@ const MemberPopoverContent = ({
onClose();
}}
>
<User size={12} /> Profile
<User size={12} /> {t('agentGraph.popover.member.actions.profile')}
</Button>
<Button
variant="outline"
@ -616,7 +633,7 @@ const MemberPopoverContent = ({
onClose();
}}
>
<Plus size={12} /> Task
<Plus size={12} /> {t('agentGraph.popover.member.actions.task')}
</Button>
</div>
</div>

View file

@ -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 = ({
>
<div className="px-1 py-0.5" style={HUD_STEPPER_STYLE}>
<StepProgressBar
steps={MINI_STEPS}
steps={miniSteps}
currentIndex={presentation.currentStepIndex}
active={presentation.isActive}
errorIndex={errorStepIndex}
@ -101,9 +103,9 @@ export const GraphProvisioningHud = ({
<Dialog open={detailsOpen} onOpenChange={setDetailsOpen}>
<DialogContent className="w-[min(1120px,92vw)] max-w-5xl p-0">
<DialogHeader className="sr-only">
<DialogTitle>Launch details</DialogTitle>
<DialogTitle>{t('agentGraph.provisioning.launchDetails')}</DialogTitle>
<DialogDescription>
Detailed team launch progress, live output and CLI logs.
{t('agentGraph.provisioning.launchDetailsDescription')}
</DialogDescription>
</DialogHeader>
<div className="max-h-[85vh] overflow-y-auto p-4">

View file

@ -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);
}

View file

@ -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';

View file

@ -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';

View file

@ -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,
});
}

View file

@ -0,0 +1,7 @@
export type {
CatalogValidationIssue,
TranslationCatalogByNamespace,
TranslationCatalogNode,
TranslationCatalogsByLocale,
} from '../domain/catalogPolicy';
export { validateCatalogCompleteness as validateTranslationCatalogs } from '../domain/catalogPolicy';

View file

@ -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<string, TranslationCatalogNode>;
export type TranslationCatalogsByLocale = Record<string, TranslationCatalogByNamespace>;
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<string>();
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]);
}

View file

@ -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;
}

View file

@ -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';

View file

@ -0,0 +1,3 @@
export function getBrowserSystemLocale(): string | null {
return globalThis.navigator?.language ?? null;
}

View file

@ -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();

View file

@ -0,0 +1,34 @@
import { RESOLVED_APP_LOCALES, TRANSLATION_NAMESPACES } from '../../contracts';
import type { ResolvedAppLocale, TranslationNamespace } from '../../contracts';
type TranslationResource = Record<string, unknown>;
type TranslationResources = Record<
ResolvedAppLocale,
Record<TranslationNamespace, TranslationResource>
>;
const catalogModules = import.meta.glob<TranslationResource>('../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<TranslationNamespace, TranslationResource>;
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;
}

View file

@ -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<TranslationNamespace, undefined>;
readonly resolvedLanguage: string | undefined;
}
export function useAppTranslation(namespace: TranslationNamespace): AppTranslationApi {
const { i18n, t } = useTranslation(namespace);
return {
t,
resolvedLanguage: i18n.resolvedLanguage,
};
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -0,0 +1,4 @@
export { useAppTranslation } from './hooks/useAppTranslation';
export { useLocaleFormatters } from './hooks/useLocaleFormatters';
export { AppLanguageSelect } from './ui/AppLanguageSelect';
export { LocalizationProvider } from './ui/LocalizationProvider';

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -0,0 +1,3 @@
{
"fallback": "Something went wrong."
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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}}"
}
}

File diff suppressed because it is too large Load diff

View file

@ -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": "не назначено"
}
}

View file

@ -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": "Подробнее"
}
}

View file

@ -0,0 +1,3 @@
{
"fallback": "Что-то пошло не так."
}

View file

@ -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": "Официальный"
}
}

View file

@ -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}}"
}
}

View file

@ -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}}"
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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) => (
<>
<Check className={`mr-2 size-3.5 shrink-0 ${isSelected ? 'opacity-100' : 'opacity-0'}`} />
<span className="text-[var(--color-text)]">{option.label}</span>
</>
),
[]
);
return (
<Combobox
options={options}
value={value}
onValueChange={(nextValue) => onValueChange(nextValue as AppLocalePreference)}
placeholder={t('locales.selectPlaceholder')}
searchPlaceholder={t('locales.searchPlaceholder')}
emptyMessage={t('locales.emptyMessage')}
disabled={disabled}
className="min-w-[180px]"
renderOption={renderOption}
/>
);
};

View file

@ -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 <I18nextProvider i18n={appI18n}>{children}</I18nextProvider>;
};

View file

@ -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<MemberLogStreamSectionProps>): 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')}
</button>
<button
type="button"
@ -90,22 +92,22 @@ export function MemberLogStreamSection({
}`}
onClick={() => setSelectedLogView('process')}
>
Process
{t('memberLogStream.tabs.process')}
</button>
</div>
{selectedLogView === 'execution' ? (
<ExecutionLogStreamView
title="Logs"
title={t('memberLogStream.logs.title')}
description={describeMemberStream()}
stream={stream}
loading={loading}
error={error}
teamName={teamName}
teamMembers={teamMembers}
loadingText="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."
loadingText={t('memberLogStream.logs.loading')}
emptyTitle={t('memberLogStream.logs.emptyTitle')}
emptyDescription={t('memberLogStream.logs.emptyDescription')}
selectionResetKey={`${teamName}:${member.name}`}
boundedHistoryNote={boundedHistoryNote}
forceSegmentHeaders

View file

@ -1,5 +1,6 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useAppTranslation } from '@features/localization/renderer';
import { MemberBadge } from '@renderer/components/team/MemberBadge';
import { MemberExecutionLog } from '@renderer/components/team/members/MemberExecutionLog';
import {
@ -237,6 +238,7 @@ export function ExecutionLogStreamView<TStream extends ExecutionLogStreamLike>({
buildSegmentRenderKey,
getSegmentMetaLabel,
}: Readonly<ExecutionLogStreamViewProps<TStream>>): React.JSX.Element {
const { t } = useAppTranslation('team');
const [selectedParticipantKey, setSelectedParticipantKey] = useState<string>('all');
const appliedSelectionResetKeyRef = useRef<string | null>(null);
const participants = stream?.participants ?? [];
@ -329,7 +331,7 @@ export function ExecutionLogStreamView<TStream extends ExecutionLogStreamLike>({
}`}
onClick={() => setSelectedParticipantKey('all')}
>
All
{t('memberLogStream.filters.all')}
</button>
{participants.map((participant) => (
<ParticipantFilterChip

View file

@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useAppTranslation } from '@features/localization/renderer';
import { useVirtualizer } from '@tanstack/react-virtual';
import { Check, Clipboard, Loader2, RefreshCw } from 'lucide-react';
@ -118,6 +119,8 @@ export function MemberRuntimeProcessLogsPanel({
enabled,
loadRuntimeLogTail,
}: Readonly<MemberRuntimeProcessLogsPanelProps>): React.JSX.Element {
const { t } = useAppTranslation('team');
const { t: tCommon } = useAppTranslation('common');
const [kind, setKind] = useState<MemberRuntimeLogKind>('stdout');
const [log, setLog] = useState<MemberRuntimeLogTailResponse | null>(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')}
</label>
<label className="flex cursor-pointer items-center gap-2 rounded-lg border border-[var(--color-border)] px-2.5 py-1.5 text-xs text-[var(--color-text-muted)]">
<input
@ -231,7 +234,7 @@ export function MemberRuntimeProcessLogsPanel({
checked={wrapLines}
onChange={(event) => setWrapLines(event.target.checked)}
/>
Wrap lines
{t('members.runtimeLogs.wrapLines')}
</label>
<button
type="button"
@ -240,7 +243,7 @@ export function MemberRuntimeProcessLogsPanel({
disabled={loading}
>
{loading ? <Loader2 size={13} className="animate-spin" /> : <RefreshCw size={13} />}
Refresh
{tCommon('actions.refresh')}
</button>
<button
type="button"
@ -267,13 +270,13 @@ export function MemberRuntimeProcessLogsPanel({
{loading && !log ? (
<div className="flex items-center gap-2 rounded-xl border border-[var(--color-border)] px-3 py-10 text-sm text-[var(--color-text-muted)]">
<Loader2 size={16} className="animate-spin" />
Loading process log tail...
{t('members.runtimeLogs.loadingTail')}
</div>
) : hasContent ? (
<ProcessLogVirtualList content={log?.content ?? ''} wrapLines={wrapLines} />
) : (
<div className="rounded-xl border border-[var(--color-border)] px-3 py-10 text-sm text-[var(--color-text-muted)]">
{statusText ?? 'No process log file captured for this member yet.'}
{statusText ?? t('members.runtimeLogs.empty')}
</div>
)}
</div>

View file

@ -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({
<section className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-raised)] p-3 text-sm">
<div className="flex items-start justify-between gap-3">
<div>
<h3 className="text-sm font-semibold text-[var(--color-text)]">Member work sync</h3>
<h3 className="text-sm font-semibold text-[var(--color-text)]">
{t('memberWorkSync.details.title')}
</h3>
<p className="mt-1 text-xs text-[var(--color-text-muted)]">{viewModel.tooltip}</p>
</div>
<MemberWorkSyncBadge viewModel={viewModel} />
@ -37,25 +42,33 @@ export function MemberWorkSyncDetails({
<dl className="mt-3 grid grid-cols-2 gap-2 text-xs">
<div>
<dt className="text-[var(--color-text-muted)]">Actionable items</dt>
<dt className="text-[var(--color-text-muted)]">
{t('memberWorkSync.details.actionableItems')}
</dt>
<dd className="font-medium text-[var(--color-text)]">{viewModel.actionableCount}</dd>
</div>
<div>
<dt className="text-[var(--color-text-muted)]">Fingerprint</dt>
<dt className="text-[var(--color-text-muted)]">
{t('memberWorkSync.details.fingerprint')}
</dt>
<dd className="font-mono text-[var(--color-text)]">
{shortFingerprint(viewModel.fingerprint)}
</dd>
</div>
<div>
<dt className="text-[var(--color-text-muted)]">Report</dt>
<dt className="text-[var(--color-text-muted)]">{t('memberWorkSync.details.report')}</dt>
<dd className="font-medium text-[var(--color-text)]">
{viewModel.reportState ?? 'none'}
{viewModel.reportState ?? t('memberWorkSync.details.none')}
</dd>
</div>
<div>
<dt className="text-[var(--color-text-muted)]">Shadow would nudge</dt>
<dt className="text-[var(--color-text-muted)]">
{t('memberWorkSync.details.shadowWouldNudge')}
</dt>
<dd className="font-medium text-[var(--color-text)]">
{viewModel.wouldNudge ? 'yes' : 'no'}
{viewModel.wouldNudge
? t('memberWorkSync.details.yes')
: t('memberWorkSync.details.no')}
</dd>
</div>
</dl>
@ -69,7 +82,7 @@ export function MemberWorkSyncDetails({
))}
{agendaItems.length > 3 ? (
<li className="text-[var(--color-text-muted)]">
{agendaItems.length - 3} more actionable item(s)
{t('memberWorkSync.details.moreActionableItems', { count: agendaItems.length - 3 })}
</li>
) : null}
</ul>
@ -77,7 +90,7 @@ export function MemberWorkSyncDetails({
{showDiagnostics && status?.diagnostics.length ? (
<p className="mt-3 text-xs text-[var(--color-text-muted)]">
Diagnostics: {status.diagnostics.join(', ')}
{t('memberWorkSync.details.diagnostics', { diagnostics: status.diagnostics.join(', ') })}
</p>
) : null}
</section>

View file

@ -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({
<section className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-raised)] p-3 text-sm">
<div className="flex items-start justify-between gap-3">
<div>
<h3 className="text-sm font-semibold text-[var(--color-text)]">Member work sync</h3>
<h3 className="text-sm font-semibold text-[var(--color-text)]">
{t('memberWorkSync.title')}
</h3>
<p className="mt-1 text-xs text-[var(--color-text-muted)]">
{loading
? 'Loading member work sync diagnostics.'
? t('memberWorkSync.loadingDiagnostics')
: error
? 'Member work sync diagnostics are unavailable.'
? t('memberWorkSync.diagnosticsUnavailable')
: viewModel.tooltip}
</p>
</div>

View file

@ -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<RecentProjectCardProps>): 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 = ({
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex shrink-0 items-center rounded-full border border-red-500/30 bg-red-500/10 px-1.5 py-0.5 text-[9px] font-medium text-red-300">
Deleted
{t('recentProjects.card.deleted')}
</span>
</TooltipTrigger>
<TooltipContent side="bottom">Project folder no longer exists</TooltipContent>
<TooltipContent side="bottom">
{t('recentProjects.card.projectFolderMissing')}
</TooltipContent>
</Tooltip>
)}
{card.pathSummary && (
@ -134,7 +139,7 @@ export const RecentProjectCard = ({
</div>
</TooltipTrigger>
<TooltipContent side="bottom">
{isDeleted ? 'Project folder no longer exists' : 'Open'}
{isDeleted ? t('recentProjects.card.projectFolderMissing') : tCommon('actions.open')}
</TooltipContent>
</Tooltip>
<Tooltip>
@ -164,17 +169,23 @@ export const RecentProjectCard = ({
<>
{card.taskCounts.inProgress > 0 && (
<span className="inline-flex items-center rounded-full bg-blue-500/15 px-1.5 py-0.5 text-[10px] font-medium text-blue-600 dark:text-blue-400">
{card.taskCounts.inProgress} active
{t('recentProjects.card.taskCounts.active', {
count: card.taskCounts.inProgress,
})}
</span>
)}
{card.taskCounts.pending > 0 && (
<span className="inline-flex items-center rounded-full bg-yellow-500/15 px-1.5 py-0.5 text-[10px] font-medium text-yellow-400">
{card.taskCounts.pending} pending
{t('recentProjects.card.taskCounts.pending', {
count: card.taskCounts.pending,
})}
</span>
)}
{card.taskCounts.completed > 0 && (
<span className="inline-flex items-center rounded-full bg-green-500/15 px-1.5 py-0.5 text-[10px] font-medium text-green-400">
{card.taskCounts.completed} done
{t('recentProjects.card.taskCounts.done', {
count: card.taskCounts.completed,
})}
</span>
)}
<span className="text-text-muted">·</span>

View file

@ -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 (
<button
className="hover:bg-surface/30 group relative flex min-h-[120px] flex-col items-center justify-center rounded-lg border border-dashed border-border bg-transparent p-4 transition-all duration-300 hover:border-border-emphasis"
onClick={onClick}
title="Select a project folder"
title={t('recentProjects.selectFolderTitle')}
>
<div className="mb-2 flex size-8 items-center justify-center rounded-md border border-dashed border-border transition-colors duration-300 group-hover:border-border-emphasis">
<FolderOpen className="size-4 text-text-muted transition-colors group-hover:text-text-secondary" />
</div>
<span className="text-xs text-text-muted transition-colors group-hover:text-text-secondary">
Select Folder
{t('recentProjects.selectFolder')}
</span>
</button>
);
@ -36,6 +38,7 @@ function SelectProjectFolderCard({
export const RecentProjectsSection = ({
searchQuery,
}: Readonly<RecentProjectsSectionProps>): React.JSX.Element => {
const { t } = useAppTranslation('dashboard');
const {
cards,
loading,
@ -102,14 +105,14 @@ export const RecentProjectsSection = ({
<FolderGit2 className="size-6 text-text-muted" />
</div>
<div className="text-center">
<p className="mb-1 text-sm text-text-secondary">Failed to load projects</p>
<p className="mb-1 text-sm text-text-secondary">{t('recentProjects.failedToLoad')}</p>
<p className="max-w-xl text-xs text-text-muted">{error}</p>
</div>
<button
onClick={() => void reload()}
className="rounded-sm border border-border bg-surface-raised px-3 py-1.5 text-xs text-text-secondary transition-colors hover:border-border-emphasis hover:text-text"
>
Retry
{t('recentProjects.retry')}
</button>
</div>
);
@ -121,8 +124,10 @@ export const RecentProjectsSection = ({
<div className="mb-4 flex size-12 items-center justify-center rounded-sm border border-border bg-surface-raised">
<Search className="size-6 text-text-muted" />
</div>
<p className="mb-1 text-sm text-text-secondary">No projects found</p>
<p className="text-xs text-text-muted">No matches for &quot;{searchQuery}&quot;</p>
<p className="mb-1 text-sm text-text-secondary">{t('recentProjects.noProjects')}</p>
<p className="text-xs text-text-muted">
{t('recentProjects.noMatches', { query: searchQuery })}
</p>
</div>
);
}
@ -133,10 +138,8 @@ export const RecentProjectsSection = ({
<div className="mb-4 flex size-12 items-center justify-center rounded-sm border border-border bg-surface-raised">
<FolderGit2 className="size-6 text-text-muted" />
</div>
<p className="mb-1 text-sm text-text-secondary">No recent projects found</p>
<p className="text-xs text-text-muted">
Recent Claude and Codex activity will appear here.
</p>
<p className="mb-1 text-sm text-text-secondary">{t('recentProjects.noRecentProjects')}</p>
<p className="text-xs text-text-muted">{t('recentProjects.emptyDescription')}</p>
</div>
);
}
@ -162,7 +165,7 @@ export const RecentProjectsSection = ({
{canLoadMore && (
<div className="flex justify-center">
<Button variant="outline" size="sm" onClick={loadMore}>
Load more
{t('recentProjects.loadMore')}
</Button>
</div>
)}

View file

@ -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<RunningTeamsSectionProps>): React.JSX.Element | null {
const { t } = useAppTranslation('team');
const { rows, hidden, openRunningTeam } = useRunningTeamsSection(searchQuery);
if (hidden) {
@ -28,7 +30,7 @@ export function RunningTeamsSection({
<section className="mb-12">
<div className="mb-3 flex items-center">
<h2 className="flex items-center gap-2 text-xs font-medium uppercase tracking-wider text-text-muted">
Running Teams
{t('runningTeams.title')}
<span className="rounded-full border border-border bg-surface-overlay px-1.5 py-0.5 text-[10px] font-medium leading-none text-text-secondary">
{rows.length}
</span>

View file

@ -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<typeof useAppTranslation>['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 ? (
<div className="flex items-center gap-2 text-xs text-[var(--color-text-secondary)]">
<Loader2 className="size-3.5 animate-spin" />
Loading provider setup...
{t('runtimeProvider.setup.loading')}
</div>
) : null}
@ -477,7 +485,7 @@ function ProviderSetupFormPanel({
disabled={busy}
onClick={actions.cancelConnect}
>
Cancel
{t('runtimeProvider.actions.cancel')}
</Button>
<Button
type="button"
@ -500,6 +508,7 @@ function RuntimeSummary({
}: Pick<RuntimeProviderManagementPanelViewProps, 'state' | 'disabled'> & {
onRefresh: () => void;
}): JSX.Element {
const { t } = useAppTranslation('settings');
const runtime = state.view?.runtime;
const loadingWithoutRuntime = state.loading && !runtime;
const defaultSourceLabel = getDefaultModelSourceLabel(state.view?.defaultModelSource);
@ -515,7 +524,7 @@ function RuntimeSummary({
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
OpenCode runtime
{t('runtimeProvider.summary.title')}
</div>
<div className="mt-1 flex flex-wrap items-center gap-2 text-xs">
<Badge
@ -533,11 +542,13 @@ function RuntimeSummary({
) : null}
{state.view?.defaultModel ? (
<span className="break-all" style={{ color: 'var(--color-text-secondary)' }}>
OpenCode default: {state.view.defaultModel}
{t('runtimeProvider.summary.defaultModel', { model: state.view.defaultModel })}
</span>
) : null}
{defaultSourceLabel ? (
<span style={{ color: 'var(--color-text-muted)' }}>Source: {defaultSourceLabel}</span>
<span style={{ color: 'var(--color-text-muted)' }}>
{t('runtimeProvider.summary.source', { source: defaultSourceLabel })}
</span>
) : null}
</div>
{state.loading ? (
@ -546,9 +557,7 @@ function RuntimeSummary({
style={{ color: 'var(--color-text-secondary)' }}
>
<Loader2 className="size-3.5 animate-spin" />
<span>
Loading managed OpenCode runtime, connected providers, and model defaults...
</span>
<span>{t('runtimeProvider.summary.loading')}</span>
</div>
) : null}
{state.view?.diagnostics.length ? (
@ -582,6 +591,7 @@ function RuntimeSummary({
}
function RuntimeProviderLoadingPlaceholder(): JSX.Element {
const { t } = useAppTranslation('settings');
return (
<div
data-testid="runtime-provider-loading-skeleton"
@ -602,7 +612,7 @@ function RuntimeProviderLoadingPlaceholder(): JSX.Element {
/>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
Loading OpenCode providers
{t('runtimeProvider.providers.loading')}
</div>
<div
className="skeleton-shimmer mt-1 h-3 w-72 max-w-full rounded-sm"
@ -778,6 +788,7 @@ const RuntimeProviderErrorAlert = ({
diagnostics = null,
testId,
}: RuntimeProviderErrorAlertProps): JSX.Element => {
const { t } = useAppTranslation('settings');
const [copied, setCopied] = useState(false);
const [headline = message, ...detailLines] = message.trim().split(/\r?\n/);
const fallbackDetails = detailLines.join('\n').trim();
@ -824,22 +835,34 @@ const RuntimeProviderErrorAlert = ({
'h-6 shrink-0 px-2 text-[11px]',
!copied && 'member-launch-diagnostics-pulse'
)}
title={copied ? 'Diagnostics copied' : 'Copy diagnostics'}
aria-label={copied ? 'Diagnostics copied' : 'Copy diagnostics'}
title={
copied
? t('runtimeProvider.diagnostics.copied')
: t('runtimeProvider.diagnostics.copy')
}
aria-label={
copied
? t('runtimeProvider.diagnostics.copied')
: t('runtimeProvider.diagnostics.copy')
}
onClick={(event) => {
event.stopPropagation();
void copyDiagnostics();
}}
>
{copied ? <Check className="mr-1 size-3" /> : <ClipboardList className="mr-1 size-3" />}
{copied ? 'Copied' : 'Copy diagnostics'}
{copied
? t('runtimeProvider.diagnostics.copiedShort')
: t('runtimeProvider.diagnostics.copy')}
</Button>
</div>
{diagnostics ? (
<div className="mt-2 space-y-2">
{diagnostics.likelyCause ? (
<div className="whitespace-pre-wrap break-words leading-5 text-red-100">
<span className="font-medium text-red-100">Likely cause: </span>
<span className="font-medium text-red-100">
{t('runtimeProvider.diagnostics.likelyCause')}{' '}
</span>
{diagnostics.likelyCause}
</div>
) : null}
@ -855,7 +878,9 @@ const RuntimeProviderErrorAlert = ({
) : null}
{hints.length > 0 ? (
<div>
<div className="mb-1 font-medium text-red-100">Hints</div>
<div className="mb-1 font-medium text-red-100">
{t('runtimeProvider.diagnostics.hints')}
</div>
<ul className="space-y-1 pl-4">
{hints.map((hint, index) => (
<li
@ -1033,6 +1058,7 @@ function ProviderRow({
hasProjectContext,
actions,
}: ProviderRowProps): JSX.Element {
const { t } = useAppTranslation('settings');
const connect = getProviderAction(provider, 'connect');
const test = getProviderAction(provider, 'test');
const canOpenConnect = provider.state !== 'connected' && connect?.enabled === true;
@ -1085,7 +1111,9 @@ function ProviderRow({
<span className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
{provider.displayName}
</span>
{provider.recommended ? <Badge variant="secondary">Recommended</Badge> : null}
{provider.recommended ? (
<Badge variant="secondary">{t('runtimeProvider.providers.recommended')}</Badge>
) : null}
<span
className={`rounded-md border px-2 py-0.5 text-[11px] ${stateClassName(provider)}`}
style={stateStyle(provider)}
@ -1099,7 +1127,7 @@ function ProviderRow({
</span>
{provider.defaultModelId ? (
<span className="break-all" style={{ color: 'var(--color-text-secondary)' }}>
OpenCode default: {provider.defaultModelId}
{t('runtimeProvider.summary.defaultModel', { model: provider.defaultModelId })}
</span>
) : 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({
<span className="text-sm font-medium text-[var(--color-text)]">
{provider.displayName}
</span>
{provider.recommended ? <Badge variant="secondary">Recommended</Badge> : null}
{provider.recommended ? (
<Badge variant="secondary">{t('runtimeProvider.providers.recommended')}</Badge>
) : null}
<span
className={`rounded-md border px-2 py-0.5 text-[11px] ${directorySetupKindClassName(provider)}`}
>
@ -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 ? (
<Badge className="bg-sky-400/15 px-1.5 py-0 text-[10px] text-sky-100">
<Star className="mr-1 size-3" />
Used in team picker
{t('runtimeProvider.badges.usedInTeamPicker')}
</Badge>
) : null}
{freeModel ? (
<Badge className="bg-emerald-400/15 px-1.5 py-0 text-[10px] text-emerald-200">free</Badge>
<Badge className="bg-emerald-400/15 px-1.5 py-0 text-[10px] text-emerald-200">
{t('runtimeProvider.badges.free')}
</Badge>
) : null}
{localRoute ? (
<>
<Badge className="bg-cyan-400/15 px-1.5 py-0 text-[10px] text-cyan-200">local</Badge>
<Badge className="bg-sky-400/15 px-1.5 py-0 text-[10px] text-sky-200">configured</Badge>
<Badge className="bg-cyan-400/15 px-1.5 py-0 text-[10px] text-cyan-200">
{t('runtimeProvider.badges.local')}
</Badge>
<Badge className="bg-sky-400/15 px-1.5 py-0 text-[10px] text-sky-200">
{t('runtimeProvider.badges.configured')}
</Badge>
</>
) : null}
{connectedRoute ? (
<Badge className="bg-emerald-400/15 px-1.5 py-0 text-[10px] text-emerald-100">
connected
{t('runtimeProvider.badges.connected')}
</Badge>
) : null}
{verified ? (
<Badge className="bg-emerald-400/15 px-1.5 py-0 text-[10px] text-emerald-100">
verified
{t('runtimeProvider.badges.verified')}
</Badge>
) : null}
{needsTest && !verified ? (
<Badge className="bg-amber-400/15 px-1.5 py-0 text-[10px] text-amber-200">needs test</Badge>
<Badge className="bg-amber-400/15 px-1.5 py-0 text-[10px] text-amber-200">
{t('runtimeProvider.badges.needsTest')}
</Badge>
) : null}
{failed ? (
<Badge className="bg-red-400/15 px-1.5 py-0 text-[10px] text-red-200">failed</Badge>
<Badge className="bg-red-400/15 px-1.5 py-0 text-[10px] text-red-200">
{t('runtimeProvider.badges.failed')}
</Badge>
) : null}
{unknown ? (
<Badge className="bg-slate-400/15 px-1.5 py-0 text-[10px] text-slate-200">unknown</Badge>
<Badge className="bg-slate-400/15 px-1.5 py-0 text-[10px] text-slate-200">
{t('runtimeProvider.badges.unknown')}
</Badge>
) : null}
{model.default ? (
<Badge className="bg-amber-400/15 px-1.5 py-0 text-[10px] text-amber-200">default</Badge>
<Badge className="bg-amber-400/15 px-1.5 py-0 text-[10px] text-amber-200">
{t('runtimeProvider.badges.default')}
</Badge>
) : null}
</div>
);
@ -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({
) : (
<CheckCircle2 className="mr-1 size-3.5" />
)}
Test
{t('runtimeProvider.actions.test')}
</Button>
</div>
</div>
@ -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<string>();
@ -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 (
<div
@ -1686,9 +1734,11 @@ function OpenCodeModelScopeControls({
>
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<div className="text-sm font-medium text-[var(--color-text)]">OpenCode defaults</div>
<div className="text-sm font-medium text-[var(--color-text)]">
{t('runtimeProvider.defaults.title')}
</div>
<div className="mt-1 text-xs text-[var(--color-text-muted)]">
{getDefaultScopeDescription(defaultScope)}
{getDefaultScopeDescription(defaultScope, t)}
</div>
</div>
<div className="inline-flex shrink-0 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-0.5">
@ -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')}
</button>
))}
</div>
@ -1712,7 +1764,7 @@ function OpenCodeModelScopeControls({
<div className="mt-3">
<div className="min-w-0">
<Label className="text-xs text-[var(--color-text-secondary)]">
{getContextControlLabel(defaultScope)}
{getContextControlLabel(defaultScope, t)}
</Label>
<div className="mt-1">
<Select
@ -1740,7 +1792,7 @@ function OpenCodeModelScopeControls({
className="mt-1 text-[11px] leading-4 text-[var(--color-text-muted)]"
title={projectPath?.trim() || undefined}
>
{getContextControlHint(defaultScope, projectPath)}
{getContextControlHint(defaultScope, projectPath, t)}
</div>
</div>
@ -1766,6 +1818,7 @@ function ConfiguredOpenCodeModelsPanel({
readonly defaultScope: RuntimeProviderDefaultScopeDto;
readonly hasProjectContext: boolean;
}): JSX.Element | null {
const { t } = useAppTranslation('settings');
const models = useMemo(() => state.view?.configuredModels ?? [], [state.view?.configuredModels]);
const [query, setQuery] = useState('');
const normalizedQuery = query.trim().toLowerCase();
@ -1791,11 +1844,10 @@ function ConfiguredOpenCodeModelsPanel({
<div className="flex flex-wrap items-start justify-between gap-2">
<div className="min-w-0">
<div className="text-sm font-medium text-[var(--color-text)]">
Launchable OpenCode models
{t('runtimeProvider.models.launchableTitle')}
</div>
<div className="text-xs text-[var(--color-text-muted)]">
Routes you can test or use in the team picker: local config, free built-in models, and
current default.
{t('runtimeProvider.models.launchableDescription')}
</div>
</div>
<div className="relative min-w-[220px] flex-1 sm:max-w-sm">
@ -1803,7 +1855,7 @@ function ConfiguredOpenCodeModelsPanel({
<Input
value={query}
onChange={(event) => 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({
<div className="mt-3 space-y-2">
{visibleModels.length === 0 ? (
<div className="rounded-md border border-dashed border-white/10 px-3 py-3 text-sm text-[var(--color-text-muted)]">
No OpenCode model routes match {query.trim()}.
{t('runtimeProvider.models.noRoutesMatch', { query: query.trim() })}
</div>
) : 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({
) : (
<CheckCircle2 className="mr-1 size-3.5" />
)}
Test
{t('runtimeProvider.actions.test')}
</Button>
<Button
type="button"
@ -1891,7 +1943,7 @@ function ConfiguredOpenCodeModelsPanel({
actions.useModelForNewTeams(model.modelId);
}}
>
Use in team picker
{t('runtimeProvider.models.useInTeamPicker')}
</Button>
<Button
type="button"
@ -1904,7 +1956,7 @@ function ConfiguredOpenCodeModelsPanel({
? undefined
: (contextRequiredTitle ??
(alreadyDefaultForScope
? 'This is already the selected OpenCode default.'
? t('runtimeProvider.models.alreadyDefault')
: unavailableTitle))
}
onClick={() => {
@ -1913,7 +1965,7 @@ function ConfiguredOpenCodeModelsPanel({
}}
>
{savingDefault ? <Loader2 className="mr-1 size-3.5 animate-spin" /> : null}
{getDefaultScopeButtonLabel(defaultScope)}
{getDefaultScopeButtonLabel(defaultScope, t)}
</Button>
</div>
</div>
@ -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 (
<div className="mt-4 space-y-3 border-t border-white/10 pt-3">
@ -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')}
</Label>
</div>
) : 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')}
</Label>
</div>
) : null}
@ -2095,6 +2148,7 @@ export function RuntimeProviderManagementPanelView({
projectContextError = null,
onProjectContextChange,
}: RuntimeProviderManagementPanelViewProps): JSX.Element {
const { t } = useAppTranslation('settings');
const [selectedSection, setSelectedSection] = useState<OpenCodeSettingsSection | null>(null);
const [defaultScope, setDefaultScope] = useState<RuntimeProviderDefaultScopeDto>('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 ? (
<span className="ml-2 rounded-full bg-white/10 px-1.5 py-0 text-[10px]">
{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 ? (
<span className="ml-2 rounded-full bg-white/10 px-1.5 py-0 text-[10px]">
{state.directoryTotalCount}
@ -2215,15 +2269,14 @@ export function RuntimeProviderManagementPanelView({
>
<div className="mb-3 flex items-center gap-2 text-sm text-[var(--color-text-secondary)]">
<Loader2 className="size-3.5 animate-spin" />
Loading OpenCode model routes...
{t('runtimeProvider.models.loadingRoutes')}
</div>
<RuntimeProviderModelLoadingSkeleton />
</div>
) : null}
{!modelsLoading && launchableModelCount === 0 ? (
<div className="rounded-lg border border-dashed border-white/10 p-4 text-sm text-[var(--color-text-muted)]">
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')}
</div>
) : null}
</TabsContent>
@ -2231,9 +2284,11 @@ export function RuntimeProviderManagementPanelView({
<TabsContent value="providers" className="mt-3 space-y-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="min-w-0">
<div className="text-sm font-medium text-[var(--color-text)]">Providers</div>
<div className="text-sm font-medium text-[var(--color-text)]">
{t('runtimeProvider.tabs.providers')}
</div>
<div className="text-xs text-[var(--color-text-muted)]">
{providerCountLabel}. Connected and recommended providers are shown first.
{t('runtimeProvider.providers.description', { count: providerCountLabel })}
</div>
</div>
{state.directorySupported ? (
@ -2249,7 +2304,7 @@ export function RuntimeProviderManagementPanelView({
) : (
<RefreshCcw className="mr-1 size-3.5" />
)}
Refresh catalog
{t('runtimeProvider.providers.refreshCatalog')}
</Button>
) : null}
</div>
@ -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 ? (
<Loader2 className="mr-1 size-3.5 animate-spin" />
) : null}
Load more providers
{t('runtimeProvider.providers.loadMore')}
</Button>
</div>
) : null}
@ -2351,7 +2406,7 @@ export function RuntimeProviderManagementPanelView({
color: 'var(--color-text-secondary)',
}}
>
No providers match that search.
{t('runtimeProvider.providers.noMatches')}
</div>
) : null}
@ -2366,7 +2421,7 @@ export function RuntimeProviderManagementPanelView({
color: 'var(--color-text-secondary)',
}}
>
No providers match that search.
{t('runtimeProvider.providers.noMatches')}
</div>
) : 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')}
</div>
) : null}
</TabsContent>

View file

@ -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 (
<div
@ -107,7 +109,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
className="block truncate text-xs font-medium leading-5"
style={{ color: 'var(--color-text)' }}
>
{SUMMARY_TITLE}
{summaryTitle}
</span>
{!expanded && viewModel.benefitsBody && (
<span
@ -136,7 +138,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
{expanded && (
<div className="mt-3 space-y-3">
<div className="min-w-0 max-w-4xl">
{viewModel.title !== SUMMARY_TITLE && (
{viewModel.title !== summaryTitle && (
<div
className="text-sm font-medium leading-6"
style={{ color: 'var(--color-text-secondary)' }}
@ -176,7 +178,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
backgroundColor: 'rgba(255, 255, 255, 0.04)',
}}
>
Detected OS: {viewModel.platformLabel}
{t('tmuxInstaller.detectedOs', { os: viewModel.platformLabel })}
</span>
)}
{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 })}
</span>
)}
{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 })}
</span>
)}
</div>
@ -258,7 +260,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
style={{ borderColor: 'var(--color-border)' }}
>
<XCircle className="size-4" />
Cancel
{t('tmuxInstaller.actions.cancel')}
</button>
)}
{primaryGuideUrl && (
@ -269,7 +271,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
style={{ borderColor: 'var(--color-border)' }}
>
<ExternalLink className="size-4" />
Manual guide
{t('tmuxInstaller.actions.manualGuide')}
</button>
)}
{viewModel.manualHintsCollapsible && (
@ -285,8 +287,10 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
<ChevronDown className="size-4" />
)}
{manualHintsExpanded
? 'Hide setup steps'
: `Show setup steps (${viewModel.manualHints.length})`}
? t('tmuxInstaller.actions.hideSetupSteps')
: t('tmuxInstaller.actions.showSetupSteps', {
count: viewModel.manualHints.length,
})}
</button>
)}
{viewModel.showRefreshButton && (
@ -297,7 +301,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
style={{ borderColor: 'var(--color-border)' }}
>
<RefreshCw className="size-4" />
Re-check
{t('tmuxInstaller.actions.recheck')}
</button>
)}
</div>
@ -305,7 +309,9 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
{viewModel.progressPercent !== null && (
<div>
<div className="mb-1 flex items-center justify-between text-[11px]">
<span style={{ color: 'var(--color-text-muted)' }}>Installer progress</span>
<span style={{ color: 'var(--color-text-muted)' }}>
{t('tmuxInstaller.installerProgress')}
</span>
<span style={{ color: 'var(--color-text-secondary)' }}>
{viewModel.progressPercent}%
</span>
@ -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')}
</button>
</form>
{viewModel.inputSecret && (
<div className="text-[11px]" style={{ color: 'var(--color-text-muted)' }}>
Password input is sent directly to the installer terminal and is not added to the
log output.
{t('tmuxInstaller.input.passwordNotice')}
</div>
)}
</div>
@ -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')}
</button>
{viewModel.detailsOpen && (
<pre

View file

@ -3,6 +3,7 @@
* Prevents invalid/unknown data from mutating persisted config.
*/
import { isAppLocalePreference } from '@features/localization';
import { migrateProviderBackendId } from '@shared/utils/providerBackend';
import * as path from 'path';
@ -328,6 +329,7 @@ function validateGeneralSection(data: unknown): ValidationSuccess<'general'> | 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` };

View file

@ -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);

View file

@ -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 (
<ErrorBoundary>
<TooltipProvider delayDuration={150} skipDelayDuration={1500}>
<ContextSwitchOverlay />
<TabbedLayout />
<ConfirmDialog />
<ToolApprovalSheet />
</TooltipProvider>
</ErrorBoundary>
<LocalizationProvider appConfig={appConfig}>
<ErrorBoundary>
<TooltipProvider delayDuration={150} skipDelayDuration={1500}>
<ContextSwitchOverlay />
<TabbedLayout />
<ConfirmDialog />
<ToolApprovalSheet />
</TooltipProvider>
</ErrorBoundary>
</LocalizationProvider>
);
};

View file

@ -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<AIChatGroupProps>): 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')}
</span>
{/* Main agent model */}

View file

@ -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),
})}
</span>
)}
</>
) : (
`Context (${allContextInjections.length})`
t('chat.context.count', { count: allContextInjections.length })
)}
</button>
</div>
@ -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')}
>
<ChevronsDown className="size-3.5" />
<span>Bottom</span>
<span>{t('chat.bottom')}</span>
</button>
)}
</div>

View file

@ -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 (
<div className="flex flex-1 items-center justify-center overflow-hidden bg-surface">
<div className="space-y-2 text-center text-text-muted">
<div className="mb-4 text-6xl">💬</div>
<div className="text-xl font-medium text-text-secondary">No conversation history</div>
<div className="text-sm">This session does not contain any messages yet.</div>
<div className="mb-4 text-6xl" aria-hidden="true">
{t('chat.empty.icon')}
</div>
<div className="text-xl font-medium text-text-secondary">{t('chat.empty.title')}</div>
<div className="text-sm">{t('chat.empty.description')}</div>
</div>
</div>
);

View file

@ -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<CompactBoundaryProps>): 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 */}
<div className="h-px flex-1" style={{ backgroundColor: TOOL_CALL_TEXT, opacity: 0.3 }} />
@ -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')}
</span>
{/* Token delta */}
@ -95,7 +97,9 @@ export const CompactBoundary = memo(function CompactBoundary({
{formatTokens(compactGroup.tokenDelta.postCompactionTokens)}
<span style={{ color: 'var(--diff-added-text)' }}>
{' '}
({formatTokens(Math.abs(compactGroup.tokenDelta.delta))} freed)
{t('chat.compact.freedTokens', {
tokens: formatTokens(Math.abs(compactGroup.tokenDelta.delta)),
})}
</span>
</span>
)}
@ -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 })}
</span>
)}
@ -152,12 +156,9 @@ export const CompactBoundary = memo(function CompactBoundary({
<Layers size={14} className="mt-0.5 shrink-0" style={{ color: COLOR_TEXT_MUTED }} />
<div className="text-xs leading-relaxed" style={{ color: COLOR_TEXT_MUTED }}>
<p className="mb-1 font-medium" style={{ color: COLOR_TEXT_SECONDARY }}>
Conversation Compacted
</p>
<p>
Previous messages were summarized to save context. The full conversation history
is preserved in the session file.
{t('chat.compact.conversationCompacted')}
</p>
<p>{t('chat.compact.summary')}</p>
</div>
</div>
)}

View file

@ -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' : ''}`}
/>
<span>
{title} ({count}) ~{formatTokens(tokenCount)} tokens
{t('contextBadge.sectionSummary', {
title,
count,
tokens: formatTokens(tokenCount),
})}
</span>
</div>
{/* Section content */}
@ -134,6 +140,7 @@ export const ContextBadge = ({
stats,
projectRoot,
}: Readonly<ContextBadgeProps>): React.ReactElement | null => {
const { t } = useAppTranslation('common');
const [showPopover, setShowPopover] = useState(false);
const [popoverStyle, setPopoverStyle] = useState<React.CSSProperties>({});
const [arrowStyle, setArrowStyle] = useState<React.CSSProperties>({});
@ -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}
>
<span>Context</span>
<span>{t('contextBadge.badge')}</span>
<span className="font-semibold">+{totalNew}</span>
</span>
@ -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')}
</div>
{/* Sections */}
@ -403,7 +410,7 @@ export const ContextBadge = ({
{/* User Messages section */}
{newUserMessageInjections.length > 0 && (
<PopoverSection
title="User Messages"
title={t('contextBadge.sections.userMessages')}
count={newUserMessageInjections.length}
tokenCount={userMessageTokens}
>
@ -411,10 +418,12 @@ export const ContextBadge = ({
<div key={injection.id} className="min-w-0">
<div className="flex items-center justify-between text-xs">
<span style={{ color: COLOR_TEXT_SECONDARY }}>
Turn {injection.turnIndex + 1}
{t('contextBadge.turn', { turn: injection.turnIndex + 1 })}
</span>
<span style={{ color: COLOR_TEXT_MUTED }}>
~{formatTokens(injection.estimatedTokens)} tokens
{t('contextBadge.tokenCount', {
tokens: formatTokens(injection.estimatedTokens),
})}
</span>
</div>
{injection.textPreview && (
@ -433,7 +442,7 @@ export const ContextBadge = ({
{/* CLAUDE.md Files section */}
{newClaudeMdInjections.length > 0 && (
<PopoverSection
title="CLAUDE.md Files"
title={t('contextBadge.sections.claudeMdFiles')}
count={newClaudeMdInjections.length}
tokenCount={claudeMdTokens}
>
@ -450,7 +459,9 @@ export const ContextBadge = ({
style={{ color: COLOR_TEXT_SECONDARY }}
/>
<div className="text-xs" style={{ color: COLOR_TEXT_MUTED }}>
~{formatTokens(injection.estimatedTokens)} tokens
{t('contextBadge.tokenCount', {
tokens: formatTokens(injection.estimatedTokens),
})}
</div>
</div>
);
@ -461,7 +472,7 @@ export const ContextBadge = ({
{/* Mentioned Files section */}
{newMentionedFileInjections.length > 0 && (
<PopoverSection
title="Mentioned Files"
title={t('contextBadge.sections.mentionedFiles')}
count={newMentionedFileInjections.length}
tokenCount={mentionedFileTokens}
>
@ -477,7 +488,9 @@ export const ContextBadge = ({
style={{ color: COLOR_TEXT_SECONDARY }}
/>
<div className="text-xs" style={{ color: COLOR_TEXT_MUTED }}>
~{formatTokens(injection.estimatedTokens)} tokens
{t('contextBadge.tokenCount', {
tokens: formatTokens(injection.estimatedTokens),
})}
</div>
</div>
);
@ -488,7 +501,7 @@ export const ContextBadge = ({
{/* Tool Outputs section */}
{newToolOutputInjections.length > 0 && (
<PopoverSection
title="Tool Outputs"
title={t('contextBadge.sections.toolOutputs')}
count={toolOutputCount}
tokenCount={toolOutputTokens}
>
@ -500,7 +513,9 @@ export const ContextBadge = ({
>
<span style={{ color: COLOR_TEXT_SECONDARY }}>{tool.toolName}</span>
<span style={{ color: COLOR_TEXT_MUTED }}>
~{formatTokens(tool.tokenCount)} tokens
{t('contextBadge.tokenCount', {
tokens: formatTokens(tool.tokenCount),
})}
</span>
</div>
))
@ -511,7 +526,7 @@ export const ContextBadge = ({
{/* Task Coordination section */}
{newTaskCoordinationInjections.length > 0 && (
<PopoverSection
title="Task Coordination"
title={t('contextBadge.sections.taskCoordination')}
count={taskCoordinationCount}
tokenCount={taskCoordinationTokens}
>
@ -523,7 +538,9 @@ export const ContextBadge = ({
>
<span style={{ color: COLOR_TEXT_SECONDARY }}>{item.label}</span>
<span style={{ color: COLOR_TEXT_MUTED }}>
~{formatTokens(item.tokenCount)} tokens
{t('contextBadge.tokenCount', {
tokens: formatTokens(item.tokenCount),
})}
</span>
</div>
))
@ -534,14 +551,14 @@ export const ContextBadge = ({
{/* Thinking + Text section */}
{newThinkingTextInjections.length > 0 && (
<PopoverSection
title="Thinking + Text"
title={t('contextBadge.sections.thinkingText')}
count={newThinkingTextInjections.length}
tokenCount={thinkingTextTokens}
>
{newThinkingTextInjections.map((injection) => (
<div key={injection.id} className="min-w-0">
<div className="text-xs" style={{ color: COLOR_TEXT_SECONDARY }}>
Turn {injection.turnIndex + 1}
{t('contextBadge.turn', { turn: injection.turnIndex + 1 })}
</div>
<div className="space-y-0.5 pl-2">
{injection.breakdown.map((item, idx) => (
@ -550,10 +567,14 @@ export const ContextBadge = ({
className="flex items-center justify-between text-xs"
>
<span style={{ color: COLOR_TEXT_MUTED }}>
{item.type === 'thinking' ? 'Thinking' : 'Text'}
{item.type === 'thinking'
? t('contextBadge.breakdown.thinking')
: t('contextBadge.breakdown.text')}
</span>
<span style={{ color: COLOR_TEXT_MUTED }}>
~{formatTokens(item.tokenCount)} tokens
{t('contextBadge.tokenCount', {
tokens: formatTokens(item.tokenCount),
})}
</span>
</div>
))}
@ -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}` }}
>
<span style={{ color: COLOR_TEXT_MUTED }}>Total new tokens</span>
<span style={{ color: COLOR_TEXT_MUTED }}>{t('contextBadge.totalNewTokens')}</span>
<span style={{ color: COLOR_TEXT_SECONDARY }}>
~{formatTokens(totalNewTokens)} tokens
{t('contextBadge.tokenCount', { tokens: formatTokens(totalNewTokens) })}
</span>
</div>
</div>,

View file

@ -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({
<Layers size={14} />
</div>
<span className="shrink-0 text-xs font-medium" style={{ color: TOOL_CALL_TEXT }}>
Compacted
{t('chat.compact.compacted')}
</span>
{item.tokenDelta && (
<span
@ -354,7 +356,9 @@ const DisplayItemRow = memo(function DisplayItemRow({
{formatTokensCompact(item.tokenDelta.postCompactionTokens)}
<span style={{ color: '#4ade80' }}>
{' '}
({formatTokensCompact(Math.abs(item.tokenDelta.delta))} freed)
{t('chat.compact.freedTokens', {
tokens: formatTokensCompact(Math.abs(item.tokenDelta.delta)),
})}
</span>
</span>
)}
@ -365,7 +369,7 @@ const DisplayItemRow = memo(function DisplayItemRow({
color: '#818cf8',
}}
>
Phase {item.phaseNumber}
{t('chat.compact.phase', { phase: item.phaseNumber })}
</span>
<span className="ml-auto shrink-0 text-[11px]" style={{ color: COLOR_TEXT_MUTED }}>
{format(new Date(item.timestamp), 'h:mm:ss a')}
@ -438,6 +442,7 @@ export const DisplayItemList = React.memo(function DisplayItemList({
timestampFormat,
showItemMetaTooltip = false,
}: Readonly<DisplayItemListProps>): React.JSX.Element {
const { t } = useAppTranslation('common');
const [replyLinkToolId, setReplyLinkToolId] = useState<string | null>(null);
const handleReplyHover = useCallback((toolId: string | null) => {
@ -447,7 +452,7 @@ export const DisplayItemList = React.memo(function DisplayItemList({
if (!items || items.length === 0) {
return (
<div className="px-3 py-2 text-sm italic text-claude-dark-text-secondary">
No items to display
{t('chat.items.empty')}
</div>
);
}

View file

@ -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<LastOutputDisplayProps>): 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')}
</span>
)}
</div>
@ -185,7 +187,7 @@ export const LastOutputDisplay = ({
style={{ color: 'var(--warning-text, #f59e0b)' }}
/>
<span className="text-sm" style={{ color: 'var(--warning-text, #f59e0b)' }}>
Request interrupted by user
{t('chat.lastOutput.requestInterrupted')}
</span>
</div>
);
@ -234,7 +236,7 @@ export const LastOutputDisplay = ({
<div className="flex items-center gap-2">
<FileCheck className="size-4" style={{ color: 'var(--plan-exit-text)' }} />
<span className="text-sm font-medium" style={{ color: 'var(--plan-exit-text)' }}>
Plan Ready for Approval
{t('chat.lastOutput.planReadyForApproval')}
</span>
</div>
<CopyButton text={planContent} inline />

View file

@ -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<DirectoryTreeNodeProps>): 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 }}
/>
<span style={{ color: COLOR_TEXT_MUTED }}>(~{formatTokens(node.tokens ?? 0)})</span>
<span style={{ color: COLOR_TEXT_MUTED }}>
{t('tokens.approxTokensParenthesized', { tokens: formatTokens(node.tokens ?? 0) })}
</span>
{node.firstSeenInGroup &&
(isClickable ? (
<button

View file

@ -4,6 +4,7 @@
import React, { useMemo } from 'react';
import { useAppTranslation } from '@features/localization/renderer';
import { CLAUDE_MD_GROUP_CONFIG, CLAUDE_MD_GROUP_ORDER } from '../types';
import { ClaudeMdSubSection } from './ClaudeMdSection';
@ -29,6 +30,8 @@ export const ClaudeMdFilesSection = ({
projectRoot,
onNavigateToTurn,
}: Readonly<ClaudeMdFilesSectionProps>): React.ReactElement | null => {
const { t } = useAppTranslation('common');
// Group CLAUDE.md injections by category
const claudeMdGroups = useMemo(() => {
const groups = new Map<ClaudeMdGroupCategory, ClaudeMdContextInjection[]>();
@ -65,7 +68,7 @@ export const ClaudeMdFilesSection = ({
return (
<CollapsibleSection
title="CLAUDE.md Files"
title={t('sessionContext.claudeMdFiles')}
count={injections.length}
tokenCount={tokenCount}
isExpanded={isExpanded}

View file

@ -4,6 +4,7 @@
import React, { useState } from 'react';
import { useAppTranslation } from '@features/localization/renderer';
import { ChevronRight } from 'lucide-react';
import { buildDirectoryTree } from '../DirectoryTree/buildDirectoryTree';
@ -28,6 +29,7 @@ export const ClaudeMdSubSection = ({
projectRoot,
onNavigateToTurn,
}: Readonly<ClaudeMdSubSectionProps>): React.ReactElement => {
const { t } = useAppTranslation('common');
const [expanded, setExpanded] = useState(true);
const sectionTokens = injections.reduce((sum, inj) => sum + inj.estimatedTokens, 0);
@ -59,7 +61,9 @@ export const ClaudeMdSubSection = ({
>
{injections.length}
</span>
<span style={{ color: 'var(--color-text-muted)' }}>(~{formatTokens(sectionTokens)})</span>
<span style={{ color: 'var(--color-text-muted)' }}>
{t('tokens.approxTokensParenthesized', { tokens: formatTokens(sectionTokens) })}
</span>
</div>
{expanded && (

View file

@ -4,6 +4,7 @@
import React from 'react';
import { useAppTranslation } from '@features/localization/renderer';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { formatTokens } from '../utils/formatting';
@ -25,6 +26,8 @@ export const CollapsibleSection = ({
onToggle,
children,
}: Readonly<CollapsibleSectionProps>): React.ReactElement => {
const { t } = useAppTranslation('common');
return (
<div
className="overflow-hidden rounded-lg"
@ -60,7 +63,7 @@ export const CollapsibleSection = ({
</span>
</div>
<span className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
~{formatTokens(tokenCount)} tokens
{t('tokens.approxTokens', { tokens: formatTokens(tokenCount) })}
</span>
</button>

View file

@ -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<FlatInjectionListProps>): 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')}
</span>
)}
{/* Token count */}

View file

@ -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<MentionedFilesSectionProps>): React.ReactElement | null => {
const { t } = useAppTranslation('common');
if (injections.length === 0) return null;
return (
<CollapsibleSection
title="Mentioned Files"
title={t('sessionContext.mentionedFiles')}
count={injections.length}
tokenCount={tokenCount}
isExpanded={isExpanded}

View file

@ -8,6 +8,7 @@
import React, { useMemo, useState } 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';
import { ChevronRight } from 'lucide-react';
@ -97,6 +98,7 @@ const ToolOutputRankedItem = ({
onNavigateToTurn?: (turnIndex: number) => 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')}
</span>
)}
</button>

View file

@ -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<SessionContextHeaderProps>): 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 = ({
<div className="text-right">
<div className="font-medium tabular-nums" style={{ color: COLOR_TEXT_SECONDARY }}>
{tokens === null
? (options?.unavailableLabel ?? 'Unavailable')
? (options?.unavailableLabel ?? t('sessionContext.metrics.unavailable'))
: `${options?.approximate ? '~' : ''}${formatTokens(tokens)}`}
</div>
{percentLabel && (
@ -99,7 +102,7 @@ export const SessionContextHeader = ({
<div className="flex items-center gap-2">
<FileText size={16} style={{ color: COLOR_TEXT_SECONDARY }} />
<h2 className="text-sm font-semibold" style={{ color: COLOR_TEXT }}>
Context
{t('sessionContext.header.title')}
</h2>
<span
className="rounded px-1.5 py-0.5 text-xs"
@ -118,7 +121,7 @@ export const SessionContextHeader = ({
onClick={onClose}
className="rounded p-1 transition-colors hover:bg-white/10"
style={{ color: COLOR_TEXT_SECONDARY }}
aria-label="Close panel"
aria-label={t('sessionContext.header.closePanel')}
>
<X size={16} />
</button>
@ -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')}
</div>
)}
@ -180,7 +182,9 @@ export const SessionContextHeader = ({
{/* Cost */}
{sessionMetrics.costUsd !== undefined && sessionMetrics.costUsd > 0 && (
<div className="col-span-2">
<span style={{ color: COLOR_TEXT_MUTED }}>Session Cost: </span>
<span style={{ color: COLOR_TEXT_MUTED }}>
{t('sessionContext.metrics.sessionCost')}{' '}
</span>
<span className="font-medium tabular-nums" style={{ color: COLOR_TEXT_SECONDARY }}>
{formatCostUsd(sessionMetrics.costUsd + (subagentCostUsd ?? 0))}
</span>
@ -188,9 +192,9 @@ export const SessionContextHeader = ({
<span style={{ color: COLOR_TEXT_MUTED }}>
{' ('}
{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')}
</button>
</>
)}
@ -218,7 +222,7 @@ export const SessionContextHeader = ({
style={{ borderTop: `1px solid ${COLOR_BORDER_SUBTLE}` }}
>
<span className="mr-1 text-[10px]" style={{ color: COLOR_TEXT_MUTED }}>
Phase:
{t('sessionContext.header.phase')}
</span>
{phaseInfo.phases.map((phase) => (
<button
@ -247,7 +251,7 @@ export const SessionContextHeader = ({
color: selectedPhase === null ? '#818cf8' : COLOR_TEXT_MUTED,
}}
>
Current
{t('sessionContext.header.current')}
</button>
</div>
)}
@ -258,7 +262,7 @@ export const SessionContextHeader = ({
style={{ borderTop: `1px solid ${COLOR_BORDER_SUBTLE}` }}
>
<span className="mr-1 text-[10px]" style={{ color: COLOR_TEXT_MUTED }}>
View:
{t('sessionContext.header.view')}
</span>
<button
onClick={() => onViewModeChange('category')}
@ -270,7 +274,7 @@ export const SessionContextHeader = ({
}}
>
<LayoutList size={10} />
Category
{t('sessionContext.header.category')}
</button>
<button
onClick={() => onViewModeChange('ranked')}
@ -282,7 +286,7 @@ export const SessionContextHeader = ({
}}
>
<ArrowDownWideNarrow size={10} />
By Size
{t('sessionContext.header.bySize')}
</button>
</div>
</div>

View file

@ -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<React.CSSProperties>({});
const [arrowStyle, setArrowStyle] = useState<React.CSSProperties>({});
@ -119,41 +121,37 @@ export const SessionContextHelpTooltip = (): React.ReactElement => {
{/* Metric definitions */}
<div>
<div className="mb-1 font-semibold" style={{ color: 'var(--color-text)' }}>
Context Used
{t('sessionContext.help.contextUsed.title')}
</div>
<p style={{ color: 'var(--color-text-secondary)', lineHeight: 1.5 }}>
Prompt input plus output tokens currently occupying the model&apos;s context
window.
{t('sessionContext.help.contextUsed.description')}
</p>
</div>
<div className="pt-2" style={{ borderTop: '1px solid var(--color-border-subtle)' }}>
<div className="mb-1 font-semibold" style={{ color: 'var(--color-text)' }}>
Prompt Input
{t('sessionContext.help.promptInput.title')}
</div>
<p style={{ color: 'var(--color-text-secondary)', lineHeight: 1.5 }}>
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')}
</p>
</div>
<div className="pt-2" style={{ borderTop: '1px solid var(--color-border-subtle)' }}>
<div className="mb-1 font-semibold" style={{ color: 'var(--color-text)' }}>
Visible Context
{t('sessionContext.help.visibleContext.title')}
</div>
<p style={{ color: 'var(--color-text-secondary)', lineHeight: 1.5 }}>
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')}
</p>
</div>
<div className="pt-2" style={{ borderTop: '1px solid var(--color-border-subtle)' }}>
<div className="mb-1 font-semibold" style={{ color: 'var(--color-text)' }}>
Availability
{t('sessionContext.help.availability.title')}
</div>
<p style={{ color: 'var(--color-text-secondary)', lineHeight: 1.5 }}>
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')}
</p>
</div>
</div>

View file

@ -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<TaskCoordinationSectionProps>): React.ReactElement | null => {
const { t } = useAppTranslation('common');
if (injections.length === 0) return null;
return (
<CollapsibleSection
title="Task Coordination"
title={t('tokens.taskCoordination')}
count={injections.length}
tokenCount={tokenCount}
isExpanded={isExpanded}

View file

@ -4,6 +4,7 @@
import React from 'react';
import { useAppTranslation } from '@features/localization/renderer';
import { ThinkingTextItem } from '../items/ThinkingTextItem';
import { CollapsibleSection } from './CollapsibleSection';
@ -25,11 +26,13 @@ export const ThinkingTextSection = ({
onToggle,
onNavigateToTurn,
}: Readonly<ThinkingTextSectionProps>): React.ReactElement | null => {
const { t } = useAppTranslation('common');
if (injections.length === 0) return null;
return (
<CollapsibleSection
title="Thinking + Text"
title={t('tokens.thinkingText')}
count={injections.length}
tokenCount={tokenCount}
isExpanded={isExpanded}

View file

@ -4,6 +4,7 @@
import React from 'react';
import { useAppTranslation } from '@features/localization/renderer';
import { ToolOutputItem } from '../items/ToolOutputItem';
import { CollapsibleSection } from './CollapsibleSection';
@ -25,11 +26,13 @@ export const ToolOutputsSection = ({
onToggle,
onNavigateToTurn,
}: Readonly<ToolOutputsSectionProps>): React.ReactElement | null => {
const { t } = useAppTranslation('common');
if (injections.length === 0) return null;
return (
<CollapsibleSection
title="Tool Outputs"
title={t('tokens.toolOutputs')}
count={injections.length}
tokenCount={tokenCount}
isExpanded={isExpanded}

View file

@ -4,6 +4,7 @@
import React from 'react';
import { useAppTranslation } from '@features/localization/renderer';
import { UserMessageItem } from '../items/UserMessageItem';
import { CollapsibleSection } from './CollapsibleSection';
@ -25,11 +26,13 @@ export const UserMessagesSection = ({
onToggle,
onNavigateToTurn,
}: Readonly<UserMessagesSectionProps>): React.ReactElement | null => {
const { t } = useAppTranslation('common');
if (injections.length === 0) return null;
return (
<CollapsibleSection
title="User Messages"
title={t('tokens.userMessages')}
count={injections.length}
tokenCount={tokenCount}
isExpanded={isExpanded}

View file

@ -5,6 +5,7 @@
import React, { useMemo, useState } from 'react';
import { useAppTranslation } from '@features/localization/renderer';
import {
COLOR_BORDER,
COLOR_SURFACE,
@ -57,6 +58,7 @@ export const SessionContextPanel = ({
onPhaseChange,
side = 'left',
}: Readonly<SessionContextPanelProps>): React.ReactElement => {
const { t } = useAppTranslation('common');
// View mode: category sections or ranked list
const [viewMode, setViewMode] = useState<ContextViewMode>('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')}
</div>
) : viewMode === 'category' ? (
<>
@ -278,7 +280,7 @@ export const SessionContextPanel = ({
color: !flatMode ? '#818cf8' : COLOR_TEXT_MUTED,
}}
>
Grouped
{t('sessionContext.view.grouped')}
</button>
<button
onClick={() => setFlatMode(true)}
@ -288,7 +290,7 @@ export const SessionContextPanel = ({
color: flatMode ? '#818cf8' : COLOR_TEXT_MUTED,
}}
>
Flat
{t('sessionContext.view.flat')}
</button>
</div>
{flatMode ? (

View file

@ -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<ClaudeMdItemProps>): 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 = ({
/>
<div className="mt-0.5 flex items-center gap-2">
<span className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
~{formatTokens(injection.estimatedTokens)} tokens
{t('tokens.approxTokens', { tokens: formatTokens(injection.estimatedTokens) })}
</span>
{isClickable ? (
<button

View file

@ -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';
import { File } from 'lucide-react';
@ -23,6 +24,7 @@ export const MentionedFileItem = ({
projectRoot,
onNavigateToTurn,
}: Readonly<MentionedFileItemProps>): React.ReactElement => {
const { t } = useAppTranslation('common');
const turnIndex = injection.firstSeenTurnIndex;
const isClickable = onNavigateToTurn && turnIndex >= 0;
const displayPath = shortenDisplayPath(injection.path, projectRoot);
@ -46,13 +48,15 @@ export const MentionedFileItem = ({
color: 'var(--color-error)',
}}
>
missing
{t('sessionContext.items.missing')}
</span>
)}
</div>
<div className="ml-4 mt-0.5 flex items-center gap-2">
<span className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
~{formatTokens(injection.estimatedTokens)} tokens
{t('sessionContext.items.tokensApprox', {
tokens: formatTokens(injection.estimatedTokens),
})}
</span>
{isClickable ? (
<span
@ -72,7 +76,7 @@ export const MentionedFileItem = ({
}
}}
>
@Turn {turnIndex + 1}
{t('sessionContext.items.turn', { turn: turnIndex + 1 })}
</span>
) : (
<span
@ -82,7 +86,7 @@ export const MentionedFileItem = ({
opacity: 0.7,
}}
>
@Turn {turnIndex + 1}
{t('sessionContext.items.turn', { turn: turnIndex + 1 })}
</span>
)}
</div>

View file

@ -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<TaskCoordinationItemProps>): 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 })}
</span>
) : (
<span className="text-xs" style={{ color: COLOR_TEXT_SECONDARY }}>
@Turn {turnIndex + 1}
{t('sessionContext.items.turn', { turn: turnIndex + 1 })}
</span>
)}
<span className="text-xs" style={{ color: COLOR_TEXT_MUTED }}>
~{formatTokens(injection.estimatedTokens)} tokens
{t('sessionContext.items.tokensApprox', {
tokens: formatTokens(injection.estimatedTokens),
})}
</span>
<span
className="rounded px-1 py-0.5 text-xs"
@ -73,7 +77,7 @@ export const TaskCoordinationItem = ({
color: COLOR_TEXT_MUTED,
}}
>
{injection.breakdown.length} item{injection.breakdown.length !== 1 ? 's' : ''}
{t('sessionContext.items.itemsCount', { count: injection.breakdown.length })}
</span>
</>
);

View file

@ -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<ThinkingTextItemProps>): 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 })}
</span>
) : (
<span className="text-xs" style={{ color: COLOR_TEXT_SECONDARY }}>
@Turn {turnIndex + 1}
{t('sessionContext.items.turn', { turn: turnIndex + 1 })}
</span>
)}
<span className="text-xs" style={{ color: COLOR_TEXT_MUTED }}>
~{formatTokens(injection.estimatedTokens)} tokens
{t('sessionContext.items.tokensApprox', {
tokens: formatTokens(injection.estimatedTokens),
})}
</span>
</button>
@ -82,7 +86,9 @@ export const ThinkingTextItem = ({
{injection.breakdown.map((item, idx) => (
<div key={`${item.type}-${idx}`} className="flex items-center gap-2 py-0.5 text-xs">
<span style={{ color: COLOR_TEXT_MUTED }}>
{item.type === 'thinking' ? 'Thinking' : 'Text'}
{item.type === 'thinking'
? t('sessionContext.items.thinking')
: t('sessionContext.items.text')}
</span>
<span style={{ color: COLOR_TEXT_MUTED, opacity: 0.7 }}>
~{formatTokens(item.tokenCount)}

View file

@ -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<ToolBreakdownItemProps>): React.ReactElement => {
const { t } = useAppTranslation('common');
return (
<div className="flex items-center gap-2 py-0.5 text-xs">
<span style={{ color: 'var(--color-text-muted)' }}>{tool.toolName}</span>
@ -30,7 +33,7 @@ export const ToolBreakdownItem = ({
fontSize: '10px',
}}
>
error
{t('states.error')}
</span>
)}
</div>

View file

@ -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<ToolOutputItemProps>): 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 })}
</span>
) : (
<span className="text-xs" style={{ color: COLOR_TEXT_SECONDARY }}>
@Turn {turnIndex + 1}
{t('sessionContext.items.turn', { turn: turnIndex + 1 })}
</span>
)}
<span className="text-xs" style={{ color: COLOR_TEXT_MUTED }}>
~{formatTokens(injection.estimatedTokens)} tokens
{t('sessionContext.items.tokensApprox', {
tokens: formatTokens(injection.estimatedTokens),
})}
</span>
<span
className="rounded px-1 py-0.5 text-xs"
@ -75,7 +79,7 @@ export const ToolOutputItem = ({
color: COLOR_TEXT_MUTED,
}}
>
{injection.toolCount} tool{injection.toolCount !== 1 ? 's' : ''}
{t('sessionContext.items.toolsCount', { count: injection.toolCount })}
</span>
</>
);

View file

@ -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<UserMessageItemProps>): 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 })}
</span>
) : (
<span className="text-xs" style={{ color: COLOR_TEXT_SECONDARY }}>
@Turn {turnIndex + 1}
{t('sessionContext.items.turn', { turn: turnIndex + 1 })}
</span>
)}
<span className="text-xs" style={{ color: COLOR_TEXT_MUTED }}>
~{formatTokens(injection.estimatedTokens)} tokens
{t('sessionContext.items.tokensApprox', {
tokens: formatTokens(injection.estimatedTokens),
})}
</span>
</div>
{injection.textPreview && (

View file

@ -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<SystemChatGroupProps>): React.JSX.Element => {
const { t } = useAppTranslation('common');
const { commandOutput, timestamp } = systemGroup;
// Clean ANSI escape codes from output
@ -34,7 +36,7 @@ const SystemChatGroupInner = ({
>
<Terminal className="size-3.5" style={{ color: 'var(--color-text-muted)' }} />
<span className="font-medium" style={{ color: 'var(--color-text-secondary)' }}>
System
{t('chat.system.label')}
</span>
<span>·</span>
<span>{format(timestamp, 'h:mm:ss a')}</span>

View file

@ -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<UserChatGroupProps>): React.JSX.Element => {
const { t } = useAppTranslation('common');
const { content, timestamp, id: groupId } = userGroup;
const [isManuallyExpanded, setIsManuallyExpanded] = useState(false);
const [validatedPaths, setValidatedPaths] = useState<Record<string, boolean>>({});
@ -544,7 +546,7 @@ const UserChatGroupInner = ({ userGroup }: Readonly<UserChatGroupProps>): React.
{format(timestamp, 'h:mm:ss a')}
</span>
<span className="text-xs font-semibold" style={{ color: 'var(--color-text-secondary)' }}>
You
{t('chat.user.you')}
</span>
<User className="size-3.5" style={{ color: 'var(--color-text-secondary)' }} />
</div>
@ -578,7 +580,7 @@ const UserChatGroupInner = ({ userGroup }: Readonly<UserChatGroupProps>): React.
style={{ color: 'var(--color-text-muted)' }}
>
<ChevronDown size={12} />
Show more
{t('chat.user.showMore')}
</button>
)}
</div>
@ -596,7 +598,7 @@ const UserChatGroupInner = ({ userGroup }: Readonly<UserChatGroupProps>): React.
}}
>
<ChevronUp size={12} />
Show less
{t('chat.user.showLess')}
</button>
</div>
) : null}
@ -613,7 +615,7 @@ const UserChatGroupInner = ({ userGroup }: Readonly<UserChatGroupProps>): 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<UserChatGroupProps>): React.
className="text-xs font-medium leading-snug"
style={{ color: 'var(--color-text-secondary)' }}
>
{commandName || 'Background task'}
{commandName || t('chat.user.backgroundTask')}
</div>
<div
className="flex items-center gap-2 text-[10px]"
style={{ color: 'var(--color-text-muted)' }}
>
<span className="capitalize">{notification.status || 'unknown'}</span>
{exitCodeMatch?.[1] ? <span>exit {exitCodeMatch[1]}</span> : null}
{exitCodeMatch?.[1] ? (
<span>{t('chat.user.exitCode', { code: exitCodeMatch[1] })}</span>
) : null}
{outputFileName ? (
<span className="flex min-w-0 items-center gap-0.5 truncate">
<FileText className="size-2.5 shrink-0" />
@ -657,7 +661,7 @@ const UserChatGroupInner = ({ userGroup }: Readonly<UserChatGroupProps>): React.
{/* Images indicator */}
{hasImages && (
<div className="text-right text-xs" style={{ color: 'var(--color-text-muted)' }}>
{content.images.length} image{content.images.length > 1 ? 's' : ''} attached
{t('chat.user.imagesAttached', { count: content.images.length })}
</div>
)}
</div>

View file

@ -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<ExecutionTraceProps> = React.memo(
searchExpandedItemId,
registerToolRef,
}): React.JSX.Element => {
const { t } = useAppTranslation('common');
const [manualExpandedItemId, setManualExpandedItemId] = useState<string | null>(null);
// Use searchExpandedItemId if set, otherwise use manually expanded item
@ -68,7 +70,7 @@ export const ExecutionTrace: React.FC<ExecutionTraceProps> = React.memo(
if (!items || items.length === 0) {
return (
<div className="px-3 py-2 text-xs" style={{ color: CARD_ICON_MUTED }}>
No execution items
{t('chat.executionTrace.empty')}
</div>
);
}
@ -157,7 +159,9 @@ export const ExecutionTrace: React.FC<ExecutionTraceProps> = 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,
})}
</div>
);
@ -168,7 +172,7 @@ export const ExecutionTrace: React.FC<ExecutionTraceProps> = React.memo(
<BaseItem
key={itemId}
icon={<MailOpen className="size-4" />}
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<ExecutionTraceProps> = React.memo(
className="shrink-0 text-xs font-medium"
style={{ color: TOOL_CALL_TEXT }}
>
Compacted
{t('chat.compact.compacted')}
</span>
{item.tokenDelta && (
<span
@ -233,7 +237,9 @@ export const ExecutionTrace: React.FC<ExecutionTraceProps> = React.memo(
{formatTokensCompact(item.tokenDelta.postCompactionTokens)}
<span style={{ color: '#4ade80' }}>
{' '}
({formatTokensCompact(Math.abs(item.tokenDelta.delta))} freed)
{t('chat.compact.freedTokens', {
tokens: formatTokensCompact(Math.abs(item.tokenDelta.delta)),
})}
</span>
</span>
)}
@ -244,7 +250,7 @@ export const ExecutionTrace: React.FC<ExecutionTraceProps> = React.memo(
color: '#818cf8',
}}
>
Phase {item.phaseNumber}
{t('chat.compact.phase', { phase: item.phaseNumber })}
</span>
<span
className="ml-auto shrink-0 text-[11px]"

View file

@ -8,6 +8,7 @@
import React, { memo, useRef } from 'react';
import { useAppTranslation } from '@features/localization/renderer';
import { CARD_ICON_MUTED } from '@renderer/constants/cssVariables';
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme';
@ -78,6 +79,7 @@ export const LinkedToolItem = memo(
registerRef,
titleText,
}: LinkedToolItemProps): React.JSX.Element => {
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}
</span>
<span className="text-xs" style={{ color: CARD_ICON_MUTED }}>
Teammate spawned
{t('chat.tools.teammateSpawned')}
</span>
</div>
);
@ -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 (
<div ref={handleRef} className="flex items-center gap-2 px-3 py-1.5">
<span className="size-2 rounded-full bg-zinc-500" />
<span className="text-xs" style={{ color: CARD_ICON_MUTED }}>
Shutdown requested &rarr;{' '}
{t('chat.tools.shutdownRequested')}{' '}
<span className="font-medium text-text-secondary">{target}</span>
</span>
</div>
@ -223,13 +225,13 @@ export const LinkedToolItem = memo(
style={{ color: 'var(--tool-item-muted)' }}
>
<StatusDot status="orphaned" />
No result received
{t('chat.tools.noResultReceived')}
</div>
)}
{/* Timing */}
<div className="text-xs" style={{ color: 'var(--tool-item-muted)' }}>
Duration: {formatDuration(linkedTool.durationMs)}
{t('chat.tools.duration', { duration: formatDuration(linkedTool.durationMs) })}
</div>
</BaseItem>
</div>

View file

@ -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<MetricsPillProps>): React.ReactElement | null => {
const { t } = useAppTranslation('common');
const [showTooltip, setShowTooltip] = useState(false);
const [tooltipStyle, setTooltipStyle] = useState<React.CSSProperties>({});
const containerRef = useRef<HTMLDivElement>(null);
@ -160,7 +162,9 @@ export const MetricsPill = memo(
<div className="space-y-1">
{hasMainImpact && (
<div className="flex items-center justify-between gap-3">
<span style={{ color: COLOR_TEXT_MUTED }}>Main Context</span>
<span style={{ color: COLOR_TEXT_MUTED }}>
{t('chat.subagent.metrics.mainContext')}
</span>
<span className="font-mono tabular-nums" style={{ color: CARD_TEXT_LIGHT }}>
{mainSessionImpact.totalTokens.toLocaleString()}
</span>
@ -181,7 +185,7 @@ export const MetricsPill = memo(
className="flex items-center justify-between gap-3 pl-2"
>
<span className="text-[10px]" style={{ color: CARD_ICON_MUTED }}>
Phase {phase.phaseNumber}
{t('chat.subagent.metrics.phase', { phase: phase.phaseNumber })}
</span>
<span
className="font-mono text-[10px] tabular-nums"

View file

@ -12,6 +12,7 @@ import {
COLOR_TEXT_MUTED,
COLOR_TEXT_SECONDARY,
} from '@renderer/constants/cssVariables';
import { useAppTranslation } from '@features/localization/renderer';
import {
getSubagentTypeColorSet,
getTeamColorSet,
@ -79,7 +80,9 @@ export const SubagentItem: React.FC<SubagentItemProps> = 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<SubagentItemProps> = 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<SubagentItemProps> = React.memo(
{subagent.team.memberName}
</span>
<span className="text-xs" style={{ color: CARD_ICON_MUTED }}>
Shutdown confirmed
{t('chat.subagent.shutdownConfirmed')}
</span>
<span className="flex-1" />
<span
@ -358,7 +361,7 @@ export const SubagentItem: React.FC<SubagentItemProps> = React.memo(
<MetricsPill
mainSessionImpact={subagent.team ? undefined : subagent.mainSessionImpact}
lastUsage={lastUsage ?? undefined}
isolatedLabel={subagent.team ? 'Context Window' : undefined}
isolatedLabel={subagent.team ? t('chat.subagent.metrics.contextWindow') : undefined}
isolatedOverride={
phaseData && phaseData.compactionCount > 0 ? phaseData.totalConsumption : undefined
}
@ -391,14 +394,14 @@ export const SubagentItem: React.FC<SubagentItemProps> = React.memo(
style={{ color: COLOR_TEXT_MUTED }}
>
<span>
<span style={{ color: CARD_ICON_MUTED }}>Type</span>{' '}
<span style={{ color: CARD_ICON_MUTED }}>{t('chat.subagent.meta.type')}</span>{' '}
<span className="font-mono" style={{ color: CARD_TEXT_LIGHT }}>
{subagentType}
</span>
</span>
<span style={{ color: CARD_SEPARATOR }}></span>
<span>
<span style={{ color: CARD_ICON_MUTED }}>Duration</span>{' '}
<span style={{ color: CARD_ICON_MUTED }}>{t('chat.subagent.meta.duration')}</span>{' '}
<span className="font-mono tabular-nums" style={{ color: CARD_TEXT_LIGHT }}>
{formatDuration(subagent.durationMs)}
</span>
@ -407,7 +410,7 @@ export const SubagentItem: React.FC<SubagentItemProps> = React.memo(
<>
<span style={{ color: CARD_SEPARATOR }}></span>
<span>
<span style={{ color: CARD_ICON_MUTED }}>Model</span>{' '}
<span style={{ color: CARD_ICON_MUTED }}>{t('chat.subagent.meta.model')}</span>{' '}
<span className={`font-mono ${getModelColorClass(modelInfo.family)}`}>
{modelInfo.name}
</span>
@ -416,7 +419,7 @@ export const SubagentItem: React.FC<SubagentItemProps> = React.memo(
)}
<span style={{ color: CARD_SEPARATOR }}></span>
<span>
<span style={{ color: CARD_ICON_MUTED }}>ID</span>{' '}
<span style={{ color: CARD_ICON_MUTED }}>{t('chat.subagent.meta.id')}</span>{' '}
<span
className="inline-block max-w-[120px] truncate align-bottom font-mono"
style={{ color: CARD_ICON_MUTED }}
@ -435,7 +438,7 @@ export const SubagentItem: React.FC<SubagentItemProps> = React.memo(
className="mb-2 text-[10px] font-semibold uppercase tracking-wider"
style={{ color: CARD_ICON_MUTED }}
>
Context Usage
{t('chat.subagent.metrics.contextUsage')}
</div>
{/* Token rows - floating alignment */}
@ -448,7 +451,7 @@ export const SubagentItem: React.FC<SubagentItemProps> = React.memo(
style={{ color: 'rgba(251, 191, 36, 0.7)' }}
/>
<span className="text-xs" style={{ color: COLOR_TEXT_SECONDARY }}>
Main Context
{t('chat.subagent.metrics.mainContext')}
</span>
</div>
<span
@ -465,7 +468,7 @@ export const SubagentItem: React.FC<SubagentItemProps> = React.memo(
<div className="flex items-center gap-2">
<Sigma className="size-3" style={{ color: 'rgba(168, 85, 247, 0.7)' }} />
<span className="text-xs" style={{ color: COLOR_TEXT_SECONDARY }}>
Total Output
{t('chat.subagent.metrics.totalOutput')}
</span>
</div>
<span
@ -475,7 +478,9 @@ export const SubagentItem: React.FC<SubagentItemProps> = React.memo(
{cumulativeMetrics.outputTokens.toLocaleString()}
<span style={{ color: CARD_ICON_MUTED }}>
{' '}
({cumulativeMetrics.turnCount} turns)
{t('chat.subagent.metrics.turns', {
count: cumulativeMetrics.turnCount,
})}
</span>
</span>
</div>
@ -489,7 +494,9 @@ export const SubagentItem: React.FC<SubagentItemProps> = React.memo(
style={{ color: 'rgba(56, 189, 248, 0.7)' }}
/>
<span className="text-xs" style={{ color: COLOR_TEXT_SECONDARY }}>
{subagent.team ? 'Context Window' : 'Subagent Context'}
{subagent.team
? t('chat.subagent.metrics.contextWindow')
: t('chat.subagent.metrics.subagentContext')}
</span>
</div>
<span
@ -509,7 +516,7 @@ export const SubagentItem: React.FC<SubagentItemProps> = React.memo(
className="flex items-center justify-between pl-5"
>
<span className="text-[11px]" style={{ color: CARD_ICON_MUTED }}>
Phase {phase.phaseNumber}
{t('chat.subagent.metrics.phase', { phase: phase.phaseNumber })}
</span>
<span
className="font-mono text-[11px] tabular-nums"
@ -567,7 +574,7 @@ export const SubagentItem: React.FC<SubagentItemProps> = React.memo(
/>
<Terminal className="size-3.5" style={{ color: CARD_ICON_MUTED }} />
<span className="text-xs" style={{ color: COLOR_TEXT_SECONDARY }}>
Execution Trace
{t('chat.subagent.trace.title')}
</span>
<span className="text-[11px]" style={{ color: CARD_ICON_MUTED }}>
· {itemsSummary}

View file

@ -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 */}
<span className="text-[10px] uppercase tracking-wide" style={{ color: CARD_ICON_MUTED }}>
Message
{t('chat.teammateMessage.message')}
</span>
{/* Reply indicator — shows which SendMessage triggered this response */}
@ -226,13 +228,13 @@ export const TeammateMessageItem = memo(
style={{ color: CARD_ICON_MUTED }}
>
<RefreshCw className="size-2.5" />
Resent
{t('chat.teammateMessage.resent')}
</span>
)}
{/* Summary */}
<span className="flex-1 truncate text-xs" style={{ color: CARD_TEXT_LIGHT }}>
{truncatedSummary || 'Teammate message'}
{truncatedSummary || t('chat.teammateMessage.fallback')}
</span>
{/* 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),
})}
</span>
)}

View file

@ -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 */}
<div>
<div className="mb-1 text-xs" style={{ color: 'var(--tool-item-muted)' }}>
Input
{t('toolViewer.input')}
</div>
<div
className="max-h-96 overflow-auto rounded p-3 font-mono text-xs"
@ -52,7 +55,16 @@ export const DefaultToolViewer = memo(function DefaultToolViewer({
color: 'var(--color-text-secondary)',
}}
>
{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'),
})}
</div>
</div>

View file

@ -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<string, unknown> | 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')}
<StatusDot status={status} />
{linkedTool.result?.tokenCount !== undefined && linkedTool.result.tokenCount > 0 && (
<span style={{ color: 'var(--color-text-muted)' }}>
~{formatTokens(linkedTool.result.tokenCount)} tokens
{t('tokens.approxTokens', { tokens: formatTokens(linkedTool.result.tokenCount) })}
</span>
)}
</div>

View file

@ -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')}
</button>
<button
type="button"
@ -85,12 +87,12 @@ export const ReadToolViewer = memo(function ReadToolViewer({ linkedTool }: ReadT
border: '1px solid var(--tag-border)',
}}
>
Preview
{t('code.preview')}
</button>
</div>
)}
{isMarkdownFile && viewMode === 'preview' ? (
<MarkdownViewer content={content} label="Markdown Preview" copyable />
<MarkdownViewer content={content} label={t('code.markdownPreview')} copyable />
) : (
<CodeBlockViewer
fileName={filePath}

View file

@ -6,6 +6,7 @@
import React, { memo } from 'react';
import { useAppTranslation } from '@features/localization/renderer';
import { CodeBlockViewer } from '@renderer/components/chat/viewers';
import type { LinkedToolItem } from '@renderer/types/groups';
@ -15,8 +16,9 @@ interface SkillToolViewerProps {
}
export const SkillToolViewer = memo(function SkillToolViewer({ linkedTool }: SkillToolViewerProps) {
const { t } = useAppTranslation('common');
const skillInstructions = linkedTool.skillInstructions;
const skillName = (linkedTool.input.skill as string) || 'Unknown Skill';
const skillName = (linkedTool.input.skill as string) || t('chat.tools.skill.unknown');
const resultContent = linkedTool.result?.content;
const resultText =
@ -34,7 +36,7 @@ export const SkillToolViewer = memo(function SkillToolViewer({ linkedTool }: Ski
{resultText && (
<div>
<div className="mb-1 text-xs" style={{ color: 'var(--tool-item-muted)' }}>
Result
{t('chat.tools.result')}
</div>
<div
className="overflow-x-auto rounded p-3 font-mono text-xs"
@ -53,7 +55,7 @@ export const SkillToolViewer = memo(function SkillToolViewer({ linkedTool }: Ski
{skillInstructions && (
<div>
<div className="mb-1 text-xs" style={{ color: 'var(--tool-item-muted)' }}>
Skill Instructions
{t('chat.tools.skill.instructions')}
</div>
<CodeBlockViewer
fileName={`${skillName} skill`}

View file

@ -6,6 +6,7 @@
import React, { memo } from 'react';
import { useAppTranslation } from '@features/localization/renderer';
import { StatusDot } from '../BaseItem';
import { renderOutput } from './renderHelpers';
@ -19,6 +20,8 @@ interface ToolErrorDisplayProps {
export const ToolErrorDisplay = memo(function ToolErrorDisplay({
linkedTool,
}: ToolErrorDisplayProps) {
const { t } = useAppTranslation('common');
if (!linkedTool.result?.isError) return null;
return (
@ -27,7 +30,7 @@ export const ToolErrorDisplay = memo(function ToolErrorDisplay({
className="mb-1 flex items-center gap-2 text-xs"
style={{ color: 'var(--tool-item-muted)' }}
>
Error
{t('states.error')}
<StatusDot status="error" />
</div>
<div

View file

@ -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 WriteToolViewerProps {
}
export const WriteToolViewer = memo(function WriteToolViewer({ linkedTool }: WriteToolViewerProps) {
const { t } = useAppTranslation('common');
const toolUseResult = linkedTool.result?.toolUseResult as Record<string, unknown> | undefined;
const filePath =
@ -37,7 +39,7 @@ export const WriteToolViewer = memo(function WriteToolViewer({ linkedTool }: Wri
return (
<div className="space-y-2">
<div className="mb-1 text-xs text-zinc-500">
{isCreate ? 'Created file' : 'Wrote to file'}
{isCreate ? t('chat.tools.write.createdFile') : t('chat.tools.write.wroteToFile')}
</div>
{isMarkdownFile && (
<div className="flex items-center justify-end gap-1">
@ -51,7 +53,7 @@ export const WriteToolViewer = memo(function WriteToolViewer({ linkedTool }: Wri
border: '1px solid var(--tag-border)',
}}
>
Code
{t('code.code')}
</button>
<button
type="button"
@ -63,12 +65,12 @@ export const WriteToolViewer = memo(function WriteToolViewer({ linkedTool }: Wri
border: '1px solid var(--tag-border)',
}}
>
Preview
{t('code.preview')}
</button>
</div>
)}
{isMarkdownFile && viewMode === 'preview' ? (
<MarkdownViewer content={content} label="Markdown Preview" copyable />
<MarkdownViewer content={content} label={t('code.markdownPreview')} copyable />
) : (
<CodeBlockViewer fileName={filePath} content={content} startLine={1} />
)}

View file

@ -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<string, unknown>): React.ReactElement {
export function renderInput(
toolName: string,
input: Record<string, unknown>,
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<string, unknown>): R
{filePath}
{replaceAll && (
<span className="ml-2" style={{ color: COLOR_TEXT_MUTED }}>
(replace all)
{labels.replaceAll}
</span>
)}
</div>
@ -110,7 +125,7 @@ export function renderInput(toolName: string, input: Record<string, unknown>): R
<div className="space-y-2">
<div>
<div className="text-xs" style={{ color: COLOR_TEXT_MUTED }}>
action
{labels.agentAction}
</div>
<div className="whitespace-pre-wrap break-all">{details.action}</div>
</div>
@ -118,7 +133,7 @@ export function renderInput(toolName: string, input: Record<string, unknown>): R
{details.teammateName && (
<div>
<div className="text-xs" style={{ color: COLOR_TEXT_MUTED }}>
teammate
{labels.agentTeammate}
</div>
<div>{details.teammateName}</div>
</div>
@ -127,7 +142,7 @@ export function renderInput(toolName: string, input: Record<string, unknown>): R
{details.teamName && (
<div>
<div className="text-xs" style={{ color: COLOR_TEXT_MUTED }}>
team
{labels.agentTeam}
</div>
<div>{details.teamName}</div>
</div>
@ -136,7 +151,7 @@ export function renderInput(toolName: string, input: Record<string, unknown>): R
{details.runtime && (
<div>
<div className="text-xs" style={{ color: COLOR_TEXT_MUTED }}>
runtime
{labels.agentRuntime}
</div>
<div>{details.runtime}</div>
</div>
@ -145,7 +160,7 @@ export function renderInput(toolName: string, input: Record<string, unknown>): R
{details.subagentType && (
<div>
<div className="text-xs" style={{ color: COLOR_TEXT_MUTED }}>
type
{labels.agentType}
</div>
<div>{details.subagentType}</div>
</div>
@ -160,7 +175,7 @@ export function renderInput(toolName: string, input: Record<string, unknown>): R
color: COLOR_TEXT_MUTED,
}}
>
Startup instructions are hidden in the UI.
{labels.startupInstructionsHidden}
</div>
</div>
);
@ -180,7 +195,7 @@ export function renderInput(toolName: string, input: Record<string, unknown>): R
))
) : (
<div className="italic" style={{ color: COLOR_TEXT_MUTED }}>
No input recorded for this tool call.
{labels.noInputRecorded}
</div>
)}
</div>

View file

@ -0,0 +1 @@
export { SessionContextPanel as SessionPanel } from './SessionContextPanel/index';

View file

@ -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({
</span>
{(startLine > 1 || endLine) && (
<span className="shrink-0 text-xs" style={{ color: 'var(--color-text-muted)' }}>
(lines {startLine}-{actualEndLine})
{t('code.linesParenthesized', { from: startLine, to: actualEndLine })}
</span>
)}
<span
@ -197,7 +199,7 @@ export const CodeBlockViewer = memo(function CodeBlockViewer({
<button
onClick={handleCopy}
className="rounded p-1 transition-colors hover:opacity-80"
title="Copy to clipboard"
title={t('actions.copyToClipboard')}
style={{ backgroundColor: 'transparent' }}
>
{isCopied ? (

View file

@ -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 && <span style={{ color: DIFF_REMOVED_TEXT }}>-{stats.removed}</span>}
{stats.added === 0 && stats.removed === 0 && (
<span style={{ color: COLOR_TEXT_MUTED }}>Changed</span>
<span style={{ color: COLOR_TEXT_MUTED }}>{t('diff.changed')}</span>
)}
</span>
{tokenCount !== undefined && tokenCount > 0 && (
<span className="ml-auto text-xs" style={{ color: COLOR_TEXT_MUTED }}>
~{formatTokens(tokenCount)} tokens
{t('tokens.approxTokens', { tokens: formatTokens(tokenCount) })}
</span>
)}
</div>
@ -449,7 +451,7 @@ export const DiffViewer = memo(function DiffViewer({
))}
{diffLines.length === 0 && (
<div className="px-3 py-2 italic" style={{ color: COLOR_TEXT_MUTED }}>
No changes detected
{t('diff.noChangesDetected')}
</div>
)}
</div>

View file

@ -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<string | null>(null);
const [error, setError] = React.useState(false);
@ -366,7 +368,7 @@ const LocalImage = React.memo(function LocalImage({
if (error) {
return (
<span className="inline-flex items-center gap-1 text-xs text-text-muted">
[Image: {alt || src}]
{t('markdown.imageFallback', { label: alt || src })}
</span>
);
}
@ -959,6 +961,7 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = 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<MarkdownViewerProps> = React.memo(function
{label}
</span>
<span className="ml-2 text-[11px]" style={{ color: COLOR_TEXT_MUTED }}>
Raw
{t('markdown.raw')}
</span>
<span className="flex-1" />
<button
@ -1025,13 +1028,9 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = React.memo(function
style={{ color: PROSE_LINK }}
onClick={() => setShowRaw(false)}
disabled={isTooLarge}
title={
isTooLarge
? 'Large content is shown as raw to prevent UI freeze'
: 'Render markdown'
}
title={isTooLarge ? t('markdown.largeContentTitle') : t('markdown.renderMarkdown')}
>
Render markdown
{t('markdown.renderMarkdown')}
</button>
{copyable && <CopyButton text={content} inline />}
</div>
@ -1042,28 +1041,23 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = React.memo(function
className="flex items-center justify-between px-3 py-2 text-xs"
style={{ color: COLOR_TEXT_MUTED }}
>
<span>Raw preview</span>
<span>{t('markdown.rawPreview')}</span>
<button
type="button"
className="underline"
style={{ color: PROSE_LINK }}
onClick={() => setShowRaw(false)}
disabled={isTooLarge}
title={
isTooLarge
? 'Large content is shown as raw to prevent UI freeze'
: 'Render markdown'
}
title={isTooLarge ? t('markdown.largeContentTitle') : t('markdown.renderMarkdown')}
>
Render markdown
{t('markdown.renderMarkdown')}
</button>
</div>
)}
{isTooLarge && (
<div className="px-3 pb-2 text-[11px]" style={{ color: COLOR_TEXT_MUTED }}>
Content is very large ({content.length.toLocaleString()} chars). Showing raw preview to
keep the UI responsive.
{t('markdown.largeContentNotice', { count: content.length.toLocaleString() })}
</div>
)}
@ -1077,7 +1071,10 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = React.memo(function
{isTruncated && (
<div className="flex items-center justify-between gap-2 px-4 pb-4 text-xs">
<span style={{ color: COLOR_TEXT_MUTED }}>
Showing {shown.length.toLocaleString()} / {content.length.toLocaleString()} chars
{t('markdown.showingChars', {
shown: shown.length.toLocaleString(),
total: content.length.toLocaleString(),
})}
</span>
<div className="flex items-center gap-2">
<button
@ -1086,7 +1083,7 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = React.memo(function
style={{ borderColor: CODE_BORDER, color: PROSE_LINK }}
onClick={() => setRawLimit((v) => Math.min(content.length, v * 2))}
>
Show more
{t('markdown.showMore')}
</button>
<button
type="button"
@ -1094,7 +1091,7 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = React.memo(function
style={{ borderColor: CODE_BORDER, color: PROSE_LINK }}
onClick={() => setRawLimit(content.length)}
>
Show all
{t('markdown.showAll')}
</button>
</div>
</div>
@ -1175,9 +1172,9 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = 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')}
</button>
{copyable && <CopyButton text={content} inline />}
</div>
@ -1195,9 +1192,9 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = React.memo(function
className="underline"
style={{ color: PROSE_LINK }}
onClick={() => setShowRaw(true)}
title="Show raw"
title={t('markdown.showRaw')}
>
Show raw
{t('markdown.showRaw')}
</button>
</div>
)}

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