feat(i18n): add localization foundation
Refs https://github.com/777genius/agent-teams-ai/issues/139
This commit is contained in:
parent
c88a8836df
commit
6855d63ec6
355 changed files with 26205 additions and 4964 deletions
29
i18next.config.ts
Normal file
29
i18next.config.ts
Normal 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',
|
||||
},
|
||||
});
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
930
pnpm-lock.yaml
930
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
145
scripts/i18n/validate.ts
Normal file
145
scripts/i18n/validate.ts
Normal 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);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
19
src/features/localization/contracts/appLocale.ts
Normal file
19
src/features/localization/contracts/appLocale.ts
Normal 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);
|
||||
}
|
||||
11
src/features/localization/contracts/index.ts
Normal file
11
src/features/localization/contracts/index.ts
Normal 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';
|
||||
13
src/features/localization/contracts/namespaces.ts
Normal file
13
src/features/localization/contracts/namespaces.ts
Normal 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';
|
||||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
export type {
|
||||
CatalogValidationIssue,
|
||||
TranslationCatalogByNamespace,
|
||||
TranslationCatalogNode,
|
||||
TranslationCatalogsByLocale,
|
||||
} from '../domain/catalogPolicy';
|
||||
export { validateCatalogCompleteness as validateTranslationCatalogs } from '../domain/catalogPolicy';
|
||||
203
src/features/localization/core/domain/catalogPolicy.ts
Normal file
203
src/features/localization/core/domain/catalogPolicy.ts
Normal 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]);
|
||||
}
|
||||
43
src/features/localization/core/domain/localePolicy.ts
Normal file
43
src/features/localization/core/domain/localePolicy.ts
Normal 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;
|
||||
}
|
||||
11
src/features/localization/index.ts
Normal file
11
src/features/localization/index.ts
Normal 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';
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export function getBrowserSystemLocale(): string | null {
|
||||
return globalThis.navigator?.language ?? null;
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
10
src/features/localization/renderer/i18next.d.ts
vendored
Normal file
10
src/features/localization/renderer/i18next.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
4
src/features/localization/renderer/index.ts
Normal file
4
src/features/localization/renderer/index.ts
Normal 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';
|
||||
900
src/features/localization/renderer/locales/en/common.json
Normal file
900
src/features/localization/renderer/locales/en/common.json
Normal 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"
|
||||
}
|
||||
}
|
||||
197
src/features/localization/renderer/locales/en/dashboard.json
Normal file
197
src/features/localization/renderer/locales/en/dashboard.json
Normal 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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"fallback": "Something went wrong."
|
||||
}
|
||||
684
src/features/localization/renderer/locales/en/extensions.json
Normal file
684
src/features/localization/renderer/locales/en/extensions.json
Normal 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"
|
||||
}
|
||||
}
|
||||
217
src/features/localization/renderer/locales/en/report.json
Normal file
217
src/features/localization/renderer/locales/en/report.json
Normal 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"
|
||||
}
|
||||
}
|
||||
983
src/features/localization/renderer/locales/en/settings.json
Normal file
983
src/features/localization/renderer/locales/en/settings.json
Normal 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}}"
|
||||
}
|
||||
}
|
||||
2415
src/features/localization/renderer/locales/en/team.json
Normal file
2415
src/features/localization/renderer/locales/en/team.json
Normal file
File diff suppressed because it is too large
Load diff
900
src/features/localization/renderer/locales/ru/common.json
Normal file
900
src/features/localization/renderer/locales/ru/common.json
Normal 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": "не назначено"
|
||||
}
|
||||
}
|
||||
197
src/features/localization/renderer/locales/ru/dashboard.json
Normal file
197
src/features/localization/renderer/locales/ru/dashboard.json
Normal 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": "Подробнее"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"fallback": "Что-то пошло не так."
|
||||
}
|
||||
684
src/features/localization/renderer/locales/ru/extensions.json
Normal file
684
src/features/localization/renderer/locales/ru/extensions.json
Normal 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": "Официальный"
|
||||
}
|
||||
}
|
||||
217
src/features/localization/renderer/locales/ru/report.json
Normal file
217
src/features/localization/renderer/locales/ru/report.json
Normal 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}}"
|
||||
}
|
||||
}
|
||||
983
src/features/localization/renderer/locales/ru/settings.json
Normal file
983
src/features/localization/renderer/locales/ru/settings.json
Normal 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}}"
|
||||
}
|
||||
}
|
||||
2415
src/features/localization/renderer/locales/ru/team.json
Normal file
2415
src/features/localization/renderer/locales/ru/team.json
Normal file
File diff suppressed because it is too large
Load diff
5402
src/features/localization/renderer/resources.d.ts
vendored
Normal file
5402
src/features/localization/renderer/resources.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load diff
64
src/features/localization/renderer/ui/AppLanguageSelect.tsx
Normal file
64
src/features/localization/renderer/ui/AppLanguageSelect.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>;
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 "{searchQuery}"</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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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` };
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,6 +106,7 @@ export const App = (): React.JSX.Element => {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<LocalizationProvider appConfig={appConfig}>
|
||||
<ErrorBoundary>
|
||||
<TooltipProvider delayDuration={150} skipDelayDuration={1500}>
|
||||
<ContextSwitchOverlay />
|
||||
|
|
@ -112,5 +115,6 @@ export const App = (): React.JSX.Element => {
|
|||
<ToolApprovalSheet />
|
||||
</TooltipProvider>
|
||||
</ErrorBoundary>
|
||||
</LocalizationProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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]"
|
||||
|
|
|
|||
|
|
@ -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 →{' '}
|
||||
{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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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`}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
1
src/renderer/components/chat/session-panel.ts
Normal file
1
src/renderer/components/chat/session-panel.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { SessionContextPanel as SessionPanel } from './SessionContextPanel/index';
|
||||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue