From a41e6d19fd0b982af3005a9415de94f3c6f16eb8 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Fri, 12 Dec 2025 22:02:23 +0800 Subject: [PATCH] Refactor code structure for improved readability and maintainability --- ccw/src/core/dashboard-generator-patch.js | 15 + ccw/src/core/dashboard-generator.js | 18 +- ccw/src/core/server.js | 1 + .../dashboard-js/components/carousel.js | 8 +- .../dashboard-js/components/hook-manager.js | 101 +- .../dashboard-js/components/navigation.js | 28 +- ccw/src/templates/dashboard-js/i18n.js | 1150 +++++++++++++++++ ccw/src/templates/dashboard-js/main.js | 3 + .../dashboard-js/views/cli-manager.js | 60 +- .../templates/dashboard-js/views/explorer.js | 56 +- ccw/src/templates/dashboard-js/views/home.js | 24 +- .../dashboard-js/views/hook-manager.js | 137 +- .../dashboard-js/views/lite-tasks.js | 32 +- .../dashboard-js/views/mcp-manager.js | 38 +- .../dashboard-js/views/session-detail.js | 107 +- ccw/src/templates/dashboard.html | 117 +- 16 files changed, 1588 insertions(+), 307 deletions(-) create mode 100644 ccw/src/templates/dashboard-js/i18n.js diff --git a/ccw/src/core/dashboard-generator-patch.js b/ccw/src/core/dashboard-generator-patch.js index 9b9ab0e8..dd4d2d0a 100644 --- a/ccw/src/core/dashboard-generator-patch.js +++ b/ccw/src/core/dashboard-generator-patch.js @@ -2,6 +2,8 @@ // Modular dashboard JS files (in dependency order) const MODULE_FILES = [ + // i18n (must be first for translations) + 'dashboard-js/i18n.js', // Base (no dependencies) 'dashboard-js/state.js', 'dashboard-js/utils.js', @@ -16,6 +18,14 @@ const MODULE_FILES = [ 'dashboard-js/components/task-drawer-core.js', 'dashboard-js/components/tabs-context.js', 'dashboard-js/components/tabs-other.js', + 'dashboard-js/components/carousel.js', + 'dashboard-js/components/notifications.js', + 'dashboard-js/components/global-notifications.js', + 'dashboard-js/components/cli-status.js', + 'dashboard-js/components/cli-history.js', + 'dashboard-js/components/mcp-manager.js', + 'dashboard-js/components/hook-manager.js', + 'dashboard-js/components/version-check.js', // Views 'dashboard-js/views/home.js', 'dashboard-js/views/project-overview.js', @@ -23,6 +33,11 @@ const MODULE_FILES = [ 'dashboard-js/views/fix-session.js', 'dashboard-js/views/lite-tasks.js', 'dashboard-js/views/session-detail.js', + 'dashboard-js/views/cli-manager.js', + 'dashboard-js/views/explorer.js', + 'dashboard-js/views/mcp-manager.js', + 'dashboard-js/views/hook-manager.js', + 'dashboard-js/views/history.js', // Navigation & Main 'dashboard-js/components/navigation.js', 'dashboard-js/main.js' diff --git a/ccw/src/core/dashboard-generator.js b/ccw/src/core/dashboard-generator.js index ff48c73c..88b80f50 100644 --- a/ccw/src/core/dashboard-generator.js +++ b/ccw/src/core/dashboard-generator.js @@ -21,10 +21,13 @@ const MODULE_CSS_FILES = [ '05-context.css', '06-cards.css', '07-managers.css', - '08-review.css' + '08-review.css', + '09-explorer.css', + '10-cli.css' ]; const MODULE_FILES = [ + 'i18n.js', // Must be loaded first for translations 'utils.js', 'state.js', 'api.js', @@ -37,12 +40,25 @@ const MODULE_FILES = [ 'components/task-drawer-core.js', 'components/task-drawer-renderers.js', 'components/flowchart.js', + 'components/carousel.js', + 'components/notifications.js', + 'components/global-notifications.js', + 'components/cli-status.js', + 'components/cli-history.js', + 'components/mcp-manager.js', + 'components/hook-manager.js', + 'components/version-check.js', 'views/home.js', 'views/project-overview.js', 'views/session-detail.js', 'views/review-session.js', 'views/lite-tasks.js', 'views/fix-session.js', + 'views/cli-manager.js', + 'views/explorer.js', + 'views/mcp-manager.js', + 'views/hook-manager.js', + 'views/history.js', 'main.js' ]; diff --git a/ccw/src/core/server.js b/ccw/src/core/server.js index ed9afda8..8b4eff89 100644 --- a/ccw/src/core/server.js +++ b/ccw/src/core/server.js @@ -82,6 +82,7 @@ function handlePostRequest(req, res, handler) { // Modular JS files in dependency order const MODULE_FILES = [ + 'i18n.js', // Must be loaded first for translations 'utils.js', 'state.js', 'api.js', diff --git a/ccw/src/templates/dashboard-js/components/carousel.js b/ccw/src/templates/dashboard-js/components/carousel.js index 93bfd15d..56d3b1c8 100644 --- a/ccw/src/templates/dashboard-js/components/carousel.js +++ b/ccw/src/templates/dashboard-js/components/carousel.js @@ -110,7 +110,7 @@ function renderCarouselSlide(direction = 'none') { `; @@ -172,7 +172,7 @@ function renderCarouselSlide(direction = 'none') {
- Progress + ${t('session.progress')} ${completed}/${taskCount}
@@ -196,7 +196,7 @@ function renderCarouselSlide(direction = 'none') {
-
Recent Tasks
+
${t('tab.tasks')}
${displayTasks.length > 0 ? displayTasks.map(task => `
@@ -204,7 +204,7 @@ function renderCarouselSlide(direction = 'none') { ${escapeHtml(task.title || task.id || 'Task')}
`).join('') : ` -
No tasks yet
+
${t('empty.noTasks')}
`}
diff --git a/ccw/src/templates/dashboard-js/components/hook-manager.js b/ccw/src/templates/dashboard-js/components/hook-manager.js index 23cbe220..1e246a19 100644 --- a/ccw/src/templates/dashboard-js/components/hook-manager.js +++ b/ccw/src/templates/dashboard-js/components/hook-manager.js @@ -440,8 +440,59 @@ function renderWizardModalContent() { if (!container || !currentWizardTemplate) return; const wizard = currentWizardTemplate; + const wizardId = wizard.id; const selectedOption = wizardConfig.triggerType || wizard.options[0].id; + // Get translated wizard name and description + const wizardName = wizardId === 'memory-update' ? t('hook.wizard.memoryUpdate') : + wizardId === 'skill-context' ? t('hook.wizard.skillContext') : wizard.name; + const wizardDesc = wizardId === 'memory-update' ? t('hook.wizard.memoryUpdateDesc') : + wizardId === 'skill-context' ? t('hook.wizard.skillContextDesc') : wizard.description; + + // Helper to get translated option names + const getOptionName = (optId) => { + if (wizardId === 'memory-update') { + if (optId === 'on-stop') return t('hook.wizard.onSessionEnd'); + if (optId === 'periodic') return t('hook.wizard.periodicUpdate'); + } + if (wizardId === 'skill-context') { + if (optId === 'keyword') return t('hook.wizard.keywordMatching'); + if (optId === 'auto') return t('hook.wizard.autoDetection'); + } + return wizard.options.find(o => o.id === optId)?.name || ''; + }; + + const getOptionDesc = (optId) => { + if (wizardId === 'memory-update') { + if (optId === 'on-stop') return t('hook.wizard.onSessionEndDesc'); + if (optId === 'periodic') return t('hook.wizard.periodicUpdateDesc'); + } + if (wizardId === 'skill-context') { + if (optId === 'keyword') return t('hook.wizard.keywordMatchingDesc'); + if (optId === 'auto') return t('hook.wizard.autoDetectionDesc'); + } + return wizard.options.find(o => o.id === optId)?.description || ''; + }; + + // Helper to get translated field labels + const getFieldLabel = (fieldKey) => { + const labels = { + 'tool': t('hook.wizard.cliTool'), + 'interval': t('hook.wizard.intervalSeconds'), + 'strategy': t('hook.wizard.updateStrategy') + }; + return labels[fieldKey] || wizard.configFields.find(f => f.key === fieldKey)?.label || fieldKey; + }; + + const getFieldDesc = (fieldKey) => { + const descs = { + 'tool': t('hook.wizard.toolForDocGen'), + 'interval': t('hook.wizard.timeBetweenUpdates'), + 'strategy': t('hook.wizard.relatedStrategy') + }; + return descs[fieldKey] || wizard.configFields.find(f => f.key === fieldKey)?.description || ''; + }; + container.innerHTML = `
@@ -450,14 +501,14 @@ function renderWizardModalContent() {
-

${escapeHtml(wizard.name)}

-

${escapeHtml(wizard.description)}

+

${escapeHtml(wizardName)}

+

${escapeHtml(wizardDesc)}

- +
${wizard.options.map(opt => ` `).join('')} @@ -476,18 +527,20 @@ function renderWizardModalContent() {
- + ${wizard.customRenderer ? window[wizard.customRenderer]() : wizard.configFields.map(field => { // Check if field should be shown for current trigger type const shouldShow = !field.showFor || field.showFor.includes(selectedOption); if (!shouldShow) return ''; const value = wizardConfig[field.key] ?? field.default; + const fieldLabel = getFieldLabel(field.key); + const fieldDesc = getFieldDesc(field.key); if (field.type === 'select') { return `
- + - ${field.description ? `

${escapeHtml(field.description)}

` : ''} + ${fieldDesc ? `

${escapeHtml(fieldDesc)}

` : ''}
`; } else if (field.type === 'number') { return `
- +
${formatIntervalDisplay(value)}
- ${field.description ? `

${escapeHtml(field.description)}

` : ''} + ${fieldDesc ? `

${escapeHtml(fieldDesc)}

` : ''}
`; } @@ -522,7 +575,7 @@ function renderWizardModalContent() {
- +
${escapeHtml(generateWizardCommand())}
@@ -530,16 +583,16 @@ function renderWizardModalContent() {
- +
@@ -593,10 +646,10 @@ function renderSkillContextConfig() { return '
' + '
' + '' + - 'Auto Detection Mode' + + '' + t('hook.wizard.autoDetectionMode') + '' + '
' + - '

SKILLs will be automatically loaded when their name appears in your prompt.

' + - '

Available SKILLs: ' + skillBadges + '

' + + '

' + t('hook.wizard.autoDetectionInfo') + '

' + + '

' + t('hook.wizard.availableSkills') + ' ' + skillBadges + '

' + '
'; } @@ -604,8 +657,8 @@ function renderSkillContextConfig() { if (skillConfigs.length === 0) { configListHtml = '
' + '' + - '

No SKILLs configured yet

' + - '

Click "Add SKILL" to configure keyword triggers

' + + '

' + t('hook.wizard.noSkillsConfigured') + '

' + + '

' + t('hook.wizard.clickAddSkill') + '

' + '
'; } else { configListHtml = skillConfigs.map(function(config, idx) { @@ -617,7 +670,7 @@ function renderSkillContextConfig() { '
' + '' + '
' + '
' + - '' + + '' + '' + - 'No SKILLs found. Create SKILL packages in .claude/skills/' + + t('hook.wizard.noSkillsFound') + '
'; } return '
' + '
' + - 'Configure SKILLs' + + '' + t('hook.wizard.configureSkills') + '' + '' + '
' + '
' + configListHtml + '
' + diff --git a/ccw/src/templates/dashboard-js/components/navigation.js b/ccw/src/templates/dashboard-js/components/navigation.js index f5e5841c..c441e17b 100644 --- a/ccw/src/templates/dashboard-js/components/navigation.js +++ b/ccw/src/templates/dashboard-js/components/navigation.js @@ -117,25 +117,27 @@ function setActiveNavItem(item) { function updateContentTitle() { const titleEl = document.getElementById('contentTitle'); if (currentView === 'project-overview') { - titleEl.textContent = 'Project Overview'; + titleEl.textContent = t('title.projectOverview'); } else if (currentView === 'mcp-manager') { - titleEl.textContent = 'MCP Server Management'; + titleEl.textContent = t('title.mcpManagement'); } else if (currentView === 'explorer') { - titleEl.textContent = 'File Explorer'; + titleEl.textContent = t('title.fileExplorer'); } else if (currentView === 'cli-manager') { - titleEl.textContent = 'CLI Tools & CCW'; + titleEl.textContent = t('title.cliTools'); } else if (currentView === 'cli-history') { - titleEl.textContent = 'CLI Execution History'; + titleEl.textContent = t('title.cliHistory'); + } else if (currentView === 'hook-manager') { + titleEl.textContent = t('title.hookManager'); } else if (currentView === 'liteTasks') { - const names = { 'lite-plan': 'Lite Plan Sessions', 'lite-fix': 'Lite Fix Sessions' }; - titleEl.textContent = names[currentLiteType] || 'Lite Tasks'; + const names = { 'lite-plan': t('title.litePlanSessions'), 'lite-fix': t('title.liteFixSessions') }; + titleEl.textContent = names[currentLiteType] || t('title.liteTasks'); } else if (currentView === 'sessionDetail') { - titleEl.textContent = 'Session Detail'; + titleEl.textContent = t('title.sessionDetail'); } else if (currentView === 'liteTaskDetail') { - titleEl.textContent = 'Lite Task Detail'; + titleEl.textContent = t('title.liteTaskDetail'); } else { - const names = { 'all': 'All Sessions', 'active': 'Active Sessions', 'archived': 'Archived Sessions' }; - titleEl.textContent = names[currentFilter] || 'Sessions'; + const names = { 'all': t('title.allSessions'), 'active': t('title.activeSessions'), 'archived': t('title.archivedSessions') }; + titleEl.textContent = names[currentFilter] || t('title.sessions'); } } @@ -205,7 +207,7 @@ async function refreshWorkspace() { renderProjectOverview(); } - showRefreshToast('Workspace refreshed', 'success'); + showRefreshToast(t('toast.workspaceRefreshed'), 'success'); } } else { // Non-server mode: just reload page @@ -213,7 +215,7 @@ async function refreshWorkspace() { } } catch (error) { console.error('Refresh failed:', error); - showRefreshToast('Refresh failed: ' + error.message, 'error'); + showRefreshToast(t('toast.refreshFailed', { error: error.message }), 'error'); } finally { btn.classList.remove('refreshing'); btn.disabled = false; diff --git a/ccw/src/templates/dashboard-js/i18n.js b/ccw/src/templates/dashboard-js/i18n.js new file mode 100644 index 00000000..76c24e43 --- /dev/null +++ b/ccw/src/templates/dashboard-js/i18n.js @@ -0,0 +1,1150 @@ +// ============================================ +// I18N - Internationalization Module +// ============================================ +// Supports English and Chinese (Simplified) + +// Current language (default: detect from browser or use 'en') +let currentLang = 'en'; + +// Translation dictionaries +const i18n = { + en: { + // App title and brand + 'app.title': 'CCW Dashboard', + 'app.brand': 'Claude Code Workflow', + + // Header + 'header.project': 'Project:', + 'header.recentProjects': 'Recent Projects', + 'header.browse': 'Browse...', + 'header.refreshWorkspace': 'Refresh workspace', + 'header.toggleTheme': 'Toggle theme', + 'header.language': 'Language', + + // Sidebar - Project section + 'nav.project': 'Project', + 'nav.overview': 'Overview', + 'nav.explorer': 'Explorer', + 'nav.status': 'Status', + 'nav.history': 'History', + + // Sidebar - Sessions section + 'nav.sessions': 'Sessions', + 'nav.all': 'All', + 'nav.active': 'Active', + 'nav.archived': 'Archived', + + // Sidebar - Lite Tasks section + 'nav.liteTasks': 'Lite Tasks', + 'nav.litePlan': 'Lite Plan', + 'nav.liteFix': 'Lite Fix', + + // Sidebar - MCP section + 'nav.mcpServers': 'MCP Servers', + 'nav.manage': 'Manage', + + // Sidebar - Hooks section + 'nav.hooks': 'Hooks', + + // Sidebar - Footer + 'nav.collapse': 'Collapse', + 'nav.expand': 'Expand', + + // Stats cards + 'stats.totalSessions': 'Total Sessions', + 'stats.activeSessions': 'Active Sessions', + 'stats.totalTasks': 'Total Tasks', + 'stats.completedTasks': 'Completed Tasks', + + // Carousel + 'carousel.noActiveSessions': 'No active sessions', + 'carousel.previous': 'Previous', + 'carousel.next': 'Next', + 'carousel.pause': 'Pause auto-play', + + // Content titles + 'title.allSessions': 'All Sessions', + 'title.activeSessions': 'Active Sessions', + 'title.archivedSessions': 'Archived Sessions', + 'title.sessions': 'Sessions', + 'title.projectOverview': 'Project Overview', + 'title.mcpManagement': 'MCP Server Management', + 'title.fileExplorer': 'File Explorer', + 'title.cliTools': 'CLI Tools & CCW', + 'title.cliHistory': 'CLI Execution History', + 'title.litePlanSessions': 'Lite Plan Sessions', + 'title.liteFixSessions': 'Lite Fix Sessions', + 'title.liteTasks': 'Lite Tasks', + 'title.sessionDetail': 'Session Detail', + 'title.liteTaskDetail': 'Lite Task Detail', + 'title.hookManager': 'Hook Manager', + + // Search + 'search.placeholder': 'Search...', + + // Session cards + 'session.status.active': 'ACTIVE', + 'session.status.archived': 'ARCHIVED', + 'session.status.planning': 'PLANNING', + 'session.tasks': 'tasks', + 'session.findings': 'findings', + 'session.dimensions': 'dimensions', + 'session.progress': 'Progress', + + // Empty states + 'empty.noSessions': 'No Sessions Found', + 'empty.noSessionsText': 'No workflow sessions match your current filter.', + 'empty.noTasks': 'No Tasks', + 'empty.noTasksText': 'This session has no tasks defined.', + 'empty.noTaskFiles': 'No Task Files', + 'empty.noTaskFilesText': 'No IMPL-*.json files found in .task/', + 'empty.noLiteSessions': 'No {type} Sessions', + 'empty.noLiteSessionsText': 'No sessions found in .workflow/.{type}/', + 'empty.noMcpServers': 'No MCP servers configured for this project', + 'empty.addMcpServersHint': 'Add servers from the available list below', + 'empty.noAdditionalMcp': 'No additional MCP servers found in other projects', + 'empty.noHooks': 'No hooks configured for this project', + 'empty.createHookHint': 'Create a hook to automate actions on tool usage', + 'empty.noGlobalHooks': 'No global hooks configured', + 'empty.globalHooksHint': 'Global hooks apply to all Claude Code sessions', + 'empty.noDiagnoses': 'No Diagnoses', + 'empty.noDiagnosesText': 'No diagnosis-*.json files found for this session.', + + // Session detail tabs + 'tab.tasks': 'Tasks', + 'tab.context': 'Context', + 'tab.summary': 'Summary', + 'tab.implPlan': 'IMPL Plan', + 'tab.conflict': 'Conflict', + 'tab.review': 'Review', + 'tab.plan': 'Plan', + 'tab.diagnoses': 'Diagnoses', + + // Session detail + 'detail.backToSessions': 'Back to Sessions', + 'detail.backToLiteTasks': 'Back to {type}', + 'detail.created': 'Created:', + 'detail.archived': 'Archived:', + 'detail.project': 'Project:', + 'detail.tasks': 'Tasks:', + 'detail.completed': 'completed', + + // Task status + 'task.status.pending': 'Pending', + 'task.status.inProgress': 'In Progress', + 'task.status.completed': 'Completed', + 'task.completed': 'completed', + 'task.inProgress': 'in progress', + 'task.pending': 'pending', + + // Task actions + 'task.quickActions': 'Quick Actions:', + 'task.allPending': 'All Pending', + 'task.allInProgress': 'All In Progress', + 'task.allCompleted': 'All Completed', + 'task.setAllConfirm': 'Set all {count} tasks to "{status}"?', + 'task.statusUpdated': 'Task {id} status updated', + 'task.tasksUpdated': 'All {count} tasks updated', + 'task.noPendingTasks': 'No pending tasks to start', + 'task.noInProgressTasks': 'No in-progress tasks to complete', + 'task.movedToInProgress': '{count} tasks moved to In Progress', + 'task.tasksCompleted': '{count} tasks completed', + + // Context tab + 'context.description': 'description:', + 'context.requirements': 'requirements:', + 'context.focusPaths': 'focus_paths:', + 'context.modificationPoints': 'modification_points:', + 'context.acceptance': 'acceptance:', + 'context.noData': 'No context data', + 'context.loading': 'Loading context data...', + 'context.loadError': 'Failed to load context: {error}', + + // Flow control + 'flow.implementationApproach': 'implementation_approach:', + 'flow.preAnalysis': 'pre_analysis:', + 'flow.targetFiles': 'target_files:', + 'flow.noData': 'No flow control data', + + // Summary tab + 'summary.loading': 'Loading summaries...', + 'summary.title': 'Summaries', + 'summary.hint': 'Session summaries will be loaded from .summaries/', + 'summary.noSummaries': 'No Summaries', + 'summary.noSummariesText': 'No summaries found in .summaries/', + + // IMPL Plan tab + 'implPlan.loading': 'Loading IMPL plan...', + 'implPlan.title': 'IMPL Plan', + 'implPlan.hint': 'IMPL plan will be loaded from IMPL_PLAN.md', + + // Review tab + 'review.loading': 'Loading review data...', + 'review.title': 'Review Data', + 'review.hint': 'Review data will be loaded from review files', + + // CLI Manager + 'cli.tools': 'CLI Tools', + 'cli.available': 'available', + 'cli.refreshStatus': 'Refresh Status', + 'cli.ready': 'Ready', + 'cli.notInstalled': 'Not Installed', + 'cli.setDefault': 'Set Default', + 'cli.default': 'Default', + 'cli.install': 'Install', + 'cli.initIndex': 'Init Index', + 'cli.geminiDesc': 'Google AI for code analysis', + 'cli.qwenDesc': 'Alibaba AI assistant', + 'cli.codexDesc': 'OpenAI code generation', + 'cli.codexLensDesc': 'Code indexing & FTS search', + 'cli.codexLensDescFull': 'Full-text code search engine', + 'cli.semanticDesc': 'AI-powered code understanding', + 'cli.semanticDescFull': 'Natural language code search', + + // CCW Install + 'ccw.install': 'CCW Install', + 'ccw.installations': 'installation', + 'ccw.installationsPlural': 'installations', + 'ccw.noInstallations': 'No installations found', + 'ccw.installCcw': 'Install CCW', + 'ccw.upgrade': 'Upgrade', + 'ccw.uninstall': 'Uninstall', + 'ccw.files': 'files', + 'ccw.globalInstall': 'Global Installation', + 'ccw.globalInstallDesc': 'Install to user home directory (~/.claude)', + 'ccw.pathInstall': 'Path Installation', + 'ccw.pathInstallDesc': 'Install to a specific project folder', + 'ccw.installPath': 'Installation Path', + 'ccw.installToPath': 'Install to Path', + 'ccw.uninstallConfirm': 'Uninstall CCW from this location?', + 'ccw.upgradeStarting': 'Starting upgrade...', + 'ccw.upgradeCompleted': 'Upgrade completed! Refreshing...', + 'ccw.upgradeFailed': 'Upgrade failed: {error}', + + // CCW Endpoint Tools + 'ccw.endpointTools': 'CCW Endpoint Tools', + 'ccw.tool': 'tool', + 'ccw.tools': 'tools', + 'ccw.noEndpointTools': 'No endpoint tools found', + 'ccw.parameters': 'Parameters', + 'ccw.required': 'required', + 'ccw.optional': 'optional', + 'ccw.default': 'Default:', + 'ccw.options': 'Options:', + 'ccw.noParams': 'This tool has no parameters', + 'ccw.usageExample': 'Usage Example', + 'ccw.endpointTool': 'endpoint tool', + + // Explorer + 'explorer.title': 'Explorer', + 'explorer.refresh': 'Refresh', + 'explorer.selectFile': 'Select a file to preview', + 'explorer.selectFileHint': 'Select a file from the tree to preview its contents', + 'explorer.loading': 'Loading file tree...', + 'explorer.loadingFile': 'Loading file...', + 'explorer.emptyDir': 'Empty directory', + 'explorer.loadError': 'Failed to load: {error}', + 'explorer.preview': 'Preview', + 'explorer.source': 'Source', + 'explorer.lines': 'lines', + 'explorer.updateClaudeMd': 'Update CLAUDE.md', + 'explorer.currentFolderOnly': 'Update CLAUDE.md (current folder only)', + 'explorer.withSubdirs': 'Update CLAUDE.md (with subdirectories)', + + // Task Queue + 'taskQueue.title': 'Update Tasks', + 'taskQueue.cli': 'CLI:', + 'taskQueue.addTask': 'Add update task', + 'taskQueue.startAll': 'Start all tasks', + 'taskQueue.clearCompleted': 'Clear completed', + 'taskQueue.noTasks': 'No tasks in queue', + 'taskQueue.noTasksHint': 'Hover folder and click icons to add tasks', + 'taskQueue.processing': 'Processing...', + 'taskQueue.updated': 'Updated successfully', + 'taskQueue.failed': 'Update failed', + 'taskQueue.currentOnly': 'Current only', + 'taskQueue.withSubdirs': 'With subdirs', + 'taskQueue.startingTasks': 'Starting {count} task(s) in parallel...', + 'taskQueue.queueCompleted': 'Queue completed: {success} succeeded, {failed} failed', + + // Update CLAUDE.md Modal + 'updateClaudeMd.title': 'Update CLAUDE.md', + 'updateClaudeMd.targetDir': 'Target Directory', + 'updateClaudeMd.cliTool': 'CLI Tool', + 'updateClaudeMd.strategy': 'Strategy', + 'updateClaudeMd.singleLayer': 'Single Layer - Current dir + child CLAUDE.md refs', + 'updateClaudeMd.multiLayer': 'Multi Layer - Generate CLAUDE.md in all subdirs', + 'updateClaudeMd.running': 'Running update...', + 'updateClaudeMd.execute': 'Execute', + 'updateClaudeMd.addToQueue': 'Add to Queue', + 'updateClaudeMd.cancel': 'Cancel', + + // MCP Manager + 'mcp.currentProject': 'Current Project MCP Servers', + 'mcp.newServer': 'New Server', + 'mcp.serversConfigured': 'servers configured', + 'mcp.enterprise': 'Enterprise MCP Servers', + 'mcp.enterpriseManaged': 'Managed', + 'mcp.enterpriseReadOnly': 'servers (read-only)', + 'mcp.user': 'User MCP Servers', + 'mcp.userServersFrom': 'servers from ~/.claude.json', + 'mcp.availableOther': 'Available from Other Projects', + 'mcp.serversAvailable': 'servers available', + 'mcp.allProjects': 'All Projects MCP Overview', + 'mcp.projects': 'projects', + 'mcp.project': 'Project', + 'mcp.servers': 'MCP Servers', + 'mcp.status': 'Status', + 'mcp.current': '(Current)', + 'mcp.noMcpServers': 'No MCP servers', + 'mcp.add': 'Add', + 'mcp.addToProject': 'Add to Project', + 'mcp.removeFromProject': 'Remove from project', + 'mcp.removeConfirm': 'Remove MCP server "{name}" from this project?', + 'mcp.readOnly': 'Read-only', + 'mcp.usedIn': 'Used in {count} project', + 'mcp.usedInPlural': 'Used in {count} projects', + 'mcp.availableToAll': 'Available to all projects from ~/.claude.json', + 'mcp.managedByOrg': 'Managed by organization (highest priority)', + 'mcp.variables': 'variables', + + // MCP Create Modal + 'mcp.createTitle': 'Create MCP Server', + 'mcp.form': 'Form', + 'mcp.json': 'JSON', + 'mcp.serverName': 'Server Name', + 'mcp.serverNamePlaceholder': 'e.g., my-mcp-server', + 'mcp.command': 'Command', + 'mcp.commandPlaceholder': 'e.g., npx, uvx, node, python', + 'mcp.arguments': 'Arguments (one per line)', + 'mcp.envVars': 'Environment Variables (KEY=VALUE per line)', + 'mcp.pasteJson': 'Paste MCP Server JSON Configuration', + 'mcp.jsonFormatsHint': 'Supports {"servers": {...}}, {"mcpServers": {...}}, and direct server config formats.', + 'mcp.previewServers': 'Preview (servers to be added):', + 'mcp.create': 'Create', + + // Hook Manager + 'hook.projectHooks': 'Project Hooks', + 'hook.projectFile': '.claude/settings.json', + 'hook.newHook': 'New Hook', + 'hook.hooksConfigured': 'hooks configured', + 'hook.globalHooks': 'Global Hooks', + 'hook.globalFile': '~/.claude/settings.json', + 'hook.wizards': 'Hook Wizards', + 'hook.guidedSetup': 'Guided Setup', + 'hook.wizardsDesc': 'Configure complex hooks with guided wizards', + 'hook.quickInstall': 'Quick Install Templates', + 'hook.oneClick': 'One-click hook installation', + 'hook.envVarsRef': 'Environment Variables Reference', + 'hook.filePaths': 'Space-separated file paths affected', + 'hook.toolName': 'Name of the tool being executed', + 'hook.toolInput': 'JSON input passed to the tool', + 'hook.sessionId': 'Current Claude session ID', + 'hook.projectDir': 'Current project directory path', + 'hook.workingDir': 'Current working directory', + 'hook.openWizard': 'Open Wizard', + 'hook.installed': 'Installed', + 'hook.installProject': 'Install (Project)', + 'hook.installGlobal': 'Global', + 'hook.uninstall': 'Uninstall', + 'hook.viewDetails': 'View template details', + 'hook.edit': 'Edit hook', + 'hook.delete': 'Delete hook', + 'hook.deleteConfirm': 'Remove this {event} hook?', + + // Hook Create Modal + 'hook.createTitle': 'Create Hook', + 'hook.event': 'Hook Event', + 'hook.selectEvent': 'Select an event...', + 'hook.preToolUse': 'PreToolUse - Before a tool is executed', + 'hook.postToolUse': 'PostToolUse - After a tool completes', + 'hook.notification': 'Notification - On notifications', + 'hook.stop': 'Stop - When agent stops', + 'hook.matcher': 'Matcher (optional)', + 'hook.matcherPlaceholder': 'e.g., Write, Edit, Bash (leave empty for all)', + 'hook.matcherHint': 'Tool name to match. Leave empty to match all tools.', + 'hook.commandLabel': 'Command', + 'hook.commandPlaceholder': 'e.g., curl, bash, node', + 'hook.argsLabel': 'Arguments (one per line)', + 'hook.scope': 'Scope', + 'hook.scopeProject': 'Project (.claude/settings.json)', + 'hook.scopeGlobal': 'Global (~/.claude/settings.json)', + 'hook.quickTemplates': 'Quick Templates', + + // Hook templates + 'hook.template.ccwNotify': 'CCW Notify', + 'hook.template.ccwNotifyDesc': 'Notify dashboard on Write', + 'hook.template.logTool': 'Log Tool Usage', + 'hook.template.logToolDesc': 'Log all tool executions', + 'hook.template.lintCheck': 'Lint Check', + 'hook.template.lintCheckDesc': 'Run eslint on file changes', + 'hook.template.gitAdd': 'Git Add', + 'hook.template.gitAddDesc': 'Auto stage written files', + + // Hook Quick Install Templates + 'hook.tpl.codexlensSync': 'CodexLens Auto-Sync', + 'hook.tpl.codexlensSyncDesc': 'Auto-update code index when files are written or edited', + 'hook.tpl.ccwDashboardNotify': 'CCW Dashboard Notify', + 'hook.tpl.ccwDashboardNotifyDesc': 'Notify CCW dashboard when files are written', + 'hook.tpl.toolLogger': 'Tool Usage Logger', + 'hook.tpl.toolLoggerDesc': 'Log all tool executions to a file', + 'hook.tpl.autoLint': 'Auto Lint Check', + 'hook.tpl.autoLintDesc': 'Run ESLint on JavaScript/TypeScript files after write', + 'hook.tpl.autoGitStage': 'Auto Git Stage', + 'hook.tpl.autoGitStageDesc': 'Automatically stage written files to git', + + // Hook Template Categories + 'hook.category.indexing': 'indexing', + 'hook.category.notification': 'notification', + 'hook.category.logging': 'logging', + 'hook.category.quality': 'quality', + 'hook.category.git': 'git', + 'hook.category.memory': 'memory', + 'hook.category.skill': 'skill', + + // Hook Wizard Templates + 'hook.wizard.memoryUpdate': 'Memory Update Hook', + 'hook.wizard.memoryUpdateDesc': 'Automatically update CLAUDE.md documentation based on code changes', + 'hook.wizard.onSessionEnd': 'On Session End', + 'hook.wizard.onSessionEndDesc': 'Update documentation when Claude session ends', + 'hook.wizard.periodicUpdate': 'Periodic Update', + 'hook.wizard.periodicUpdateDesc': 'Update documentation at regular intervals during session', + 'hook.wizard.skillContext': 'SKILL Context Loader', + 'hook.wizard.skillContextDesc': 'Automatically load SKILL packages based on keywords in user prompts', + 'hook.wizard.keywordMatching': 'Keyword Matching', + 'hook.wizard.keywordMatchingDesc': 'Load specific SKILLs when keywords are detected in prompt', + 'hook.wizard.autoDetection': 'Auto Detection', + 'hook.wizard.autoDetectionDesc': 'Automatically detect and load SKILLs by name in prompt', + + // Hook Wizard Labels + 'hook.wizard.cliTools': 'CLI Tools:', + 'hook.wizard.event': 'Event:', + 'hook.wizard.availableSkills': 'Available SKILLs:', + 'hook.wizard.loading': 'Loading...', + 'hook.wizard.matches': 'Matches:', + 'hook.wizard.whenToTrigger': 'When to Trigger', + 'hook.wizard.configuration': 'Configuration', + 'hook.wizard.commandPreview': 'Generated Command Preview', + 'hook.wizard.installTo': 'Install To', + 'hook.wizard.installHook': 'Install Hook', + 'hook.wizard.noSkillsConfigured': 'No SKILLs configured yet', + 'hook.wizard.clickAddSkill': 'Click "Add SKILL" to configure keyword triggers', + 'hook.wizard.configureSkills': 'Configure SKILLs', + 'hook.wizard.addSkill': 'Add SKILL', + 'hook.wizard.selectSkill': 'Select SKILL...', + 'hook.wizard.triggerKeywords': 'Trigger Keywords (comma-separated)', + 'hook.wizard.autoDetectionMode': 'Auto Detection Mode', + 'hook.wizard.autoDetectionInfo': 'SKILLs will be automatically loaded when their name appears in your prompt.', + 'hook.wizard.noSkillsFound': 'No SKILLs found. Create SKILL packages in .claude/skills/', + 'hook.wizard.noSkillConfigs': '# No SKILL configurations yet', + 'hook.wizard.cliTool': 'CLI Tool', + 'hook.wizard.intervalSeconds': 'Interval (seconds)', + 'hook.wizard.updateStrategy': 'Update Strategy', + 'hook.wizard.toolForDocGen': 'Tool for documentation generation', + 'hook.wizard.timeBetweenUpdates': 'Time between updates', + 'hook.wizard.relatedStrategy': 'Related: changed modules, Single-layer: current directory', + + // Lite Tasks + 'lite.plan': 'PLAN', + 'lite.fix': 'FIX', + 'lite.summary': 'Summary', + 'lite.rootCause': 'Root Cause', + 'lite.fixStrategy': 'Fix Strategy', + 'lite.approach': 'Approach', + 'lite.userRequirements': 'User Requirements', + 'lite.focusPaths': 'Focus Paths', + 'lite.metadata': 'Metadata', + 'lite.severity': 'Severity:', + 'lite.riskLevel': 'Risk Level:', + 'lite.estimatedTime': 'Estimated Time:', + 'lite.complexity': 'Complexity:', + 'lite.execution': 'Execution:', + 'lite.fixTasks': 'Fix Tasks', + 'lite.modificationPoints': 'Modification Points:', + 'lite.implementationSteps': 'Implementation Steps:', + 'lite.verification': 'Verification:', + 'lite.rawJson': 'Raw JSON', + 'lite.noPlanData': 'No Plan Data', + 'lite.noPlanDataText': 'No {file} found for this session.', + 'lite.diagnosisSummary': 'Diagnosis Summary', + 'lite.diagnosisDetails': 'Diagnosis Details', + 'lite.totalDiagnoses': 'Total Diagnoses:', + 'lite.angles': 'Angles:', + + // Modals + 'modal.contentPreview': 'Content Preview', + 'modal.raw': 'Raw', + 'modal.preview': 'Preview', + 'modal.templateDetails': 'Template Details', + 'modal.sessionJson': 'Session JSON', + 'modal.copyToClipboard': 'Copy to Clipboard', + + // Toast messages + 'toast.workspaceRefreshed': 'Workspace refreshed', + 'toast.refreshFailed': 'Refresh failed: {error}', + 'toast.statusUpdateRequires': 'Status update requires server mode', + 'toast.bulkUpdateRequires': 'Bulk update requires server mode', + 'toast.failedToUpdate': 'Failed to update status', + 'toast.errorUpdating': 'Error updating status: {error}', + 'toast.failedToBulkUpdate': 'Failed to bulk update', + 'toast.errorInBulk': 'Error in bulk update: {error}', + 'toast.enterPrompt': 'Please enter a prompt', + 'toast.enterPath': 'Please enter a path', + 'toast.commandCopied': 'Command copied: {command}', + 'toast.runCommand': 'Run: {command}', + 'toast.completed': 'Completed', + 'toast.failed': 'Failed', + 'toast.error': 'Error: {error}', + 'toast.templateNotFound': 'Template not found', + + // Footer + 'footer.generated': 'Generated:', + 'footer.version': 'CCW Dashboard v1.0', + + // Common + 'common.cancel': 'Cancel', + 'common.create': 'Create', + 'common.save': 'Save', + 'common.delete': 'Delete', + 'common.edit': 'Edit', + 'common.close': 'Close', + 'common.refresh': 'Refresh', + 'common.loading': 'Loading...', + 'common.error': 'Error', + 'common.success': 'Success', + 'common.warning': 'Warning', + 'common.info': 'Info', + 'common.remove': 'Remove', + 'common.removeFromRecent': 'Remove from recent', + 'common.noDescription': 'No description', + }, + + zh: { + // App title and brand + 'app.title': 'CCW 控制面板', + 'app.brand': 'Claude Code 工作流', + + // Header + 'header.project': '项目:', + 'header.recentProjects': '最近项目', + 'header.browse': '浏览...', + 'header.refreshWorkspace': '刷新工作区', + 'header.toggleTheme': '切换主题', + 'header.language': '语言', + + // Sidebar - Project section + 'nav.project': '项目', + 'nav.overview': '概览', + 'nav.explorer': '文件浏览器', + 'nav.status': '状态', + 'nav.history': '历史', + + // Sidebar - Sessions section + 'nav.sessions': '会话', + 'nav.all': '全部', + 'nav.active': '活跃', + 'nav.archived': '已归档', + + // Sidebar - Lite Tasks section + 'nav.liteTasks': '轻量任务', + 'nav.litePlan': '轻量规划', + 'nav.liteFix': '轻量修复', + + // Sidebar - MCP section + 'nav.mcpServers': 'MCP 服务器', + 'nav.manage': '管理', + + // Sidebar - Hooks section + 'nav.hooks': '钩子', + + // Sidebar - Footer + 'nav.collapse': '收起', + 'nav.expand': '展开', + + // Stats cards + 'stats.totalSessions': '总会话数', + 'stats.activeSessions': '活跃会话', + 'stats.totalTasks': '总任务数', + 'stats.completedTasks': '已完成任务', + + // Carousel + 'carousel.noActiveSessions': '暂无活跃会话', + 'carousel.previous': '上一个', + 'carousel.next': '下一个', + 'carousel.pause': '暂停自动播放', + + // Content titles + 'title.allSessions': '所有会话', + 'title.activeSessions': '活跃会话', + 'title.archivedSessions': '已归档会话', + 'title.sessions': '会话', + 'title.projectOverview': '项目概览', + 'title.mcpManagement': 'MCP 服务器管理', + 'title.fileExplorer': '文件浏览器', + 'title.cliTools': 'CLI 工具 & CCW', + 'title.cliHistory': 'CLI 执行历史', + 'title.litePlanSessions': '轻量规划会话', + 'title.liteFixSessions': '轻量修复会话', + 'title.liteTasks': '轻量任务', + 'title.sessionDetail': '会话详情', + 'title.liteTaskDetail': '轻量任务详情', + 'title.hookManager': '钩子管理', + + // Search + 'search.placeholder': '搜索...', + + // Session cards + 'session.status.active': '活跃', + 'session.status.archived': '已归档', + 'session.status.planning': '规划中', + 'session.tasks': '个任务', + 'session.findings': '个发现', + 'session.dimensions': '个维度', + 'session.progress': '进度', + + // Empty states + 'empty.noSessions': '未找到会话', + 'empty.noSessionsText': '没有符合当前筛选条件的工作流会话。', + 'empty.noTasks': '暂无任务', + 'empty.noTasksText': '该会话没有定义任务。', + 'empty.noTaskFiles': '未找到任务文件', + 'empty.noTaskFilesText': '在 .task/ 目录中未找到 IMPL-*.json 文件', + 'empty.noLiteSessions': '暂无 {type} 会话', + 'empty.noLiteSessionsText': '在 .workflow/.{type}/ 目录中未找到会话', + 'empty.noMcpServers': '该项目未配置 MCP 服务器', + 'empty.addMcpServersHint': '从下方可用列表中添加服务器', + 'empty.noAdditionalMcp': '其他项目中未找到其他 MCP 服务器', + 'empty.noHooks': '该项目未配置钩子', + 'empty.createHookHint': '创建钩子以自动化工具使用时的操作', + 'empty.noGlobalHooks': '未配置全局钩子', + 'empty.globalHooksHint': '全局钩子适用于所有 Claude Code 会话', + 'empty.noDiagnoses': '暂无诊断', + 'empty.noDiagnosesText': '未找到该会话的 diagnosis-*.json 文件。', + + // Session detail tabs + 'tab.tasks': '任务', + 'tab.context': '上下文', + 'tab.summary': '摘要', + 'tab.implPlan': '实现计划', + 'tab.conflict': '冲突', + 'tab.review': '审查', + 'tab.plan': '计划', + 'tab.diagnoses': '诊断', + + // Session detail + 'detail.backToSessions': '返回会话列表', + 'detail.backToLiteTasks': '返回 {type}', + 'detail.created': '创建时间:', + 'detail.archived': '归档时间:', + 'detail.project': '项目:', + 'detail.tasks': '任务:', + 'detail.completed': '已完成', + + // Task status + 'task.status.pending': '待处理', + 'task.status.inProgress': '进行中', + 'task.status.completed': '已完成', + 'task.completed': '已完成', + 'task.inProgress': '进行中', + 'task.pending': '待处理', + + // Task actions + 'task.quickActions': '快捷操作:', + 'task.allPending': '全部待处理', + 'task.allInProgress': '全部进行中', + 'task.allCompleted': '全部完成', + 'task.setAllConfirm': '将所有 {count} 个任务设置为"{status}"?', + 'task.statusUpdated': '任务 {id} 状态已更新', + 'task.tasksUpdated': '所有 {count} 个任务已更新', + 'task.noPendingTasks': '没有待处理的任务', + 'task.noInProgressTasks': '没有进行中的任务', + 'task.movedToInProgress': '{count} 个任务已移至进行中', + 'task.tasksCompleted': '{count} 个任务已完成', + + // Context tab + 'context.description': '描述:', + 'context.requirements': '需求:', + 'context.focusPaths': '关注路径:', + 'context.modificationPoints': '修改点:', + 'context.acceptance': '验收标准:', + 'context.noData': '暂无上下文数据', + 'context.loading': '正在加载上下文数据...', + 'context.loadError': '加载上下文失败: {error}', + + // Flow control + 'flow.implementationApproach': '实现方案:', + 'flow.preAnalysis': '预分析:', + 'flow.targetFiles': '目标文件:', + 'flow.noData': '暂无流程控制数据', + + // Summary tab + 'summary.loading': '正在加载摘要...', + 'summary.title': '摘要', + 'summary.hint': '会话摘要将从 .summaries/ 加载', + 'summary.noSummaries': '暂无摘要', + 'summary.noSummariesText': '在 .summaries/ 中未找到摘要', + + // IMPL Plan tab + 'implPlan.loading': '正在加载实现计划...', + 'implPlan.title': '实现计划', + 'implPlan.hint': '实现计划将从 IMPL_PLAN.md 加载', + + // Review tab + 'review.loading': '正在加载审查数据...', + 'review.title': '审查数据', + 'review.hint': '审查数据将从审查文件加载', + + // CLI Manager + 'cli.tools': 'CLI 工具', + 'cli.available': '可用', + 'cli.refreshStatus': '刷新状态', + 'cli.ready': '就绪', + 'cli.notInstalled': '未安装', + 'cli.setDefault': '设为默认', + 'cli.default': '默认', + 'cli.install': '安装', + 'cli.initIndex': '初始化索引', + 'cli.geminiDesc': 'Google AI 代码分析', + 'cli.qwenDesc': '阿里通义 AI 助手', + 'cli.codexDesc': 'OpenAI 代码生成', + 'cli.codexLensDesc': '代码索引 & 全文搜索', + 'cli.codexLensDescFull': '全文代码搜索引擎', + 'cli.semanticDesc': 'AI 驱动的代码理解', + 'cli.semanticDescFull': '自然语言代码搜索', + + // CCW Install + 'ccw.install': 'CCW 安装', + 'ccw.installations': '个安装', + 'ccw.installationsPlural': '个安装', + 'ccw.noInstallations': '未找到安装', + 'ccw.installCcw': '安装 CCW', + 'ccw.upgrade': '升级', + 'ccw.uninstall': '卸载', + 'ccw.files': '个文件', + 'ccw.globalInstall': '全局安装', + 'ccw.globalInstallDesc': '安装到用户主目录 (~/.claude)', + 'ccw.pathInstall': '路径安装', + 'ccw.pathInstallDesc': '安装到指定项目文件夹', + 'ccw.installPath': '安装路径', + 'ccw.installToPath': '安装到路径', + 'ccw.uninstallConfirm': '从此位置卸载 CCW?', + 'ccw.upgradeStarting': '开始升级...', + 'ccw.upgradeCompleted': '升级完成!正在刷新...', + 'ccw.upgradeFailed': '升级失败: {error}', + + // CCW Endpoint Tools + 'ccw.endpointTools': 'CCW 端点工具', + 'ccw.tool': '个工具', + 'ccw.tools': '个工具', + 'ccw.noEndpointTools': '未找到端点工具', + 'ccw.parameters': '参数', + 'ccw.required': '必填', + 'ccw.optional': '可选', + 'ccw.default': '默认值:', + 'ccw.options': '选项:', + 'ccw.noParams': '此工具没有参数', + 'ccw.usageExample': '使用示例', + 'ccw.endpointTool': '端点工具', + + // Explorer + 'explorer.title': '浏览器', + 'explorer.refresh': '刷新', + 'explorer.selectFile': '选择文件预览', + 'explorer.selectFileHint': '从树中选择文件以预览其内容', + 'explorer.loading': '正在加载文件树...', + 'explorer.loadingFile': '正在加载文件...', + 'explorer.emptyDir': '空目录', + 'explorer.loadError': '加载失败: {error}', + 'explorer.preview': '预览', + 'explorer.source': '源码', + 'explorer.lines': '行', + 'explorer.updateClaudeMd': '更新 CLAUDE.md', + 'explorer.currentFolderOnly': '更新 CLAUDE.md(仅当前文件夹)', + 'explorer.withSubdirs': '更新 CLAUDE.md(包含子目录)', + + // Task Queue + 'taskQueue.title': '更新任务', + 'taskQueue.cli': 'CLI:', + 'taskQueue.addTask': '添加更新任务', + 'taskQueue.startAll': '开始所有任务', + 'taskQueue.clearCompleted': '清除已完成', + 'taskQueue.noTasks': '队列中没有任务', + 'taskQueue.noTasksHint': '悬停文件夹并点击图标添加任务', + 'taskQueue.processing': '处理中...', + 'taskQueue.updated': '更新成功', + 'taskQueue.failed': '更新失败', + 'taskQueue.currentOnly': '仅当前', + 'taskQueue.withSubdirs': '含子目录', + 'taskQueue.startingTasks': '正在并行启动 {count} 个任务...', + 'taskQueue.queueCompleted': '队列完成: {success} 个成功, {failed} 个失败', + + // Update CLAUDE.md Modal + 'updateClaudeMd.title': '更新 CLAUDE.md', + 'updateClaudeMd.targetDir': '目标目录', + 'updateClaudeMd.cliTool': 'CLI 工具', + 'updateClaudeMd.strategy': '策略', + 'updateClaudeMd.singleLayer': '单层 - 仅当前目录 + 子 CLAUDE.md 引用', + 'updateClaudeMd.multiLayer': '多层 - 在所有子目录生成 CLAUDE.md', + 'updateClaudeMd.running': '正在更新...', + 'updateClaudeMd.execute': '执行', + 'updateClaudeMd.addToQueue': '添加到队列', + 'updateClaudeMd.cancel': '取消', + + // MCP Manager + 'mcp.currentProject': '当前项目 MCP 服务器', + 'mcp.newServer': '新建服务器', + 'mcp.serversConfigured': '个服务器已配置', + 'mcp.enterprise': '企业 MCP 服务器', + 'mcp.enterpriseManaged': '托管', + 'mcp.enterpriseReadOnly': '个服务器(只读)', + 'mcp.user': '用户 MCP 服务器', + 'mcp.userServersFrom': '个服务器来自 ~/.claude.json', + 'mcp.availableOther': '其他项目可用', + 'mcp.serversAvailable': '个服务器可用', + 'mcp.allProjects': '所有项目 MCP 概览', + 'mcp.projects': '个项目', + 'mcp.project': '项目', + 'mcp.servers': 'MCP 服务器', + 'mcp.status': '状态', + 'mcp.current': '(当前)', + 'mcp.noMcpServers': '无 MCP 服务器', + 'mcp.add': '添加', + 'mcp.addToProject': '添加到项目', + 'mcp.removeFromProject': '从项目移除', + 'mcp.removeConfirm': '从此项目移除 MCP 服务器 "{name}"?', + 'mcp.readOnly': '只读', + 'mcp.usedIn': '用于 {count} 个项目', + 'mcp.usedInPlural': '用于 {count} 个项目', + 'mcp.availableToAll': '可用于所有项目,来自 ~/.claude.json', + 'mcp.managedByOrg': '由组织管理(最高优先级)', + 'mcp.variables': '个变量', + + // MCP Create Modal + 'mcp.createTitle': '创建 MCP 服务器', + 'mcp.form': '表单', + 'mcp.json': 'JSON', + 'mcp.serverName': '服务器名称', + 'mcp.serverNamePlaceholder': '例如: my-mcp-server', + 'mcp.command': '命令', + 'mcp.commandPlaceholder': '例如: npx, uvx, node, python', + 'mcp.arguments': '参数(每行一个)', + 'mcp.envVars': '环境变量(每行 KEY=VALUE)', + 'mcp.pasteJson': '粘贴 MCP 服务器 JSON 配置', + 'mcp.jsonFormatsHint': '支持 {"servers": {...}}、{"mcpServers": {...}} 和直接服务器配置格式。', + 'mcp.previewServers': '预览(将添加的服务器):', + 'mcp.create': '创建', + + // Hook Manager + 'hook.projectHooks': '项目钩子', + 'hook.projectFile': '.claude/settings.json', + 'hook.newHook': '新建钩子', + 'hook.hooksConfigured': '个钩子已配置', + 'hook.globalHooks': '全局钩子', + 'hook.globalFile': '~/.claude/settings.json', + 'hook.wizards': '钩子向导', + 'hook.guidedSetup': '引导设置', + 'hook.wizardsDesc': '通过向导配置复杂钩子', + 'hook.quickInstall': '快速安装模板', + 'hook.oneClick': '一键安装钩子', + 'hook.envVarsRef': '环境变量参考', + 'hook.filePaths': '受影响文件的空格分隔路径', + 'hook.toolName': '正在执行的工具名称', + 'hook.toolInput': '传递给工具的 JSON 输入', + 'hook.sessionId': '当前 Claude 会话 ID', + 'hook.projectDir': '当前项目目录路径', + 'hook.workingDir': '当前工作目录', + 'hook.openWizard': '打开向导', + 'hook.installed': '已安装', + 'hook.installProject': '安装(项目)', + 'hook.installGlobal': '全局', + 'hook.uninstall': '卸载', + 'hook.viewDetails': '查看模板详情', + 'hook.edit': '编辑钩子', + 'hook.delete': '删除钩子', + 'hook.deleteConfirm': '删除此 {event} 钩子?', + + // Hook Create Modal + 'hook.createTitle': '创建钩子', + 'hook.event': '钩子事件', + 'hook.selectEvent': '选择事件...', + 'hook.preToolUse': 'PreToolUse - 工具执行前', + 'hook.postToolUse': 'PostToolUse - 工具完成后', + 'hook.notification': 'Notification - 通知时', + 'hook.stop': 'Stop - 代理停止时', + 'hook.matcher': '匹配器(可选)', + 'hook.matcherPlaceholder': '例如: Write, Edit, Bash(留空匹配所有)', + 'hook.matcherHint': '要匹配的工具名称。留空匹配所有工具。', + 'hook.commandLabel': '命令', + 'hook.commandPlaceholder': '例如: curl, bash, node', + 'hook.argsLabel': '参数(每行一个)', + 'hook.scope': '作用域', + 'hook.scopeProject': '项目(.claude/settings.json)', + 'hook.scopeGlobal': '全局(~/.claude/settings.json)', + 'hook.quickTemplates': '快速模板', + + // Hook templates + 'hook.template.ccwNotify': 'CCW 通知', + 'hook.template.ccwNotifyDesc': '写入时通知控制面板', + 'hook.template.logTool': '工具使用日志', + 'hook.template.logToolDesc': '记录所有工具执行', + 'hook.template.lintCheck': 'Lint 检查', + 'hook.template.lintCheckDesc': '文件更改时运行 eslint', + 'hook.template.gitAdd': 'Git 暂存', + 'hook.template.gitAddDesc': '自动暂存写入的文件', + + // Hook Quick Install Templates + 'hook.tpl.codexlensSync': 'CodexLens 自动同步', + 'hook.tpl.codexlensSyncDesc': '文件写入或编辑时自动更新代码索引', + 'hook.tpl.ccwDashboardNotify': 'CCW 控制面板通知', + 'hook.tpl.ccwDashboardNotifyDesc': '文件写入时通知 CCW 控制面板', + 'hook.tpl.toolLogger': '工具使用记录器', + 'hook.tpl.toolLoggerDesc': '将所有工具执行记录到文件', + 'hook.tpl.autoLint': '自动 Lint 检查', + 'hook.tpl.autoLintDesc': '写入后对 JavaScript/TypeScript 文件运行 ESLint', + 'hook.tpl.autoGitStage': '自动 Git 暂存', + 'hook.tpl.autoGitStageDesc': '自动将写入的文件添加到 Git 暂存区', + + // Hook Template Categories + 'hook.category.indexing': '索引', + 'hook.category.notification': '通知', + 'hook.category.logging': '日志', + 'hook.category.quality': '质量', + 'hook.category.git': 'Git', + 'hook.category.memory': '记忆', + 'hook.category.skill': '技能', + + // Hook Wizard Templates + 'hook.wizard.memoryUpdate': '记忆更新钩子', + 'hook.wizard.memoryUpdateDesc': '根据代码更改自动更新 CLAUDE.md 文档', + 'hook.wizard.onSessionEnd': '会话结束时', + 'hook.wizard.onSessionEndDesc': 'Claude 会话结束时更新文档', + 'hook.wizard.periodicUpdate': '定期更新', + 'hook.wizard.periodicUpdateDesc': '会话期间定期更新文档', + 'hook.wizard.skillContext': 'SKILL 上下文加载器', + 'hook.wizard.skillContextDesc': '根据用户提示中的关键词自动加载 SKILL 包', + 'hook.wizard.keywordMatching': '关键词匹配', + 'hook.wizard.keywordMatchingDesc': '当提示中检测到关键词时加载特定 SKILL', + 'hook.wizard.autoDetection': '自动检测', + 'hook.wizard.autoDetectionDesc': '根据提示中的名称自动检测并加载 SKILL', + + // Hook Wizard Labels + 'hook.wizard.cliTools': 'CLI 工具:', + 'hook.wizard.event': '事件:', + 'hook.wizard.availableSkills': '可用 SKILL:', + 'hook.wizard.loading': '加载中...', + 'hook.wizard.matches': '匹配:', + 'hook.wizard.whenToTrigger': '触发时机', + 'hook.wizard.configuration': '配置', + 'hook.wizard.commandPreview': '生成的命令预览', + 'hook.wizard.installTo': '安装到', + 'hook.wizard.installHook': '安装钩子', + 'hook.wizard.noSkillsConfigured': '尚未配置任何 SKILL', + 'hook.wizard.clickAddSkill': '点击"添加 SKILL"配置关键词触发器', + 'hook.wizard.configureSkills': '配置 SKILL', + 'hook.wizard.addSkill': '添加 SKILL', + 'hook.wizard.selectSkill': '选择 SKILL...', + 'hook.wizard.triggerKeywords': '触发关键词(逗号分隔)', + 'hook.wizard.autoDetectionMode': '自动检测模式', + 'hook.wizard.autoDetectionInfo': '当 SKILL 名称出现在提示中时将自动加载。', + 'hook.wizard.noSkillsFound': '未找到 SKILL。请在 .claude/skills/ 中创建 SKILL 包', + 'hook.wizard.noSkillConfigs': '# 尚未配置 SKILL', + 'hook.wizard.cliTool': 'CLI 工具', + 'hook.wizard.intervalSeconds': '间隔(秒)', + 'hook.wizard.updateStrategy': '更新策略', + 'hook.wizard.toolForDocGen': '用于生成文档的工具', + 'hook.wizard.timeBetweenUpdates': '更新之间的时间间隔', + 'hook.wizard.relatedStrategy': 'related: 已更改的模块, single-layer: 当前目录', + + // Lite Tasks + 'lite.plan': '规划', + 'lite.fix': '修复', + 'lite.summary': '摘要', + 'lite.rootCause': '根本原因', + 'lite.fixStrategy': '修复策略', + 'lite.approach': '方法', + 'lite.userRequirements': '用户需求', + 'lite.focusPaths': '关注路径', + 'lite.metadata': '元数据', + 'lite.severity': '严重性:', + 'lite.riskLevel': '风险等级:', + 'lite.estimatedTime': '预计时间:', + 'lite.complexity': '复杂度:', + 'lite.execution': '执行:', + 'lite.fixTasks': '修复任务', + 'lite.modificationPoints': '修改点:', + 'lite.implementationSteps': '实现步骤:', + 'lite.verification': '验证:', + 'lite.rawJson': '原始 JSON', + 'lite.noPlanData': '暂无计划数据', + 'lite.noPlanDataText': '未找到该会话的 {file}。', + 'lite.diagnosisSummary': '诊断摘要', + 'lite.diagnosisDetails': '诊断详情', + 'lite.totalDiagnoses': '总诊断数:', + 'lite.angles': '分析角度:', + + // Modals + 'modal.contentPreview': '内容预览', + 'modal.raw': '原始', + 'modal.preview': '预览', + 'modal.templateDetails': '模板详情', + 'modal.sessionJson': '会话 JSON', + 'modal.copyToClipboard': '复制到剪贴板', + + // Toast messages + 'toast.workspaceRefreshed': '工作区已刷新', + 'toast.refreshFailed': '刷新失败: {error}', + 'toast.statusUpdateRequires': '状态更新需要服务器模式', + 'toast.bulkUpdateRequires': '批量更新需要服务器模式', + 'toast.failedToUpdate': '更新状态失败', + 'toast.errorUpdating': '更新状态出错: {error}', + 'toast.failedToBulkUpdate': '批量更新失败', + 'toast.errorInBulk': '批量更新出错: {error}', + 'toast.enterPrompt': '请输入提示', + 'toast.enterPath': '请输入路径', + 'toast.commandCopied': '命令已复制: {command}', + 'toast.runCommand': '运行: {command}', + 'toast.completed': '已完成', + 'toast.failed': '失败', + 'toast.error': '错误: {error}', + 'toast.templateNotFound': '未找到模板', + + // Footer + 'footer.generated': '生成时间:', + 'footer.version': 'CCW 控制面板 v1.0', + + // Common + 'common.cancel': '取消', + 'common.create': '创建', + 'common.save': '保存', + 'common.delete': '删除', + 'common.edit': '编辑', + 'common.close': '关闭', + 'common.refresh': '刷新', + 'common.loading': '加载中...', + 'common.error': '错误', + 'common.success': '成功', + 'common.warning': '警告', + 'common.info': '信息', + 'common.remove': '移除', + 'common.removeFromRecent': '从最近中移除', + 'common.noDescription': '无描述', + } +}; + +/** + * Initialize i18n - detect browser language or use stored preference + */ +function initI18n() { + // Check stored preference + const stored = localStorage.getItem('ccw-lang'); + if (stored && i18n[stored]) { + currentLang = stored; + } else { + // Detect browser language + const browserLang = navigator.language || navigator.userLanguage; + if (browserLang.startsWith('zh')) { + currentLang = 'zh'; + } else { + currentLang = 'en'; + } + } + + // Apply translations to DOM + applyTranslations(); + updateLangToggle(); +} + +/** + * Get translation for a key with optional replacements + * @param {string} key - Translation key + * @param {Object} replacements - Optional key-value pairs for replacements + * @returns {string} Translated string + */ +function t(key, replacements = {}) { + const dict = i18n[currentLang] || i18n.en; + let text = dict[key] || i18n.en[key] || key; + + // Apply replacements + for (const [k, v] of Object.entries(replacements)) { + text = text.replace(new RegExp(`\\{${k}\\}`, 'g'), v); + } + + return text; +} + +/** + * Switch language + * @param {string} lang - Language code ('en' or 'zh') + */ +function switchLang(lang) { + if (i18n[lang]) { + currentLang = lang; + localStorage.setItem('ccw-lang', lang); + applyTranslations(); + updateLangToggle(); + + // Re-render current view to update dynamic content + if (typeof updateContentTitle === 'function') { + updateContentTitle(); + } + } +} + +/** + * Apply translations to all elements with data-i18n attribute + */ +function applyTranslations() { + document.querySelectorAll('[data-i18n]').forEach(el => { + const key = el.dataset.i18n; + const translated = t(key); + + // Handle different element types + if (el.tagName === 'INPUT' && el.type === 'text') { + el.placeholder = translated; + } else if (el.hasAttribute('title')) { + el.title = translated; + } else { + el.textContent = translated; + } + }); + + // Update elements with data-i18n-title + document.querySelectorAll('[data-i18n-title]').forEach(el => { + el.title = t(el.dataset.i18nTitle); + }); + + // Update elements with data-i18n-placeholder + document.querySelectorAll('[data-i18n-placeholder]').forEach(el => { + el.placeholder = t(el.dataset.i18nPlaceholder); + }); + + // Update document title + document.title = t('app.title'); +} + +/** + * Update language toggle button state + */ +function updateLangToggle() { + const langBtn = document.getElementById('langToggle'); + if (langBtn) { + langBtn.textContent = currentLang.toUpperCase(); + langBtn.title = t('header.language'); + } +} + +/** + * Get current language + * @returns {string} Current language code + */ +function getLang() { + return currentLang; +} + +/** + * Check if current language is Chinese + * @returns {boolean} + */ +function isZh() { + return currentLang === 'zh'; +} diff --git a/ccw/src/templates/dashboard-js/main.js b/ccw/src/templates/dashboard-js/main.js index b28a74ad..3b8d9c25 100644 --- a/ccw/src/templates/dashboard-js/main.js +++ b/ccw/src/templates/dashboard-js/main.js @@ -5,6 +5,9 @@ document.addEventListener('DOMContentLoaded', async () => { // Initialize Lucide icons (must be first to render SVG icons) try { lucide.createIcons(); } catch (e) { console.error('Lucide icons init failed:', e); } + // Initialize i18n (must be early to translate static content) + try { initI18n(); } catch (e) { console.error('I18n init failed:', e); } + // Initialize components with error handling to prevent cascading failures try { initTheme(); } catch (e) { console.error('Theme init failed:', e); } try { initSidebar(); } catch (e) { console.error('Sidebar init failed:', e); } diff --git a/ccw/src/templates/dashboard-js/views/cli-manager.js b/ccw/src/templates/dashboard-js/views/cli-manager.js index 53516d95..0906ae9d 100644 --- a/ccw/src/templates/dashboard-js/views/cli-manager.js +++ b/ccw/src/templates/dashboard-js/views/cli-manager.js @@ -79,9 +79,9 @@ function renderToolsSection() { if (!container) return; var toolDescriptions = { - gemini: 'Google AI for code analysis', - qwen: 'Alibaba AI assistant', - codex: 'OpenAI code generation' + gemini: t('cli.geminiDesc'), + qwen: t('cli.qwenDesc'), + codex: t('cli.codexDesc') }; var tools = ['gemini', 'qwen', 'codex']; @@ -97,17 +97,17 @@ function renderToolsSection() { '' + '
' + '
' + tool.charAt(0).toUpperCase() + tool.slice(1) + - (isDefault ? 'Default' : '') + + (isDefault ? '' + t('cli.default') + '' : '') + '
' + '
' + toolDescriptions[tool] + '
' + '
' + '
' + '
' + (isAvailable - ? ' Ready' - : ' Not Installed') + + ? ' ' + t('cli.ready') + '' + : ' ' + t('cli.notInstalled') + '') + (isAvailable && !isDefault - ? '' + ? '' : '') + '
' + '
'; @@ -119,15 +119,15 @@ function renderToolsSection() { '' + '
' + '
CodexLens Index
' + - '
' + (codexLensStatus.ready ? 'Code indexing & FTS search' : 'Full-text code search engine') + '
' + + '
' + (codexLensStatus.ready ? t('cli.codexLensDesc') : t('cli.codexLensDescFull')) + '
' + '
' + '
' + '
' + (codexLensStatus.ready ? ' v' + (codexLensStatus.version || 'installed') + '' + - '' - : ' Not Installed' + - '') + + '' + : ' ' + t('cli.notInstalled') + '' + + '') + '
' + '
'; @@ -153,10 +153,10 @@ function renderToolsSection() { container.innerHTML = '
' + '
' + - '

CLI Tools

' + - '' + available + '/' + tools.length + ' available' + + '

' + t('cli.tools') + '

' + + '' + available + '/' + tools.length + ' ' + t('cli.available') + '' + '
' + - '' + '
' + @@ -179,9 +179,9 @@ function renderCcwSection() { if (ccwInstallations.length === 0) { installationsHtml = '
' + '' + - '

No installations found

' + + '

' + t('ccw.noInstallations') + '

' + '' + + ' ' + t('ccw.installCcw') + '' + '
'; } else { installationsHtml = '
'; @@ -224,8 +224,8 @@ function renderCcwSection() { container.innerHTML = '
' + '
' + - '

CCW Install

' + - '' + ccwInstallations.length + ' installation' + (ccwInstallations.length !== 1 ? 's' : '') + '' + + '

' + t('ccw.install') + '

' + + '' + ccwInstallations.length + ' ' + (ccwInstallations.length !== 1 ? t('ccw.installationsPlural') : t('ccw.installations')) + '' + '
' + '
' + '' + + ' ' + t('common.refresh') + '' + '
'; } else { toolsHtml = '
' + @@ -284,8 +284,8 @@ function renderCcwEndpointToolsSection() { container.innerHTML = '
' + '
' + - '

CCW Endpoint Tools

' + - '' + count + ' tool' + (count !== 1 ? 's' : '') + '' + + '

' + t('ccw.endpointTools') + '

' + + '' + count + ' ' + (count !== 1 ? t('ccw.tools') : t('ccw.tool')) + '' + '
' + '
-
Loading file tree...
+
${t('explorer.loading')}
- Select a file to preview + ${t('explorer.selectFile')}
-
Select a file from the tree to preview its contents
+
${t('explorer.selectFileHint')}
-
+
0
@@ -94,12 +94,12 @@ async function renderExplorer() {
- Update Tasks + ${t('taskQueue.title')}
- +
- - -
- No tasks in queue -

Hover folder and click or to add tasks

+ ${t('taskQueue.noTasks')} +

${t('taskQueue.noTasksHint')}

@@ -167,7 +167,7 @@ async function loadExplorerTree(dirPath) { */ function renderTreeLevel(files, parentPath, depth) { if (!files || files.length === 0) { - return `
Empty directory
`; + return `
${t('explorer.emptyDir')}
`; } return files.map(file => { @@ -190,10 +190,10 @@ function renderTreeLevel(files, parentPath, depth) { ` : ''}
- -
@@ -425,8 +425,8 @@ async function previewFile(filePath) {
${isMarkdown ? `
- - + +
` : ''} `; @@ -458,7 +458,7 @@ async function previewFile(filePath) { previewContent.innerHTML = `
${data.language} - ${data.lines} lines + ${data.lines} ${t('explorer.lines')} ${formatFileSize(data.size)}
${escapeHtml(data.content)}
@@ -725,8 +725,8 @@ function renderTaskQueue() { if (updateTaskQueue.length === 0) { listEl.innerHTML = `
- No tasks in queue -

Right-click a folder or click "Add Task" to queue CLAUDE.md updates

+ ${t('taskQueue.noTasks')} +

${t('taskQueue.noTasksHint')}

`; return; @@ -735,8 +735,8 @@ function renderTaskQueue() { listEl.innerHTML = updateTaskQueue.map(task => { const folderName = task.path.split('/').pop() || task.path; const strategyLabel = task.strategy === 'multi-layer' - ? ' With subdirs' - : ' Current only'; + ? ' ' + t('taskQueue.withSubdirs') + : ' ' + t('taskQueue.currentOnly'); const statusIcon = { 'pending': '', 'running': '', @@ -796,7 +796,7 @@ async function executeTask(task) { // Update status to running task.status = 'running'; - task.message = 'Processing...'; + task.message = t('taskQueue.processing'); renderTaskQueue(); addGlobalNotification('info', `Processing: ${folderName}`, `Strategy: ${task.strategy}, Tool: ${task.tool}`, 'Explorer'); @@ -816,12 +816,12 @@ async function executeTask(task) { if (result.success) { task.status = 'completed'; - task.message = 'Updated successfully'; + task.message = t('taskQueue.updated'); addGlobalNotification('success', `Completed: ${folderName}`, result.message, 'Explorer'); return { success: true }; } else { task.status = 'failed'; - task.message = result.error || 'Update failed'; + task.message = result.error || t('taskQueue.failed'); addGlobalNotification('error', `Failed: ${folderName}`, result.error || 'Unknown error', 'Explorer'); return { success: false }; } @@ -848,7 +848,7 @@ async function startTaskQueue() { isTaskRunning = true; document.getElementById('startQueueBtn').disabled = true; - addGlobalNotification('info', `Starting ${pendingTasks.length} task(s) in parallel...`, null, 'Explorer'); + addGlobalNotification('info', t('taskQueue.startingTasks', { count: pendingTasks.length }), null, 'Explorer'); // Execute all tasks in parallel const results = await Promise.all(pendingTasks.map(task => executeTask(task))); @@ -861,7 +861,7 @@ async function startTaskQueue() { // Summary notification addGlobalNotification( failCount === 0 ? 'success' : 'warning', - `Queue completed: ${successCount} succeeded, ${failCount} failed`, + t('taskQueue.queueCompleted', { success: successCount, failed: failCount }), null, 'Explorer' ); diff --git a/ccw/src/templates/dashboard-js/views/home.js b/ccw/src/templates/dashboard-js/views/home.js index ad8c50ff..378d87bd 100644 --- a/ccw/src/templates/dashboard-js/views/home.js +++ b/ccw/src/templates/dashboard-js/views/home.js @@ -74,8 +74,8 @@ function renderSessions() { container.innerHTML = `
-
No Sessions Found
-
No workflow sessions match your current filter.
+
${t('empty.noSessions')}
+
${t('empty.noSessionsText')}
`; if (typeof lucide !== 'undefined') lucide.createIcons(); @@ -111,13 +111,13 @@ function renderSessionCard(session) { if (!isActive) { // Archived sessions always show as ARCHIVED regardless of status field statusClass = 'archived'; - statusText = 'ARCHIVED'; + statusText = t('session.status.archived'); } else if (isPlanning) { statusClass = 'planning'; - statusText = 'PLANNING'; + statusText = t('session.status.planning'); } else { statusClass = 'active'; - statusText = 'ACTIVE'; + statusText = t('session.status.active'); } // Store session data for modal @@ -146,11 +146,11 @@ function renderSessionCard(session) {
${formatDate(date)} - ${taskCount} tasks + ${taskCount} ${t('session.tasks')}
${taskCount > 0 && !isPlanning ? `
- Progress + ${t('session.progress')}
@@ -188,13 +188,13 @@ function renderReviewSessionCard(session, sessionKey, typeBadge, isActive, isPla let statusClass, statusText; if (!isActive) { statusClass = 'archived'; - statusText = 'ARCHIVED'; + statusText = t('session.status.archived'); } else if (isPlanning) { statusClass = 'planning'; - statusText = 'PLANNING'; + statusText = t('session.status.planning'); } else { statusClass = 'active'; - statusText = 'ACTIVE'; + statusText = t('session.status.active'); } // Card class includes planning modifier for special styling (only for active sessions) @@ -214,7 +214,7 @@ function renderReviewSessionCard(session, sessionKey, typeBadge, isActive, isPla
${formatDate(date)} - ${totalFindings} findings + ${totalFindings} ${t('session.findings')}
${totalFindings > 0 ? `
@@ -225,7 +225,7 @@ function renderReviewSessionCard(session, sessionKey, typeBadge, isActive, isPla ${lowCount > 0 ? ` ${lowCount}` : ''}
- ${dimensions.length} dimensions + ${dimensions.length} ${t('session.dimensions')}
` : ''} diff --git a/ccw/src/templates/dashboard-js/views/hook-manager.js b/ccw/src/templates/dashboard-js/views/hook-manager.js index d425e2b9..4ddb8e26 100644 --- a/ccw/src/templates/dashboard-js/views/hook-manager.js +++ b/ccw/src/templates/dashboard-js/views/hook-manager.js @@ -29,21 +29,21 @@ async function renderHookManager() {
-

Project Hooks

- .claude/settings.json +

${t('hook.projectHooks')}

+ ${t('hook.projectFile')}
- ${projectHookCount} hooks configured + ${projectHookCount} ${t('hook.hooksConfigured')}
${projectHookCount === 0 ? `
-

No hooks configured for this project

-

Create a hook to automate actions on tool usage

+

${t('empty.noHooks')}

+

${t('empty.createHookHint')}

` : `
@@ -56,16 +56,16 @@ async function renderHookManager() {
-

Global Hooks

- ~/.claude/settings.json +

${t('hook.globalHooks')}

+ ${t('hook.globalFile')}
- ${globalHookCount} hooks configured + ${globalHookCount} ${t('hook.hooksConfigured')}
${globalHookCount === 0 ? `
-

No global hooks configured

-

Global hooks apply to all Claude Code sessions

+

${t('empty.noGlobalHooks')}

+

${t('empty.globalHooksHint')}

` : `
@@ -78,10 +78,10 @@ async function renderHookManager() {
-

Hook Wizards

- Guided Setup +

${t('hook.wizards')}

+ ${t('hook.guidedSetup')}
- Configure complex hooks with guided wizards + ${t('hook.wizardsDesc')}
@@ -93,23 +93,23 @@ async function renderHookManager() {
-

Quick Install Templates

- One-click hook installation +

${t('hook.quickInstall')}

+ ${t('hook.oneClick')}
- ${renderQuickInstallCard('codexlens-update', 'CodexLens Auto-Sync', 'Auto-update code index when files are written or edited', 'PostToolUse', 'Write|Edit')} - ${renderQuickInstallCard('ccw-notify', 'CCW Dashboard Notify', 'Notify CCW dashboard when files are written', 'PostToolUse', 'Write')} - ${renderQuickInstallCard('log-tool', 'Tool Usage Logger', 'Log all tool executions to a file', 'PostToolUse', 'All')} - ${renderQuickInstallCard('lint-check', 'Auto Lint Check', 'Run ESLint on JavaScript/TypeScript files after write', 'PostToolUse', 'Write')} - ${renderQuickInstallCard('git-add', 'Auto Git Stage', 'Automatically stage written files to git', 'PostToolUse', 'Write')} + ${renderQuickInstallCard('codexlens-update', t('hook.tpl.codexlensSync'), t('hook.tpl.codexlensSyncDesc'), 'PostToolUse', 'Write|Edit')} + ${renderQuickInstallCard('ccw-notify', t('hook.tpl.ccwDashboardNotify'), t('hook.tpl.ccwDashboardNotifyDesc'), 'PostToolUse', 'Write')} + ${renderQuickInstallCard('log-tool', t('hook.tpl.toolLogger'), t('hook.tpl.toolLoggerDesc'), 'PostToolUse', 'All')} + ${renderQuickInstallCard('lint-check', t('hook.tpl.autoLint'), t('hook.tpl.autoLintDesc'), 'PostToolUse', 'Write')} + ${renderQuickInstallCard('git-add', t('hook.tpl.autoGitStage'), t('hook.tpl.autoGitStageDesc'), 'PostToolUse', 'Write')}
-

Environment Variables Reference

+

${t('hook.envVarsRef')}

@@ -117,29 +117,29 @@ async function renderHookManager() {
$CLAUDE_FILE_PATHS - Space-separated file paths affected + ${t('hook.filePaths')}
$CLAUDE_TOOL_NAME - Name of the tool being executed + ${t('hook.toolName')}
$CLAUDE_TOOL_INPUT - JSON input passed to the tool + ${t('hook.toolInput')}
$CLAUDE_SESSION_ID - Current Claude session ID + ${t('hook.sessionId')}
$CLAUDE_PROJECT_DIR - Current project directory path + ${t('hook.projectDir')}
$CLAUDE_WORKING_DIR - Current working directory + ${t('hook.workingDir')}
@@ -153,6 +153,9 @@ async function renderHookManager() { // Initialize Lucide icons if (typeof lucide !== 'undefined') lucide.createIcons(); + + // Load available SKILLs for skill-context wizard + loadAvailableSkills(); } // Load available SKILLs for skill-context wizard @@ -161,25 +164,25 @@ async function loadAvailableSkills() { const response = await fetch(`/api/skills?path=${encodeURIComponent(projectPath)}`); if (!response.ok) throw new Error('Failed to load skills'); const data = await response.json(); - + const container = document.getElementById('skill-discovery-skill-context'); if (container && data.skills) { if (data.skills.length === 0) { container.innerHTML = ` - Available SKILLs: - No SKILLs found in .claude/skills/ + ${t('hook.wizard.availableSkills')} + ${t('hook.wizard.noSkillsFound').split('.')[0]} `; } else { const skillBadges = data.skills.map(skill => ` ${escapeHtml(skill.name)} `).join(''); container.innerHTML = ` - Available SKILLs: + ${t('hook.wizard.availableSkills')}
${skillBadges}
`; } } - + // Store skills for wizard use window.availableSkills = data.skills || []; } catch (err) { @@ -187,8 +190,8 @@ async function loadAvailableSkills() { const container = document.getElementById('skill-discovery-skill-context'); if (container) { container.innerHTML = ` - Available SKILLs: - Error loading skills + ${t('hook.wizard.availableSkills')} + ${t('toast.loadFailed', { error: err.message })} `; } } @@ -201,21 +204,52 @@ function renderWizardCard(wizardId) { const wizard = WIZARD_TEMPLATES[wizardId]; if (!wizard) return ''; + // Get translated wizard name and description + const wizardName = wizardId === 'memory-update' ? t('hook.wizard.memoryUpdate') : + wizardId === 'skill-context' ? t('hook.wizard.skillContext') : wizard.name; + const wizardDesc = wizardId === 'memory-update' ? t('hook.wizard.memoryUpdateDesc') : + wizardId === 'skill-context' ? t('hook.wizard.skillContextDesc') : wizard.description; + + // Translate options + const getOptionName = (wizardId, optId) => { + if (wizardId === 'memory-update') { + if (optId === 'on-stop') return t('hook.wizard.onSessionEnd'); + if (optId === 'periodic') return t('hook.wizard.periodicUpdate'); + } + if (wizardId === 'skill-context') { + if (optId === 'keyword') return t('hook.wizard.keywordMatching'); + if (optId === 'auto') return t('hook.wizard.autoDetection'); + } + return wizard.options.find(o => o.id === optId)?.name || ''; + }; + + const getOptionDesc = (wizardId, optId) => { + if (wizardId === 'memory-update') { + if (optId === 'on-stop') return t('hook.wizard.onSessionEndDesc'); + if (optId === 'periodic') return t('hook.wizard.periodicUpdateDesc'); + } + if (wizardId === 'skill-context') { + if (optId === 'keyword') return t('hook.wizard.keywordMatchingDesc'); + if (optId === 'auto') return t('hook.wizard.autoDetectionDesc'); + } + return wizard.options.find(o => o.id === optId)?.description || ''; + }; + // Determine what to show in the tools/skills section - const toolsSection = wizard.requiresSkillDiscovery + const toolsSection = wizard.requiresSkillDiscovery ? `
- Event: + ${t('hook.wizard.event')} UserPromptSubmit
- Available SKILLs: - Loading... + ${t('hook.wizard.availableSkills')} + ${t('hook.wizard.loading')}
` : `
- CLI Tools: + ${t('hook.wizard.cliTools')} gemini qwen codex @@ -230,8 +264,8 @@ function renderWizardCard(wizardId) {
-

${escapeHtml(wizard.name)}

-

${escapeHtml(wizard.description)}

+

${escapeHtml(wizardName)}

+

${escapeHtml(wizardDesc)}

@@ -240,7 +274,7 @@ function renderWizardCard(wizardId) { ${wizard.options.map(opt => `
- ${escapeHtml(opt.name)}: ${escapeHtml(opt.description)} + ${escapeHtml(getOptionName(wizardId, opt.id))}: ${escapeHtml(getOptionDesc(wizardId, opt.id))}
`).join('')}
@@ -250,7 +284,7 @@ function renderWizardCard(wizardId) {
`; @@ -333,6 +367,7 @@ function renderQuickInstallCard(templateId, title, description, event, matcher) const isInstalled = isHookTemplateInstalled(templateId); const template = HOOK_TEMPLATES[templateId]; const category = template?.category || 'general'; + const categoryTranslated = t(`hook.category.${category}`) || category; return `
@@ -346,7 +381,7 @@ function renderQuickInstallCard(templateId, title, description, event, matcher)
@@ -356,9 +391,9 @@ function renderQuickInstallCard(templateId, title, description, event, matcher) ${event} - Matches: ${matcher} + ${t('hook.wizard.matches')} ${matcher} - ${category} + ${categoryTranslated}
@@ -366,18 +401,18 @@ function renderQuickInstallCard(templateId, title, description, event, matcher) ` : ` `}
@@ -486,7 +521,7 @@ function attachHookEventListeners() { const event = button.dataset.event; const index = parseInt(button.dataset.index); - if (confirm(`Remove this ${event} hook?`)) { + if (confirm(t('hook.deleteConfirm', { event: event }))) { await removeHook(scope, event, index); } }); diff --git a/ccw/src/templates/dashboard-js/views/lite-tasks.js b/ccw/src/templates/dashboard-js/views/lite-tasks.js index 5e76d0a7..f77831d2 100644 --- a/ccw/src/templates/dashboard-js/views/lite-tasks.js +++ b/ccw/src/templates/dashboard-js/views/lite-tasks.js @@ -15,8 +15,8 @@ function renderLiteTasks() { container.innerHTML = `
-
No ${currentLiteType} Sessions
-
No sessions found in .workflow/.${currentLiteType}/
+
${t('empty.noLiteSessions', { type: currentLiteType })}
+
${t('empty.noLiteSessionsText', { type: currentLiteType })}
`; if (typeof lucide !== 'undefined') lucide.createIcons(); @@ -55,13 +55,13 @@ function renderLiteTaskCard(session) {
${escapeHtml(session.id)}
- ${session.type === 'lite-plan' ? ' PLAN' : ' FIX'} + ${session.type === 'lite-plan' ? ' ' + t('lite.plan') : ' ' + t('lite.fix')}
${formatDate(session.createdAt)} - ${tasks.length} tasks + ${tasks.length} ${t('session.tasks')}
@@ -116,12 +116,12 @@ function showLiteTaskDetailPage(sessionKey) {
- Created: + ${t('detail.created')} ${formatDate(session.createdAt)}
- Tasks: - ${tasks.length} tasks + ${t('detail.tasks')} + ${tasks.length} ${t('session.tasks')}
@@ -129,27 +129,27 @@ function showLiteTaskDetailPage(sessionKey) {
${session.type === 'lite-fix' ? ` ` : ''}
@@ -232,8 +232,8 @@ function renderLiteTasksTab(session, tasks, completed, inProgress, pending) { return `
-
No Tasks
-
This session has no tasks defined.
+
${t('empty.noTasks')}
+
${t('empty.noTasksText')}
`; } @@ -506,8 +506,8 @@ function renderDiagnosesTab(session) { return `
-
No Diagnoses
-
No diagnosis-*.json files found for this session.
+
${t('empty.noDiagnoses')}
+
${t('empty.noDiagnosesText')}
`; } diff --git a/ccw/src/templates/dashboard-js/views/mcp-manager.js b/ccw/src/templates/dashboard-js/views/mcp-manager.js index 82201717..3c01d7b9 100644 --- a/ccw/src/templates/dashboard-js/views/mcp-manager.js +++ b/ccw/src/templates/dashboard-js/views/mcp-manager.js @@ -41,20 +41,20 @@ async function renderMcpManager() {
-

Current Project MCP Servers

+

${t('mcp.currentProject')}

- ${currentProjectServerNames.length} servers configured + ${currentProjectServerNames.length} ${t('mcp.serversConfigured')}
${currentProjectServerNames.length === 0 ? `
-

No MCP servers configured for this project

-

Add servers from the available list below

+

${t('empty.noMcpServers')}

+

${t('empty.addMcpServersHint')}

` : `
@@ -109,13 +109,13 @@ async function renderMcpManager() {
-

Available from Other Projects

- ${otherProjectServers.length} servers available +

${t('mcp.availableOther')}

+ ${otherProjectServers.length} ${t('mcp.serversAvailable')}
${otherProjectServers.length === 0 ? `
-

No additional MCP servers found in other projects

+

${t('empty.noAdditionalMcp')}

` : `
@@ -129,17 +129,17 @@ async function renderMcpManager() {
-

All Projects MCP Overview

- ${Object.keys(mcpAllProjects).length} projects +

${t('mcp.allProjects')}

+ ${Object.keys(mcpAllProjects).length} ${t('mcp.projects')}
- - - + + + @@ -158,7 +158,7 @@ async function renderMcpManager() {
${escapeHtml(path.split('\\').pop() || path)} - ${isCurrentProject ? '(Current)' : ''} + ${isCurrentProject ? `${t('mcp.current')}` : ''}
${escapeHtml(path)}
@@ -167,7 +167,7 @@ async function renderMcpManager() {
ProjectMCP ServersStatus${t('mcp.project')}${t('mcp.servers')}${t('mcp.status')}
${serverNames.length === 0 - ? 'No MCP servers' + ? `${t('mcp.noMcpServers')}` : serverNames.map(serverName => { const isEnabled = !projectDisabled.includes(serverName); return ` @@ -247,7 +247,7 @@ function renderMcpServerCard(serverName, serverConfig, isEnabled, isInCurrentPro
` : ''} @@ -271,7 +271,7 @@ function renderAvailableServerCard(serverName, serverInfo) { data-server-name="${escapeHtml(serverName)}" data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "'")}' data-action="add"> - Add + ${t('mcp.add')} @@ -306,7 +306,7 @@ function renderGlobalServerCard(serverName, serverConfig, source = 'user') { data-server-name="${escapeHtml(serverName)}" data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "'")}' data-action="add"> - Add to Project + ${t('mcp.addToProject')} @@ -403,7 +403,7 @@ function attachMcpEventListeners() { document.querySelectorAll('.mcp-server-card button[data-action="remove"]').forEach(btn => { btn.addEventListener('click', async (e) => { const serverName = e.target.dataset.serverName; - if (confirm(`Remove MCP server "${serverName}" from this project?`)) { + if (confirm(t('mcp.removeConfirm', { name: serverName }))) { await removeMcpServerFromProject(serverName); } }); diff --git a/ccw/src/templates/dashboard-js/views/session-detail.js b/ccw/src/templates/dashboard-js/views/session-detail.js index 5b69a542..79d6b5b5 100644 --- a/ccw/src/templates/dashboard-js/views/session-detail.js +++ b/ccw/src/templates/dashboard-js/views/session-detail.js @@ -43,14 +43,14 @@ function showSessionDetailPage(sessionKey) {

${escapeHtml(session.session_id)}

${session.type || 'workflow'} - ${isActive ? 'ACTIVE' : 'ARCHIVED'} + ${isActive ? t('session.status.active') : t('session.status.archived')}
@@ -59,22 +59,22 @@ function showSessionDetailPage(sessionKey) {
- Created: + ${t('detail.created')} ${formatDate(session.created_at)}
${session.archived_at ? `
- Archived: + ${t('detail.archived')} ${formatDate(session.archived_at)}
` : ''}
- Project: + ${t('detail.project')} ${escapeHtml(session.project || '-')}
- Tasks: - ${completed}/${tasks.length} completed + ${t('detail.tasks')} + ${completed}/${tasks.length} ${t('detail.completed')}
@@ -82,26 +82,26 @@ function showSessionDetailPage(sessionKey) {
- + ${session.hasReview ? ` ` : ''}
@@ -178,33 +178,33 @@ function renderTasksTab(session, tasks, completed, inProgress, pending) {
- ${completed} completed - ${inProgress} in progress - ${pending} pending + ${completed} ${t('task.completed')} + ${inProgress} ${t('task.inProgress')} + ${pending} ${t('task.pending')}
- Quick Actions: - - -
${showLoading ? ` -
Loading task details...
+
${t('common.loading')}
` : (tasks.length === 0 ? `
-
No Tasks
-
This session has no tasks defined.
+
${t('empty.noTasks')}
+
${t('empty.noTasksText')}
` : tasks.map(task => renderDetailTaskItem(task)).join(''))}
@@ -217,7 +217,7 @@ async function loadFullTaskDetails() { if (!session || !window.SERVER_MODE || !session.path) return; const tasksContainer = document.getElementById('tasksListContent'); - tasksContainer.innerHTML = '
Loading full task details...
'; + tasksContainer.innerHTML = `
${t('common.loading')}
`; try { const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=tasks`); @@ -231,15 +231,15 @@ async function loadFullTaskDetails() { tasksContainer.innerHTML = `
-
No Task Files
-
No IMPL-*.json files found in .task/
+
${t('empty.noTaskFiles')}
+
${t('empty.noTaskFilesText')}
`; if (typeof lucide !== 'undefined') lucide.createIcons(); } } } catch (err) { - tasksContainer.innerHTML = `
Failed to load tasks: ${err.message}
`; + tasksContainer.innerHTML = `
${t('context.loadError', { error: err.message })}
`; } } @@ -271,9 +271,9 @@ function renderDetailTaskItem(task) { function formatStatusLabel(status) { const labels = { - 'pending': 'Pending', - 'in_progress': 'In Progress', - 'completed': 'Completed' + 'pending': `${t('task.status.pending')}`, + 'in_progress': `${t('task.status.inProgress')}`, + 'completed': `${t('task.status.completed')}` }; return labels[status] || status; } @@ -562,7 +562,7 @@ function showRawSessionJson(sessionKey) { async function updateSingleTaskStatus(taskId, newStatus) { const session = sessionDataStore[currentSessionDetailKey]; if (!session || !window.SERVER_MODE || !session.path) { - showToast('Status update requires server mode', 'error'); + showToast(t('toast.statusUpdateRequires'), 'error'); return; } @@ -582,14 +582,14 @@ async function updateSingleTaskStatus(taskId, newStatus) { // Update UI updateTaskItemUI(taskId, newStatus); updateTaskStatsBar(); - showToast(`Task ${taskId} status updated`, 'success'); + showToast(t('task.statusUpdated', { id: taskId }), 'success'); } else { - showToast(result.error || 'Failed to update status', 'error'); + showToast(result.error || t('toast.failedToUpdate'), 'error'); // Revert select revertTaskSelect(taskId); } } catch (error) { - showToast('Error updating status: ' + error.message, 'error'); + showToast(t('toast.errorUpdating', { error: error.message }), 'error'); revertTaskSelect(taskId); } } @@ -597,14 +597,15 @@ async function updateSingleTaskStatus(taskId, newStatus) { async function bulkSetAllStatus(newStatus) { const session = sessionDataStore[currentSessionDetailKey]; if (!session || !window.SERVER_MODE || !session.path) { - showToast('Bulk update requires server mode', 'error'); + showToast(t('toast.bulkUpdateRequires'), 'error'); return; } const taskIds = currentDrawerTasks.map(t => t.task_id || t.id); if (taskIds.length === 0) return; - if (!confirm(`Set all ${taskIds.length} tasks to "${formatStatusLabel(newStatus)}"?`)) { + const statusLabel = t(`task.status.${newStatus === 'in_progress' ? 'inProgress' : newStatus}`); + if (!confirm(t('task.setAllConfirm', { count: taskIds.length, status: statusLabel }))) { return; } @@ -624,12 +625,12 @@ async function bulkSetAllStatus(newStatus) { // Update all task UIs taskIds.forEach(id => updateTaskItemUI(id, newStatus)); updateTaskStatsBar(); - showToast(`All ${taskIds.length} tasks updated`, 'success'); + showToast(t('task.tasksUpdated', { count: taskIds.length }), 'success'); } else { - showToast(result.error || 'Failed to bulk update', 'error'); + showToast(result.error || t('toast.failedToBulkUpdate'), 'error'); } } catch (error) { - showToast('Error in bulk update: ' + error.message, 'error'); + showToast(t('toast.errorInBulk', { error: error.message }), 'error'); } } @@ -645,7 +646,7 @@ async function bulkSetPendingToInProgress() { .map(t => t.task_id || t.id); if (pendingTaskIds.length === 0) { - showToast('No pending tasks to start', 'info'); + showToast(t('task.noPendingTasks'), 'info'); return; } @@ -664,12 +665,12 @@ async function bulkSetPendingToInProgress() { if (result.success) { pendingTaskIds.forEach(id => updateTaskItemUI(id, 'in_progress')); updateTaskStatsBar(); - showToast(`${pendingTaskIds.length} tasks moved to In Progress`, 'success'); + showToast(t('task.movedToInProgress', { count: pendingTaskIds.length }), 'success'); } else { - showToast(result.error || 'Failed to update', 'error'); + showToast(result.error || t('toast.failedToUpdate'), 'error'); } } catch (error) { - showToast('Error: ' + error.message, 'error'); + showToast(t('toast.error', { error: error.message }), 'error'); } } @@ -685,7 +686,7 @@ async function bulkSetInProgressToCompleted() { .map(t => t.task_id || t.id); if (inProgressTaskIds.length === 0) { - showToast('No in-progress tasks to complete', 'info'); + showToast(t('task.noInProgressTasks'), 'info'); return; } @@ -704,12 +705,12 @@ async function bulkSetInProgressToCompleted() { if (result.success) { inProgressTaskIds.forEach(id => updateTaskItemUI(id, 'completed')); updateTaskStatsBar(); - showToast(`${inProgressTaskIds.length} tasks completed`, 'success'); + showToast(t('task.tasksCompleted', { count: inProgressTaskIds.length }), 'success'); } else { - showToast(result.error || 'Failed to update', 'error'); + showToast(result.error || t('toast.failedToUpdate'), 'error'); } } catch (error) { - showToast('Error: ' + error.message, 'error'); + showToast(t('toast.error', { error: error.message }), 'error'); } } @@ -743,9 +744,9 @@ function updateTaskStatsBar() { const statsBar = document.querySelector('.task-stats-bar'); if (statsBar) { statsBar.innerHTML = ` - ${completed} completed - ${inProgress} in progress - ${pending} pending + ${completed} ${t('task.completed')} + ${inProgress} ${t('task.inProgress')} + ${pending} ${t('task.pending')} `; // Reinitialize Lucide icons if (typeof lucide !== 'undefined') lucide.createIcons(); diff --git a/ccw/src/templates/dashboard.html b/ccw/src/templates/dashboard.html index 38b25320..a01b3bcb 100644 --- a/ccw/src/templates/dashboard.html +++ b/ccw/src/templates/dashboard.html @@ -256,27 +256,27 @@
- +
-
+ + + - @@ -306,25 +311,25 @@
- Project + Project
@@ -333,22 +338,22 @@
- Sessions + Sessions
@@ -358,17 +363,17 @@
- Lite Tasks + Lite Tasks
@@ -378,12 +383,12 @@
- MCP Servers + MCP Servers
@@ -393,12 +398,12 @@
- Hooks + Hooks
@@ -410,7 +415,7 @@
@@ -424,22 +429,22 @@
0
-
Total Sessions
+
Total Sessions
0
-
Active Sessions
+
Active Sessions
0
-
Total Tasks
+
Total Tasks
0
-
Completed Tasks
+
Completed Tasks
@@ -451,7 +456,7 @@
@@ -459,7 +464,7 @@ - -
@@ -483,10 +488,10 @@
-

All Sessions

+

All Sessions

-
@@ -500,14 +505,14 @@
-
Generated: -
-
CCW Dashboard v1.0
+
Generated: -
+
CCW Dashboard v1.0
-

Task Details

+

Task Details

@@ -522,11 +527,11 @@
-

Content Preview

+

Content Preview

- - + +
@@ -544,10 +549,10 @@
-

Create MCP Server

+

Create MCP Server

- - + +
@@ -555,13 +560,13 @@
- - Server Name * +
- - Command * +
@@ -570,7 +575,7 @@ class="w-full px-3 py-2 border border-border rounded-lg bg-background text-foreground text-sm font-mono focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 resize-none">
- +
@@ -578,7 +583,7 @@