feat(dashboard): Add i18n support for localized UI text

- Add outputLanguage field to ProjectConfig type
- Create /config.json endpoint in vite.config.ts
- Build locale files for 5 languages (en, zh, zh-TW, ja, ko)
- Add I18nProvider context and useI18n hook
- Update 5 components (ProjectOverview, NodeInfo, FileExplorer, FilterPanel, PersonaSelector)
- Dashboard reads language from config.json and displays localized UI

All tests passed:
- Core: 670 tests
- Dashboard: 42 tests
This commit is contained in:
zhushen
2026-05-11 19:00:05 +08:00
parent 656289121c
commit 752fe59e0c
18 changed files with 788 additions and 132 deletions
@@ -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);
@@ -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" });
});
});
});
@@ -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
@@ -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 } 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;
@@ -118,6 +120,7 @@ function Dashboard({ accessToken }: { accessToken: string }) {
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false);
const [metaTheme, setMetaTheme] = useState<ThemeConfig | null>(null);
const [sidebarTab, setSidebarTab] = useState<SidebarTab>("info");
const [outputLanguage, setOutputLanguage] = useState<string | undefined>();
const viewMode = useDashboardStore((s) => s.viewMode);
const setViewMode = useDashboardStore((s) => s.setViewMode);
const isKnowledgeGraph = useDashboardStore((s) => s.isKnowledgeGraph);
@@ -139,6 +142,12 @@ 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(() => {
@@ -399,7 +408,8 @@ function Dashboard({ accessToken }: { accessToken: string }) {
}
return (
<ThemeProvider metaTheme={metaTheme}>
<I18nProvider language={outputLanguage ?? "en"}>
<ThemeProvider metaTheme={metaTheme}>
<div className="h-screen w-screen flex flex-col bg-root text-text-primary noise-overlay">
{/* Header */}
<header className="flex items-center px-3 sm:px-5 py-3 bg-surface border-b border-border-subtle shrink-0 gap-2 sm:gap-4">
@@ -662,6 +672,7 @@ function Dashboard({ accessToken }: { accessToken: string }) {
)}
</div>
</ThemeProvider>
</I18nProvider>
);
}
@@ -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<Set<string>>(() => new Set());
@@ -176,7 +178,7 @@ export default function FileExplorer() {
if (!graph) {
return (
<div className="h-full flex items-center justify-center p-5 text-sm text-text-muted">
No graph loaded
{t.common.noGraphLoaded}
</div>
);
}
@@ -185,15 +187,15 @@ export default function FileExplorer() {
<div className="h-full flex flex-col min-h-0">
<div className="px-4 py-3 border-b border-border-subtle shrink-0">
<div className="text-[11px] font-semibold uppercase tracking-wider text-accent">
Analyzed Files
{t.fileExplorer.analyzedFiles}
</div>
<div className="text-xs text-text-muted mt-1">
{totalFiles} files from the current knowledge graph
{totalFiles} {t.fileExplorer.filesFromGraph}
</div>
</div>
<div className="flex-1 overflow-auto py-2">
{entries.length === 0 ? (
<div className="px-4 py-6 text-sm text-text-muted">No file paths found.</div>
<div className="px-4 py-6 text-sm text-text-muted">{t.fileExplorer.noFilePathsFound}</div>
) : (
entries.map((entry) => (
<FileTreeRow
@@ -1,6 +1,7 @@
import { useEffect, useRef } from "react";
import { useDashboardStore, ALL_NODE_TYPES, ALL_COMPLEXITIES, ALL_EDGE_CATEGORIES } from "../store";
import type { NodeType, Complexity, EdgeCategory } from "../store";
import { useI18n } from "../contexts/I18nContext";
export default function FilterPanel() {
const graph = useDashboardStore((s) => 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<HTMLDivElement>(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"
/>
</svg>
Filter
{t.common.filter}
</button>
{filterPanelOpen && (
@@ -106,7 +108,7 @@ export default function FilterPanel() {
{/* Node Types */}
<div>
<h3 className="text-xs font-semibold text-text-secondary uppercase tracking-wider mb-2">
Node Types
{t.filterPanel.nodeTypes}
</h3>
<div className="space-y-1.5">
{allNodeTypes.map((type) => (
@@ -129,7 +131,7 @@ export default function FilterPanel() {
{/* Complexity */}
<div>
<h3 className="text-xs font-semibold text-text-secondary uppercase tracking-wider mb-2">
Complexity
{t.filterPanel.complexity}
</h3>
<div className="space-y-1.5">
{allComplexities.map((complexity) => (
@@ -153,7 +155,7 @@ export default function FilterPanel() {
{layers.length > 0 && (
<div>
<h3 className="text-xs font-semibold text-text-secondary uppercase tracking-wider mb-2">
Layers
{t.filterPanel.layers}
</h3>
<div className="space-y-1.5">
{layers.map((layer) => (
@@ -178,7 +180,7 @@ export default function FilterPanel() {
{/* Edge Categories */}
<div>
<h3 className="text-xs font-semibold text-text-secondary uppercase tracking-wider mb-2">
Edge Categories
{t.filterPanel.edgeCategories}
</h3>
<div className="space-y-1.5">
{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}
</button>
)}
</div>
@@ -1,5 +1,6 @@
import { useState } from "react";
import { useDashboardStore } from "../store";
import { useI18n } from "../contexts/I18nContext";
import type { NodeType, EdgeType, KnowledgeGraph, GraphNode } from "@understand-anything/core/types";
// Badge color classes keyed by NodeType — must be kept in sync with core NodeType union.
@@ -33,56 +34,9 @@ const complexityBadgeColors: Record<string, string> = {
complex: "text-[#c97070] border border-[#c97070]/30 bg-[#c97070]/10",
};
/**
* Human-readable directional labels for all 29 edge types.
* Must be kept in sync with core EdgeType.
*/
const EDGE_LABELS: Record<EdgeType, { forward: string; backward: string }> = {
imports: { forward: "imports", backward: "imported by" },
exports: { forward: "exports to", backward: "exported by" },
contains: { forward: "contains", backward: "contained in" },
inherits: { forward: "inherits from", backward: "inherited by" },
implements: { forward: "implements", backward: "implemented by" },
calls: { forward: "calls", backward: "called by" },
subscribes: { forward: "subscribes to", backward: "subscribed by" },
publishes: { forward: "publishes to", backward: "consumed by" },
middleware: { forward: "middleware for", backward: "uses middleware" },
reads_from: { forward: "reads from", backward: "read by" },
writes_to: { forward: "writes to", backward: "written by" },
transforms: { forward: "transforms", backward: "transformed by" },
validates: { forward: "validates", backward: "validated by" },
depends_on: { forward: "depends on", backward: "depended on by" },
tested_by: { forward: "tested by", backward: "tests" },
configures: { forward: "configures", backward: "configured by" },
related: { forward: "related to", backward: "related to" },
similar_to: { forward: "similar to", backward: "similar to" },
deploys: { forward: "deploys", backward: "deployed by" },
serves: { forward: "serves", backward: "served by" },
migrates: { forward: "migrates", backward: "migrated by" },
documents: { forward: "documents", backward: "documented by" },
provisions: { forward: "provisions", backward: "provisioned by" },
routes: { forward: "routes to", backward: "routed from" },
defines_schema: { forward: "defines schema for", backward: "schema defined by" },
triggers: { forward: "triggers", backward: "triggered by" },
contains_flow: { forward: "contains flow", backward: "flow in" },
flow_step: { forward: "flow step", backward: "step of" },
cross_domain: { forward: "cross-domain to", backward: "cross-domain from" },
cites: { forward: "cites", backward: "cited by" },
contradicts: { forward: "contradicts", backward: "contradicted by" },
builds_on: { forward: "builds on", backward: "built upon by" },
exemplifies: { forward: "exemplifies", backward: "exemplified by" },
categorized_under: { forward: "categorized under", backward: "categorizes" },
authored_by: { forward: "authored by", backward: "authored" },
};
/**
* Returns a human-readable directional label for an edge type.
* Falls back to formatted type name for unknown edge types.
*/
function getDirectionalLabel(edgeType: string, isSource: boolean): string {
const labels = (EDGE_LABELS as Record<string, { forward: string; backward: string }>)[edgeType];
function getDirectionalLabel(edgeType: string, isSource: boolean, t: ReturnType<typeof useI18n>["t"]): string {
const labels = t.edgeLabels[edgeType as EdgeType];
if (!labels) {
// Fallback for unknown edge types
const formatted = edgeType.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
return isSource ? formatted : `${formatted} (reverse)`;
}
@@ -91,6 +45,7 @@ function getDirectionalLabel(edgeType: string, isSource: boolean): string {
function KnowledgeNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeGraph }) {
const navigateToNode = useDashboardStore((s) => s.navigateToNode);
const { t } = useI18n();
const meta = node.knowledgeMeta;
// Wikilinks (outgoing related edges)
@@ -117,7 +72,7 @@ function KnowledgeNodeDetails({ node, graph }: { node: GraphNode; graph: Knowled
<div className="space-y-3">
{categoryNode && (
<div>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Category</h4>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">{t.nodeInfo.category}</h4>
<button
type="button"
onClick={() => navigateToNode(categoryNode.id)}
@@ -130,7 +85,7 @@ function KnowledgeNodeDetails({ node, graph }: { node: GraphNode; graph: Knowled
{meta?.wikilinks && meta.wikilinks.length > 0 && (
<div>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">
Wikilinks ({wikilinks.length})
{t.nodeInfo.wikilinks} ({wikilinks.length})
</h4>
<div className="space-y-1 max-h-[200px] overflow-auto">
{wikilinks.map((n) => (
@@ -149,7 +104,7 @@ function KnowledgeNodeDetails({ node, graph }: { node: GraphNode; graph: Knowled
{backlinks.length > 0 && (
<div>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">
Backlinks ({backlinks.length})
{t.nodeInfo.backlinks} ({backlinks.length})
</h4>
<div className="space-y-1 max-h-[200px] overflow-auto">
{backlinks.map((n) => (
@@ -167,11 +122,11 @@ function KnowledgeNodeDetails({ node, graph }: { node: GraphNode; graph: Knowled
)}
{meta?.content && (
<div>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Preview</h4>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">{t.common.preview}</h4>
<div className="text-[11px] text-text-secondary leading-relaxed bg-elevated rounded-lg p-3 max-h-[300px] overflow-auto whitespace-pre-wrap font-mono">
{meta.content.slice(0, 1500)}
{meta.content.length > 1500 && (
<span className="text-text-muted">... (truncated)</span>
<span className="text-text-muted">... {t.common.truncated}</span>
)}
</div>
</div>
@@ -183,6 +138,7 @@ function KnowledgeNodeDetails({ node, graph }: { node: GraphNode; graph: Knowled
function DomainNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeGraph }) {
const navigateToDomain = useDashboardStore((s) => s.navigateToDomain);
const selectNode = useDashboardStore((s) => s.selectNode);
const { t } = useI18n();
const meta = node.domainMeta;
if (node.type === "domain") {
@@ -195,7 +151,7 @@ function DomainNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeG
<div className="space-y-3">
{Array.isArray(meta?.entities) && meta.entities.length > 0 ? (
<div>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Entities</h4>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">{t.nodeInfo.entities}</h4>
<div className="flex flex-wrap gap-1">
{meta.entities.map((e) => (
<span key={e} className="text-[11px] px-2 py-0.5 rounded bg-elevated text-text-secondary">{e}</span>
@@ -205,7 +161,7 @@ function DomainNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeG
) : null}
{Array.isArray(meta?.businessRules) && meta.businessRules.length > 0 ? (
<div>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Business Rules</h4>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">{t.nodeInfo.businessRules}</h4>
<ul className="text-[11px] text-text-secondary space-y-1">
{meta.businessRules.map((r, i) => (
<li key={i} className="flex gap-1.5"><span className="text-accent shrink-0">-</span>{r}</li>
@@ -215,7 +171,7 @@ function DomainNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeG
) : null}
{Array.isArray(meta?.crossDomainInteractions) && meta.crossDomainInteractions.length > 0 ? (
<div>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Cross-Domain</h4>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">{t.nodeInfo.crossDomain}</h4>
<ul className="text-[11px] text-text-secondary space-y-1">
{meta.crossDomainInteractions.map((c, i) => (
<li key={i}>{c}</li>
@@ -225,7 +181,7 @@ function DomainNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeG
) : null}
{flows.length > 0 && (
<div>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Flows</h4>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">{t.nodeInfo.flows}</h4>
<div className="space-y-1">
{flows.map((f) => (
<button
@@ -255,13 +211,13 @@ function DomainNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeG
<div className="space-y-3">
{meta?.entryPoint ? (
<div>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Entry Point</h4>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">{t.nodeInfo.entryPoint}</h4>
<div className="text-[11px] font-mono text-accent">{meta.entryPoint}</div>
</div>
) : null}
{steps.length > 0 && (
<div>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Steps</h4>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">{t.nodeInfo.steps}</h4>
<ol className="space-y-1">
{steps.map((s, i) => (
<li key={s.id}>
@@ -287,7 +243,7 @@ function DomainNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeG
return (
<div className="space-y-3">
<div>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">Implementation</h4>
<h4 className="text-[10px] uppercase tracking-wider text-text-muted mb-1">{t.nodeInfo.implementation}</h4>
<div className="text-[11px] font-mono text-text-secondary">
{node.filePath}
{node.lineRange && <span className="text-text-muted">:{node.lineRange[0]}-{node.lineRange[1]}</span>}
@@ -306,6 +262,7 @@ export default function NodeInfo() {
const nodeHistory = useDashboardStore((s) => s.nodeHistory);
const goBackNode = useDashboardStore((s) => s.goBackNode);
const [languageExpanded, setLanguageExpanded] = useState(true);
const { t } = useI18n();
const navigateToNode = useDashboardStore((s) => s.navigateToNode);
const navigateToHistoryIndex = useDashboardStore((s) => s.navigateToHistoryIndex);
@@ -327,7 +284,7 @@ export default function NodeInfo() {
if (!node) {
return (
<div className="h-full w-full flex items-center justify-center bg-surface">
<p className="text-text-muted text-sm">Select a node to see details</p>
<p className="text-text-muted text-sm">{t.common.selectNode}</p>
</div>
);
}
@@ -369,7 +326,7 @@ export default function NodeInfo() {
className="text-[10px] font-semibold text-gold hover:text-gold-bright transition-colors flex items-center gap-1"
>
<span></span>
<span>Back</span>
<span>{t.common.back}</span>
</button>
<span className="text-text-muted text-[10px]"></span>
{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}
</button>
</div>
@@ -431,7 +388,7 @@ export default function NodeInfo() {
<div className="text-xs text-text-secondary mb-4 rounded-lg border border-border-subtle bg-elevated/60 p-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="font-medium text-text-muted mb-1">File</div>
<div className="font-medium text-text-muted mb-1">{t.common.file}</div>
<div className="font-mono truncate" title={node.filePath}>
{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}
</button>
</div>
</div>
@@ -466,7 +423,7 @@ export default function NodeInfo() {
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
Language Concepts
{t.nodeInfo.languageConcepts}
</button>
{languageExpanded && (
<div className="bg-accent/5 border border-accent/20 rounded-lg p-3">
@@ -481,7 +438,7 @@ export default function NodeInfo() {
{node.tags.length > 0 && (
<div className="mb-4">
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-2">
Tags
{t.common.tags}
</h3>
<div className="flex flex-wrap gap-1.5">
{node.tags.map((tag) => (
@@ -510,7 +467,7 @@ export default function NodeInfo() {
{childNodes.length > 0 && (
<div className="mb-4">
<h3 className="text-[11px] font-semibold text-gold uppercase tracking-wider mb-2">
Defined in this file ({childNodes.length})
{t.nodeInfo.definedInThisFile} ({childNodes.length})
</h3>
<div className="space-y-1">
{childNodes.map((child) => {
@@ -548,14 +505,14 @@ export default function NodeInfo() {
{otherConnections.length > 0 && (
<div>
<h3 className="text-[11px] font-semibold text-gold uppercase tracking-wider mb-2">
Connections ({otherConnections.length})
{t.common.connections} ({otherConnections.length})
</h3>
<div className="space-y-1.5">
{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 (
@@ -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 (
<div className="flex items-center gap-1 bg-elevated rounded-lg p-0.5">
@@ -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 (
<div className="h-full w-full flex items-center justify-center">
<p className="text-text-muted text-sm">Loading project...</p>
<p className="text-text-muted text-sm">{t.common.loading}</p>
</div>
);
}
@@ -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<string, number> = {};
for (const node of nodes) {
typeCounts[node.type] = (typeCounts[node.type] ?? 0) + 1;
}
// Count complexity
const complexityCounts: Record<string, number> = { 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<string, number>();
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 (
<div className="h-full w-full overflow-auto p-5 animate-fade-slide-in">
@@ -66,26 +64,26 @@ export default function ProjectOverview() {
<div className="grid grid-cols-2 gap-3 mb-6">
<div className="bg-elevated rounded-lg p-3 border border-border-subtle">
<div className="text-2xl font-mono font-medium text-accent">{nodes.length}</div>
<div className="text-[11px] text-text-muted uppercase tracking-wider mt-1">Nodes</div>
<div className="text-[11px] text-text-muted uppercase tracking-wider mt-1">{t.projectOverview.nodes}</div>
</div>
<div className="bg-elevated rounded-lg p-3 border border-border-subtle">
<div className="text-2xl font-mono font-medium text-accent">{edges.length}</div>
<div className="text-[11px] text-text-muted uppercase tracking-wider mt-1">Edges</div>
<div className="text-[11px] text-text-muted uppercase tracking-wider mt-1">{t.projectOverview.edges}</div>
</div>
<div className="bg-elevated rounded-lg p-3 border border-border-subtle">
<div className="text-2xl font-mono font-medium text-accent">{layers.length}</div>
<div className="text-[11px] text-text-muted uppercase tracking-wider mt-1">Layers</div>
<div className="text-[11px] text-text-muted uppercase tracking-wider mt-1">{t.projectOverview.layers}</div>
</div>
<div className="bg-elevated rounded-lg p-3 border border-border-subtle">
<div className="text-2xl font-mono font-medium text-accent">{Object.keys(typeCounts).length}</div>
<div className="text-[11px] text-text-muted uppercase tracking-wider mt-1">Types</div>
<div className="text-[11px] text-text-muted uppercase tracking-wider mt-1">{t.projectOverview.types}</div>
</div>
</div>
{/* File Types breakdown */}
{hasNonCodeNodes && (
<div className="mb-5">
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-2">File Types</h3>
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-2">{t.projectOverview.fileTypes}</h3>
<div className="space-y-1.5">
{categoryBreakdown.filter((c) => c.count > 0).map((cat) => (
<div key={cat.label} className="flex items-center gap-2">
@@ -104,7 +102,7 @@ export default function ProjectOverview() {
{/* Languages */}
{project.languages.length > 0 && (
<div className="mb-5">
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-2">Languages</h3>
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-2">{t.projectOverview.languages}</h3>
<div className="flex flex-wrap gap-1.5">
{project.languages.map((lang) => (
<span key={lang} className="text-[11px] glass text-text-secondary px-2.5 py-1 rounded-full">
@@ -118,7 +116,7 @@ export default function ProjectOverview() {
{/* Frameworks */}
{project.frameworks.length > 0 && (
<div className="mb-5">
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-2">Frameworks</h3>
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-2">{t.projectOverview.frameworks}</h3>
<div className="flex flex-wrap gap-1.5">
{project.frameworks.map((fw) => (
<span key={fw} className="text-[11px] glass text-text-secondary px-2.5 py-1 rounded-full">
@@ -131,7 +129,7 @@ export default function ProjectOverview() {
{/* Node Type Breakdown */}
<div className="mb-5">
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-3">Node Type Distribution</h3>
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-3">{t.projectOverview.nodeTypeDistribution}</h3>
<div className="space-y-2">
{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) && (
<div className="mb-5">
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-3">Complexity Distribution</h3>
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-3">{t.projectOverview.complexityDistribution}</h3>
<div className="grid grid-cols-3 gap-2">
<div className="bg-elevated rounded-lg p-2 border border-border-subtle text-center">
<div className="text-lg font-mono font-medium text-green-400">{complexityCounts.simple}</div>
<div className="text-[10px] text-text-muted uppercase tracking-wider mt-0.5">Simple</div>
<div className="text-[10px] text-text-muted uppercase tracking-wider mt-0.5">{t.projectOverview.simple}</div>
</div>
<div className="bg-elevated rounded-lg p-2 border border-border-subtle text-center">
<div className="text-lg font-mono font-medium text-yellow-400">{complexityCounts.moderate}</div>
<div className="text-[10px] text-text-muted uppercase tracking-wider mt-0.5">Moderate</div>
<div className="text-[10px] text-text-muted uppercase tracking-wider mt-0.5">{t.projectOverview.moderate}</div>
</div>
<div className="bg-elevated rounded-lg p-2 border border-border-subtle text-center">
<div className="text-lg font-mono font-medium text-red-400">{complexityCounts.complex}</div>
<div className="text-[10px] text-text-muted uppercase tracking-wider mt-0.5">Complex</div>
<div className="text-[10px] text-text-muted uppercase tracking-wider mt-0.5">{t.projectOverview.complex}</div>
</div>
</div>
</div>
@@ -179,7 +177,7 @@ export default function ProjectOverview() {
{/* Top Connected Nodes */}
{topNodes.length > 0 && (
<div className="mb-5">
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-3">Most Connected Nodes</h3>
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-3">{t.projectOverview.mostConnectedNodes}</h3>
<div className="space-y-2">
{topNodes.map((node, idx) => (
<div
@@ -200,14 +198,14 @@ export default function ProjectOverview() {
{/* Average Connections */}
<div className="mb-5 bg-elevated rounded-lg p-3 border border-border-subtle">
<div className="flex items-center justify-between">
<span className="text-xs text-text-secondary">Avg Connections per Node</span>
<span className="text-xs text-text-secondary">{t.projectOverview.avgConnectionsPerNode}</span>
<span className="text-lg font-mono font-medium text-accent">{avgConnections}</span>
</div>
</div>
{/* Analyzed at */}
<div className="text-[11px] text-text-muted mb-6">
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' })}
</div>
{/* 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}
</button>
)}
</div>
@@ -0,0 +1,44 @@
import { createContext, useContext, useMemo, type ReactNode } from "react";
import { getLocale, resolveLocaleKey, type Locale, type LocaleKey } from "../locales";
interface I18nContextValue {
locale: Locale;
localeKey: LocaleKey;
t: Locale;
}
const I18nContext = createContext<I18nContextValue | null>(null);
export function useI18n(): I18nContextValue {
const ctx = useContext(I18nContext);
if (!ctx) {
throw new Error("useI18n must be used within an I18nProvider");
}
return ctx;
}
export function I18nProvider({
language,
children,
}: {
language?: string;
children: ReactNode;
}) {
const localeKey = useMemo(() => resolveLocaleKey(language), [language]);
const locale = useMemo(() => getLocale(localeKey), [localeKey]);
const value = useMemo(
() => ({
locale,
localeKey,
t: locale,
}),
[locale, localeKey]
);
return (
<I18nContext.Provider value={value}>
{children}
</I18nContext.Provider>
);
}
@@ -0,0 +1,115 @@
export const en = {
common: {
loading: "Loading project...",
noGraphLoaded: "No graph loaded",
selectNode: "Select a node to see details",
back: "Back",
focus: "Focus",
unfocus: "Unfocus",
openCode: "Open code",
file: "File",
tags: "Tags",
connections: "Connections",
filter: "Filter",
resetAll: "Reset All",
analyzed: "Analyzed",
startGuidedTour: "Start Guided Tour",
truncated: "(truncated)",
preview: "Preview",
doubleClickToOpen: "double-click to open",
},
projectOverview: {
nodes: "Nodes",
edges: "Edges",
layers: "Layers",
types: "Types",
fileTypes: "File Types",
code: "Code",
config: "Config",
docs: "Docs",
infra: "Infra",
data: "Data",
domain: "Domain",
languages: "Languages",
frameworks: "Frameworks",
nodeTypeDistribution: "Node Type Distribution",
complexityDistribution: "Complexity Distribution",
simple: "Simple",
moderate: "Moderate",
complex: "Complex",
mostConnectedNodes: "Most Connected Nodes",
avgConnectionsPerNode: "Avg Connections per Node",
},
nodeInfo: {
definedInThisFile: "Defined in this file",
languageConcepts: "Language Concepts",
category: "Category",
wikilinks: "Wikilinks",
backlinks: "Backlinks",
entities: "Entities",
businessRules: "Business Rules",
crossDomain: "Cross-Domain",
flows: "Flows",
entryPoint: "Entry Point",
steps: "Steps",
implementation: "Implementation",
},
fileExplorer: {
analyzedFiles: "Analyzed Files",
filesFromGraph: "files from the current knowledge graph",
noFilePathsFound: "No file paths found.",
},
filterPanel: {
nodeTypes: "Node Types",
complexity: "Complexity",
layers: "Layers",
edgeCategories: "Edge Categories",
},
personaSelector: {
overview: "Overview",
overviewDesc: "High-level architecture view",
learn: "Learn",
learnDesc: "Full dashboard with guided learning",
deepDive: "Deep Dive",
deepDiveDesc: "Code-focused with chat",
},
edgeLabels: {
imports: { forward: "imports", backward: "imported by" },
exports: { forward: "exports to", backward: "exported by" },
contains: { forward: "contains", backward: "contained in" },
inherits: { forward: "inherits from", backward: "inherited by" },
implements: { forward: "implements", backward: "implemented by" },
calls: { forward: "calls", backward: "called by" },
subscribes: { forward: "subscribes to", backward: "subscribed by" },
publishes: { forward: "publishes to", backward: "consumed by" },
middleware: { forward: "middleware for", backward: "uses middleware" },
reads_from: { forward: "reads from", backward: "read by" },
writes_to: { forward: "writes to", backward: "written by" },
transforms: { forward: "transforms", backward: "transformed by" },
validates: { forward: "validates", backward: "validated by" },
depends_on: { forward: "depends on", backward: "depended on by" },
tested_by: { forward: "tested by", backward: "tests" },
configures: { forward: "configures", backward: "configured by" },
related: { forward: "related to", backward: "related to" },
similar_to: { forward: "similar to", backward: "similar to" },
deploys: { forward: "deploys", backward: "deployed by" },
serves: { forward: "serves", backward: "served by" },
migrates: { forward: "migrates", backward: "migrated by" },
documents: { forward: "documents", backward: "documented by" },
provisions: { forward: "provisions", backward: "provisioned by" },
routes: { forward: "routes to", backward: "routed from" },
defines_schema: { forward: "defines schema for", backward: "schema defined by" },
triggers: { forward: "triggers", backward: "triggered by" },
contains_flow: { forward: "contains flow", backward: "flow in" },
flow_step: { forward: "flow step", backward: "step of" },
cross_domain: { forward: "cross-domain to", backward: "cross-domain from" },
cites: { forward: "cites", backward: "cited by" },
contradicts: { forward: "contradicts", backward: "contradicted by" },
builds_on: { forward: "builds on", backward: "built upon by" },
exemplifies: { forward: "exemplifies", backward: "exemplified by" },
categorized_under: { forward: "categorized under", backward: "categorizes" },
authored_by: { forward: "authored by", backward: "authored" },
},
};
export default en;
@@ -0,0 +1,32 @@
import en from "./en";
import zh from "./zh";
import zhTW from "./zh-TW";
import ja from "./ja";
import ko from "./ko";
export type LocaleKey = "en" | "zh" | "zh-TW" | "ja" | "ko";
export type Locale = typeof en;
export const locales: Record<LocaleKey, Locale> = {
en,
zh,
"zh-TW": zhTW,
ja,
ko,
};
export function getLocale(key: LocaleKey): Locale {
return locales[key] ?? locales.en;
}
export function resolveLocaleKey(lang: string | undefined): LocaleKey {
if (!lang) return "en";
const normalized = lang.toLowerCase().replace(/[_\s]/g, "-");
if (normalized === "zh" || normalized === "chinese" || normalized === "zh-cn") return "zh";
if (normalized === "zh-tw" || normalized === "traditional-chinese") return "zh-TW";
if (normalized === "ja" || normalized === "japanese") return "ja";
if (normalized === "ko" || normalized === "korean") return "ko";
return "en";
}
export { en, zh, zhTW as "zh-TW", ja, ko };
@@ -0,0 +1,115 @@
export const ja = {
common: {
loading: "プロジェクトを読み込み中...",
noGraphLoaded: "知識グラフが読み込まれていません",
selectNode: "ノードを選択して詳細を表示",
back: "戻る",
focus: "フォーカス",
unfocus: "フォーカス解除",
openCode: "コードを開く",
file: "ファイル",
tags: "タグ",
connections: "接続",
filter: "フィルター",
resetAll: "すべてリセット",
analyzed: "分析日時",
startGuidedTour: "ガイド付きツアーを開始",
truncated: "(省略)",
preview: "プレビュー",
doubleClickToOpen: "ダブルクリックで開く",
},
projectOverview: {
nodes: "ノード",
edges: "エッジ",
layers: "レイヤー",
types: "タイプ",
fileTypes: "ファイルタイプ",
code: "コード",
config: "設定",
docs: "ドキュメント",
infra: "インフラ",
data: "データ",
domain: "ドメイン",
languages: "プログラミング言語",
frameworks: "フレームワーク",
nodeTypeDistribution: "ノードタイプ分布",
complexityDistribution: "複雑度分布",
simple: "単純",
moderate: "中程度",
complex: "複雑",
mostConnectedNodes: "最も接続されているノード",
avgConnectionsPerNode: "ノード平均接続数",
},
nodeInfo: {
definedInThisFile: "このファイルで定義",
languageConcepts: "言語概念",
category: "カテゴリ",
wikilinks: "Wikilinks",
backlinks: "Backlinks",
entities: "エンティティ",
businessRules: "ビジネスルール",
crossDomain: "クロスドメイン",
flows: "フロー",
entryPoint: "エントリポイント",
steps: "ステップ",
implementation: "実装",
},
fileExplorer: {
analyzedFiles: "分析済みファイル",
filesFromGraph: "現在の知識グラフからのファイル",
noFilePathsFound: "ファイルパスが見つかりません。",
},
filterPanel: {
nodeTypes: "ノードタイプ",
complexity: "複雑度",
layers: "レイヤー",
edgeCategories: "エッジカテゴリ",
},
personaSelector: {
overview: "概要",
overviewDesc: "高レベルアーキテクチャビュー",
learn: "学習",
learnDesc: "ガイド付き学習付き完全ダッシュボード",
deepDive: "詳細",
deepDiveDesc: "コード中心のチャット",
},
edgeLabels: {
imports: { forward: "インポート", backward: "インポートされる" },
exports: { forward: "エクスポート", backward: "エクスポートされる" },
contains: { forward: "含む", backward: "含まれる" },
inherits: { forward: "継承", backward: "継承される" },
implements: { forward: "実装", backward: "実装される" },
calls: { forward: "呼び出す", backward: "呼び出される" },
subscribes: { forward: "購読", backward: "購読される" },
publishes: { forward: "公開", backward: "消費される" },
middleware: { forward: "ミドルウェア", backward: "ミドルウェアを使用" },
reads_from: { forward: "読み取り", backward: "読み取られる" },
writes_to: { forward: "書き込み", backward: "書き込まれる" },
transforms: { forward: "変換", backward: "変換される" },
validates: { forward: "検証", backward: "検証される" },
depends_on: { forward: "依存", backward: "依存される" },
tested_by: { forward: "テストされる", backward: "テスト" },
configures: { forward: "設定", backward: "設定される" },
related: { forward: "関連", backward: "関連" },
similar_to: { forward: "類似", backward: "類似" },
deploys: { forward: "デプロイ", backward: "デプロイされる" },
serves: { forward: "提供", backward: "提供される" },
migrates: { forward: "移行", backward: "移行される" },
documents: { forward: "ドキュメント化", backward: "ドキュメント化される" },
provisions: { forward: "提供", backward: "提供される" },
routes: { forward: "ルーティング", backward: "ルーティングされる" },
defines_schema: { forward: "スキーマ定義", backward: "スキーマ定義される" },
triggers: { forward: "トリガー", backward: "トリガーされる" },
contains_flow: { forward: "フローを含む", backward: "フロー内" },
flow_step: { forward: "フローステップ", backward: "ステップの" },
cross_domain: { forward: "クロスドメイン", backward: "クロスドメインから" },
cites: { forward: "引用", backward: "引用される" },
contradicts: { forward: "矛盾", backward: "矛盾される" },
builds_on: { forward: "基礎", backward: "基礎となる" },
exemplifies: { forward: "例示", backward: "例示される" },
categorized_under: { forward: "カテゴリ化", backward: "カテゴリ化する" },
authored_by: { forward: "作成者", backward: "作成" },
},
};
export default ja;
@@ -0,0 +1,115 @@
export const ko = {
common: {
loading: "프로젝트 로딩 중...",
noGraphLoaded: "지식 그래프가 로드되지 않음",
selectNode: "노드를 선택하여 상세 정보 확인",
back: "뒤로",
focus: "포커스",
unfocus: "포커스 해제",
openCode: "코드 열기",
file: "파일",
tags: "태그",
connections: "연결",
filter: "필터",
resetAll: "모두 재설정",
analyzed: "분석 시간",
startGuidedTour: "가이드 투어 시작",
truncated: "(생략)",
preview: "미리보기",
doubleClickToOpen: "두 번 클릭하여 열기",
},
projectOverview: {
nodes: "노드",
edges: "엣지",
layers: "레이어",
types: "타입",
fileTypes: "파일 타입",
code: "코드",
config: "설정",
docs: "문서",
infra: "인프라",
data: "데이터",
domain: "도메인",
languages: "프로그래밍 언어",
frameworks: "프레임워크",
nodeTypeDistribution: "노드 타입 분포",
complexityDistribution: "복잡도 분포",
simple: "단순",
moderate: "중간",
complex: "복잡",
mostConnectedNodes: "가장 많이 연결된 노드",
avgConnectionsPerNode: "노드 평균 연결 수",
},
nodeInfo: {
definedInThisFile: "이 파일에 정義",
languageConcepts: "언어 개념",
category: "카테고리",
wikilinks: "Wikilinks",
backlinks: "Backlinks",
entities: "엔티티",
businessRules: "비즈니스 규칙",
crossDomain: "크로스 도메인",
flows: "플로우",
entryPoint: "진입점",
steps: "단계",
implementation: "구현",
},
fileExplorer: {
analyzedFiles: "분석된 파일",
filesFromGraph: "현재 지식 그래프의 파일",
noFilePathsFound: "파일 경로를 찾을 수 없습니다.",
},
filterPanel: {
nodeTypes: "노드 타입",
complexity: "복잡도",
layers: "레이어",
edgeCategories: "엣지 카테고리",
},
personaSelector: {
overview: "개요",
overviewDesc: "고수준 아키텍처 뷰",
learn: "학습",
learnDesc: "가이드 학습 포함 완전 대시보드",
deepDive: "심층",
deepDiveDesc: "코드 중심 채팅",
},
edgeLabels: {
imports: { forward: "임포트", backward: "임포트됨" },
exports: { forward: "내보내기", backward: "내보내기됨" },
contains: { forward: "포함", backward: "포함됨" },
inherits: { forward: "상속", backward: "상속됨" },
implements: { forward: "구현", backward: "구현됨" },
calls: { forward: "호출", backward: "호출됨" },
subscribes: { forward: "구독", backward: "구독됨" },
publishes: { forward: "게시", backward: "소비됨" },
middleware: { forward: "미들웨어", backward: "미들웨어 사용" },
reads_from: { forward: "읽기", backward: "읽기됨" },
writes_to: { forward: "쓰기", backward: "쓰기됨" },
transforms: { forward: "변환", backward: "변환됨" },
validates: { forward: "검증", backward: "검증됨" },
depends_on: { forward: "종속", backward: "종속됨" },
tested_by: { forward: "테스트됨", backward: "테스트" },
configures: { forward: "설정", backward: "설정됨" },
related: { forward: "관련", backward: "관련" },
similar_to: { forward: "유사", backward: "유사" },
deploys: { forward: "배포", backward: "배포됨" },
serves: { forward: "서비스", backward: "서비스됨" },
migrates: { forward: "마이그레이션", backward: "마이그레이션됨" },
documents: { forward: "문서화", backward: "문서화됨" },
provisions: { forward: "제공", backward: "제공됨" },
routes: { forward: "라우팅", backward: "라우팅됨" },
defines_schema: { forward: "스키마 정의", backward: "스키마 정義됨" },
triggers: { forward: "트리거", backward: "트리거됨" },
contains_flow: { forward: "플로우 포함", backward: "플로우 내" },
flow_step: { forward: "플로우 단계", backward: "단계의" },
cross_domain: { forward: "크로스 도메인", backward: "크로스 도메인에서" },
cites: { forward: "인용", backward: "인용됨" },
contradicts: { forward: "반박", backward: "반박됨" },
builds_on: { forward: "기반", backward: "기반됨" },
exemplifies: { forward: "예시", backward: "예시됨" },
categorized_under: { forward: "카테고리화", backward: "카테고리화함" },
authored_by: { forward: "작성자", backward: "작성" },
},
};
export default ko;
@@ -0,0 +1,115 @@
export const zhTW = {
common: {
loading: "載入專案...",
noGraphLoaded: "未載入知識圖谱",
selectNode: "選擇節點查看詳情",
back: "返回",
focus: "聚焦",
unfocus: "取消聚焦",
openCode: "開啟程式碼",
file: "檔案",
tags: "標籤",
connections: "連結",
filter: "篩選",
resetAll: "重置全部",
analyzed: "分析時間",
startGuidedTour: "開始導覽",
truncated: "(已截斷)",
preview: "預覽",
doubleClickToOpen: "雙擊開啟",
},
projectOverview: {
nodes: "節點",
edges: "邊",
layers: "層級",
types: "類型",
fileTypes: "檔案類型",
code: "程式碼",
config: "配置",
docs: "文件",
infra: "基礎設施",
data: "資料",
domain: "領域",
languages: "程式語言",
frameworks: "框架",
nodeTypeDistribution: "節點類型分布",
complexityDistribution: "複雜度分布",
simple: "簡單",
moderate: "中等",
complex: "複雜",
mostConnectedNodes: "連結最多的節點",
avgConnectionsPerNode: "節點平均連結數",
},
nodeInfo: {
definedInThisFile: "在此檔案中定義",
languageConcepts: "語言概念",
category: "分類",
wikilinks: "維基連結",
backlinks: "反向連結",
entities: "實體",
businessRules: "業務規則",
crossDomain: "跨領域",
flows: "流程",
entryPoint: "入口點",
steps: "步驟",
implementation: "實作",
},
fileExplorer: {
analyzedFiles: "已分析檔案",
filesFromGraph: "來自目前知識圖谱的檔案",
noFilePathsFound: "未找到檔案路徑。",
},
filterPanel: {
nodeTypes: "節點類型",
complexity: "複雜度",
layers: "層級",
edgeCategories: "邊類別",
},
personaSelector: {
overview: "概覽",
overviewDesc: "高層次架構視圖",
learn: "學習",
learnDesc: "完整儀表板與導覽學習",
deepDive: "深入",
deepDiveDesc: "程式碼聚焦與對話",
},
edgeLabels: {
imports: { forward: "導入", backward: "被導入" },
exports: { forward: "導出到", backward: "被導出" },
contains: { forward: "包含", backward: "被包含" },
inherits: { forward: "繼承自", backward: "被繼承" },
implements: { forward: "實作", backward: "被實作" },
calls: { forward: "呼叫", backward: "被呼叫" },
subscribes: { forward: "訂閱", backward: "被訂閱" },
publishes: { forward: "發布到", backward: "被消費" },
middleware: { forward: "中介軟體", backward: "使用中介軟體" },
reads_from: { forward: "讀取", backward: "被讀取" },
writes_to: { forward: "寫入", backward: "被寫入" },
transforms: { forward: "轉換", backward: "被轉換" },
validates: { forward: "驗證", backward: "被驗證" },
depends_on: { forward: "依賴", backward: "被依賴" },
tested_by: { forward: "被測試", backward: "測試" },
configures: { forward: "配置", backward: "被配置" },
related: { forward: "相關", backward: "相關" },
similar_to: { forward: "相似", backward: "相似" },
deploys: { forward: "部署", backward: "被部署" },
serves: { forward: "服務", backward: "被服務" },
migrates: { forward: "遷移", backward: "被遷移" },
documents: { forward: "文件化", backward: "被文件化" },
provisions: { forward: "提供", backward: "被提供" },
routes: { forward: "路由到", backward: "被路由" },
defines_schema: { forward: "定義架構", backward: "架構被定義" },
triggers: { forward: "觸發", backward: "被觸發" },
contains_flow: { forward: "包含流程", backward: "流程所在" },
flow_step: { forward: "流程步驟", backward: "步驟所属" },
cross_domain: { forward: "跨領域到", backward: "跨領域来自" },
cites: { forward: "引用", backward: "被引用" },
contradicts: { forward: "反駁", backward: "被反駁" },
builds_on: { forward: "基於", backward: "作為基礎" },
exemplifies: { forward: "例證", backward: "被例證" },
categorized_under: { forward: "归类於", backward: "归类" },
authored_by: { forward: "作者", backward: "著作" },
},
};
export default zhTW;
@@ -0,0 +1,115 @@
export const zh = {
common: {
loading: "加载项目...",
noGraphLoaded: "未加载知识图谱",
selectNode: "选择节点查看详情",
back: "返回",
focus: "聚焦",
unfocus: "取消聚焦",
openCode: "打开代码",
file: "文件",
tags: "标签",
connections: "连接",
filter: "筛选",
resetAll: "重置全部",
analyzed: "分析时间",
startGuidedTour: "开始导览",
truncated: "(已截断)",
preview: "预览",
doubleClickToOpen: "双击打开",
},
projectOverview: {
nodes: "节点",
edges: "边",
layers: "层级",
types: "类型",
fileTypes: "文件类型",
code: "代码",
config: "配置",
docs: "文档",
infra: "基础设施",
data: "数据",
domain: "领域",
languages: "编程语言",
frameworks: "框架",
nodeTypeDistribution: "节点类型分布",
complexityDistribution: "复杂度分布",
simple: "简单",
moderate: "中等",
complex: "复杂",
mostConnectedNodes: "连接最多的节点",
avgConnectionsPerNode: "节点平均连接数",
},
nodeInfo: {
definedInThisFile: "在此文件中定义",
languageConcepts: "语言概念",
category: "分类",
wikilinks: "维基链接",
backlinks: "反向链接",
entities: "实体",
businessRules: "业务规则",
crossDomain: "跨领域",
flows: "流程",
entryPoint: "入口点",
steps: "步骤",
implementation: "实现",
},
fileExplorer: {
analyzedFiles: "已分析文件",
filesFromGraph: "来自当前知识图谱的文件",
noFilePathsFound: "未找到文件路径。",
},
filterPanel: {
nodeTypes: "节点类型",
complexity: "复杂度",
layers: "层级",
edgeCategories: "边类别",
},
personaSelector: {
overview: "概览",
overviewDesc: "高层次架构视图",
learn: "学习",
learnDesc: "完整仪表盘与导览学习",
deepDive: "深入",
deepDiveDesc: "代码聚焦与对话",
},
edgeLabels: {
imports: { forward: "导入", backward: "被导入" },
exports: { forward: "导出到", backward: "被导出" },
contains: { forward: "包含", backward: "被包含" },
inherits: { forward: "继承自", backward: "被继承" },
implements: { forward: "实现", backward: "被实现" },
calls: { forward: "调用", backward: "被调用" },
subscribes: { forward: "订阅", backward: "被订阅" },
publishes: { forward: "发布到", backward: "被消费" },
middleware: { forward: "中间件", backward: "使用中间件" },
reads_from: { forward: "读取", backward: "被读取" },
writes_to: { forward: "写入", backward: "被写入" },
transforms: { forward: "转换", backward: "被转换" },
validates: { forward: "验证", backward: "被验证" },
depends_on: { forward: "依赖", backward: "被依赖" },
tested_by: { forward: "被测试", backward: "测试" },
configures: { forward: "配置", backward: "被配置" },
related: { forward: "相关", backward: "相关" },
similar_to: { forward: "相似", backward: "相似" },
deploys: { forward: "部署", backward: "被部署" },
serves: { forward: "服务", backward: "被服务" },
migrates: { forward: "迁移", backward: "被迁移" },
documents: { forward: "文档化", backward: "被文档化" },
provisions: { forward: "提供", backward: "被提供" },
routes: { forward: "路由到", backward: "被路由" },
defines_schema: { forward: "定义架构", backward: "架构被定义" },
triggers: { forward: "触发", backward: "被触发" },
contains_flow: { forward: "包含流程", backward: "流程所在" },
flow_step: { forward: "流程步骤", backward: "步骤所属" },
cross_domain: { forward: "跨领域到", backward: "跨领域来自" },
cites: { forward: "引用", backward: "被引用" },
contradicts: { forward: "反驳", backward: "被反驳" },
builds_on: { forward: "基于", backward: "作为基础" },
exemplifies: { forward: "例证", backward: "被例证" },
categorized_under: { forward: "归类于", backward: "归类" },
authored_by: { forward: "作者", backward: "著作" },
},
};
export default zh;
@@ -252,6 +252,7 @@ export default defineConfig({
pathname === "/domain-graph.json" ||
pathname === "/diff-overlay.json" ||
pathname === "/meta.json" ||
pathname === "/config.json" ||
pathname === "/file-content.json";
if (!isProtectedEndpoint) {
@@ -272,6 +273,24 @@ export default defineConfig({
return;
}
if (pathname === "/config.json") {
const configCandidates = graphFileCandidates("config.json");
for (const candidate of configCandidates) {
if (fs.existsSync(candidate)) {
try {
const raw = JSON.parse(fs.readFileSync(candidate, "utf-8"));
sendJson(res, 200, raw);
return;
} catch {
sendJson(res, 500, { error: "Failed to read config file" });
return;
}
}
}
sendJson(res, 200, { autoUpdate: false, outputLanguage: "en" });
return;
}
const fileName =
pathname === "/diff-overlay.json"
? "diff-overlay.json"
@@ -1,2 +1,15 @@
packages:
- "packages/*"
allowBuilds:
esbuild: set this to true or false
tree-sitter-c: set this to true or false
tree-sitter-c-sharp: set this to true or false
tree-sitter-cpp: set this to true or false
tree-sitter-go: set this to true or false
tree-sitter-java: set this to true or false
tree-sitter-javascript: set this to true or false
tree-sitter-php: set this to true or false
tree-sitter-python: set this to true or false
tree-sitter-ruby: set this to true or false
tree-sitter-rust: set this to true or false
tree-sitter-typescript: set this to true or false