diff --git a/package.json b/package.json index 504c00a..3b79775 100644 --- a/package.json +++ b/package.json @@ -14,5 +14,22 @@ "devDependencies": { "typescript": "^5.7.0", "vitest": "^3.1.0" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "esbuild", + "sharp", + "tree-sitter-c", + "tree-sitter-c-sharp", + "tree-sitter-cpp", + "tree-sitter-go", + "tree-sitter-java", + "tree-sitter-javascript", + "tree-sitter-php", + "tree-sitter-python", + "tree-sitter-ruby", + "tree-sitter-rust", + "tree-sitter-typescript" + ] } } diff --git a/understand-anything-plugin/agents/architecture-analyzer.md b/understand-anything-plugin/agents/architecture-analyzer.md index b6dab09..58c005c 100644 --- a/understand-anything-plugin/agents/architecture-analyzer.md +++ b/understand-anything-plugin/agents/architecture-analyzer.md @@ -14,6 +14,11 @@ You are an expert software architect. Your job is to analyze a codebase's file s Given a list of file nodes (with paths, summaries, tags, and node types) and import edges, identify 3-10 logical architecture layers and assign every file node to exactly one layer. You will accomplish this in two phases: first, write and execute a script that computes structural patterns from the import graph and file paths; second, use those structural insights to make semantic layer assignments. +**Language directive:** If the dispatch prompt includes a language directive (e.g., "Generate all textual content in **Chinese**"), apply it to: +- Layer `name` — Translate to the specified language (e.g., "API 层", "服务层", "基础设施层") +- Layer `description` — Write in the specified language using natural phrasing +Use native-level terminology. Keep established English terms when appropriate (e.g., "CI/CD", "ORM", "REST API" may remain untranslated in some languages). + --- ## Phase 1 -- Structural Analysis Script diff --git a/understand-anything-plugin/agents/file-analyzer.md b/understand-anything-plugin/agents/file-analyzer.md index d7d2e69..6d0e506 100644 --- a/understand-anything-plugin/agents/file-analyzer.md +++ b/understand-anything-plugin/agents/file-analyzer.md @@ -17,6 +17,12 @@ For each file in the batch provided to you, extract structural data via a script **File categories in this batch:** Each file has a `fileCategory` field indicating its type: `code`, `config`, `docs`, `infra`, `data`, `script`, or `markup`. Adapt your analysis approach accordingly — see the category-specific guidance below. +**Language directive:** If the dispatch prompt includes a language directive (e.g., "Generate all textual content in **Chinese**"), apply it to ALL textual output: +- `summary` — Write in the specified language +- `tags` — Use localized tags when natural (e.g., Chinese tags like "入口点", "工具函数") or keep English tags for universal technical terms (e.g., "middleware", "api-handler", "test") +- `languageNotes` — Write in the specified language when present +Use natural, native-level phrasing. Keep technical terms in English when no standard translation exists. + --- ## Phase 1 -- Structural Extraction (Bundled Script) diff --git a/understand-anything-plugin/agents/project-scanner.md b/understand-anything-plugin/agents/project-scanner.md index ed84a80..2cedacc 100644 --- a/understand-anything-plugin/agents/project-scanner.md +++ b/understand-anything-plugin/agents/project-scanner.md @@ -14,6 +14,8 @@ You are a meticulous project inventory specialist. Your job is to scan a codebas Scan the project directory provided in the prompt and produce a JSON inventory. You will accomplish this in two phases: first, write and execute a discovery script that performs all deterministic file scanning; second, review the script's results and add a human-readable project description. +**Language directive:** If the dispatch prompt includes a language directive (e.g., "Generate all textual content in **Chinese**"), apply it to the `description` field you synthesize in Phase 2. Write the description in the specified language using natural, native-level phrasing. Keep technical terms in English when no standard translation exists (e.g., "middleware", "hook", "barrel"). + --- ## Phase 1 -- Discovery Script diff --git a/understand-anything-plugin/agents/tour-builder.md b/understand-anything-plugin/agents/tour-builder.md index 41f5ef9..ce12d1b 100644 --- a/understand-anything-plugin/agents/tour-builder.md +++ b/understand-anything-plugin/agents/tour-builder.md @@ -14,6 +14,12 @@ You are an expert technical educator who designs learning paths through codebase Given a codebase's nodes, edges, and layers, design a guided tour that teaches the project's architecture and key concepts. The tour must reference only real node IDs from the provided graph data. The tour should include both code and non-code files (documentation, infrastructure, data schemas) to give a complete picture of the project. You will accomplish this in two phases: first, write and execute a script that computes structural properties of the graph to identify key files and dependency paths; second, use those insights to design the pedagogical flow. +**Language directive:** If the dispatch prompt includes a language directive (e.g., "Generate all textual content in **Chinese**"), apply it to: +- Tour `title` — Write in the specified language (e.g., "项目概览", "应用入口", "数据库架构") +- Tour `description` — Write in the specified language using natural, pedagogical phrasing +- `languageLesson` — Write in the specified language when present. Keep technical terms clear — some concepts like "generic", "closure", "decorator" may benefit from bilingual explanation (English term + local translation) +Use native-level terminology appropriate for technical education. + --- ## Phase 1 -- Graph Topology Script diff --git a/understand-anything-plugin/package.json b/understand-anything-plugin/package.json index e553094..60c9ca0 100644 --- a/understand-anything-plugin/package.json +++ b/understand-anything-plugin/package.json @@ -15,22 +15,5 @@ "@types/node": "^22.0.0", "typescript": "^5.7.0", "vitest": "^3.1.0" - }, - "pnpm": { - "onlyBuiltDependencies": [ - "esbuild", - "sharp", - "tree-sitter-c", - "tree-sitter-c-sharp", - "tree-sitter-cpp", - "tree-sitter-go", - "tree-sitter-java", - "tree-sitter-javascript", - "tree-sitter-php", - "tree-sitter-python", - "tree-sitter-ruby", - "tree-sitter-rust", - "tree-sitter-typescript" - ] } } diff --git a/understand-anything-plugin/packages/core/src/persistence/index.ts b/understand-anything-plugin/packages/core/src/persistence/index.ts index f14b011..d69857b 100644 --- a/understand-anything-plugin/packages/core/src/persistence/index.ts +++ b/understand-anything-plugin/packages/core/src/persistence/index.ts @@ -130,7 +130,7 @@ export function loadFingerprints(projectRoot: string): FingerprintStore | null { } } -const DEFAULT_CONFIG: ProjectConfig = { autoUpdate: false }; +const DEFAULT_CONFIG: ProjectConfig = { autoUpdate: false, outputLanguage: "en" }; export function saveConfig(projectRoot: string, config: ProjectConfig): void { const dir = ensureDir(projectRoot); diff --git a/understand-anything-plugin/packages/core/src/persistence/persistence.test.ts b/understand-anything-plugin/packages/core/src/persistence/persistence.test.ts index 02e0a1c..3a694fb 100644 --- a/understand-anything-plugin/packages/core/src/persistence/persistence.test.ts +++ b/understand-anything-plugin/packages/core/src/persistence/persistence.test.ts @@ -189,7 +189,7 @@ describe("persistence", () => { it("should return default config when no file exists", () => { const loaded = loadConfig(tempDir); - expect(loaded).toEqual({ autoUpdate: false }); + expect(loaded).toEqual({ autoUpdate: false, outputLanguage: "en" }); }); it("should return default config when config.json is corrupted", () => { @@ -198,7 +198,7 @@ describe("persistence", () => { writeFileSync(join(dir, "config.json"), "not json!!", "utf-8"); const loaded = loadConfig(tempDir); - expect(loaded).toEqual({ autoUpdate: false }); + expect(loaded).toEqual({ autoUpdate: false, outputLanguage: "en" }); }); }); }); diff --git a/understand-anything-plugin/packages/core/src/types.ts b/understand-anything-plugin/packages/core/src/types.ts index 890a9f0..b7a0fa6 100644 --- a/understand-anything-plugin/packages/core/src/types.ts +++ b/understand-anything-plugin/packages/core/src/types.ts @@ -113,9 +113,10 @@ export interface AnalysisMeta { theme?: ThemeConfig; } -// Project config (for auto-update opt-in) +// Project config (for auto-update opt-in and language preference) export interface ProjectConfig { autoUpdate: boolean; + outputLanguage?: string; } // Non-code structural sub-interfaces diff --git a/understand-anything-plugin/packages/dashboard/src/App.tsx b/understand-anything-plugin/packages/dashboard/src/App.tsx index 4aed9bf..6835e0a 100644 --- a/understand-anything-plugin/packages/dashboard/src/App.tsx +++ b/understand-anything-plugin/packages/dashboard/src/App.tsx @@ -23,6 +23,7 @@ import type { KeyboardShortcut } from "./hooks/useKeyboardShortcuts"; import { ThemeProvider } from "./themes/index.ts"; import { ThemePicker } from "./components/ThemePicker.tsx"; import type { ThemeConfig } from "./themes/index.ts"; +import { I18nProvider, useI18n } from "./contexts/I18nContext.tsx"; // Lazy-load heavy / optional components so they ship in separate chunks. const CodeViewer = lazy(() => import("./components/CodeViewer")); @@ -44,6 +45,7 @@ function dataUrl(fileName: string, token: string | null): string { "domain-graph.json": import.meta.env.VITE_DOMAIN_GRAPH_URL, "meta.json": import.meta.env.VITE_META_URL, "diff-overlay.json": import.meta.env.VITE_DIFF_OVERLAY_URL, + "config.json": import.meta.env.VITE_CONFIG_URL, }; const url = envMap[fileName]; if (url) return url; @@ -95,42 +97,13 @@ function App() { } function Dashboard({ accessToken }: { accessToken: string }) { - const graph = useDashboardStore((s) => s.graph); const setGraph = useDashboardStore((s) => s.setGraph); - const selectedNodeId = useDashboardStore((s) => s.selectedNodeId); - const tourActive = useDashboardStore((s) => s.tourActive); - const persona = useDashboardStore((s) => s.persona); - const codeViewerOpen = useDashboardStore((s) => s.codeViewerOpen); - const codeViewerExpanded = useDashboardStore((s) => s.codeViewerExpanded); - const expandCodeViewer = useDashboardStore((s) => s.expandCodeViewer); - const collapseCodeViewer = useDashboardStore((s) => s.collapseCodeViewer); + const setDomainGraph = useDashboardStore((s) => s.setDomainGraph); const setDiffOverlay = useDashboardStore((s) => s.setDiffOverlay); - const pathFinderOpen = useDashboardStore((s) => s.pathFinderOpen); - const togglePathFinder = useDashboardStore((s) => s.togglePathFinder); - const nodeTypeFilters = useDashboardStore((s) => s.nodeTypeFilters); - const toggleNodeTypeFilter = useDashboardStore((s) => s.toggleNodeTypeFilter); - const detailLevel = useDashboardStore((s) => s.detailLevel); - const setDetailLevel = useDashboardStore((s) => s.setDetailLevel); - const showFunctionsInClassView = useDashboardStore((s) => s.showFunctionsInClassView); - const toggleShowFunctionsInClassView = useDashboardStore((s) => s.toggleShowFunctionsInClassView); const [loadError, setLoadError] = useState(null); const [graphIssues, setGraphIssues] = useState([]); - const [showKeyboardHelp, setShowKeyboardHelp] = useState(false); const [metaTheme, setMetaTheme] = useState(null); - const [sidebarTab, setSidebarTab] = useState("info"); - const viewMode = useDashboardStore((s) => s.viewMode); - const setViewMode = useDashboardStore((s) => s.setViewMode); - const isKnowledgeGraph = useDashboardStore((s) => s.isKnowledgeGraph); - const domainGraph = useDashboardStore((s) => s.domainGraph); - const setDomainGraph = useDashboardStore((s) => s.setDomainGraph); - const layoutIssues = useDashboardStore((s) => s.layoutIssues); - const isMobile = useIsMobile(); - // Schema issues + ELK layout issues share the WarningBanner — graph-load - // problems and dashboard rendering problems are equally surfaced. - const allIssues = useMemo( - () => [...graphIssues, ...layoutIssues], - [graphIssues, layoutIssues], - ); + const [outputLanguage, setOutputLanguage] = useState(); useEffect(() => { fetch(dataUrl("meta.json", accessToken)) @@ -139,130 +112,14 @@ function Dashboard({ accessToken }: { accessToken: string }) { if (meta?.theme) setMetaTheme(meta.theme); }) .catch(() => {}); + fetch(dataUrl("config.json", accessToken)) + .then((r) => (r.ok ? r.json() : null)) + .then((config) => { + if (config?.outputLanguage) setOutputLanguage(config.outputLanguage); + }) + .catch(() => {}); }, []); - useEffect(() => { - if (selectedNodeId) setSidebarTab("info"); - }, [selectedNodeId]); - - // Define keyboard shortcuts - const shortcuts = useMemo( - () => [ - // Help - { - key: "?", - shiftKey: true, - description: "Show keyboard shortcuts", - action: () => setShowKeyboardHelp((prev) => !prev), - category: "General", - }, - // Navigation - { - key: "Escape", - description: "Close panels and modals / go back to overview", - action: () => { - // Read from store at invocation time to avoid stale closures - const state = useDashboardStore.getState(); - if (state.pathFinderOpen) { - state.togglePathFinder(); - } else if (state.filterPanelOpen) { - state.toggleFilterPanel(); - } else if (state.exportMenuOpen) { - state.toggleExportMenu(); - } else if (state.codeViewerExpanded) { - state.collapseCodeViewer(); - } else if (state.codeViewerOpen) { - state.closeCodeViewer(); - } else if (state.selectedNodeId) { - state.selectNode(null); - } else if (state.navigationLevel === "layer-detail") { - state.navigateToOverview(); - } else if (state.tourActive) { - state.stopTour(); - } else { - setShowKeyboardHelp(false); - } - }, - category: "Navigation", - }, - { - key: "/", - description: "Focus search bar", - action: () => { - const searchInput = document.querySelector( - 'input[placeholder*="Search"]' - ); - searchInput?.focus(); - }, - category: "Navigation", - }, - // Tour controls - { - key: "ArrowRight", - description: "Next tour step", - action: () => { - const state = useDashboardStore.getState(); - if (state.tourActive) { - state.nextTourStep(); - } - }, - category: "Tour", - }, - { - key: "ArrowLeft", - description: "Previous tour step", - action: () => { - const state = useDashboardStore.getState(); - if (state.tourActive) { - state.prevTourStep(); - } - }, - category: "Tour", - }, - // View toggles - { - key: "d", - description: "Toggle diff mode", - action: () => { - const state = useDashboardStore.getState(); - state.toggleDiffMode(); - }, - category: "View", - }, - { - key: "f", - description: "Toggle filter panel", - action: () => { - const state = useDashboardStore.getState(); - state.toggleFilterPanel(); - }, - category: "View", - }, - { - key: "e", - description: "Toggle export menu", - action: () => { - const state = useDashboardStore.getState(); - state.toggleExportMenu(); - }, - category: "View", - }, - { - key: "p", - description: "Open path finder", - action: () => { - const state = useDashboardStore.getState(); - state.togglePathFinder(); - }, - category: "View", - }, - ], - [] - ); - - // Register keyboard shortcuts - useKeyboardShortcuts(shortcuts); - useEffect(() => { fetch(dataUrl("knowledge-graph.json", accessToken)) .then((res) => res.json()) @@ -271,9 +128,8 @@ function Dashboard({ accessToken }: { accessToken: string }) { if (result.success && result.data) { setGraph(result.data); setGraphIssues(result.issues); - // Auto-detect knowledge graph kind if ((data as Record).kind === "knowledge") { - setViewMode("knowledge"); + useDashboardStore.getState().setViewMode("knowledge"); useDashboardStore.getState().setIsKnowledgeGraph(true); } for (const issue of result.issues) { @@ -318,9 +174,7 @@ function Dashboard({ accessToken }: { accessToken: string }) { } } }) - .catch(() => { - // Silently ignore - diff overlay is optional - }); + .catch(() => {}); }, [setDiffOverlay]); useEffect(() => { @@ -338,11 +192,183 @@ function Dashboard({ accessToken }: { accessToken: string }) { console.warn(`[domain-graph] validation failed: ${result.fatal}`); } }) - .catch(() => { - // Silently ignore — domain graph is optional - }); + .catch(() => {}); }, [setDomainGraph]); + return ( + + + + + + ); +} + +function DashboardContent({ + accessToken, + loadError, + graphIssues, +}: { + accessToken: string; + loadError: string | null; + graphIssues: GraphIssue[]; +}) { + const graph = useDashboardStore((s) => s.graph); + const selectedNodeId = useDashboardStore((s) => s.selectedNodeId); + const tourActive = useDashboardStore((s) => s.tourActive); + const persona = useDashboardStore((s) => s.persona); + const codeViewerOpen = useDashboardStore((s) => s.codeViewerOpen); + const codeViewerExpanded = useDashboardStore((s) => s.codeViewerExpanded); + const expandCodeViewer = useDashboardStore((s) => s.expandCodeViewer); + const collapseCodeViewer = useDashboardStore((s) => s.collapseCodeViewer); + const pathFinderOpen = useDashboardStore((s) => s.pathFinderOpen); + const togglePathFinder = useDashboardStore((s) => s.togglePathFinder); + const nodeTypeFilters = useDashboardStore((s) => s.nodeTypeFilters); + const toggleNodeTypeFilter = useDashboardStore((s) => s.toggleNodeTypeFilter); + const detailLevel = useDashboardStore((s) => s.detailLevel); + const setDetailLevel = useDashboardStore((s) => s.setDetailLevel); + const showFunctionsInClassView = useDashboardStore((s) => s.showFunctionsInClassView); + const toggleShowFunctionsInClassView = useDashboardStore((s) => s.toggleShowFunctionsInClassView); + const [showKeyboardHelp, setShowKeyboardHelp] = useState(false); + const [sidebarTab, setSidebarTab] = useState("info"); + const viewMode = useDashboardStore((s) => s.viewMode); + const setViewMode = useDashboardStore((s) => s.setViewMode); + const isKnowledgeGraph = useDashboardStore((s) => s.isKnowledgeGraph); + const domainGraph = useDashboardStore((s) => s.domainGraph); + const layoutIssues = useDashboardStore((s) => s.layoutIssues); + const isMobile = useIsMobile(); + const { t } = useI18n(); + const allIssues = useMemo( + () => [...graphIssues, ...layoutIssues], + [graphIssues, layoutIssues], + ); + + useEffect(() => { + if (selectedNodeId) setSidebarTab("info"); + }, [selectedNodeId]); + + // Define keyboard shortcuts + const shortcuts = useMemo( + () => [ + // Help + { + key: "?", + shiftKey: true, + description: t.keyboardShortcuts.showHelp, + action: () => setShowKeyboardHelp((prev) => !prev), + category: "General", + }, + // Navigation + { + key: "Escape", + description: t.keyboardShortcuts.escapeDesc, + action: () => { + // Read from store at invocation time to avoid stale closures + const state = useDashboardStore.getState(); + if (state.pathFinderOpen) { + state.togglePathFinder(); + } else if (state.filterPanelOpen) { + state.toggleFilterPanel(); + } else if (state.exportMenuOpen) { + state.toggleExportMenu(); + } else if (state.codeViewerExpanded) { + state.collapseCodeViewer(); + } else if (state.codeViewerOpen) { + state.closeCodeViewer(); + } else if (state.selectedNodeId) { + state.selectNode(null); + } else if (state.navigationLevel === "layer-detail") { + state.navigateToOverview(); + } else if (state.tourActive) { + state.stopTour(); + } else { + setShowKeyboardHelp(false); + } + }, + category: "Navigation", + }, + { + key: "/", + description: t.keyboardShortcuts.focusSearch, + action: () => { + const searchInput = document.querySelector( + '[data-testid="search-input"]' + ); + searchInput?.focus(); + }, + category: "Navigation", + }, + // Tour controls + { + key: "ArrowRight", + description: t.keyboardShortcuts.nextStep, + action: () => { + const state = useDashboardStore.getState(); + if (state.tourActive) { + state.nextTourStep(); + } + }, + category: "Tour", + }, + { + key: "ArrowLeft", + description: t.keyboardShortcuts.prevStep, + action: () => { + const state = useDashboardStore.getState(); + if (state.tourActive) { + state.prevTourStep(); + } + }, + category: "Tour", + }, + // View toggles + { + key: "d", + description: t.keyboardShortcuts.toggleDiff, + action: () => { + const state = useDashboardStore.getState(); + state.toggleDiffMode(); + }, + category: "View", + }, + { + key: "f", + description: t.keyboardShortcuts.toggleFilter, + action: () => { + const state = useDashboardStore.getState(); + state.toggleFilterPanel(); + }, + category: "View", + }, + { + key: "e", + description: t.keyboardShortcuts.toggleExport, + action: () => { + const state = useDashboardStore.getState(); + state.toggleExportMenu(); + }, + category: "View", + }, + { + key: "p", + description: t.keyboardShortcuts.openPathFinder, + action: () => { + const state = useDashboardStore.getState(); + state.togglePathFinder(); + }, + category: "View", + }, + ], + [t] + ); + + // Register keyboard shortcuts + useKeyboardShortcuts(shortcuts); + // Determine sidebar content // NodeInfo always takes priority when a node is selected. // Learn mode adds LearnPanel below it; otherwise ProjectOverview shows when idle. @@ -373,7 +399,7 @@ function Dashboard({ accessToken }: { accessToken: string }) { : "text-text-muted hover:text-text-primary hover:bg-elevated" }`} > - {tab === "info" ? "Info" : "Files"} + {tab === "info" ? t.sidebar.info : t.sidebar.files} ))} @@ -385,28 +411,25 @@ function Dashboard({ accessToken }: { accessToken: string }) { if (isMobile) { return ( - - - + ); } return ( -
{/* Header */}
{/* Left — fixed */}

- {graph?.project.name ?? "Understand Anything"} + {graph?.project.name ?? t.common.appName}

@@ -417,26 +440,26 @@ function Dashboard({ accessToken }: { accessToken: string }) {
@@ -455,55 +478,55 @@ function Dashboard({ accessToken }: { accessToken: string }) {
{detailLevel === "class" && ( )} )}
{(isKnowledgeGraph ? [ - { key: "knowledge" as const, label: "All", color: "var(--color-node-article)" }, + { key: "knowledge" as const, label: t.nodeTypeLabels.all, color: "var(--color-node-article)" }, ] : [ - { key: "code" as const, label: "Code", color: "var(--color-node-file)" }, - { key: "config" as const, label: "Config", color: "var(--color-node-config)" }, - { key: "docs" as const, label: "Docs", color: "var(--color-node-document)" }, - { key: "infra" as const, label: "Infra", color: "var(--color-node-service)" }, - { key: "data" as const, label: "Data", color: "var(--color-node-table)" }, - { key: "domain" as const, label: "Domain", color: "var(--color-node-concept)" }, - { key: "knowledge" as const, label: "Knowledge", color: "var(--color-node-article)" }, + { key: "code" as const, label: t.nodeTypeLabels.code, color: "var(--color-node-file)" }, + { key: "config" as const, label: t.nodeTypeLabels.config, color: "var(--color-node-config)" }, + { key: "docs" as const, label: t.nodeTypeLabels.docs, color: "var(--color-node-document)" }, + { key: "infra" as const, label: t.nodeTypeLabels.infra, color: "var(--color-node-service)" }, + { key: "data" as const, label: t.nodeTypeLabels.data, color: "var(--color-node-table)" }, + { key: "domain" as const, label: t.nodeTypeLabels.domain, color: "var(--color-node-concept)" }, + { key: "knowledge" as const, label: t.nodeTypeLabels.knowledge, color: "var(--color-node-article)" }, ]).map((cat) => (
@@ -661,7 +684,6 @@ function Dashboard({ accessToken }: { accessToken: string }) { )}
-
); } diff --git a/understand-anything-plugin/packages/dashboard/src/components/Breadcrumb.tsx b/understand-anything-plugin/packages/dashboard/src/components/Breadcrumb.tsx index 9bbb47b..bb00949 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/Breadcrumb.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/Breadcrumb.tsx @@ -1,10 +1,12 @@ import { useDashboardStore } from "../store"; +import { useI18n } from "../contexts/I18nContext"; export default function Breadcrumb() { const navigationLevel = useDashboardStore((s) => s.navigationLevel); const activeLayerId = useDashboardStore((s) => s.activeLayerId); const graph = useDashboardStore((s) => s.graph); const navigateToOverview = useDashboardStore((s) => s.navigateToOverview); + const { t } = useI18n(); const activeLayer = graph?.layers.find((l) => l.id === activeLayerId); @@ -12,7 +14,7 @@ export default function Breadcrumb() {
{navigationLevel === "overview" && (
- Project Overview + {t.breadcrumb.projectOverview}
)} @@ -22,14 +24,14 @@ export default function Breadcrumb() { onClick={navigateToOverview} className="text-gold hover:text-gold-bright transition-colors" > - Project + {t.breadcrumb.project} - {activeLayer?.name ?? "Layer"} + {activeLayer?.name ?? t.layer.defaultName} - (Esc to go back) + ({t.breadcrumb.escBack})
)} diff --git a/understand-anything-plugin/packages/dashboard/src/components/CodeViewer.tsx b/understand-anything-plugin/packages/dashboard/src/components/CodeViewer.tsx index 66ad67c..592fb1a 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/CodeViewer.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/CodeViewer.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import { Highlight, themes } from "prism-react-renderer"; import { useDashboardStore } from "../store"; +import { useI18n } from "../contexts/I18nContext"; interface CodeViewerProps { accessToken: string; @@ -78,6 +79,7 @@ export default function CodeViewer({ source: null, error: null, }); + const { t } = useI18n(); useEffect(() => { if (!node?.filePath) { @@ -125,7 +127,7 @@ export default function CodeViewer({ if (!node) { return (
-

No file selected

+

{t.codeViewer.noFile}

); } @@ -133,8 +135,8 @@ export default function CodeViewer({ const source = state.source; const language = source?.language ?? fallbackLanguage(node.filePath); const lineInfo = highlightedRange - ? `Lines ${highlightedRange.start}-${highlightedRange.end}` - : "Full file"; + ? `${t.codeViewer.lines} ${highlightedRange.start}-${highlightedRange.end}` + : t.codeViewer.fullFile; const isModal = presentation === "modal"; const handleClose = onClose ?? closeCodeViewer; @@ -170,8 +172,8 @@ export default function CodeViewer({ type="button" onClick={onExpand} className="text-text-muted hover:text-text-primary transition-colors" - title="Open larger code viewer" - aria-label="Open larger code viewer" + title={t.codeViewer.openLarger} + aria-label={t.codeViewer.openLarger} > @@ -182,8 +184,8 @@ export default function CodeViewer({ type="button" onClick={handleClose} className="text-text-muted hover:text-text-primary transition-colors" - title={isModal ? "Close expanded code viewer" : "Close code viewer"} - aria-label={isModal ? "Close expanded code viewer" : "Close code viewer"} + title={isModal ? t.codeViewer.closeExpanded : t.codeViewer.closeViewer} + aria-label={isModal ? t.codeViewer.closeExpanded : t.codeViewer.closeViewer} > @@ -194,13 +196,13 @@ export default function CodeViewer({
{state.status === "loading" && ( -
Loading source...
+
{t.codeViewer.loading}
)} {state.status === "error" && (
-
Source unavailable
+
{t.codeViewer.sourceUnavailable}

{state.error}

@@ -209,7 +211,7 @@ export default function CodeViewer({ {source && ( <>
- {source.lineCount} lines + {source.lineCount} {t.codeViewer.linesLabel} {formatBytes(source.sizeBytes)}
diff --git a/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx b/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx index bfc50b4..5dffd2d 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx @@ -2,6 +2,7 @@ import { memo } from "react"; import { Handle, Position } from "@xyflow/react"; import type { NodeProps, Node } from "@xyflow/react"; import type { NodeType } from "@understand-anything/core/types"; +import { useI18n } from "../contexts/I18nContext"; // Color maps keyed by NodeType — must be kept in sync with core NodeType union. const typeColors: Record = { @@ -88,6 +89,7 @@ function CustomNodeComponent({ const barColor = typeColors[knownType] ?? typeColors.file; const textColor = typeTextColors[knownType] ?? typeTextColors.file; const complexityColor = complexityColors[data.complexity] ?? complexityColors.simple; + const { t } = useI18n(); if (import.meta.env.DEV && !(knownType in typeColors)) { console.warn(`[CustomNode] Unknown node type "${data.nodeType}" — using "file" colors`); @@ -159,8 +161,8 @@ function CustomNodeComponent({ )}
diff --git a/understand-anything-plugin/packages/dashboard/src/components/DiffToggle.tsx b/understand-anything-plugin/packages/dashboard/src/components/DiffToggle.tsx index f912d1a..a3dd2ff 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/DiffToggle.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/DiffToggle.tsx @@ -1,10 +1,12 @@ import { useDashboardStore } from "../store"; +import { useI18n } from "../contexts/I18nContext"; export default function DiffToggle() { const diffMode = useDashboardStore((s) => s.diffMode); const toggleDiffMode = useDashboardStore((s) => s.toggleDiffMode); const changedNodeIds = useDashboardStore((s) => s.changedNodeIds); const affectedNodeIds = useDashboardStore((s) => s.affectedNodeIds); + const { t } = useI18n(); const hasDiff = changedNodeIds.size > 0; @@ -23,9 +25,9 @@ export default function DiffToggle() { title={ hasDiff ? diffMode - ? "Hide diff overlay" - : "Show diff overlay" - : "No diff data loaded" + ? t.diffToggle.hideOverlay + : t.diffToggle.showOverlay + : t.diffToggle.noData } > Diff {diffMode && hasDiff ? "ON" : "OFF"} @@ -39,7 +41,7 @@ export default function DiffToggle() { style={{ backgroundColor: "var(--color-diff-changed)" }} /> - Changed + {t.diffToggle.changed} ({changedNodeIds.size}) @@ -51,7 +53,7 @@ export default function DiffToggle() { style={{ backgroundColor: "var(--color-diff-affected)" }} /> - Affected + {t.diffToggle.affected} ({affectedNodeIds.size}) diff --git a/understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx b/understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx index 6730000..ff15846 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx @@ -17,6 +17,7 @@ import type { FlowFlowNode } from "./FlowNode"; import StepNode from "./StepNode"; import type { StepFlowNode } from "./StepNode"; import { useDashboardStore } from "../store"; +import { useI18n } from "../contexts/I18nContext"; import { mergeElkPositions, nodesToElkInput } from "../utils/layout"; import { applyElkLayout } from "../utils/elk-layout"; import type { KnowledgeGraph, GraphNode } from "@understand-anything/core/types"; @@ -167,6 +168,7 @@ function DomainGraphViewInner() { const domainGraph = useDashboardStore((s) => s.domainGraph); const activeDomainId = useDashboardStore((s) => s.activeDomainId); const clearActiveDomain = useDashboardStore((s) => s.clearActiveDomain); + const { t } = useI18n(); // Build structural nodes/edges/dims synchronously; only the layout call // itself is async, so we memo the structural pieces and run ELK in an @@ -237,7 +239,7 @@ function DomainGraphViewInner() { onClick={() => clearActiveDomain()} className="px-3 py-1.5 text-xs rounded-lg bg-elevated border border-border-subtle text-text-secondary hover:text-text-primary transition-colors" > - Back to domains + {t.domainView.backToDomains} )} diff --git a/understand-anything-plugin/packages/dashboard/src/components/ExportMenu.tsx b/understand-anything-plugin/packages/dashboard/src/components/ExportMenu.tsx index e3ed709..2a35fc9 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/ExportMenu.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/ExportMenu.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef } from "react"; import { useDashboardStore } from "../store"; +import { useI18n } from "../contexts/I18nContext"; import type { KnowledgeGraph } from "@understand-anything/core/types"; import { filterNodes, filterEdges } from "../utils/filters"; @@ -26,6 +27,7 @@ export default function ExportMenu() { const toggleExportMenu = useDashboardStore((s) => s.toggleExportMenu); const reactFlowInstance = useDashboardStore((s) => s.reactFlowInstance); const persona = useDashboardStore((s) => s.persona); + const { t } = useI18n(); const containerRef = useRef(null); @@ -218,7 +220,7 @@ export default function ExportMenu() { {exportMenuOpen && ( @@ -247,7 +249,7 @@ export default function ExportMenu() { - Export as PNG + {t.export.asPNG} diff --git a/understand-anything-plugin/packages/dashboard/src/components/FileExplorer.tsx b/understand-anything-plugin/packages/dashboard/src/components/FileExplorer.tsx index a0697d1..e4940f6 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/FileExplorer.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/FileExplorer.tsx @@ -1,6 +1,7 @@ import { useMemo, useState } from "react"; import type { GraphNode } from "@understand-anything/core/types"; import { useDashboardStore } from "../store"; +import { useI18n } from "../contexts/I18nContext"; interface FileEntry { name: string; @@ -142,6 +143,7 @@ export default function FileExplorer() { const graph = useDashboardStore((s) => s.graph); const openCodeViewer = useDashboardStore((s) => s.openCodeViewer); const navigateToNode = useDashboardStore((s) => s.navigateToNode); + const { t } = useI18n(); const entries = useMemo(() => buildFileTree(graph?.nodes ?? []), [graph]); const [expanded, setExpanded] = useState>(() => new Set()); @@ -176,7 +178,7 @@ export default function FileExplorer() { if (!graph) { return (
- No graph loaded + {t.common.noGraphLoaded}
); } @@ -185,15 +187,15 @@ export default function FileExplorer() {
- Analyzed Files + {t.fileExplorer.analyzedFiles}
- {totalFiles} files from the current knowledge graph + {totalFiles} {t.fileExplorer.filesFromGraph}
{entries.length === 0 ? ( -
No file paths found.
+
{t.fileExplorer.noFilePathsFound}
) : ( entries.map((entry) => ( s.graph); @@ -10,6 +11,7 @@ export default function FilterPanel() { const hasActiveFilters = useDashboardStore((s) => s.hasActiveFilters); const filterPanelOpen = useDashboardStore((s) => s.filterPanelOpen); const toggleFilterPanel = useDashboardStore((s) => s.toggleFilterPanel); + const { t } = useI18n(); const containerRef = useRef(null); @@ -97,7 +99,7 @@ export default function FilterPanel() { d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" /> - Filter + {t.common.filter} {filterPanelOpen && ( @@ -106,7 +108,7 @@ export default function FilterPanel() { {/* Node Types */}

- Node Types + {t.filterPanel.nodeTypes}

{allNodeTypes.map((type) => ( @@ -129,7 +131,7 @@ export default function FilterPanel() { {/* Complexity */}

- Complexity + {t.filterPanel.complexity}

{allComplexities.map((complexity) => ( @@ -153,7 +155,7 @@ export default function FilterPanel() { {layers.length > 0 && (

- Layers + {t.filterPanel.layers}

{layers.map((layer) => ( @@ -178,7 +180,7 @@ export default function FilterPanel() { {/* Edge Categories */}

- Edge Categories + {t.filterPanel.edgeCategories}

{allEdgeCategories.map((category) => ( @@ -206,7 +208,7 @@ export default function FilterPanel() { onClick={resetFilters} className="w-full px-3 py-1.5 text-sm bg-elevated hover:bg-gold/20 text-text-secondary hover:text-gold rounded-lg transition-colors" > - Reset All + {t.common.resetAll} )}
diff --git a/understand-anything-plugin/packages/dashboard/src/components/KeyboardShortcutsHelp.tsx b/understand-anything-plugin/packages/dashboard/src/components/KeyboardShortcutsHelp.tsx index b05c65e..87d0381 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/KeyboardShortcutsHelp.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/KeyboardShortcutsHelp.tsx @@ -1,5 +1,6 @@ import type { KeyboardShortcut } from "../hooks/useKeyboardShortcuts"; import { formatShortcutKey } from "../hooks/useKeyboardShortcuts"; +import { useI18n } from "../contexts/I18nContext"; interface KeyboardShortcutsHelpProps { shortcuts: KeyboardShortcut[]; @@ -10,6 +11,8 @@ export default function KeyboardShortcutsHelp({ shortcuts, onClose, }: KeyboardShortcutsHelpProps) { + const { t } = useI18n(); + // Group shortcuts by category const groupedShortcuts = shortcuts.reduce((acc, shortcut) => { if (!acc[shortcut.category]) { @@ -19,6 +22,14 @@ export default function KeyboardShortcutsHelp({ return acc; }, {} as Record); + // Translate category names + const categoryTranslations: Record = { + "General": t.keyboardShortcuts.general, + "Navigation": t.keyboardShortcuts.navigation, + "Tour": t.keyboardShortcuts.tour, + "View": t.keyboardShortcuts.view, + }; + return (

- Keyboard Shortcuts + {t.keyboardShortcuts.title}

- Press ? anytime to toggle this help + {t.keyboardShortcuts.toggleHint}

- Steps + {t.learnPanel.steps}

{tourSteps.map((step, i) => (

- Tour + {t.learnPanel.tour}

{currentTourStep + 1} / {totalSteps} @@ -97,7 +99,7 @@ export default function LearnPanel() { onClick={stopTour} className="text-[10px] text-text-muted hover:text-text-secondary transition-colors" > - Exit Tour + {t.learnPanel.exitTour}
@@ -213,13 +215,13 @@ export default function LearnPanel() { disabled={isFirst} className="flex-1 text-xs bg-elevated text-text-secondary py-1.5 rounded-lg hover:bg-surface disabled:opacity-40 disabled:cursor-not-allowed transition-colors" > - Prev + {t.learnPanel.prev}
diff --git a/understand-anything-plugin/packages/dashboard/src/components/MobileBottomNav.tsx b/understand-anything-plugin/packages/dashboard/src/components/MobileBottomNav.tsx index 44b42ef..83662ca 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/MobileBottomNav.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/MobileBottomNav.tsx @@ -1,4 +1,5 @@ import type { ReactNode } from "react"; +import { useI18n } from "../contexts/I18nContext"; export type MobileTab = "graph" | "info" | "files"; @@ -7,61 +8,58 @@ interface Props { onTabChange: (tab: MobileTab) => void; } -const tabs: { id: MobileTab; label: string; icon: ReactNode }[] = [ - { - id: "graph", - label: "Graph", - icon: ( - - - - - - - ), - }, - { - id: "info", - label: "Info", - icon: ( - - - - - ), - }, - { - id: "files", - label: "Files", - icon: ( - - - - ), - }, -]; +const tabIcons: Record = { + graph: ( + + + + + + + ), + info: ( + + + + + ), + files: ( + + + + ), +}; + +const tabOrder: MobileTab[] = ["graph", "info", "files"]; export default function MobileBottomNav({ activeTab, onTabChange }: Props) { + const { t } = useI18n(); + const labels: Record = { + graph: t.mobile.graph, + info: t.mobile.info, + files: t.mobile.files, + }; + return (
)}
- Diff overlay + {t.drawer.diffOverlay}
- Node types + {t.drawer.nodeTypes}
{filterDefs.map((cat) => { const active = nodeTypeFilters[cat.key] !== false; @@ -203,7 +207,7 @@ export default function MobileDrawer({ {graph && (graph.layers?.length ?? 0) > 0 && (
- Layers + {t.drawer.layers}
@@ -211,7 +215,7 @@ export default function MobileDrawer({ )}
- Tools + {t.drawer.tools}
@@ -231,7 +235,7 @@ export default function MobileDrawer({ d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" /> - Path + {t.drawer.path}
diff --git a/understand-anything-plugin/packages/dashboard/src/components/MobileLayout.tsx b/understand-anything-plugin/packages/dashboard/src/components/MobileLayout.tsx index 72d3d4e..27e4801 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/MobileLayout.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/MobileLayout.tsx @@ -1,6 +1,7 @@ import { lazy, Suspense, useEffect, useState } from "react"; import type { GraphIssue } from "@understand-anything/core/schema"; import { useDashboardStore } from "../store"; +import { useI18n } from "../contexts/I18nContext"; import GraphView from "./GraphView"; import DomainGraphView from "./DomainGraphView"; import KnowledgeGraphView from "./KnowledgeGraphView"; @@ -45,6 +46,7 @@ export default function MobileLayout({ const closeCodeViewer = useDashboardStore((s) => s.closeCodeViewer); const pathFinderOpen = useDashboardStore((s) => s.pathFinderOpen); const togglePathFinder = useDashboardStore((s) => s.togglePathFinder); + const { t } = useI18n(); const [activeTab, setActiveTab] = useState("graph"); const [drawerOpen, setDrawerOpen] = useState(false); @@ -96,7 +98,7 @@ export default function MobileLayout({

- {graph?.project.name ?? "Understand Anything"} + {graph?.project.name ?? t.common.appName}

{historyNodes.slice(-3).map((h, i, arr) => ( @@ -419,7 +376,7 @@ export default function NodeInfo() { : "text-text-muted border border-border-subtle hover:text-gold hover:border-gold/30" }`} > - {focusNodeId === node.id ? "Unfocus" : "Focus"} + {focusNodeId === node.id ? t.common.unfocus : t.common.focus}
@@ -431,7 +388,7 @@ export default function NodeInfo() {
-
File
+
{t.common.file}
{node.filePath} {node.lineRange && ( @@ -446,7 +403,7 @@ export default function NodeInfo() { onClick={() => openCodeViewer(node.id)} className="shrink-0 text-[10px] font-semibold uppercase tracking-wider px-2.5 py-1 rounded border border-accent/30 text-accent hover:text-accent-bright hover:border-accent/60 transition-colors" > - Open code + {t.common.openCode}
@@ -466,7 +423,7 @@ export default function NodeInfo() { > - Language Concepts + {t.nodeInfo.languageConcepts} {languageExpanded && (
@@ -481,7 +438,7 @@ export default function NodeInfo() { {node.tags.length > 0 && (

- Tags + {t.common.tags}

{node.tags.map((tag) => ( @@ -510,7 +467,7 @@ export default function NodeInfo() { {childNodes.length > 0 && (

- Defined in this file ({childNodes.length}) + {t.nodeInfo.definedInThisFile} ({childNodes.length})

{childNodes.map((child) => { @@ -548,14 +505,14 @@ export default function NodeInfo() { {otherConnections.length > 0 && (

- Connections ({otherConnections.length}) + {t.common.connections} ({otherConnections.length})

{otherConnections.map((edge, i) => { const isSource = edge.source === node.id; const otherId = isSource ? edge.target : edge.source; const otherNode = activeGraph?.nodes.find((n) => n.id === otherId); - const dirLabel = getDirectionalLabel(edge.type, isSource); + const dirLabel = getDirectionalLabel(edge.type, isSource, t); const arrow = isSource ? "\u2192" : "\u2190"; return ( diff --git a/understand-anything-plugin/packages/dashboard/src/components/PersonaSelector.tsx b/understand-anything-plugin/packages/dashboard/src/components/PersonaSelector.tsx index d496a7c..941e3a2 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/PersonaSelector.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/PersonaSelector.tsx @@ -1,27 +1,29 @@ import { useDashboardStore } from "../store"; +import { useI18n } from "../contexts/I18nContext"; import type { Persona } from "../store"; -const personas: { id: Persona; label: string; description: string }[] = [ - { - id: "non-technical", - label: "Overview", - description: "High-level architecture view", - }, - { - id: "junior", - label: "Learn", - description: "Full dashboard with guided learning", - }, - { - id: "experienced", - label: "Deep Dive", - description: "Code-focused with chat", - }, -]; - export default function PersonaSelector() { const persona = useDashboardStore((s) => s.persona); const setPersona = useDashboardStore((s) => s.setPersona); + const { t } = useI18n(); + + const personas: { id: Persona; label: string; description: string }[] = [ + { + id: "non-technical", + label: t.personaSelector.overview, + description: t.personaSelector.overviewDesc, + }, + { + id: "junior", + label: t.personaSelector.learn, + description: t.personaSelector.learnDesc, + }, + { + id: "experienced", + label: t.personaSelector.deepDive, + description: t.personaSelector.deepDiveDesc, + }, + ]; return (
diff --git a/understand-anything-plugin/packages/dashboard/src/components/ProjectOverview.tsx b/understand-anything-plugin/packages/dashboard/src/components/ProjectOverview.tsx index 1760437..c9cb478 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/ProjectOverview.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/ProjectOverview.tsx @@ -1,13 +1,15 @@ import { useDashboardStore } from "../store"; +import { useI18n } from "../contexts/I18nContext"; export default function ProjectOverview() { const graph = useDashboardStore((s) => s.graph); const startTour = useDashboardStore((s) => s.startTour); + const { t } = useI18n(); if (!graph) { return (
-

Loading project...

+

{t.common.loading}

); } @@ -15,13 +17,11 @@ export default function ProjectOverview() { const { project, nodes, edges, layers } = graph; const hasTour = graph.tour.length > 0; - // Count node types const typeCounts: Record = {}; for (const node of nodes) { typeCounts[node.type] = (typeCounts[node.type] ?? 0) + 1; } - // Count complexity const complexityCounts: Record = { simple: 0, moderate: 0, complex: 0 }; for (const node of nodes) { if (node.complexity) { @@ -29,7 +29,6 @@ export default function ProjectOverview() { } } - // Find top connected nodes const nodeConnections = new Map(); for (const edge of edges) { nodeConnections.set(edge.source, (nodeConnections.get(edge.source) ?? 0) + 1); @@ -45,16 +44,15 @@ export default function ProjectOverview() { const avgConnections = nodes.length > 0 ? (edges.length * 2 / nodes.length).toFixed(1) : "0"; - // Category breakdowns const categoryBreakdown = [ - { label: "Code", color: "var(--color-node-file)", count: (typeCounts["file"] ?? 0) + (typeCounts["function"] ?? 0) + (typeCounts["class"] ?? 0) + (typeCounts["module"] ?? 0) + (typeCounts["concept"] ?? 0) }, - { label: "Config", color: "var(--color-node-config)", count: typeCounts["config"] ?? 0 }, - { label: "Docs", color: "var(--color-node-document)", count: typeCounts["document"] ?? 0 }, - { label: "Infra", color: "var(--color-node-service)", count: (typeCounts["service"] ?? 0) + (typeCounts["resource"] ?? 0) + (typeCounts["pipeline"] ?? 0) }, - { label: "Data", color: "var(--color-node-table)", count: (typeCounts["table"] ?? 0) + (typeCounts["endpoint"] ?? 0) + (typeCounts["schema"] ?? 0) }, - { label: "Domain", color: "var(--color-node-concept)", count: (typeCounts["domain"] ?? 0) + (typeCounts["flow"] ?? 0) + (typeCounts["step"] ?? 0) }, + { label: t.projectOverview.code, color: "var(--color-node-file)", count: (typeCounts["file"] ?? 0) + (typeCounts["function"] ?? 0) + (typeCounts["class"] ?? 0) + (typeCounts["module"] ?? 0) + (typeCounts["concept"] ?? 0) }, + { label: t.projectOverview.config, color: "var(--color-node-config)", count: typeCounts["config"] ?? 0 }, + { label: t.projectOverview.docs, color: "var(--color-node-document)", count: typeCounts["document"] ?? 0 }, + { label: t.projectOverview.infra, color: "var(--color-node-service)", count: (typeCounts["service"] ?? 0) + (typeCounts["resource"] ?? 0) + (typeCounts["pipeline"] ?? 0) }, + { label: t.projectOverview.data, color: "var(--color-node-table)", count: (typeCounts["table"] ?? 0) + (typeCounts["endpoint"] ?? 0) + (typeCounts["schema"] ?? 0) }, + { label: t.projectOverview.domain, color: "var(--color-node-concept)", count: (typeCounts["domain"] ?? 0) + (typeCounts["flow"] ?? 0) + (typeCounts["step"] ?? 0) }, ]; - const hasNonCodeNodes = categoryBreakdown.some((c) => c.label !== "Code" && c.count > 0); + const hasNonCodeNodes = categoryBreakdown.some((c) => c.label !== t.projectOverview.code && c.count > 0); return (
@@ -66,26 +64,26 @@ export default function ProjectOverview() {
{nodes.length}
-
Nodes
+
{t.projectOverview.nodes}
{edges.length}
-
Edges
+
{t.projectOverview.edges}
{layers.length}
-
Layers
+
{t.projectOverview.layers}
{Object.keys(typeCounts).length}
-
Types
+
{t.projectOverview.types}
{/* File Types breakdown */} {hasNonCodeNodes && (
-

File Types

+

{t.projectOverview.fileTypes}

{categoryBreakdown.filter((c) => c.count > 0).map((cat) => (
@@ -104,7 +102,7 @@ export default function ProjectOverview() { {/* Languages */} {project.languages.length > 0 && (
-

Languages

+

{t.projectOverview.languages}

{project.languages.map((lang) => ( @@ -118,7 +116,7 @@ export default function ProjectOverview() { {/* Frameworks */} {project.frameworks.length > 0 && (
-

Frameworks

+

{t.projectOverview.frameworks}

{project.frameworks.map((fw) => ( @@ -131,7 +129,7 @@ export default function ProjectOverview() { {/* Node Type Breakdown */}
-

Node Type Distribution

+

{t.projectOverview.nodeTypeDistribution}

{Object.entries(typeCounts) .sort((a, b) => b[1] - a[1]) @@ -158,19 +156,19 @@ export default function ProjectOverview() { {/* Complexity Breakdown */} {Object.values(complexityCounts).some((c) => c > 0) && (
-

Complexity Distribution

+

{t.projectOverview.complexityDistribution}

{complexityCounts.simple}
-
Simple
+
{t.projectOverview.simple}
{complexityCounts.moderate}
-
Moderate
+
{t.projectOverview.moderate}
{complexityCounts.complex}
-
Complex
+
{t.projectOverview.complex}
@@ -179,7 +177,7 @@ export default function ProjectOverview() { {/* Top Connected Nodes */} {topNodes.length > 0 && (
-

Most Connected Nodes

+

{t.projectOverview.mostConnectedNodes}

{topNodes.map((node, idx) => (
- Avg Connections per Node + {t.projectOverview.avgConnectionsPerNode} {avgConnections}
{/* Analyzed at */}
- Analyzed: {new Date(project.analyzedAt).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })} + {t.common.analyzed}: {new Date(project.analyzedAt).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })}
{/* Start Tour button */} @@ -216,7 +214,7 @@ export default function ProjectOverview() { onClick={startTour} className="w-full bg-accent/10 border border-accent/30 text-accent text-sm font-medium py-2.5 px-4 rounded-lg hover:bg-accent/20 transition-all duration-200" > - Start Guided Tour + {t.common.startGuidedTour} )}
diff --git a/understand-anything-plugin/packages/dashboard/src/components/SearchBar.tsx b/understand-anything-plugin/packages/dashboard/src/components/SearchBar.tsx index a7135e6..88ef3f5 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/SearchBar.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/SearchBar.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useDashboardStore } from "../store"; +import { useI18n } from "../contexts/I18nContext"; const typeBadgeColors: Record = { file: "text-node-file border border-node-file/30 bg-node-file/10", @@ -28,6 +29,7 @@ export default function SearchBar() { const navigateToNodeInLayer = useDashboardStore((s) => s.navigateToNodeInLayer); const searchMode = useDashboardStore((s) => s.searchMode); const setSearchMode = useDashboardStore((s) => s.setSearchMode); + const { t } = useI18n(); const [dropdownOpen, setDropdownOpen] = useState(false); const containerRef = useRef(null); @@ -104,7 +106,8 @@ export default function SearchBar() { value={searchQuery} onChange={handleInputChange} onFocus={() => setDropdownOpen(true)} - placeholder="Search nodes by name, summary, or tags..." + placeholder={t.search.placeholder} + data-testid="search-input" className="flex-1 min-w-0 bg-elevated text-text-primary text-sm rounded-lg px-3 py-1.5 border border-border-subtle focus:outline-none focus:border-accent/50 placeholder-text-muted" />
@@ -116,7 +119,7 @@ export default function SearchBar() { : "text-text-muted hover:text-text-secondary" }`} > - Fuzzy + {t.search.fuzzy}
{searchQuery.trim() && ( - {searchResults.length} result{searchResults.length !== 1 ? "s" : ""}{" "} + {searchResults.length} {t.search.result}{searchResults.length !== 1 ? "s" : ""}{" "} ({searchMode}) )} diff --git a/understand-anything-plugin/packages/dashboard/src/components/ThemePicker.tsx b/understand-anything-plugin/packages/dashboard/src/components/ThemePicker.tsx index 49eea73..8c7e9e7 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/ThemePicker.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/ThemePicker.tsx @@ -1,11 +1,13 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useTheme, PRESETS } from "../themes/index.ts"; import type { HeadingFont } from "../themes/index.ts"; +import { useI18n } from "../contexts/I18nContext"; export function ThemePicker() { const { config, preset, setPreset, setAccent, setHeadingFont } = useTheme(); const [open, setOpen] = useState(false); const ref = useRef(null); + const { t } = useI18n(); // Close on outside click useEffect(() => { @@ -41,7 +43,7 @@ export function ThemePicker() { {open && ( @@ -67,7 +69,7 @@ export function ThemePicker() { {/* Presets */}
- Theme + {t.themePicker.theme}
{PRESETS.map((p) => ( @@ -119,7 +121,7 @@ export function ThemePicker() { {/* Accent swatches */}
- Accent Color + {t.themePicker.accentColor}
{preset.accentSwatches.map((swatch) => ( @@ -141,13 +143,13 @@ export function ThemePicker() { {/* Heading font */}
- Heading Font + {t.themePicker.headingFont}
{([ - { id: "serif" as HeadingFont, label: "Serif", sample: "Aa" }, - { id: "sans" as HeadingFont, label: "Sans", sample: "Aa" }, - { id: "mono" as HeadingFont, label: "Mono", sample: "Aa" }, + { id: "serif" as HeadingFont, label: t.themePicker.serif, sample: "Aa" }, + { id: "sans" as HeadingFont, label: t.themePicker.sans, sample: "Aa" }, + { id: "mono" as HeadingFont, label: t.themePicker.mono, sample: "Aa" }, ]).map((opt) => (