Merge pull request #142 from zhushen12580/feature/language-parameter

feat: Add --language parameter for localized content generation
This commit is contained in:
Yuxiang Lin
2026-05-12 11:07:14 +08:00
committed by GitHub
43 changed files with 2238 additions and 478 deletions
+17
View File
@@ -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"
]
}
}
@@ -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
@@ -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)
@@ -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
@@ -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
-17
View File
@@ -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"
]
}
}
@@ -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, useI18n } from "./contexts/I18nContext.tsx";
// Lazy-load heavy / optional components so they ship in separate chunks.
const CodeViewer = lazy(() => import("./components/CodeViewer"));
@@ -44,6 +45,7 @@ function dataUrl(fileName: string, token: string | null): string {
"domain-graph.json": import.meta.env.VITE_DOMAIN_GRAPH_URL,
"meta.json": import.meta.env.VITE_META_URL,
"diff-overlay.json": import.meta.env.VITE_DIFF_OVERLAY_URL,
"config.json": import.meta.env.VITE_CONFIG_URL,
};
const url = envMap[fileName];
if (url) return url;
@@ -95,42 +97,13 @@ function App() {
}
function Dashboard({ accessToken }: { accessToken: string }) {
const graph = useDashboardStore((s) => s.graph);
const setGraph = useDashboardStore((s) => s.setGraph);
const selectedNodeId = useDashboardStore((s) => s.selectedNodeId);
const tourActive = useDashboardStore((s) => s.tourActive);
const persona = useDashboardStore((s) => s.persona);
const codeViewerOpen = useDashboardStore((s) => s.codeViewerOpen);
const codeViewerExpanded = useDashboardStore((s) => s.codeViewerExpanded);
const expandCodeViewer = useDashboardStore((s) => s.expandCodeViewer);
const collapseCodeViewer = useDashboardStore((s) => s.collapseCodeViewer);
const setDomainGraph = useDashboardStore((s) => s.setDomainGraph);
const setDiffOverlay = useDashboardStore((s) => s.setDiffOverlay);
const pathFinderOpen = useDashboardStore((s) => s.pathFinderOpen);
const togglePathFinder = useDashboardStore((s) => s.togglePathFinder);
const nodeTypeFilters = useDashboardStore((s) => s.nodeTypeFilters);
const toggleNodeTypeFilter = useDashboardStore((s) => s.toggleNodeTypeFilter);
const detailLevel = useDashboardStore((s) => s.detailLevel);
const setDetailLevel = useDashboardStore((s) => s.setDetailLevel);
const showFunctionsInClassView = useDashboardStore((s) => s.showFunctionsInClassView);
const toggleShowFunctionsInClassView = useDashboardStore((s) => s.toggleShowFunctionsInClassView);
const [loadError, setLoadError] = useState<string | null>(null);
const [graphIssues, setGraphIssues] = useState<GraphIssue[]>([]);
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false);
const [metaTheme, setMetaTheme] = useState<ThemeConfig | null>(null);
const [sidebarTab, setSidebarTab] = useState<SidebarTab>("info");
const viewMode = useDashboardStore((s) => s.viewMode);
const setViewMode = useDashboardStore((s) => s.setViewMode);
const isKnowledgeGraph = useDashboardStore((s) => s.isKnowledgeGraph);
const domainGraph = useDashboardStore((s) => s.domainGraph);
const setDomainGraph = useDashboardStore((s) => s.setDomainGraph);
const layoutIssues = useDashboardStore((s) => s.layoutIssues);
const isMobile = useIsMobile();
// Schema issues + ELK layout issues share the WarningBanner — graph-load
// problems and dashboard rendering problems are equally surfaced.
const allIssues = useMemo(
() => [...graphIssues, ...layoutIssues],
[graphIssues, layoutIssues],
);
const [outputLanguage, setOutputLanguage] = useState<string | undefined>();
useEffect(() => {
fetch(dataUrl("meta.json", accessToken))
@@ -139,130 +112,14 @@ function Dashboard({ accessToken }: { accessToken: string }) {
if (meta?.theme) setMetaTheme(meta.theme);
})
.catch(() => {});
fetch(dataUrl("config.json", accessToken))
.then((r) => (r.ok ? r.json() : null))
.then((config) => {
if (config?.outputLanguage) setOutputLanguage(config.outputLanguage);
})
.catch(() => {});
}, []);
useEffect(() => {
if (selectedNodeId) setSidebarTab("info");
}, [selectedNodeId]);
// Define keyboard shortcuts
const shortcuts = useMemo<KeyboardShortcut[]>(
() => [
// Help
{
key: "?",
shiftKey: true,
description: "Show keyboard shortcuts",
action: () => setShowKeyboardHelp((prev) => !prev),
category: "General",
},
// Navigation
{
key: "Escape",
description: "Close panels and modals / go back to overview",
action: () => {
// Read from store at invocation time to avoid stale closures
const state = useDashboardStore.getState();
if (state.pathFinderOpen) {
state.togglePathFinder();
} else if (state.filterPanelOpen) {
state.toggleFilterPanel();
} else if (state.exportMenuOpen) {
state.toggleExportMenu();
} else if (state.codeViewerExpanded) {
state.collapseCodeViewer();
} else if (state.codeViewerOpen) {
state.closeCodeViewer();
} else if (state.selectedNodeId) {
state.selectNode(null);
} else if (state.navigationLevel === "layer-detail") {
state.navigateToOverview();
} else if (state.tourActive) {
state.stopTour();
} else {
setShowKeyboardHelp(false);
}
},
category: "Navigation",
},
{
key: "/",
description: "Focus search bar",
action: () => {
const searchInput = document.querySelector<HTMLInputElement>(
'input[placeholder*="Search"]'
);
searchInput?.focus();
},
category: "Navigation",
},
// Tour controls
{
key: "ArrowRight",
description: "Next tour step",
action: () => {
const state = useDashboardStore.getState();
if (state.tourActive) {
state.nextTourStep();
}
},
category: "Tour",
},
{
key: "ArrowLeft",
description: "Previous tour step",
action: () => {
const state = useDashboardStore.getState();
if (state.tourActive) {
state.prevTourStep();
}
},
category: "Tour",
},
// View toggles
{
key: "d",
description: "Toggle diff mode",
action: () => {
const state = useDashboardStore.getState();
state.toggleDiffMode();
},
category: "View",
},
{
key: "f",
description: "Toggle filter panel",
action: () => {
const state = useDashboardStore.getState();
state.toggleFilterPanel();
},
category: "View",
},
{
key: "e",
description: "Toggle export menu",
action: () => {
const state = useDashboardStore.getState();
state.toggleExportMenu();
},
category: "View",
},
{
key: "p",
description: "Open path finder",
action: () => {
const state = useDashboardStore.getState();
state.togglePathFinder();
},
category: "View",
},
],
[]
);
// Register keyboard shortcuts
useKeyboardShortcuts(shortcuts);
useEffect(() => {
fetch(dataUrl("knowledge-graph.json", accessToken))
.then((res) => res.json())
@@ -271,9 +128,8 @@ function Dashboard({ accessToken }: { accessToken: string }) {
if (result.success && result.data) {
setGraph(result.data);
setGraphIssues(result.issues);
// Auto-detect knowledge graph kind
if ((data as Record<string, unknown>).kind === "knowledge") {
setViewMode("knowledge");
useDashboardStore.getState().setViewMode("knowledge");
useDashboardStore.getState().setIsKnowledgeGraph(true);
}
for (const issue of result.issues) {
@@ -318,9 +174,7 @@ function Dashboard({ accessToken }: { accessToken: string }) {
}
}
})
.catch(() => {
// Silently ignore - diff overlay is optional
});
.catch(() => {});
}, [setDiffOverlay]);
useEffect(() => {
@@ -338,11 +192,183 @@ function Dashboard({ accessToken }: { accessToken: string }) {
console.warn(`[domain-graph] validation failed: ${result.fatal}`);
}
})
.catch(() => {
// Silently ignore — domain graph is optional
});
.catch(() => {});
}, [setDomainGraph]);
return (
<I18nProvider language={outputLanguage ?? "en"}>
<ThemeProvider metaTheme={metaTheme}>
<DashboardContent
accessToken={accessToken}
loadError={loadError}
graphIssues={graphIssues}
/>
</ThemeProvider>
</I18nProvider>
);
}
function DashboardContent({
accessToken,
loadError,
graphIssues,
}: {
accessToken: string;
loadError: string | null;
graphIssues: GraphIssue[];
}) {
const graph = useDashboardStore((s) => s.graph);
const selectedNodeId = useDashboardStore((s) => s.selectedNodeId);
const tourActive = useDashboardStore((s) => s.tourActive);
const persona = useDashboardStore((s) => s.persona);
const codeViewerOpen = useDashboardStore((s) => s.codeViewerOpen);
const codeViewerExpanded = useDashboardStore((s) => s.codeViewerExpanded);
const expandCodeViewer = useDashboardStore((s) => s.expandCodeViewer);
const collapseCodeViewer = useDashboardStore((s) => s.collapseCodeViewer);
const pathFinderOpen = useDashboardStore((s) => s.pathFinderOpen);
const togglePathFinder = useDashboardStore((s) => s.togglePathFinder);
const nodeTypeFilters = useDashboardStore((s) => s.nodeTypeFilters);
const toggleNodeTypeFilter = useDashboardStore((s) => s.toggleNodeTypeFilter);
const detailLevel = useDashboardStore((s) => s.detailLevel);
const setDetailLevel = useDashboardStore((s) => s.setDetailLevel);
const showFunctionsInClassView = useDashboardStore((s) => s.showFunctionsInClassView);
const toggleShowFunctionsInClassView = useDashboardStore((s) => s.toggleShowFunctionsInClassView);
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false);
const [sidebarTab, setSidebarTab] = useState<SidebarTab>("info");
const viewMode = useDashboardStore((s) => s.viewMode);
const setViewMode = useDashboardStore((s) => s.setViewMode);
const isKnowledgeGraph = useDashboardStore((s) => s.isKnowledgeGraph);
const domainGraph = useDashboardStore((s) => s.domainGraph);
const layoutIssues = useDashboardStore((s) => s.layoutIssues);
const isMobile = useIsMobile();
const { t } = useI18n();
const allIssues = useMemo(
() => [...graphIssues, ...layoutIssues],
[graphIssues, layoutIssues],
);
useEffect(() => {
if (selectedNodeId) setSidebarTab("info");
}, [selectedNodeId]);
// Define keyboard shortcuts
const shortcuts = useMemo<KeyboardShortcut[]>(
() => [
// Help
{
key: "?",
shiftKey: true,
description: t.keyboardShortcuts.showHelp,
action: () => setShowKeyboardHelp((prev) => !prev),
category: "General",
},
// Navigation
{
key: "Escape",
description: t.keyboardShortcuts.escapeDesc,
action: () => {
// Read from store at invocation time to avoid stale closures
const state = useDashboardStore.getState();
if (state.pathFinderOpen) {
state.togglePathFinder();
} else if (state.filterPanelOpen) {
state.toggleFilterPanel();
} else if (state.exportMenuOpen) {
state.toggleExportMenu();
} else if (state.codeViewerExpanded) {
state.collapseCodeViewer();
} else if (state.codeViewerOpen) {
state.closeCodeViewer();
} else if (state.selectedNodeId) {
state.selectNode(null);
} else if (state.navigationLevel === "layer-detail") {
state.navigateToOverview();
} else if (state.tourActive) {
state.stopTour();
} else {
setShowKeyboardHelp(false);
}
},
category: "Navigation",
},
{
key: "/",
description: t.keyboardShortcuts.focusSearch,
action: () => {
const searchInput = document.querySelector<HTMLInputElement>(
'[data-testid="search-input"]'
);
searchInput?.focus();
},
category: "Navigation",
},
// Tour controls
{
key: "ArrowRight",
description: t.keyboardShortcuts.nextStep,
action: () => {
const state = useDashboardStore.getState();
if (state.tourActive) {
state.nextTourStep();
}
},
category: "Tour",
},
{
key: "ArrowLeft",
description: t.keyboardShortcuts.prevStep,
action: () => {
const state = useDashboardStore.getState();
if (state.tourActive) {
state.prevTourStep();
}
},
category: "Tour",
},
// View toggles
{
key: "d",
description: t.keyboardShortcuts.toggleDiff,
action: () => {
const state = useDashboardStore.getState();
state.toggleDiffMode();
},
category: "View",
},
{
key: "f",
description: t.keyboardShortcuts.toggleFilter,
action: () => {
const state = useDashboardStore.getState();
state.toggleFilterPanel();
},
category: "View",
},
{
key: "e",
description: t.keyboardShortcuts.toggleExport,
action: () => {
const state = useDashboardStore.getState();
state.toggleExportMenu();
},
category: "View",
},
{
key: "p",
description: t.keyboardShortcuts.openPathFinder,
action: () => {
const state = useDashboardStore.getState();
state.togglePathFinder();
},
category: "View",
},
],
[t]
);
// Register keyboard shortcuts
useKeyboardShortcuts(shortcuts);
// Determine sidebar content
// NodeInfo always takes priority when a node is selected.
// Learn mode adds LearnPanel below it; otherwise ProjectOverview shows when idle.
@@ -373,7 +399,7 @@ function Dashboard({ accessToken }: { accessToken: string }) {
: "text-text-muted hover:text-text-primary hover:bg-elevated"
}`}
>
{tab === "info" ? "Info" : "Files"}
{tab === "info" ? t.sidebar.info : t.sidebar.files}
</button>
))}
</div>
@@ -385,28 +411,25 @@ function Dashboard({ accessToken }: { accessToken: string }) {
if (isMobile) {
return (
<ThemeProvider metaTheme={metaTheme}>
<MobileLayout
accessToken={accessToken}
showKeyboardHelp={showKeyboardHelp}
setShowKeyboardHelp={setShowKeyboardHelp}
loadError={loadError}
allIssues={allIssues}
shortcuts={shortcuts}
/>
</ThemeProvider>
<MobileLayout
accessToken={accessToken}
showKeyboardHelp={showKeyboardHelp}
setShowKeyboardHelp={setShowKeyboardHelp}
loadError={loadError}
allIssues={allIssues}
shortcuts={shortcuts}
/>
);
}
return (
<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">
{/* Left — fixed */}
<div className="flex items-center gap-3 sm:gap-5 shrink-0 min-w-0">
<h1 className="font-heading text-base sm:text-lg text-text-primary tracking-wide truncate max-w-[160px] sm:max-w-[220px] lg:max-w-none">
{graph?.project.name ?? "Understand Anything"}
{graph?.project.name ?? t.common.appName}
</h1>
<div className="w-px h-5 bg-border-subtle hidden sm:block" />
<PersonaSelector />
@@ -417,26 +440,26 @@ function Dashboard({ accessToken }: { accessToken: string }) {
<button
type="button"
onClick={() => setViewMode("domain")}
title="Switch to domain view"
title={t.drawer.domain}
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
viewMode === "domain"
? "bg-accent/20 text-accent"
: "text-text-muted hover:text-text-secondary"
}`}
>
Domain
{t.drawer.domain}
</button>
<button
type="button"
onClick={() => setViewMode("structural")}
title="Switch to structural view"
title={t.drawer.structural}
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
viewMode === "structural"
? "bg-accent/20 text-accent"
: "text-text-muted hover:text-text-secondary"
}`}
>
Structural
{t.drawer.structural}
</button>
</div>
</>
@@ -455,55 +478,55 @@ function Dashboard({ accessToken }: { accessToken: string }) {
<button
type="button"
onClick={() => setDetailLevel("file")}
title="Files only — architecture-level dependencies (fast)"
title={t.detailLevel.filesTitle}
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
detailLevel === "file"
? "bg-accent/20 text-accent"
: "text-text-muted hover:text-text-secondary"
}`}
>
Files
{t.detailLevel.files}
</button>
<button
type="button"
onClick={() => setDetailLevel("class")}
title="Files + Classes — code structure with inheritance"
title={t.detailLevel.classesTitle}
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
detailLevel === "class"
? "bg-accent/20 text-accent"
: "text-text-muted hover:text-text-secondary"
}`}
>
+Classes
{t.detailLevel.classes}
</button>
</div>
{detailLevel === "class" && (
<button
type="button"
onClick={toggleShowFunctionsInClassView}
title="Toggle function nodes (may slow down rendering)"
title={t.detailLevel.fnTitle}
className={`text-[10px] font-semibold uppercase tracking-wider px-2 py-1 rounded border transition-colors ${
showFunctionsInClassView
? "border-amber-500/50 bg-amber-500/10 text-amber-400"
: "border-border-medium bg-elevated text-text-muted hover:text-text-secondary"
}`}
>
fn
{t.detailLevel.fn}
</button>
)}
</>
)}
<div className="flex items-center gap-1">
{(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) => (
<button
key={cat.key}
@@ -537,7 +560,7 @@ function Dashboard({ accessToken }: { accessToken: string }) {
<button
onClick={togglePathFinder}
className="flex items-center gap-1.5 px-2 sm:px-3 py-1.5 rounded-lg text-sm bg-elevated text-text-secondary hover:text-text-primary transition-colors"
title="Find path between nodes (P)"
title={t.pathFinder.title}
>
<svg
className="w-4 h-4"
@@ -552,13 +575,13 @@ function Dashboard({ accessToken }: { accessToken: string }) {
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
/>
</svg>
<span className="hidden md:inline">Path</span>
<span className="hidden md:inline">{t.common.path}</span>
</button>
<ThemePicker />
<button
onClick={() => setShowKeyboardHelp(true)}
className="text-text-muted hover:text-accent transition-colors"
title="Keyboard shortcuts (Shift + ?)"
title={t.keyboardShortcuts.showHelp}
>
<svg
className="w-5 h-5"
@@ -604,7 +627,7 @@ function Dashboard({ accessToken }: { accessToken: string }) {
<GraphView />
)}
<div className="absolute top-3 right-3 text-sm text-text-muted/60 pointer-events-none select-none">
Press <kbd className="kbd">?</kbd> for keyboard shortcuts
{t.common.pressKeyboard}
</div>
</div>
@@ -661,7 +684,6 @@ function Dashboard({ accessToken }: { accessToken: string }) {
</Suspense>
)}
</div>
</ThemeProvider>
);
}
@@ -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() {
<div className="absolute top-4 left-4 z-10 flex items-center gap-2">
{navigationLevel === "overview" && (
<div className="px-4 py-2 rounded-full bg-elevated border border-border-subtle text-xs font-semibold tracking-wider uppercase text-text-secondary shadow-lg">
Project Overview
{t.breadcrumb.projectOverview}
</div>
)}
@@ -22,14 +24,14 @@ export default function Breadcrumb() {
onClick={navigateToOverview}
className="text-gold hover:text-gold-bright transition-colors"
>
Project
{t.breadcrumb.project}
</button>
<span className="text-text-muted"></span>
<span className="text-text-primary">
{activeLayer?.name ?? "Layer"}
{activeLayer?.name ?? t.layer.defaultName}
</span>
<span className="text-text-muted ml-1 text-[10px] normal-case tracking-normal">
(Esc to go back)
({t.breadcrumb.escBack})
</span>
</div>
)}
@@ -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 (
<div className="h-full w-full flex items-center justify-center bg-surface">
<p className="text-text-muted text-sm">No file selected</p>
<p className="text-text-muted text-sm">{t.codeViewer.noFile}</p>
</div>
);
}
@@ -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}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 9V4h5M20 15v5h-5M4 4l6 6M20 20l-6-6" />
@@ -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}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
@@ -194,13 +196,13 @@ export default function CodeViewer({
<div className="flex-1 min-h-0 overflow-auto bg-root">
{state.status === "loading" && (
<div className="p-5 text-sm text-text-muted">Loading source...</div>
<div className="p-5 text-sm text-text-muted">{t.codeViewer.loading}</div>
)}
{state.status === "error" && (
<div className="p-5">
<div className="rounded-lg border border-border-subtle bg-elevated p-4">
<div className="text-sm font-medium text-text-primary mb-2">Source unavailable</div>
<div className="text-sm font-medium text-text-primary mb-2">{t.codeViewer.sourceUnavailable}</div>
<p className="text-sm text-text-secondary leading-relaxed">{state.error}</p>
</div>
</div>
@@ -209,7 +211,7 @@ export default function CodeViewer({
{source && (
<>
<div className="px-4 py-2 border-b border-border-subtle bg-surface text-[11px] text-text-muted flex items-center justify-between">
<span>{source.lineCount} lines</span>
<span>{source.lineCount} {t.codeViewer.linesLabel}</span>
<span>{formatBytes(source.sizeBytes)}</span>
</div>
<Highlight code={source.content} language={language} theme={themes.vsDark}>
@@ -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<NodeType, string> = {
@@ -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({
<span
className="inline-block w-1.5 h-1.5 rounded-full bg-node-function shadow-[0_0_4px_rgba(90,158,111,0.6)]"
role="img"
aria-label="Tested"
title="Has tests"
aria-label={t.customNode.tested}
title={t.customNode.hasTests}
/>
)}
</div>
@@ -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)" }}
/>
<span className="text-text-secondary text-[11px]">
Changed
{t.diffToggle.changed}
<span className="text-text-muted ml-0.5">
({changedNodeIds.size})
</span>
@@ -51,7 +53,7 @@ export default function DiffToggle() {
style={{ backgroundColor: "var(--color-diff-affected)" }}
/>
<span className="text-text-secondary text-[11px]">
Affected
{t.diffToggle.affected}
<span className="text-text-muted ml-0.5">
({affectedNodeIds.size})
</span>
@@ -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}
</button>
</div>
)}
@@ -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<HTMLDivElement>(null);
@@ -218,7 +220,7 @@ export default function ExportMenu() {
<button
onClick={toggleExportMenu}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm bg-elevated text-text-secondary hover:text-text-primary transition-colors"
title="Export graph (E)"
title={t.export.title}
>
<svg
className="w-4 h-4"
@@ -233,7 +235,7 @@ export default function ExportMenu() {
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
Export
{t.export.label}
</button>
{exportMenuOpen && (
@@ -247,7 +249,7 @@ export default function ExportMenu() {
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span>Export as PNG</span>
<span>{t.export.asPNG}</span>
</button>
<button
onClick={exportSVG}
@@ -257,7 +259,7 @@ export default function ExportMenu() {
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
</svg>
<span>Export as SVG</span>
<span>{t.export.asSVG}</span>
</button>
<button
onClick={exportJSON}
@@ -267,7 +269,7 @@ export default function ExportMenu() {
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
<span>Export as JSON</span>
<span>{t.export.asJSON}</span>
</button>
</div>
</div>
@@ -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 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<string, KeyboardShortcut[]>);
// Translate category names
const categoryTranslations: Record<string, string> = {
"General": t.keyboardShortcuts.general,
"Navigation": t.keyboardShortcuts.navigation,
"Tour": t.keyboardShortcuts.tour,
"View": t.keyboardShortcuts.view,
};
return (
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50"
@@ -32,10 +43,10 @@ export default function KeyboardShortcutsHelp({
<div className="sticky top-0 glass-heavy border-b border-border-subtle px-6 py-4 flex items-center justify-between">
<div>
<h2 className="text-xl font-heading text-text-primary">
Keyboard Shortcuts
{t.keyboardShortcuts.title}
</h2>
<p className="text-xs text-text-muted mt-1">
Press <kbd className="kbd">?</kbd> anytime to toggle this help
{t.keyboardShortcuts.toggleHint}
</p>
</div>
<button
@@ -63,7 +74,7 @@ export default function KeyboardShortcutsHelp({
{Object.entries(groupedShortcuts).map(([category, categoryShortcuts]) => (
<div key={category}>
<h3 className="text-sm font-semibold text-accent uppercase tracking-wider mb-3">
{category}
{categoryTranslations[category] ?? category}
</h3>
<div className="space-y-2">
{categoryShortcuts.map((shortcut, index) => (
@@ -85,7 +96,7 @@ export default function KeyboardShortcutsHelp({
{/* Footer */}
<div className="sticky bottom-0 glass-heavy border-t border-border-subtle px-6 py-3 text-center">
<p className="text-xs text-text-muted">
Press <kbd className="kbd">ESC</kbd> to close
{t.keyboardShortcuts.closeHint}
</p>
</div>
</div>
@@ -1,4 +1,5 @@
import { useDashboardStore } from "../store";
import { useI18n } from "../contexts/I18nContext";
// Shared layer color palette — used by LayerLegend, LayerClusterNode, PortalNode, and GraphView
export const LAYER_PALETTE = [
@@ -19,6 +20,7 @@ export default function LayerLegend() {
const graph = useDashboardStore((s) => s.graph);
const navigationLevel = useDashboardStore((s) => s.navigationLevel);
const activeLayerId = useDashboardStore((s) => s.activeLayerId);
const { t } = useI18n();
const layers = graph?.layers ?? [];
const hasLayers = layers.length > 0;
@@ -31,8 +33,8 @@ export default function LayerLegend() {
<div className="flex items-center gap-2">
<span className="text-[11px] font-medium text-text-secondary whitespace-nowrap">
{navigationLevel === "overview"
? `${layers.length} layers`
: activeLayer?.name ?? "Layer"}
? `${layers.length} ${t.layer.label}`
: activeLayer?.name ?? t.layer.defaultName}
</span>
<div className="flex items-center gap-3">
@@ -1,6 +1,7 @@
import { useMemo } from "react";
import ReactMarkdown from "react-markdown";
import { useDashboardStore } from "../store";
import { useI18n } from "../contexts/I18nContext";
export default function LearnPanel() {
const graph = useDashboardStore((s) => s.graph);
@@ -12,6 +13,7 @@ export default function LearnPanel() {
const nextTourStep = useDashboardStore((s) => s.nextTourStep);
const prevTourStep = useDashboardStore((s) => s.prevTourStep);
const selectNode = useDashboardStore((s) => s.selectNode);
const { t } = useI18n();
const tourSteps = useMemo(
() => graph?.tour ? [...graph.tour].sort((a, b) => a.order - b.order) : [],
@@ -25,9 +27,9 @@ export default function LearnPanel() {
<div className="h-full w-full flex items-center justify-center">
<div className="text-center px-4">
<div className="text-2xl mb-2 text-text-muted">&#x1f9ed;</div>
<p className="text-text-muted text-sm">No tour available</p>
<p className="text-text-muted text-sm">{t.learnPanel.noTour}</p>
<p className="text-text-muted text-xs mt-1">
Generate a tour from your knowledge graph to get a guided walkthrough
{t.learnPanel.noTourHint}
</p>
</div>
</div>
@@ -39,9 +41,9 @@ export default function LearnPanel() {
return (
<div className="h-full w-full overflow-auto p-5">
<div className="mb-4">
<h2 className="text-lg font-heading text-text-primary mb-1">Project Tour</h2>
<h2 className="text-lg font-heading text-text-primary mb-1">{t.learnPanel.projectTour}</h2>
<p className="text-xs text-text-muted">
{tourSteps.length} steps &middot; Guided walkthrough of the codebase
{tourSteps.length} {t.learnPanel.steps} &middot; {t.learnPanel.guidedWalkthrough}
</p>
</div>
@@ -49,12 +51,12 @@ export default function LearnPanel() {
onClick={startTour}
className="w-full mb-4 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-colors"
>
Start Tour
{t.learnPanel.startTour}
</button>
<div className="space-y-2">
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider mb-2">
Steps
{t.learnPanel.steps}
</h3>
{tourSteps.map((step, i) => (
<div
@@ -87,7 +89,7 @@ export default function LearnPanel() {
<div className="flex items-center justify-between px-3 py-2 border-b border-border-subtle shrink-0">
<div className="flex items-center gap-2">
<h3 className="text-[11px] font-semibold text-accent uppercase tracking-wider">
Tour
{t.learnPanel.tour}
</h3>
<span className="text-xs text-text-muted">
{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}
</button>
</div>
@@ -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}
</button>
<button
onClick={isLast ? stopTour : nextTourStep}
className="flex-1 text-xs bg-accent/10 border border-accent/30 text-accent py-1.5 rounded-lg hover:bg-accent/20 transition-colors"
>
{isLast ? "Finish" : "Next"}
{isLast ? t.learnPanel.finish : t.learnPanel.next}
</button>
</div>
</div>
@@ -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: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6}>
<circle cx="6" cy="7" r="2" />
<circle cx="18" cy="7" r="2" />
<circle cx="12" cy="17" r="2" />
<path strokeLinecap="round" d="M7.6 8.5L11 15.5M16.4 8.5L13 15.5M8 7h8" />
</svg>
),
},
{
id: "info",
label: "Info",
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6}>
<circle cx="12" cy="12" r="9" />
<path strokeLinecap="round" strokeLinejoin="round" d="M12 11v5M12 8h.01" />
</svg>
),
},
{
id: "files",
label: "Files",
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4 6.5A1.5 1.5 0 0 1 5.5 5h3.382a1.5 1.5 0 0 1 1.342.83l.671 1.34A1.5 1.5 0 0 0 12.236 8H18.5A1.5 1.5 0 0 1 20 9.5v8a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 4 17.5z"
/>
</svg>
),
},
];
const tabIcons: Record<MobileTab, ReactNode> = {
graph: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6}>
<circle cx="6" cy="7" r="2" />
<circle cx="18" cy="7" r="2" />
<circle cx="12" cy="17" r="2" />
<path strokeLinecap="round" d="M7.6 8.5L11 15.5M16.4 8.5L13 15.5M8 7h8" />
</svg>
),
info: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6}>
<circle cx="12" cy="12" r="9" />
<path strokeLinecap="round" strokeLinejoin="round" d="M12 11v5M12 8h.01" />
</svg>
),
files: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4 6.5A1.5 1.5 0 0 1 5.5 5h3.382a1.5 1.5 0 0 1 1.342.83l.671 1.34A1.5 1.5 0 0 0 12.236 8H18.5A1.5 1.5 0 0 1 20 9.5v8a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 4 17.5z"
/>
</svg>
),
};
const tabOrder: MobileTab[] = ["graph", "info", "files"];
export default function MobileBottomNav({ activeTab, onTabChange }: Props) {
const { t } = useI18n();
const labels: Record<MobileTab, string> = {
graph: t.mobile.graph,
info: t.mobile.info,
files: t.mobile.files,
};
return (
<nav className="flex shrink-0 bg-surface border-t border-border-subtle">
{tabs.map((tab) => {
const active = activeTab === tab.id;
{tabOrder.map((id) => {
const active = activeTab === id;
return (
<button
key={tab.id}
key={id}
type="button"
onClick={() => onTabChange(tab.id)}
onClick={() => onTabChange(id)}
className={`relative flex-1 flex flex-col items-center justify-center gap-1 py-2.5 text-[10px] font-semibold uppercase tracking-[0.14em] transition-colors ${
active ? "text-accent" : "text-text-muted hover:text-text-secondary"
}`}
aria-current={active ? "page" : undefined}
>
<span className="w-5 h-5">{tab.icon}</span>
{tab.label}
<span className="w-5 h-5">{tabIcons[id]}</span>
{labels[id]}
{active && (
<span className="absolute top-0 left-1/2 -translate-x-1/2 w-8 h-px bg-accent" />
)}
@@ -1,5 +1,6 @@
import { useEffect } from "react";
import { useDashboardStore } from "../store";
import { useI18n } from "../contexts/I18nContext";
import PersonaSelector from "./PersonaSelector";
import DiffToggle from "./DiffToggle";
import LayerLegend from "./LayerLegend";
@@ -20,19 +21,7 @@ interface NodeTypeFilterDef {
color: string;
}
const STRUCTURAL_FILTERS: NodeTypeFilterDef[] = [
{ key: "code", label: "Code", color: "var(--color-node-file)" },
{ key: "config", label: "Config", color: "var(--color-node-config)" },
{ key: "docs", label: "Docs", color: "var(--color-node-document)" },
{ key: "infra", label: "Infra", color: "var(--color-node-service)" },
{ key: "data", label: "Data", color: "var(--color-node-table)" },
{ key: "domain", label: "Domain", color: "var(--color-node-concept)" },
{ key: "knowledge", label: "Knowledge", color: "var(--color-node-article)" },
];
const KNOWLEDGE_FILTERS: NodeTypeFilterDef[] = [
{ key: "knowledge", label: "All", color: "var(--color-node-article)" },
];
function SectionLabel({ children }: { children: React.ReactNode }) {
return (
@@ -55,6 +44,21 @@ export default function MobileDrawer({
const setViewMode = useDashboardStore((s) => s.setViewMode);
const nodeTypeFilters = useDashboardStore((s) => s.nodeTypeFilters);
const toggleNodeTypeFilter = useDashboardStore((s) => s.toggleNodeTypeFilter);
const { t } = useI18n();
const structuralFilters: NodeTypeFilterDef[] = [
{ key: "code", label: t.nodeTypeLabels.code, color: "var(--color-node-file)" },
{ key: "config", label: t.nodeTypeLabels.config, color: "var(--color-node-config)" },
{ key: "docs", label: t.nodeTypeLabels.docs, color: "var(--color-node-document)" },
{ key: "infra", label: t.nodeTypeLabels.infra, color: "var(--color-node-service)" },
{ key: "data", label: t.nodeTypeLabels.data, color: "var(--color-node-table)" },
{ key: "domain", label: t.nodeTypeLabels.domain, color: "var(--color-node-concept)" },
{ key: "knowledge", label: t.nodeTypeLabels.knowledge, color: "var(--color-node-article)" },
];
const knowledgeFilters: NodeTypeFilterDef[] = [
{ key: "knowledge", label: t.nodeTypeLabels.all, color: "var(--color-node-article)" },
];
useEffect(() => {
if (!open) return;
@@ -75,7 +79,7 @@ export default function MobileDrawer({
};
}, [open]);
const filterDefs = isKnowledgeGraph ? KNOWLEDGE_FILTERS : STRUCTURAL_FILTERS;
const filterDefs = isKnowledgeGraph ? knowledgeFilters : structuralFilters;
const showViewToggle = Boolean(graph && !isKnowledgeGraph && domainGraph);
return (
@@ -105,10 +109,10 @@ export default function MobileDrawer({
<header className="flex items-center justify-between px-5 py-4 border-b border-border-subtle shrink-0">
<div>
<span className="text-[10px] font-semibold uppercase tracking-[0.2em] text-accent">
Controls
{t.drawer.controls}
</span>
<h2 className="font-heading text-lg text-text-primary mt-0.5 leading-none">
{graph?.project.name ?? "Dashboard"}
{graph?.project.name ?? t.drawer.dashboard}
</h2>
</div>
<button
@@ -132,13 +136,13 @@ export default function MobileDrawer({
{/* Body */}
<div className="flex-1 overflow-auto px-5 py-5 space-y-7">
<section>
<SectionLabel>Role</SectionLabel>
<SectionLabel>{t.drawer.role}</SectionLabel>
<PersonaSelector />
</section>
{showViewToggle && (
<section>
<SectionLabel>View</SectionLabel>
<SectionLabel>{t.drawer.view}</SectionLabel>
<div className="inline-flex items-center bg-elevated rounded-lg p-0.5">
<button
type="button"
@@ -149,7 +153,7 @@ export default function MobileDrawer({
: "text-text-muted hover:text-text-secondary"
}`}
>
Domain
{t.drawer.domain}
</button>
<button
type="button"
@@ -160,19 +164,19 @@ export default function MobileDrawer({
: "text-text-muted hover:text-text-secondary"
}`}
>
Structural
{t.drawer.structural}
</button>
</div>
</section>
)}
<section>
<SectionLabel>Diff overlay</SectionLabel>
<SectionLabel>{t.drawer.diffOverlay}</SectionLabel>
<DiffToggle />
</section>
<section>
<SectionLabel>Node types</SectionLabel>
<SectionLabel>{t.drawer.nodeTypes}</SectionLabel>
<div className="flex flex-wrap gap-1.5">
{filterDefs.map((cat) => {
const active = nodeTypeFilters[cat.key] !== false;
@@ -203,7 +207,7 @@ export default function MobileDrawer({
{graph && (graph.layers?.length ?? 0) > 0 && (
<section>
<SectionLabel>Layers</SectionLabel>
<SectionLabel>{t.drawer.layers}</SectionLabel>
<div className="-mx-1">
<LayerLegend />
</div>
@@ -211,7 +215,7 @@ export default function MobileDrawer({
)}
<section>
<SectionLabel>Tools</SectionLabel>
<SectionLabel>{t.drawer.tools}</SectionLabel>
<div className="flex flex-wrap items-center gap-2">
<FilterPanel />
<ExportMenu />
@@ -231,7 +235,7 @@ export default function MobileDrawer({
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
/>
</svg>
Path
{t.drawer.path}
</button>
<ThemePicker />
<button
@@ -241,7 +245,7 @@ export default function MobileDrawer({
onClose();
}}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm bg-elevated text-text-secondary hover:text-text-primary transition-colors"
aria-label="Keyboard shortcuts"
aria-label={t.drawer.help}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
@@ -251,7 +255,7 @@ export default function MobileDrawer({
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
Help
{t.drawer.help}
</button>
</div>
</section>
@@ -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<MobileTab>("graph");
const [drawerOpen, setDrawerOpen] = useState(false);
@@ -96,7 +98,7 @@ export default function MobileLayout({
</button>
<h1 className="font-heading text-base flex-1 min-w-0 truncate text-center text-text-primary tracking-wide">
{graph?.project.name ?? "Understand Anything"}
{graph?.project.name ?? t.common.appName}
</h1>
<button
@@ -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>
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useDashboardStore } from "../store";
import { useI18n } from "../contexts/I18nContext";
const typeBadgeColors: Record<string, string> = {
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<HTMLDivElement>(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"
/>
<div className="flex items-center gap-1 bg-elevated rounded-lg p-0.5 shrink-0">
@@ -116,7 +119,7 @@ export default function SearchBar() {
: "text-text-muted hover:text-text-secondary"
}`}
>
Fuzzy
{t.search.fuzzy}
</button>
<button
onClick={() => setSearchMode("semantic")}
@@ -126,12 +129,12 @@ export default function SearchBar() {
: "text-text-muted hover:text-text-secondary"
}`}
>
Semantic
{t.search.semantic}
</button>
</div>
{searchQuery.trim() && (
<span className="hidden sm:inline text-xs text-text-muted shrink-0">
{searchResults.length} result{searchResults.length !== 1 ? "s" : ""}{" "}
{searchResults.length} {t.search.result}{searchResults.length !== 1 ? "s" : ""}{" "}
<span className="text-text-muted">({searchMode})</span>
</span>
)}
@@ -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<HTMLDivElement>(null);
const { t } = useI18n();
// Close on outside click
useEffect(() => {
@@ -41,7 +43,7 @@ export function ThemePicker() {
<button
onClick={() => setOpen((v) => !v)}
className="flex items-center gap-1.5 px-2 py-1 rounded text-xs text-text-secondary hover:text-text-primary transition-colors"
title="Change theme"
title={t.themePicker.changeTheme}
>
<svg
width="14"
@@ -59,7 +61,7 @@ export function ThemePicker() {
<circle cx="12" cy="7" r="1.5" fill="currentColor" />
<circle cx="16" cy="10" r="1.5" fill="currentColor" />
</svg>
<span className="hidden sm:inline">Theme</span>
<span className="hidden sm:inline">{t.common.theme}</span>
</button>
{open && (
@@ -67,7 +69,7 @@ export function ThemePicker() {
{/* Presets */}
<div>
<div className="text-[10px] font-semibold text-text-muted uppercase tracking-wider mb-2">
Theme
{t.themePicker.theme}
</div>
<div className="space-y-1">
{PRESETS.map((p) => (
@@ -119,7 +121,7 @@ export function ThemePicker() {
{/* Accent swatches */}
<div>
<div className="text-[10px] font-semibold text-text-muted uppercase tracking-wider mb-2">
Accent Color
{t.themePicker.accentColor}
</div>
<div className="flex gap-2 flex-wrap">
{preset.accentSwatches.map((swatch) => (
@@ -141,13 +143,13 @@ export function ThemePicker() {
{/* Heading font */}
<div>
<div className="text-[10px] font-semibold text-text-muted uppercase tracking-wider mb-2">
Heading Font
{t.themePicker.headingFont}
</div>
<div className="flex gap-1">
{([
{ 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) => (
<button
key={opt.id}
@@ -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,272 @@
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",
appName: "Understand Anything",
pressKeyboard: "Press ? for keyboard shortcuts",
path: "Path",
theme: "Theme",
},
projectOverview: {
nodes: "Nodes",
edges: "Edges",
layers: "Layers",
types: "Types",
fileTypes: "File Types",
code: "Code",
config: "Config",
docs: "Docs",
infra: "Infra",
data: "Data",
domain: "Domain",
knowledge: "Knowledge",
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",
},
sidebar: {
info: "Info",
files: "Files",
},
mobile: {
graph: "Graph",
info: "Info",
files: "Files",
},
drawer: {
controls: "Controls",
dashboard: "Dashboard",
role: "Role",
view: "View",
diffOverlay: "Diff overlay",
nodeTypes: "Node types",
layers: "Layers",
tools: "Tools",
path: "Path",
help: "Help",
structural: "Structural",
domain: "Domain",
},
domainView: {
backToDomains: "Back to domains",
},
detailLevel: {
filesTitle: "Files only — architecture-level dependencies (fast)",
classesTitle: "Files + Classes — code structure with inheritance",
files: "Files",
classes: "+Classes",
fnTitle: "Toggle function nodes (may slow down rendering)",
fn: "fn",
},
nodeTypeLabels: {
all: "All",
code: "Code",
config: "Config",
docs: "Docs",
infra: "Infra",
data: "Data",
domain: "Domain",
knowledge: "Knowledge",
},
tokenGate: {
validating: "Validating...",
continue: "Continue",
},
diffToggle: {
hideOverlay: "Hide diff overlay",
showOverlay: "Show diff overlay",
noData: "No diff data loaded",
changed: "Changed",
affected: "Affected",
},
learnPanel: {
finish: "Finish",
next: "Next",
prev: "Prev",
noTour: "No tour available",
noTourHint: "Generate a tour from your knowledge graph to get a guided walkthrough",
projectTour: "Project Tour",
steps: "steps",
stepsTitle: "Steps",
guidedWalkthrough: "Guided walkthrough of the codebase",
startTour: "Start Tour",
tour: "Tour",
exitTour: "Exit Tour",
},
layer: {
defaultName: "Layer",
label: "layers",
},
breadcrumb: {
projectOverview: "Project Overview",
project: "Project",
escBack: "Esc to go back",
},
warningBanner: {
dropped: "Dropped",
fatal: "Fatal",
},
themePicker: {
changeTheme: "Change theme",
theme: "Theme",
accentColor: "Accent Color",
headingFont: "Heading Font",
serif: "Serif",
sans: "Sans",
mono: "Mono",
},
codeViewer: {
fullFile: "Full file",
lines: "Lines",
linesLabel: "lines",
noFile: "No file selected",
loading: "Loading source...",
openLarger: "Open larger code viewer",
closeExpanded: "Close expanded code viewer",
closeViewer: "Close code viewer",
sourceUnavailable: "Source unavailable",
},
customNode: {
tested: "Tested",
hasTests: "Has tests",
},
ariaLabels: {
openMenu: "Open menu",
closeMenu: "Close menu",
settings: "Settings",
hideSearch: "Hide search",
showSearch: "Show search",
},
nodeTypeFilter: {
hide: "Hide",
show: "Show",
nodesLabel: "nodes",
},
keyboardShortcuts: {
showHelp: "Show keyboard shortcuts",
general: "General",
navigation: "Navigation",
tour: "Tour",
view: "View",
focusSearch: "Focus search bar",
nextStep: "Next tour step",
prevStep: "Previous tour step",
toggleDiff: "Toggle diff mode",
toggleFilter: "Toggle filter panel",
toggleExport: "Toggle export menu",
openPathFinder: "Open path finder",
title: "Keyboard Shortcuts",
toggleHint: "Press ? anytime to toggle this help",
closeHint: "Press ESC to close",
escapeDesc: "Close panels and modals / go back to overview",
},
search: {
placeholder: "Search nodes by name, summary, or tags...",
fuzzy: "Fuzzy",
semantic: "Semantic",
result: "result",
},
export: {
label: "Export",
title: "Export graph (E)",
asPNG: "Export as PNG",
asSVG: "Export as SVG",
asJSON: "Export as JSON",
},
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" },
},
pathFinder: {
title: "Find path between nodes (P)",
},
};
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,272 @@
export const ja = {
common: {
loading: "プロジェクトを読み込み中...",
noGraphLoaded: "知識グラフが読み込まれていません",
selectNode: "ノードを選択して詳細を表示",
back: "戻る",
focus: "フォーカス",
unfocus: "フォーカス解除",
openCode: "コードを開く",
file: "ファイル",
tags: "タグ",
connections: "接続",
filter: "フィルター",
resetAll: "すべてリセット",
analyzed: "分析日時",
startGuidedTour: "ガイド付きツアーを開始",
truncated: "(省略)",
preview: "プレビュー",
doubleClickToOpen: "ダブルクリックで開く",
appName: "Understand Anything",
pressKeyboard: "? を押してキーボードショートカットを表示",
path: "パス",
theme: "テーマ",
},
projectOverview: {
nodes: "ノード",
edges: "エッジ",
layers: "レイヤー",
types: "タイプ",
fileTypes: "ファイルタイプ",
code: "コード",
config: "設定",
docs: "ドキュメント",
infra: "インフラ",
data: "データ",
domain: "ドメイン",
knowledge: "ナレッジ",
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: "コード中心のチャット",
},
sidebar: {
info: "情報",
files: "ファイル",
},
mobile: {
graph: "グラフ",
info: "情報",
files: "ファイル",
},
drawer: {
controls: "コントロール",
dashboard: "ダッシュボード",
role: "ロール",
view: "ビュー",
diffOverlay: "差分オーバーレイ",
nodeTypes: "ノードタイプ",
layers: "レイヤー",
tools: "ツール",
path: "パス",
help: "ヘルプ",
structural: "構造",
domain: "ドメイン",
},
domainView: {
backToDomains: "ドメインに戻る",
},
detailLevel: {
filesTitle: "ファイルのみ — アーキテクチャレベルの依存関係(高速)",
classesTitle: "ファイル + クラス — 継承を含むコード構造",
files: "ファイル",
classes: "+クラス",
fnTitle: "関数ノードを切り替え(レンダリングが遅くなる可能性)",
fn: "fn",
},
nodeTypeLabels: {
all: "すべて",
code: "コード",
config: "設定",
docs: "ドキュメント",
infra: "インフラ",
data: "データ",
domain: "ドメイン",
knowledge: "ナレッジ",
},
tokenGate: {
validating: "検証中...",
continue: "続行",
},
diffToggle: {
hideOverlay: "差分オーバーレイを非表示",
showOverlay: "差分オーバーレイを表示",
noData: "差分データが読み込まれていません",
changed: "変更済み",
affected: "影響あり",
},
learnPanel: {
finish: "完了",
next: "次へ",
prev: "前へ",
noTour: "ツアーがありません",
noTourHint: "知識グラフからツアーを生成してコードベースのガイド付きウォークスルーを取得",
projectTour: "プロジェクトツアー",
steps: "ステップ",
stepsTitle: "ステップ",
guidedWalkthrough: "コードベースのガイド付きウォークスルー",
startTour: "ツアー開始",
tour: "ツアー",
exitTour: "ツアー終了",
},
layer: {
defaultName: "レイヤー",
label: "レイヤー",
},
breadcrumb: {
projectOverview: "プロジェクト概要",
project: "プロジェクト",
escBack: "Escで戻る",
},
warningBanner: {
dropped: "削除済み",
fatal: "致命的",
},
themePicker: {
changeTheme: "テーマ変更",
theme: "テーマ",
accentColor: "アクセント色",
headingFont: "見出しフォント",
serif: "セリフ",
sans: "サン",
mono: "モノ",
},
codeViewer: {
fullFile: "ファイル全体",
lines: "行",
linesLabel: "行",
noFile: "ファイル未選択",
loading: "ソース読み込み中...",
openLarger: "大きなコードビューアを開く",
closeExpanded: "展開したコードビューアを閉じる",
closeViewer: "コードビューアを閉じる",
sourceUnavailable: "ソースが利用できません",
},
customNode: {
tested: "テスト済み",
hasTests: "テストあり",
},
ariaLabels: {
openMenu: "メニューを開く",
closeMenu: "メニューを閉じる",
settings: "設定",
hideSearch: "検索を非表示",
showSearch: "検索を表示",
},
nodeTypeFilter: {
hide: "非表示",
show: "表示",
nodesLabel: "ノード",
},
keyboardShortcuts: {
showHelp: "キーボードショートカットを表示",
general: "一般",
navigation: "ナビゲーション",
tour: "ツアー",
view: "ビュー",
focusSearch: "検索バーにフォーカス",
nextStep: "次のツアーステップ",
prevStep: "前のツアーステップ",
toggleDiff: "差分モード切り替え",
toggleFilter: "フィルターパネル切り替え",
toggleExport: "エクスポートメニュー切り替え",
openPathFinder: "パスファインダーを開く",
title: "キーボードショートカット",
toggleHint: "いつでも ? を押してこのヘルプを切り替え",
closeHint: "ESC を押して閉じる",
escapeDesc: "パネルとモーダルを閉じる / 概要に戻る",
},
search: {
placeholder: "ノード名、概要、タグで検索...",
fuzzy: "ファジー",
semantic: "セマンティック",
result: "結果",
},
export: {
label: "エクスポート",
title: "グラフをエクスポート (E)",
asPNG: "PNGでエクスポート",
asSVG: "SVGでエクスポート",
asJSON: "JSONでエクスポート",
},
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: "作成" },
},
pathFinder: {
title: "ノード間のパスを検索 (P)",
},
};
export default ja;
@@ -0,0 +1,272 @@
export const ko = {
common: {
loading: "프로젝트 로딩 중...",
noGraphLoaded: "지식 그래프가 로드되지 않음",
selectNode: "노드를 선택하여 상세 정보 확인",
back: "뒤로",
focus: "포커스",
unfocus: "포커스 해제",
openCode: "코드 열기",
file: "파일",
tags: "태그",
connections: "연결",
filter: "필터",
resetAll: "모두 재설정",
analyzed: "분석 시간",
startGuidedTour: "가이드 투어 시작",
truncated: "(생략)",
preview: "미리보기",
doubleClickToOpen: "두 번 클릭하여 열기",
appName: "Understand Anything",
pressKeyboard: "? 키를 눌러 키보드 단축키 보기",
path: "경로",
theme: "테마",
},
projectOverview: {
nodes: "노드",
edges: "엣지",
layers: "레이어",
types: "타입",
fileTypes: "파일 타입",
code: "코드",
config: "설정",
docs: "문서",
infra: "인프라",
data: "데이터",
domain: "도메인",
knowledge: "지식",
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: "코드 중심 채팅",
},
sidebar: {
info: "정보",
files: "파일",
},
mobile: {
graph: "그래프",
info: "정보",
files: "파일",
},
drawer: {
controls: "컨트롤",
dashboard: "대시보드",
role: "역할",
view: "보기",
diffOverlay: "차분 오버레이",
nodeTypes: "노드 타입",
layers: "레이어",
tools: "도구",
path: "경로",
help: "도움말",
structural: "구조",
domain: "도메인",
},
domainView: {
backToDomains: "도메인으로 돌아가기",
},
detailLevel: {
filesTitle: "파일만 — 아키텍처 레벨 의존성 (빠름)",
classesTitle: "파일 + 클래스 — 상속 포함 코드 구조",
files: "파일",
classes: "+클래스",
fnTitle: "함수 노드 토글 (렌더링 속도 저하 가능)",
fn: "fn",
},
nodeTypeLabels: {
all: "모두",
code: "코드",
config: "설정",
docs: "문서",
infra: "인프라",
data: "데이터",
domain: "도메인",
knowledge: "지식",
},
tokenGate: {
validating: "검증 중...",
continue: "계속",
},
diffToggle: {
hideOverlay: "차분 오버레이 숨기기",
showOverlay: "차분 오버레이 표시",
noData: "차분 데이터가 로드되지 않음",
changed: "변경됨",
affected: "영향받음",
},
learnPanel: {
finish: "완료",
next: "다음",
prev: "이전",
noTour: "투어 없음",
noTourHint: "지식 그래프에서 투어를 생성하여 코드베이스의 가이드 워크스루를 얻으세요",
projectTour: "프로젝트 투어",
steps: "단계",
stepsTitle: "단계",
guidedWalkthrough: "코드베이스 가이드 워크스루",
startTour: "투어 시작",
tour: "투어",
exitTour: "투어 종료",
},
layer: {
defaultName: "레이어",
label: "레이어",
},
breadcrumb: {
projectOverview: "프로젝트 개요",
project: "프로젝트",
escBack: "Esc로 돌아가기",
},
warningBanner: {
dropped: "삭제됨",
fatal: "치명적",
},
themePicker: {
changeTheme: "테마 변경",
theme: "테마",
accentColor: "강조색",
headingFont: "제목 폰트",
serif: "세리프",
sans: "산스",
mono: "모노",
},
codeViewer: {
fullFile: "전체 파일",
lines: "행",
linesLabel: "행",
noFile: "파일 선택 안 됨",
loading: "소스 로딩 중...",
openLarger: "더 큰 코드 뷰어 열기",
closeExpanded: "확장된 코드 뷰어 닫기",
closeViewer: "코드 뷰어 닫기",
sourceUnavailable: "소스 사용 불가",
},
customNode: {
tested: "테스트됨",
hasTests: "테스트 있음",
},
ariaLabels: {
openMenu: "메뉴 열기",
closeMenu: "메뉴 닫기",
settings: "설정",
hideSearch: "검색 숨기기",
showSearch: "검색 표시",
},
nodeTypeFilter: {
hide: "숨기기",
show: "표시",
nodesLabel: "노드",
},
keyboardShortcuts: {
showHelp: "키보드 단축키 표시",
general: "일반",
navigation: "탐색",
tour: "투어",
view: "보기",
focusSearch: "검색창 포커스",
nextStep: "다음 투어 단계",
prevStep: "이전 투어 단계",
toggleDiff: "차분 모드 전환",
toggleFilter: "필터 패널 전환",
toggleExport: "내보내기 메뉴 전환",
openPathFinder: "경로 찾기 열기",
title: "키보드 단축키",
toggleHint: "언제든 ?를 눌러 이 도움말을 토글",
closeHint: "ESC를 눌러 닫기",
escapeDesc: "패널 및 모달 닫기 / 개요로 돌아가기",
},
search: {
placeholder: "노드 이름, 요약, 태그로 검색...",
fuzzy: "퍼지",
semantic: "시맨틱",
result: "결과",
},
export: {
label: "내보내기",
title: "그래프 내보내기 (E)",
asPNG: "PNG로 내보내기",
asSVG: "SVG로 내보내기",
asJSON: "JSON으로 내보내기",
},
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: "작성" },
},
pathFinder: {
title: "노드 간 경로 찾기 (P)",
},
};
export default ko;
@@ -0,0 +1,272 @@
export const zhTW = {
common: {
loading: "載入專案...",
noGraphLoaded: "未載入知識圖谱",
selectNode: "選擇節點查看詳情",
back: "返回",
focus: "聚焦",
unfocus: "取消聚焦",
openCode: "開啟程式碼",
file: "檔案",
tags: "標籤",
connections: "連結",
filter: "篩選",
resetAll: "重置全部",
analyzed: "分析時間",
startGuidedTour: "開始導覽",
truncated: "(已截斷)",
preview: "預覽",
doubleClickToOpen: "雙擊開啟",
appName: "Understand Anything",
pressKeyboard: "按 ? 查看鍵盤快捷鍵",
path: "路徑",
theme: "主題",
},
projectOverview: {
nodes: "節點",
edges: "邊",
layers: "層級",
types: "類型",
fileTypes: "檔案類型",
code: "程式碼",
config: "配置",
docs: "文件",
infra: "基礎設施",
data: "資料",
domain: "領域",
knowledge: "知識",
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: "程式碼聚焦與對話",
},
sidebar: {
info: "資訊",
files: "檔案",
},
mobile: {
graph: "圖谱",
info: "資訊",
files: "檔案",
},
drawer: {
controls: "控制",
dashboard: "儀表板",
role: "角色",
view: "視圖",
diffOverlay: "差異覆蓋",
nodeTypes: "節點類型",
layers: "層級",
tools: "工具",
path: "路徑",
help: "幫助",
structural: "結構",
domain: "領域",
},
domainView: {
backToDomains: "返回領域列表",
},
detailLevel: {
filesTitle: "僅檔案 — 架構級依賴(快速)",
classesTitle: "檔案 + 類別 — 程式碼結構及繼承關係",
files: "檔案",
classes: "+類別",
fnTitle: "切換函數節點(可能降低渲染速度)",
fn: "函數",
},
nodeTypeLabels: {
all: "全部",
code: "程式碼",
config: "配置",
docs: "文件",
infra: "基礎設施",
data: "資料",
domain: "領域",
knowledge: "知識",
},
tokenGate: {
validating: "驗證中...",
continue: "繼續",
},
diffToggle: {
hideOverlay: "隱藏差異覆蓋",
showOverlay: "顯示差異覆蓋",
noData: "未載入差異資料",
changed: "已修改",
affected: "受影響",
},
learnPanel: {
finish: "完成",
next: "下一步",
prev: "上一步",
noTour: "無導覽可用",
noTourHint: "從知識圖谱生成導覽以獲取程式碼庫的引導式講解",
projectTour: "專案導覽",
steps: "步",
stepsTitle: "步驟",
guidedWalkthrough: "程式碼庫引導式講解",
startTour: "開始導覽",
tour: "導覽",
exitTour: "退出導覽",
},
layer: {
defaultName: "層級",
label: "層",
},
breadcrumb: {
projectOverview: "專案概覽",
project: "專案",
escBack: "按 Esc 返回",
},
warningBanner: {
dropped: "已捨棄",
fatal: "致命錯誤",
},
themePicker: {
changeTheme: "變更主題",
theme: "主題",
accentColor: "強調色",
headingFont: "標題字型",
serif: "襯線",
sans: "無襯線",
mono: "等寬",
},
codeViewer: {
fullFile: "完整檔案",
lines: "行",
linesLabel: "行",
noFile: "未選擇檔案",
loading: "載入原始碼中...",
openLarger: "開啟更大的程式碼檢視器",
closeExpanded: "關閉展開的程式碼檢視器",
closeViewer: "關閉程式碼檢視器",
sourceUnavailable: "原始碼不可用",
},
customNode: {
tested: "已測試",
hasTests: "有測試",
},
ariaLabels: {
openMenu: "開啟選單",
closeMenu: "關閉選單",
settings: "設定",
hideSearch: "隱藏搜尋",
showSearch: "顯示搜尋",
},
nodeTypeFilter: {
hide: "隱藏",
show: "顯示",
nodesLabel: "節點",
},
keyboardShortcuts: {
showHelp: "顯示鍵盤快捷鍵",
general: "一般",
navigation: "導航",
tour: "導覽",
view: "檢視",
focusSearch: "聚焦搜尋列",
nextStep: "下一步導覽",
prevStep: "上一步導覽",
toggleDiff: "切換差異模式",
toggleFilter: "切換篩選面板",
toggleExport: "切換匯出選單",
openPathFinder: "開啟路徑尋找器",
title: "鍵盤快捷鍵",
toggleHint: "按 ? 隨時切換此幫助",
closeHint: "按 ESC 關閉",
escapeDesc: "關閉面板和彈窗 / 返回概覽",
},
search: {
placeholder: "搜尋節點名稱、摘要或標籤...",
fuzzy: "模糊",
semantic: "語意",
result: "結果",
},
export: {
label: "匯出",
title: "匯出圖谱 (E)",
asPNG: "匯出為 PNG",
asSVG: "匯出為 SVG",
asJSON: "匯出為 JSON",
},
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: "著作" },
},
pathFinder: {
title: "尋找節點間路徑 (P)",
},
};
export default zhTW;
@@ -0,0 +1,272 @@
export const zh = {
common: {
loading: "加载项目...",
noGraphLoaded: "未加载知识图谱",
selectNode: "选择节点查看详情",
back: "返回",
focus: "聚焦",
unfocus: "取消聚焦",
openCode: "打开代码",
file: "文件",
tags: "标签",
connections: "连接",
filter: "筛选",
resetAll: "重置全部",
analyzed: "分析时间",
startGuidedTour: "开始导览",
truncated: "(已截断)",
preview: "预览",
doubleClickToOpen: "双击打开",
appName: "Understand Anything",
pressKeyboard: "按 ? 查看键盘快捷键",
path: "路径",
theme: "主题",
},
projectOverview: {
nodes: "节点",
edges: "边",
layers: "层级",
types: "类型",
fileTypes: "文件类型",
code: "代码",
config: "配置",
docs: "文档",
infra: "基础设施",
data: "数据",
domain: "领域",
knowledge: "知识",
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: "代码聚焦与对话",
},
sidebar: {
info: "信息",
files: "文件",
},
mobile: {
graph: "图谱",
info: "信息",
files: "文件",
},
drawer: {
controls: "控制",
dashboard: "仪表盘",
role: "角色",
view: "视图",
diffOverlay: "差异覆盖",
nodeTypes: "节点类型",
layers: "层级",
tools: "工具",
path: "路径",
help: "帮助",
structural: "结构",
domain: "领域",
},
domainView: {
backToDomains: "返回领域列表",
},
detailLevel: {
filesTitle: "仅文件 — 架构级依赖(快速)",
classesTitle: "文件 + 类 — 代码结构及继承关系",
files: "文件",
classes: "+类",
fnTitle: "切换函数节点(可能降低渲染速度)",
fn: "函数",
},
nodeTypeLabels: {
all: "全部",
code: "代码",
config: "配置",
docs: "文档",
infra: "基础设施",
data: "数据",
domain: "领域",
knowledge: "知识",
},
tokenGate: {
validating: "验证中...",
continue: "继续",
},
diffToggle: {
hideOverlay: "隐藏差异覆盖",
showOverlay: "显示差异覆盖",
noData: "未加载差异数据",
changed: "已修改",
affected: "受影响",
},
learnPanel: {
finish: "完成",
next: "下一步",
prev: "上一步",
noTour: "无导览可用",
noTourHint: "从知识图谱生成导览以获取代码库的引导式讲解",
projectTour: "项目导览",
steps: "步",
stepsTitle: "步骤",
guidedWalkthrough: "代码库引导式讲解",
startTour: "开始导览",
tour: "导览",
exitTour: "退出导览",
},
layer: {
defaultName: "层级",
label: "层",
},
breadcrumb: {
projectOverview: "项目概览",
project: "项目",
escBack: "按 Esc 返回",
},
warningBanner: {
dropped: "已丢弃",
fatal: "致命错误",
},
themePicker: {
changeTheme: "更换主题",
theme: "主题",
accentColor: "强调色",
headingFont: "标题字体",
serif: "衬线",
sans: "无衬线",
mono: "等宽",
},
codeViewer: {
fullFile: "完整文件",
lines: "行",
linesLabel: "行",
noFile: "未选择文件",
loading: "加载源码中...",
openLarger: "打开更大的代码查看器",
closeExpanded: "关闭展开的代码查看器",
closeViewer: "关闭代码查看器",
sourceUnavailable: "源码不可用",
},
customNode: {
tested: "已测试",
hasTests: "有测试",
},
ariaLabels: {
openMenu: "打开菜单",
closeMenu: "关闭菜单",
settings: "设置",
hideSearch: "隐藏搜索",
showSearch: "显示搜索",
},
nodeTypeFilter: {
hide: "隐藏",
show: "显示",
nodesLabel: "节点",
},
keyboardShortcuts: {
showHelp: "显示键盘快捷键",
general: "通用",
navigation: "导航",
tour: "导览",
view: "视图",
focusSearch: "聚焦搜索栏",
nextStep: "下一步导览",
prevStep: "上一步导览",
toggleDiff: "切换差异模式",
toggleFilter: "切换筛选面板",
toggleExport: "切换导出菜单",
openPathFinder: "打开路径查找器",
title: "键盘快捷键",
toggleHint: "按 ? 随时切换此帮助",
closeHint: "按 ESC 关闭",
escapeDesc: "关闭面板和弹窗 / 返回概览",
},
search: {
placeholder: "搜索节点名称、摘要或标签...",
fuzzy: "模糊",
semantic: "语义",
result: "结果",
},
export: {
label: "导出",
title: "导出图谱 (E)",
asPNG: "导出为 PNG",
asSVG: "导出为 SVG",
asJSON: "导出为 JSON",
},
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: "著作" },
},
pathFinder: {
title: "查找节点间路径 (P)",
},
};
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,7 +1,7 @@
---
name: understand
description: Analyze a codebase to produce an interactive knowledge graph for understanding architecture, components, and relationships
argument-hint: ["[path] [--full|--auto-update|--no-auto-update|--review]"]
argument-hint: ["[path] [--full|--auto-update|--no-auto-update|--review|--language <lang>]"]
---
# /understand
@@ -15,6 +15,7 @@ Analyze the current codebase and produce a `knowledge-graph.json` file in `.unde
- `--auto-update` — Enable automatic graph updates on commit (writes `autoUpdate: true` to `.understand-anything/config.json`)
- `--no-auto-update` — Disable automatic graph updates (writes `autoUpdate: false` to `.understand-anything/config.json`)
- `--review` — Run full LLM graph-reviewer instead of inline deterministic validation
- `--language <lang>` — Generate all textual content (summaries, descriptions, tags, titles, languageNotes, languageLesson) in the specified language. Accepts ISO 639-1 codes (`zh`, `ja`, `ko`, `en`, `es`, `fr`, `de`, etc.) or friendly names (`chinese`, `japanese`, `korean`, `english`, `spanish`, etc.). Locale variants supported: `zh-TW`, `zh-HK`, etc. Defaults to `en` (English). Stores preference in `.understand-anything/config.json` for consistency across incremental updates.
- A directory path (e.g. `/path/to/repo` or `../other-project`) — Analyze the given directory instead of the current working directory
---
@@ -110,11 +111,27 @@ Determine whether to run a full analysis or incremental update.
mkdir -p $PROJECT_ROOT/.understand-anything/tmp
```
3.5. **Auto-update configuration:**
- If `--auto-update` is in `$ARGUMENTS`: write `{"autoUpdate": true}` to `$PROJECT_ROOT/.understand-anything/config.json`
- If `--no-auto-update` is in `$ARGUMENTS`: write `{"autoUpdate": false}` to `$PROJECT_ROOT/.understand-anything/config.json`
- These flags only set the config — analysis proceeds normally regardless.
- If `--auto-update` is in `$ARGUMENTS`: write `{"autoUpdate": true}` to `$PROJECT_ROOT/.understand-anything/config.json`
- If `--no-auto-update` is in `$ARGUMENTS`: write `{"autoUpdate": false}` to `$PROJECT_ROOT/.understand-anything/config.json`
- These flags only set the config — analysis proceeds normally regardless.
4. **Check for subdomain knowledge graphs to merge:**
3.6. **Language configuration:**
- Parse `$ARGUMENTS` for `--language <lang>` flag. If found, extract the language code.
- **Language code normalization:** Map friendly names to ISO codes:
- `chinese` → `zh`, `japanese` → `ja`, `korean` → `ko`, `english` → `en`, `spanish` → `es`, `french` → `fr`, `german` → `de`, `portuguese` → `pt`, `russian` → `ru`, `arabic` → `ar`, etc.
- Locale variants: `zh-TW`, `zh-HK`, `zh-CN`, `pt-BR`, etc. are preserved as-is.
- If `--language` is NOT specified:
- Check `$PROJECT_ROOT/.understand-anything/config.json` for an existing `outputLanguage` field. If present, use that.
- If no stored preference, default to `en` (English).
- If `--language` IS specified:
- Update `$PROJECT_ROOT/.understand-anything/config.json` with the new language: merge `{"outputLanguage": "<lang>"}` into existing config.
- Store as `$OUTPUT_LANGUAGE` for use throughout all phases.
- **Language directive template:** Store as `$LANGUAGE_DIRECTIVE`:
```markdown
> **Language directive**: Generate all textual content (summaries, descriptions, tags, titles, languageNotes, languageLesson) in **{language}**. Maintain technical accuracy while using natural, native-level phrasing in the target language. Keep technical terms in English when no standard translation exists (e.g., "middleware", "hook", "barrel").
```
4. **Check for subdomain knowledge graphs to merge:**
List all `*knowledge-graph*.json` files in `$PROJECT_ROOT/.understand-anything/` **excluding** `knowledge-graph.json` itself (e.g. `frontend-knowledge-graph.json`, `backend-knowledge-graph.json`). If any subdomain graphs exist, run the merge script bundled with this skill (located next to this SKILL.md file — use the skill directory path, not the project root):
```bash
python <SKILL_DIR>/merge-subdomain-graphs.py $PROJECT_ROOT
@@ -211,6 +228,8 @@ Dispatch a subagent using the `project-scanner` agent definition (at `agents/pro
> ```
>
> Use this context to produce more accurate project name, description, and framework detection. The README and manifest are authoritative — prefer their information over heuristics.
>
> $LANGUAGE_DIRECTIVE
Pass these parameters in the dispatch prompt:
@@ -257,6 +276,8 @@ For each batch, dispatch a subagent using the `file-analyzer` agent definition (
>
> Project: `<projectName>` — `<projectDescription>`
> Languages: `<languages from Phase 1>`
>
> $LANGUAGE_DIRECTIVE
Before dispatching each batch, construct `batchImportData` from `$IMPORT_MAP`:
```json
@@ -348,9 +369,10 @@ After the subagent completes, read `$PROJECT_ROOT/.understand-anything/intermedi
## Phase 4 — ARCHITECTURE
**Build the combined prompt template:**
1. Use the `architecture-analyzer` agent definition (at `agents/architecture-analyzer.md`).
2. **Language context injection:** For each language detected in Phase 1 (e.g., `python`, `markdown`, `dockerfile`, `yaml`, `sql`, `terraform`, `graphql`, `protobuf`, `shell`, `html`, `css`), read the file at `./languages/<language-id>.md` (e.g., `./languages/python.md`, `./languages/dockerfile.md`) and append its content after the base template under a `## Language Context` header. If the file does not exist for a detected language, skip it silently and continue. These files are in the `languages/` subdirectory next to this SKILL.md file. **Include non-code language snippets** — they provide edge patterns and summary styles for non-code files.
3. **Framework addendum injection:** For each framework detected in Phase 1 (e.g., `Django`), read the file at `./frameworks/<framework-id-lowercase>.md` (e.g., `./frameworks/django.md`) and append its full content after the language context. If the file does not exist for a detected framework, skip it silently and continue. These files are in the `frameworks/` subdirectory next to this SKILL.md file.
1. Use the `architecture-analyzer` agent definition (at `agents/architecture-analyzer.md`).
2. **Language context injection:** For each language detected in Phase 1 (e.g., `python`, `markdown`, `dockerfile`, `yaml`, `sql`, `terraform`, `graphql`, `protobuf`, `shell`, `html`, `css`), read the file at `./languages/<language-id>.md` (e.g., `./languages/python.md`, `./languages/dockerfile.md`) and append its content after the base template under a `## Language Context` header. If the file does not exist for a detected language, skip it silently and continue. These files are in the `languages/` subdirectory next to this SKILL.md file. **Include non-code language snippets** — they provide edge patterns and summary styles for non-code files.
3. **Framework addendum injection:** For each framework detected in Phase 1 (e.g., `Django`), read the file at `./frameworks/<framework-id-lowercase>.md` (e.g., `./frameworks/django.md`) and append its full content after the language context. If the file does not exist for a detected framework, skip it silently and continue. These files are in the `frameworks/` subdirectory next to this SKILL.md file.
4. **Output locale injection:** If `$OUTPUT_LANGUAGE` is NOT `en` (English), read the locale guidance file at `./locales/<language-code>.md` (e.g., `./locales/zh.md`, `./locales/ja.md`, `./locales/ko.md`) and append its content after the framework addendums under a `## Output Language Guidelines` header. This provides language-specific guidance for tag naming conventions, summary style, and layer name translations. If the locale file does not exist for the specified language, skip silently — the `$LANGUAGE_DIRECTIVE` still applies. These files are in the `locales/` subdirectory next to this SKILL.md file.
Append the language/framework context and the following additional context to the agent's prompt:
@@ -364,6 +386,8 @@ Append the language/framework context and the following additional context to th
> ```
>
> Use the directory tree, language context, and framework addendums (appended above) to inform layer assignments. Directory structure is strong evidence for layer boundaries. Non-code files (config, docs, infrastructure, data) should be assigned to appropriate layers — see the prompt template for guidance.
>
> $LANGUAGE_DIRECTIVE
Pass these parameters in the dispatch prompt:
@@ -437,6 +461,8 @@ Dispatch a subagent using the `tour-builder` agent definition (at `agents/tour-b
> Project entry point: `$ENTRY_POINT`
>
> Use the README to align the tour narrative with the project's own documentation. Start the tour from the entry point if one was detected. The tour should tell the same story the README tells, but through the lens of actual code structure.
>
> $LANGUAGE_DIRECTIVE
Pass these parameters in the dispatch prompt:
@@ -0,0 +1,44 @@
# English Output Guidelines
This file provides language-specific guidance for generating knowledge graph content in English.
## Tag Conventions
Use lowercase, hyphenated tags in English:
| Pattern | Recommended Tags |
|---------|-----------------|
| Entry point file | `entry-point`, `barrel`, `exports` |
| Utility functions | `utility`, `helpers`, `common` |
| API handlers | `api-handler`, `controller`, `endpoint` |
| Data models | `data-model`, `entity`, `schema` |
| Test files | `test`, `spec`, `unit-test` |
| Configuration | `configuration`, `build-system`, `settings` |
| Infrastructure | `infrastructure`, `deployment`, `containerization` |
| Documentation | `documentation`, `guide`, `reference` |
## Summary Style
Write 1-2 sentence summaries that:
- Describe **purpose** and **role** in the project
- Use active voice ("Provides...", "Handles...", "Manages...")
- Avoid restating the filename
**Examples:**
- Good: "Provides date formatting and string sanitization helpers used across the API layer."
- Bad: "The utils file contains utility functions."
## Technical Terms
Keep these terms in English (no translation needed):
- `middleware`, `hook`, `barrel`, `entry-point`
- `ORM`, `REST API`, `CI/CD`, `CRUD`
- `singleton`, `factory`, `observer`
- `middleware`, `interceptor`, `guard`
## Layer Names
Use standard English layer names:
- `API Layer`, `Service Layer`, `Data Layer`, `UI Layer`
- `Infrastructure`, `Configuration`, `Documentation`
- `Utility Layer`, `Middleware Layer`, `Test Layer`
@@ -0,0 +1,49 @@
# 日本語出力ガイドライン (Japanese)
本ファイルは、日本語でナレッジグラフコンテンツを生成する際の言語固有のガイドラインを提供します。
## タグの命名規則
日本語タグまたは英語の一般的な技術用語を使用:
| パターン | 推奨タグ |
|---------|---------|
| エントリーポイント | `入口点`, `barrel`, `exports` または `entry-point` |
| ユーティリティ | `ユーティリティ`, `helpers`, `utility` |
| APIハンドラー | `api-handler`, `controller`, `endpoint` |
| データモデル | `データモデル`, `entity`, `schema` または `data-model` |
| テストファイル | `テスト`, `unit-test`, `test` |
| 設定ファイル | `設定`, `build-system`, `configuration` |
| インフラ | `インフラ`, `deployment`, `infrastructure` |
| ドキュメント | `ドキュメント`, `guide`, `documentation` |
**混合戦略:** 一般的な技術用語は英語を保持(`middleware`, `api-handler`など)、説明用タグは日本語を使用可能。
## サマリーのスタイル
1-2文のサマリーを日本語で記述:
- ファイルの**目的**と**役割**を説明
- 能動態を使用(「提供する...」「処理する...」「管理する...」)
- ファイル名の繰り返しを避ける
**例:**
- 良い: "API層全体で使用される日付フォーマットと文字列サニタイズのヘルパー関数を提供。"
- 悪い: "utilsファイルにはユーティリティ関数が含まれています。"
## 技術用語
以下の用語は英語を保持(標準翻訳がない場合):
- `middleware`, `hook`, `barrel`, `entry-point`
- `ORM`, `REST API`, `CI/CD`, `CRUD`
- `singleton`, `factory`, `observer`
- `interceptor`, `guard`
## レイヤー名
日本語のレイヤー名を使用:
- `API層`, `サービス層`, `データ層`, `UI層`
- `インフラ`, `設定`, `ドキュメント`
- `ユーティリティ層`, `ミドルウェア層`, `テスト層`
または英語を保持(チームの慣習に従う):
- `API Layer`, `Service Layer`, `Data Layer`
@@ -0,0 +1,49 @@
# 한국어 출력 가이드라인 (Korean)
이 파일은 한국어로 지식 그래프 콘텐츠를 생성할 때의 언어별 가이드를 제공합니다.
## 태그 명명 규칙
한국어 태그 또는 영어 일반 기술 용어 사용:
| 패턴 | 추천 태그 |
|------|---------|
| 진입점 파일 | `진입점`, `barrel`, `exports` 또는 `entry-point` |
| 유틸리티 함수 | `유틸리티`, `helpers`, `utility` |
| API 핸들러 | `api-handler`, `controller`, `endpoint` |
| 데이터 모델 | `데이터모델`, `entity`, `schema` 또는 `data-model` |
| 테스트 파일 | `테스트`, `unit-test`, `test` |
| 설정 파일 | `설정`, `build-system`, `configuration` |
| 인프라 | `인프라`, `deployment`, `infrastructure` |
| 문서 | `문서`, `guide`, `documentation` |
**혼합 전략:** 일반 기술 용어는 영어 유지 (`middleware`, `api-handler` 등), 설명용 태그는 한국어 사용 가능.
## 요약 스타일
1-2문장 요약을 한국어로 작성:
- 파일의 **목적**과 **역할** 설명
- 능동태 사용 ("제공하는...", "처리하는...", "관리하는...")
- 파일명 반복 피하기
**예시:**
- 좋음: "API 레이어 전체에서 사용되는 날짜 포맷 및 문자열 정제 헬per 함수를 제공."
- 나쁨: "utils 파일에는 유틸리티 함수가 포함되어 있습니다."
## 기술 용어
다음 용어는 영어 유지 (표준 번역 없음):
- `middleware`, `hook`, `barrel`, `entry-point`
- `ORM`, `REST API`, `CI/CD`, `CRUD`
- `singleton`, `factory`, `observer`
- `interceptor`, `guard`
## 레이어 이름
한국어 레이어 이름 사용:
- `API 레이어`, `서비스 레이어`, `데이터 레이어`, `UI 레이어`
- `인프라`, `설정`, `문서`
- `유틸리티 레이어`, `미들웨어 레이어`, `테스트 레이어`
또는 영어 유지 (팀 관습에 따라):
- `API Layer`, `Service Layer`, `Data Layer`
@@ -0,0 +1,49 @@
# 繁體中文輸出指南 (Chinese Traditional)
本文件提供生成繁體中文知識圖譜內容的語言指導。
## 標籤約定
推薦使用繁體中文標籤或英文通用技術術語:
| 模式 | 推薦標籤 |
|------|---------|
| 入口檔案 | `入口點`, `barrel`, `匯出``entry-point` |
| 工具函數 | `工具函數`, `helpers`, `common``utility` |
| API處理器 | `api-handler`, `控制器`, `端點` |
| 資料模型 | `資料模型`, `entity`, `schema``data-model` |
| 測試檔案 | `測試`, `單元測試`, `test` |
| 設定檔 | `設定`, `建構系統`, `settings``configuration` |
| 基礎架構 | `基礎架構`, `部署`, `容器化``infrastructure` |
| 文件 | `文件`, `指南`, `參考``documentation` |
**混合策略:** 通用技術術語保留英文(如 `middleware`, `api-handler`),描述性標籤可使用繁體中文。
## 摘要風格
用繁體中文撰寫1-2句摘要:
- 描述檔案的**目的**和**作用**
- 使用主動語態("提供...", "處理...", "管理..."
- 避免重複檔名
**範例:**
- 好: "提供日期格式化和字串清洗工具函數,被 API 層廣泛使用。"
- 差: "utils 檔案包含工具函數。"
## 技術術語
以下術語建議保留英文(暫無標準翻譯):
- `middleware`, `hook`, `barrel`, `entry-point`
- `ORM`, `REST API`, `CI/CD`, `CRUD`
- `singleton`, `factory`, `observer`
- `interceptor`, `guard`
## 層級名稱
使用繁體中文層級名稱:
- `API 層`, `服務層`, `資料層`, `UI 層`
- `基礎架構`, `設定`, `文件`
- `工具層`, `中介軟體層`, `測試層`
或保留英文(根據團隊習慣):
- `API Layer`, `Service Layer`, `Data Layer`
@@ -0,0 +1,49 @@
# 中文输出指南 (Chinese Simplified)
本文件提供生成中文知识图谱内容的语言指导。
## 标签约定
推荐使用中文标签或英文通用技术术语:
| 模式 | 推荐标签 |
|------|---------|
| 入口文件 | `入口点`, `barrel`, `导出``entry-point` |
| 工具函数 | `工具函数`, `helpers`, `common``utility` |
| API处理器 | `api-handler`, `控制器`, `端点` |
| 数据模型 | `数据模型`, `entity`, `schema``data-model` |
| 测试文件 | `测试`, `单元测试`, `test` |
| 配置文件 | `配置`, `构建系统`, `settings``configuration` |
| 基础设施 | `基础设施`, `部署`, `容器化``infrastructure` |
| 文档 | `文档`, `指南`, `参考``documentation` |
**混合策略:** 通用技术术语保留英文(如 `middleware`, `api-handler`),描述性标签可使用中文。
## 摘要风格
用中文撰写1-2句摘要:
- 描述文件的**目的**和**作用**
- 使用主动语态("提供...", "处理...", "管理..."
- 避免重复文件名
**示例:**
- 好: "提供日期格式化和字符串清洗工具函数,被 API 层广泛使用。"
- 差: "utils 文件包含工具函数。"
## 技术术语
以下术语建议保留英文(暂无标准翻译):
- `middleware`, `hook`, `barrel`, `entry-point`
- `ORM`, `REST API`, `CI/CD`, `CRUD`
- `singleton`, `factory`, `observer`
- `interceptor`, `guard`
## 层级名称
使用中文层级名称:
- `API 层`, `服务层`, `数据层`, `UI 层`
- `基础设施`, `配置`, `文档`
- `工具层`, `中间件层`, `测试层`
或保留英文(根据团队习惯):
- `API Layer`, `Service Layer`, `Data Layer`