diff --git a/package.json b/package.json index 504c00a1..3b797752 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 b6dab092..58c005c1 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 d7d2e698..6d0e5067 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 ed84a80c..2cedacc0 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 41f5ef90..ce12d1b6 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 e5530940..60c9ca09 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 f14b011f..d69857be 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 02e0a1c5..3a694fb2 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 890a9f0d..b7a0fa6e 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 4aed9bff..6835e0a7 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,8 +97,127 @@ function App() { } function Dashboard({ accessToken }: { accessToken: string }) { - const graph = useDashboardStore((s) => s.graph); const setGraph = useDashboardStore((s) => s.setGraph); + const setDomainGraph = useDashboardStore((s) => s.setDomainGraph); + const setDiffOverlay = useDashboardStore((s) => s.setDiffOverlay); + const [loadError, setLoadError] = useState(null); + const [graphIssues, setGraphIssues] = useState([]); + const [metaTheme, setMetaTheme] = useState(null); + const [outputLanguage, setOutputLanguage] = useState(); + + useEffect(() => { + fetch(dataUrl("meta.json", accessToken)) + .then((r) => (r.ok ? r.json() : null)) + .then((meta) => { + 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(() => { + fetch(dataUrl("knowledge-graph.json", accessToken)) + .then((res) => res.json()) + .then((data: unknown) => { + const result = validateGraph(data); + if (result.success && result.data) { + setGraph(result.data); + setGraphIssues(result.issues); + if ((data as Record).kind === "knowledge") { + useDashboardStore.getState().setViewMode("knowledge"); + useDashboardStore.getState().setIsKnowledgeGraph(true); + } + for (const issue of result.issues) { + if (issue.level === "auto-corrected") { + console.warn(`[graph] auto-corrected: ${issue.message}`); + } else if (issue.level === "dropped") { + console.error(`[graph] dropped: ${issue.message}`); + } + } + } else if (result.fatal) { + console.error("Knowledge graph validation failed:", result.fatal); + setLoadError(`Invalid knowledge graph: ${result.fatal}`); + } else { + console.error("Knowledge graph validation failed: unknown error"); + setLoadError("Invalid knowledge graph: unknown validation error"); + } + }) + .catch((err) => { + console.error("Failed to load knowledge graph:", err); + setLoadError(`Failed to load knowledge graph: ${err instanceof Error ? err.message : String(err)}`); + }); + }, [setGraph]); + + useEffect(() => { + fetch(dataUrl("diff-overlay.json", accessToken)) + .then((res) => { + if (!res.ok) return null; + return res.json(); + }) + .then((data: unknown) => { + if ( + data && + typeof data === "object" && + "changedNodeIds" in data && + "affectedNodeIds" in data && + Array.isArray((data as Record).changedNodeIds) && + Array.isArray((data as Record).affectedNodeIds) + ) { + const d = data as { changedNodeIds: string[]; affectedNodeIds: string[] }; + if (d.changedNodeIds.length > 0) { + setDiffOverlay(d.changedNodeIds, d.affectedNodeIds); + } + } + }) + .catch(() => {}); + }, [setDiffOverlay]); + + useEffect(() => { + fetch(dataUrl("domain-graph.json", accessToken)) + .then((res) => { + if (!res.ok) return null; + return res.json(); + }) + .then((data: unknown) => { + if (!data) return; + const result = validateGraph(data); + if (result.success && result.data) { + setDomainGraph(result.data); + } else if (result.fatal) { + console.warn(`[domain-graph] validation failed: ${result.fatal}`); + } + }) + .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); @@ -104,7 +225,6 @@ function Dashboard({ accessToken }: { accessToken: string }) { const codeViewerExpanded = useDashboardStore((s) => s.codeViewerExpanded); const expandCodeViewer = useDashboardStore((s) => s.expandCodeViewer); const collapseCodeViewer = useDashboardStore((s) => s.collapseCodeViewer); - const setDiffOverlay = useDashboardStore((s) => s.setDiffOverlay); const pathFinderOpen = useDashboardStore((s) => s.pathFinderOpen); const togglePathFinder = useDashboardStore((s) => s.togglePathFinder); const nodeTypeFilters = useDashboardStore((s) => s.nodeTypeFilters); @@ -113,34 +233,20 @@ function Dashboard({ accessToken }: { accessToken: string }) { 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 { t } = useI18n(); const allIssues = useMemo( () => [...graphIssues, ...layoutIssues], [graphIssues, layoutIssues], ); - useEffect(() => { - fetch(dataUrl("meta.json", accessToken)) - .then((r) => (r.ok ? r.json() : null)) - .then((meta) => { - if (meta?.theme) setMetaTheme(meta.theme); - }) - .catch(() => {}); - }, []); - useEffect(() => { if (selectedNodeId) setSidebarTab("info"); }, [selectedNodeId]); @@ -152,14 +258,14 @@ function Dashboard({ accessToken }: { accessToken: string }) { { key: "?", shiftKey: true, - description: "Show keyboard shortcuts", + description: t.keyboardShortcuts.showHelp, action: () => setShowKeyboardHelp((prev) => !prev), category: "General", }, // Navigation { key: "Escape", - description: "Close panels and modals / go back to overview", + description: t.keyboardShortcuts.escapeDesc, action: () => { // Read from store at invocation time to avoid stale closures const state = useDashboardStore.getState(); @@ -187,10 +293,10 @@ function Dashboard({ accessToken }: { accessToken: string }) { }, { key: "/", - description: "Focus search bar", + description: t.keyboardShortcuts.focusSearch, action: () => { const searchInput = document.querySelector( - 'input[placeholder*="Search"]' + '[data-testid="search-input"]' ); searchInput?.focus(); }, @@ -199,7 +305,7 @@ function Dashboard({ accessToken }: { accessToken: string }) { // Tour controls { key: "ArrowRight", - description: "Next tour step", + description: t.keyboardShortcuts.nextStep, action: () => { const state = useDashboardStore.getState(); if (state.tourActive) { @@ -210,7 +316,7 @@ function Dashboard({ accessToken }: { accessToken: string }) { }, { key: "ArrowLeft", - description: "Previous tour step", + description: t.keyboardShortcuts.prevStep, action: () => { const state = useDashboardStore.getState(); if (state.tourActive) { @@ -222,7 +328,7 @@ function Dashboard({ accessToken }: { accessToken: string }) { // View toggles { key: "d", - description: "Toggle diff mode", + description: t.keyboardShortcuts.toggleDiff, action: () => { const state = useDashboardStore.getState(); state.toggleDiffMode(); @@ -231,7 +337,7 @@ function Dashboard({ accessToken }: { accessToken: string }) { }, { key: "f", - description: "Toggle filter panel", + description: t.keyboardShortcuts.toggleFilter, action: () => { const state = useDashboardStore.getState(); state.toggleFilterPanel(); @@ -240,7 +346,7 @@ function Dashboard({ accessToken }: { accessToken: string }) { }, { key: "e", - description: "Toggle export menu", + description: t.keyboardShortcuts.toggleExport, action: () => { const state = useDashboardStore.getState(); state.toggleExportMenu(); @@ -249,7 +355,7 @@ function Dashboard({ accessToken }: { accessToken: string }) { }, { key: "p", - description: "Open path finder", + description: t.keyboardShortcuts.openPathFinder, action: () => { const state = useDashboardStore.getState(); state.togglePathFinder(); @@ -257,92 +363,12 @@ function Dashboard({ accessToken }: { accessToken: string }) { category: "View", }, ], - [] + [t] ); // Register keyboard shortcuts useKeyboardShortcuts(shortcuts); - useEffect(() => { - fetch(dataUrl("knowledge-graph.json", accessToken)) - .then((res) => res.json()) - .then((data: unknown) => { - const result = validateGraph(data); - 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().setIsKnowledgeGraph(true); - } - for (const issue of result.issues) { - if (issue.level === "auto-corrected") { - console.warn(`[graph] auto-corrected: ${issue.message}`); - } else if (issue.level === "dropped") { - console.error(`[graph] dropped: ${issue.message}`); - } - } - } else if (result.fatal) { - console.error("Knowledge graph validation failed:", result.fatal); - setLoadError(`Invalid knowledge graph: ${result.fatal}`); - } else { - console.error("Knowledge graph validation failed: unknown error"); - setLoadError("Invalid knowledge graph: unknown validation error"); - } - }) - .catch((err) => { - console.error("Failed to load knowledge graph:", err); - setLoadError(`Failed to load knowledge graph: ${err instanceof Error ? err.message : String(err)}`); - }); - }, [setGraph]); - - useEffect(() => { - fetch(dataUrl("diff-overlay.json", accessToken)) - .then((res) => { - if (!res.ok) return null; - return res.json(); - }) - .then((data: unknown) => { - if ( - data && - typeof data === "object" && - "changedNodeIds" in data && - "affectedNodeIds" in data && - Array.isArray((data as Record).changedNodeIds) && - Array.isArray((data as Record).affectedNodeIds) - ) { - const d = data as { changedNodeIds: string[]; affectedNodeIds: string[] }; - if (d.changedNodeIds.length > 0) { - setDiffOverlay(d.changedNodeIds, d.affectedNodeIds); - } - } - }) - .catch(() => { - // Silently ignore - diff overlay is optional - }); - }, [setDiffOverlay]); - - useEffect(() => { - fetch(dataUrl("domain-graph.json", accessToken)) - .then((res) => { - if (!res.ok) return null; - return res.json(); - }) - .then((data: unknown) => { - if (!data) return; - const result = validateGraph(data); - if (result.success && result.data) { - setDomainGraph(result.data); - } else if (result.fatal) { - console.warn(`[domain-graph] validation failed: ${result.fatal}`); - } - }) - .catch(() => { - // Silently ignore — domain graph is optional - }); - }, [setDomainGraph]); - // 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 9bbb47bc..bb009497 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 66ad67ca..592fb1a3 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 bfc50b41..5dffd2df 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 f912d1ae..a3dd2ffc 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 67300009..ff158467 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 e3ed7093..2a35fc9c 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 a0697d1f..e4940f6c 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 b05c65ee..87d0381b 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 44b42eff..83662cac 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 72d3d4ee..27e48015 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 d496a7cd..941e3a2f 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 1760437a..c9cb478a 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 a7135e61..88ef3f5c 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 49eea733..8c7e9e71 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) => (