diff --git a/package.json b/package.json index a532f86..66d3516 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ai-engineer-coach", - "displayName": "AI Engineer Coach", - "description": "Analyze your AI coding assistant usage across VS Code, Xcode, Claude, Codex, and OpenCode. Read-only, zero telemetry.", + "displayName": "%extension.displayName%", + "description": "%extension.description%", "version": "0.1.0", "publisher": "ai-engineer-coach", "author": { @@ -44,17 +44,17 @@ "commands": [ { "command": "aiEngineerCoach.open", - "title": "AI Engineer Coach: Open Dashboard", + "title": "%command.open%", "icon": "$(graph)" }, { "command": "aiEngineerCoach.reload", - "title": "AI Engineer Coach: Reload Data", + "title": "%command.reload%", "icon": "$(refresh)" }, { "command": "aiEngineerCoach.reviewLocalRules", - "title": "AI Engineer Coach: Review Local Rule Approvals", + "title": "%command.reviewRules%", "icon": "$(shield)" } ], @@ -62,7 +62,7 @@ "activitybar": [ { "id": "aiEngineerCoach", - "title": "AI Engineer Coach", + "title": "%views.container.title%", "icon": "$(graph)" } ] @@ -71,10 +71,21 @@ "aiEngineerCoach": [ { "id": "aiEngineerCoach.welcome", - "name": "Dashboard", + "name": "%views.dashboard.name%", "type": "webview" } ] + }, + "configuration": { + "title": "AI Engineer Coach", + "properties": { + "aiEngineerCoach.locale": { + "type": "string", + "enum": ["auto", "en", "ru"], + "default": "auto", + "description": "%config.locale.description%" + } + } } }, "scripts": { diff --git a/package.nls.json b/package.nls.json new file mode 100644 index 0000000..5969acb --- /dev/null +++ b/package.nls.json @@ -0,0 +1,10 @@ +{ + "extension.displayName": "AI Engineer Coach", + "extension.description": "Analyze your AI coding assistant usage across VS Code, Cursor, Claude, Codex, and OpenCode. Read-only, zero telemetry.", + "command.open": "AI Engineer Coach: Open Dashboard", + "command.reload": "AI Engineer Coach: Reload Data", + "command.reviewRules": "AI Engineer Coach: Review Local Rule Approvals", + "views.container.title": "AI Engineer Coach", + "views.dashboard.name": "Dashboard", + "config.locale.description": "Dashboard language. auto = follow VS Code/Cursor display language." +} diff --git a/package.nls.ru.json b/package.nls.ru.json new file mode 100644 index 0000000..bfb44fb --- /dev/null +++ b/package.nls.ru.json @@ -0,0 +1,10 @@ +{ + "extension.displayName": "AI Engineer Coach", + "extension.description": "Анализ работы с ИИ в VS Code, Cursor, Claude, Codex и OpenCode. Только локально, без телеметрии.", + "command.open": "AI Engineer Coach: Открыть панель", + "command.reload": "AI Engineer Coach: Обновить данные", + "command.reviewRules": "AI Engineer Coach: Проверить локальные правила", + "views.container.title": "AI Engineer Coach", + "views.dashboard.name": "Обзор", + "config.locale.description": "Язык панели. auto — как в Cursor/VS Code; ru — всегда русский." +} diff --git a/src/ui-locale.ts b/src/ui-locale.ts new file mode 100644 index 0000000..38a2869 --- /dev/null +++ b/src/ui-locale.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Resolve UI language for webviews (extension host). + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { resolveLocale, type Locale } from './webview/i18n'; + +export function getUiLanguageTag(): string { + const pref = vscode.workspace.getConfiguration('aiEngineerCoach').get('locale', 'auto'); + if (pref === 'en') return 'en'; + if (pref === 'ru') return 'ru'; + return vscode.env.language; +} + +export function getUiLocale(): Locale { + return resolveLocale(getUiLanguageTag()); +} diff --git a/src/webview/app.ts b/src/webview/app.ts index 321f66f..48d5284 100644 --- a/src/webview/app.ts +++ b/src/webview/app.ts @@ -5,7 +5,10 @@ /* Webview entry -- runs in the browser context inside the VS Code webview */ +import { setLocaleFromDocument } from './i18n'; import { AntiPatternData, DateFilter, StatsResult } from '../core/types'; + +setLocaleFromDocument(); import { $, $$, rpc, destroyCharts, initMessageListener, withErrorBoundary } from './shared'; import { html, render, unmount, ComponentChildren } from './render'; import { renderDashboard } from './page-dashboard'; diff --git a/src/webview/i18n.ts b/src/webview/i18n.ts new file mode 100644 index 0000000..da038a2 --- /dev/null +++ b/src/webview/i18n.ts @@ -0,0 +1,163 @@ +/*--------------------------------------------------------------------------------------------- + * UI strings — English / Russian (sidebar, dashboard shell, commands). + *--------------------------------------------------------------------------------------------*/ + +export type Locale = 'en' | 'ru'; + +const MESSAGES = { + en: { + 'nav.observe': 'Observe', + 'nav.measure': 'Measure', + 'nav.improve': 'Improve', + 'nav.levelUp': 'Level Up', + 'nav.dashboard': 'Dashboard', + 'nav.timeline': 'Timeline', + 'nav.codingMoments': 'Coding Moments', + 'nav.output': 'Output', + 'nav.burndown': 'Burndown', + 'nav.patterns': 'Patterns', + 'nav.antiPatterns': 'Anti-Patterns', + 'nav.skillFinder': 'Skill Finder', + 'nav.contextHealth': 'Context Health', + 'nav.levelUpPage': 'Level Up', + 'filter.workspace': 'Workspace', + 'filter.current': 'Current', + 'filter.all': 'All', + 'filter.searchWs': 'Search workspaces...', + 'filter.harness': 'Harness', + 'filter.allHarnesses': 'All Harnesses', + 'sidebar.title': 'AI Engineer Coach', + 'sidebar.noData': 'No data yet — sync your sessions to get started.', + 'sidebar.harnesses': 'Detected harnesses', + 'sidebar.lastSync': 'Last synced', + 'sidebar.explore': 'Explore AI Insights', + 'sidebar.sync': 'Sync Sessions', + 'dash.calibrating': 'Calibrating the dashboard vibes...', + 'dash.vibes': 'Dashboard vibes sampled across {n} dimensions', + 'dash.requests': 'Requests', + 'dash.sessions': 'Sessions', + 'dash.loc': 'AI LoC', + 'dash.workspaces': 'Workspaces', + 'dash.tokenHidden': 'Token Usage & Burndown temporarily hidden', + 'dash.tokenHiddenDesc': + 'These features are disabled until we can verify that reported numbers align with GitHub billing data.', + 'dash.apSummary': 'Anti-Patterns Summary', + 'dash.viewAllAp': 'View All Anti-Patterns →', + 'dash.skillFinder': 'Skill Finder', + 'dash.openSkills': 'Open Full View →', + 'dash.skillDesc': + 'Scans your prompt history for repeated patterns that waste time re-explaining the same tasks.', + 'dash.skillAnalyze': 'Analyze your prompt history to discover skill opportunities.', + 'dash.scanSkills': 'Scan for Skills', + 'dash.dailyActivity': 'Daily Activity', + 'dash.topWs': 'Top Workspaces by Requests', + 'dash.byHarness': 'Requests by Harness', + 'dash.close': 'Close', + 'score.mergeWizard': 'Merge Wizard', + 'score.shipGoblin': 'Ship Goblin Deluxe', + 'score.vibeGremlin': 'Vibe Refactor Gremlin', + 'score.rubberDuck': 'Rubber Duck Ringleader', + 'score.stackTrace': 'Stack Trace Survivor', + 'langBanner': + 'Main menu is in Russian. Some inner pages are still in English — we are translating them gradually.', + }, + ru: { + 'nav.observe': 'Наблюдение', + 'nav.measure': 'Метрики', + 'nav.improve': 'Улучшение', + 'nav.levelUp': 'Развитие', + 'nav.dashboard': 'Обзор', + 'nav.timeline': 'Таймлайн', + 'nav.codingMoments': 'Моменты кода', + 'nav.output': 'Выработка', + 'nav.burndown': 'Расход токенов', + 'nav.patterns': 'Паттерны', + 'nav.antiPatterns': 'Антипаттерны', + 'nav.skillFinder': 'Поиск навыков', + 'nav.contextHealth': 'Контекст', + 'nav.levelUpPage': 'Обучение', + 'filter.workspace': 'Проект', + 'filter.current': 'Текущий', + 'filter.all': 'Все', + 'filter.searchWs': 'Поиск проектов...', + 'filter.harness': 'Среда ИИ', + 'filter.allHarnesses': 'Все среды', + 'sidebar.title': 'AI Engineer Coach', + 'sidebar.noData': 'Пока нет данных — синхронизируйте сессии.', + 'sidebar.harnesses': 'Обнаруженные среды', + 'sidebar.lastSync': 'Обновлено', + 'sidebar.explore': 'Открыть аналитику', + 'sidebar.sync': 'Синхронизировать', + 'dash.calibrating': 'Считаем сводку…', + 'dash.vibes': 'Сводка по {n} направлениям', + 'dash.requests': 'Запросы', + 'dash.sessions': 'Сессии', + 'dash.loc': 'Строк ИИ', + 'dash.workspaces': 'Проекты', + 'dash.tokenHidden': 'Токены и Burndown временно скрыты', + 'dash.tokenHiddenDesc': + 'Раздел отключён, пока не сверим цифры с биллингом GitHub.', + 'dash.apSummary': 'Сводка антипаттернов', + 'dash.viewAllAp': 'Все антипаттерны →', + 'dash.skillFinder': 'Поиск навыков', + 'dash.openSkills': 'Полный вид →', + 'dash.skillDesc': + 'Ищет повторяющиеся промпты, чтобы оформить их в навыки и не объяснять одно и то же.', + 'dash.skillAnalyze': 'Проанализируйте историю промптов и найдите полезные навыки.', + 'dash.scanSkills': 'Сканировать навыки', + 'dash.dailyActivity': 'Активность по дням', + 'dash.topWs': 'Топ проектов по запросам', + 'dash.byHarness': 'Запросы по среде ИИ', + 'dash.close': 'Закрыть', + 'score.mergeWizard': 'Мастер слияний', + 'score.shipGoblin': 'Гоблин релизов', + 'score.vibeGremlin': 'Гремлин рефакторинга', + 'score.rubberDuck': 'Утконаводитель', + 'score.stackTrace': 'Выживший в стектрейсе', + 'langBanner': + 'Меню на русском. Часть внутренних страниц пока на английском — перевод добавляется постепенно.', + }, +} as const; + +export type MessageKey = keyof typeof MESSAGES.en; + +let activeLocale: Locale = 'en'; + +export function resolveLocale(vscodeLanguage: string): Locale { + const lang = (vscodeLanguage || 'en').toLowerCase(); + return lang.startsWith('ru') ? 'ru' : 'en'; +} + +export function setActiveLocale(locale: Locale): void { + activeLocale = locale; +} + +export function getActiveLocale(): Locale { + return activeLocale; +} + +export function setLocaleFromDocument(): void { + const lang = document.documentElement.getAttribute('lang') || 'en'; + activeLocale = resolveLocale(lang); +} + +export function t(key: MessageKey, locale?: Locale): string { + const loc = locale ?? activeLocale; + return MESSAGES[loc][key] ?? MESSAGES.en[key]; +} + +export function tFormat(key: MessageKey, vars: Record, locale?: Locale): string { + let s = t(key, locale); + for (const [k, v] of Object.entries(vars)) { + s = s.replace(`{${k}}`, String(v)); + } + return s; +} + +export function funScoreLabel(score: number, locale?: Locale): string { + if (score >= 90) return t('score.mergeWizard', locale); + if (score >= 75) return t('score.shipGoblin', locale); + if (score >= 60) return t('score.vibeGremlin', locale); + if (score >= 40) return t('score.rubberDuck', locale); + return t('score.stackTrace', locale); +} diff --git a/src/webview/page-dashboard.ts b/src/webview/page-dashboard.ts index 8d7df45..ecc3ad2 100644 --- a/src/webview/page-dashboard.ts +++ b/src/webview/page-dashboard.ts @@ -10,6 +10,7 @@ import { FF_TOKEN_REPORTING_ENABLED } from '../core/constants'; import { rpc, rpcAllSettled, createChart, formatNum, COLORS, PALETTE, harnessColor, destroyChartById, scoreColor, scoreLabel } from './shared'; import { html, render, CanvasEl, ScoreRing, PctBadge } from './render'; import { setSkillCache, getSkillCache } from './skill-cache'; +import { funScoreLabel, t, tFormat } from './i18n'; // Module-level view state — survives filter/harness changes. let activeMetric = 'requests'; @@ -42,14 +43,6 @@ function SkillCard({ title, subtitle }: { title: string; subtitle: string }) { `; } -function funScoreLabel(score: number): string { - if (score >= 90) return 'Merge Wizard'; - if (score >= 75) return 'Ship Goblin Deluxe'; - if (score >= 60) return 'Vibe Refactor Gremlin'; - if (score >= 40) return 'Rubber Duck Ringleader'; - return 'Stack Trace Survivor'; -} - function normalizeDashboardLanguage(label: string): string { const normalized = label.trim().toLowerCase(); return DASHBOARD_LANGUAGE_ALIASES[normalized] || normalized; @@ -106,7 +99,7 @@ function renderDashboardMarkup(
<${ScoreRing} score=${overallScore} color=${overallColor} size=${64} />
${funScoreLabel(overallScore)}
-
${scores.length > 0 ? 'Dashboard vibes sampled across ' + scores.length + ' dimensions' : 'Calibrating the dashboard vibes...'}
+
${scores.length > 0 ? tFormat('dash.vibes', { n: scores.length }) : t('dash.calibrating')}
${langs.length > 0 && html` @@ -116,20 +109,20 @@ function renderDashboardMarkup(
-
${formatNum(totalReqs)}
Requests
-
${formatNum(totalSessions)}
Sessions
-
${formatNum(totalLoc)}
AI LoC
-
${stats.totalWorkspaces}
Workspaces
+
${formatNum(totalReqs)}
${t('dash.requests')}
+
${formatNum(totalSessions)}
${t('dash.sessions')}
+
${formatNum(totalLoc)}
${t('dash.loc')}
+
${stats.totalWorkspaces}
${t('dash.workspaces')}
${harnesses.length > 0 && html`
${harnesses.map((h, i) => html`${h}`)}
`}
- ${!FF_TOKEN_REPORTING_ENABLED && html`
\u2139
Token Usage & Burndown temporarily hidden

These features are disabled until we can verify that reported numbers align with GitHub's billing data. They will be re-enabled once validated.

`} - ${scores.length > 0 && html`

Anti-Patterns Summary

View All Anti-Patterns \u2192
${scores.map(g => html`<${PracticeCard} g=${g} />`)}
`} -

Scans your prompt history for repeated patterns that waste time re-explaining the same tasks.

${!skillCache && html`

Analyze your prompt history to discover skill opportunities.

`}
-

Daily Activity

<${CanvasEl} id="dailyChart" height=${160} />
-
<${CanvasEl} id="wsChart" height=${140} title="Top Workspaces by Requests" /><${CanvasEl} id="harnessChart" height=${140} title="Requests by Harness" />
-
Top Workspaces by Requests
+ ${!FF_TOKEN_REPORTING_ENABLED && html`
\u2139
${t('dash.tokenHidden')}

${t('dash.tokenHiddenDesc')}

`} + ${scores.length > 0 && html`

${t('dash.apSummary')}

${t('dash.viewAllAp')}
${scores.map(g => html`<${PracticeCard} g=${g} />`)}
`} +

${t('dash.skillFinder')}

${t('dash.openSkills')}

${t('dash.skillDesc')}

${!skillCache && html`

${t('dash.skillAnalyze')}

`}
+

${t('dash.dailyActivity')}

<${CanvasEl} id="dailyChart" height=${160} />
+
<${CanvasEl} id="wsChart" height=${140} title=${t('dash.topWs')} /><${CanvasEl} id="harnessChart" height=${140} title=${t('dash.byHarness')} />
+
${t('dash.topWs')}
`, container); } diff --git a/src/webview/panel-html.ts b/src/webview/panel-html.ts index 4d17c52..dd1c6ad 100644 --- a/src/webview/panel-html.ts +++ b/src/webview/panel-html.ts @@ -6,69 +6,84 @@ import * as vscode from 'vscode'; import { getNonce } from './panel-shared'; import { FF_TOKEN_REPORTING_ENABLED } from '../core/constants'; +import { resolveLocale, t } from './i18n'; -export function getDashboardHtml(webview: vscode.Webview, extensionUri: vscode.Uri): string { +export function getDashboardHtml( + webview: vscode.Webview, + extensionUri: vscode.Uri, + vscodeLanguage: string, +): string { const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, 'dist', 'webview', 'app.js')); const styleUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, 'dist', 'webview', 'styles.css')); const nonce = getNonce(); + const locale = resolveLocale(vscodeLanguage); + const langAttr = locale === 'ru' ? 'ru' : 'en'; + const burndownNav = FF_TOKEN_REPORTING_ENABLED + ? `
  • ${t('nav.burndown', locale)}
  • ` + : ''; return ` - + -AI Engineer Coach +${t('sidebar.title', locale)}
    -
    +
    + ${locale === 'ru' ? `
    ${t('langBanner', locale)}
    ` : ''} +
    +
    `; } -export function getErrorHtml(message: string): string { +export function getErrorHtml(message: string, vscodeLanguage?: string): string { + const locale = resolveLocale(vscodeLanguage || 'en'); + const title = locale === 'ru' ? 'Ошибка' : 'Error'; const escaped = message.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>'); return ` - + -

    Error

    ${escaped}

    +

    ${title}

    ${escaped}

    `; -} \ No newline at end of file +} diff --git a/src/webview/panel-sidebar.ts b/src/webview/panel-sidebar.ts index 4744bb6..6f9d09d 100644 --- a/src/webview/panel-sidebar.ts +++ b/src/webview/panel-sidebar.ts @@ -6,6 +6,8 @@ import * as vscode from 'vscode'; import { loadSidebarStats } from '../core/cache'; import { getNonce } from './panel-shared'; +import { getUiLanguageTag } from '../ui-locale'; +import { resolveLocale, t } from './i18n'; export class DashboardSidebarProvider implements vscode.WebviewViewProvider { public static instance: DashboardSidebarProvider | undefined; @@ -40,21 +42,23 @@ export class DashboardSidebarProvider implements vscode.WebviewViewProvider { } private renderHtml(nonce: string): string { + const locale = resolveLocale(getUiLanguageTag()); + const langAttr = locale === 'ru' ? 'ru' : 'en'; const stats = loadSidebarStats(); const statsHtml = stats ? ` ` : ` `; return ` - + @@ -118,10 +122,10 @@ button.secondary:hover { background: var(--vscode-button-secondaryHoverBackgroun -

    AI Engineer Coach

    +

    ${t('sidebar.title', locale)}

    ${statsHtml}
    - - + +