feat(dashboard): Complete i18n translation for all UI components

This commit is contained in:
zhushen
2026-05-12 01:53:58 +08:00
Unverified
parent e1650f627c
commit a3ec91bf39
20 changed files with 1158 additions and 329 deletions
@@ -23,7 +23,7 @@ import type { KeyboardShortcut } from "./hooks/useKeyboardShortcuts";
import { ThemeProvider } from "./themes/index.ts";
import { ThemePicker } from "./components/ThemePicker.tsx";
import type { ThemeConfig } from "./themes/index.ts";
import { I18nProvider } from "./contexts/I18nContext.tsx";
import { I18nProvider, useI18n } from "./contexts/I18nContext.tsx";
// Lazy-load heavy / optional components so they ship in separate chunks.
const CodeViewer = lazy(() => import("./components/CodeViewer"));
@@ -97,43 +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 [outputLanguage, setOutputLanguage] = useState<string | undefined>();
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],
);
useEffect(() => {
fetch(dataUrl("meta.json", accessToken))
@@ -150,128 +120,6 @@ function Dashboard({ accessToken }: { accessToken: string }) {
.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())
@@ -280,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) {
@@ -327,9 +174,7 @@ function Dashboard({ accessToken }: { accessToken: string }) {
}
}
})
.catch(() => {
// Silently ignore - diff overlay is optional
});
.catch(() => {});
}, [setDiffOverlay]);
useEffect(() => {
@@ -347,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>(
'input[placeholder*="Search"]'
);
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.
@@ -382,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>
@@ -394,31 +411,25 @@ function Dashboard({ accessToken }: { accessToken: string }) {
if (isMobile) {
return (
<I18nProvider language={outputLanguage ?? "en"}>
<ThemeProvider metaTheme={metaTheme}>
<MobileLayout
accessToken={accessToken}
showKeyboardHelp={showKeyboardHelp}
setShowKeyboardHelp={setShowKeyboardHelp}
loadError={loadError}
allIssues={allIssues}
shortcuts={shortcuts}
/>
</ThemeProvider>
</I18nProvider>
<MobileLayout
accessToken={accessToken}
showKeyboardHelp={showKeyboardHelp}
setShowKeyboardHelp={setShowKeyboardHelp}
loadError={loadError}
allIssues={allIssues}
shortcuts={shortcuts}
/>
);
}
return (
<I18nProvider language={outputLanguage ?? "en"}>
<ThemeProvider metaTheme={metaTheme}>
<div className="h-screen w-screen flex flex-col bg-root text-text-primary noise-overlay">
{/* Header */}
<header className="flex items-center px-3 sm:px-5 py-3 bg-surface border-b border-border-subtle shrink-0 gap-2 sm:gap-4">
{/* 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 />
@@ -429,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>
</>
@@ -467,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}
@@ -549,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"
@@ -564,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"
@@ -616,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>
@@ -673,8 +684,6 @@ function Dashboard({ accessToken }: { accessToken: string }) {
</Suspense>
)}
</div>
</ThemeProvider>
</I18nProvider>
);
}
@@ -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,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 { 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,7 @@ export default function SearchBar() {
value={searchQuery}
onChange={handleInputChange}
onFocus={() => setDropdownOpen(true)}
placeholder="Search nodes by name, summary, or tags..."
placeholder={t.search.placeholder}
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 +118,7 @@ export default function SearchBar() {
: "text-text-muted hover:text-text-secondary"
}`}
>
Fuzzy
{t.search.fuzzy}
</button>
<button
onClick={() => setSearchMode("semantic")}
@@ -126,12 +128,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}
@@ -17,6 +17,10 @@ export const en = {
truncated: "(truncated)",
preview: "Preview",
doubleClickToOpen: "double-click to open",
appName: "Understand Anything",
pressKeyboard: "Press ? for keyboard shortcuts",
path: "Path",
theme: "Theme",
},
projectOverview: {
nodes: "Nodes",
@@ -30,6 +34,7 @@ export const en = {
infra: "Infra",
data: "Data",
domain: "Domain",
knowledge: "Knowledge",
languages: "Languages",
frameworks: "Frameworks",
nodeTypeDistribution: "Node Type Distribution",
@@ -73,6 +78,155 @@ export const en = {
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" },
@@ -110,6 +264,9 @@ export const en = {
categorized_under: { forward: "categorized under", backward: "categorizes" },
authored_by: { forward: "authored by", backward: "authored" },
},
pathFinder: {
title: "Find path between nodes (P)",
},
};
export default en;
@@ -17,6 +17,10 @@ export const ja = {
truncated: "(省略)",
preview: "プレビュー",
doubleClickToOpen: "ダブルクリックで開く",
appName: "Understand Anything",
pressKeyboard: "? を押してキーボードショートカットを表示",
path: "パス",
theme: "テーマ",
},
projectOverview: {
nodes: "ノード",
@@ -30,6 +34,7 @@ export const ja = {
infra: "インフラ",
data: "データ",
domain: "ドメイン",
knowledge: "ナレッジ",
languages: "プログラミング言語",
frameworks: "フレームワーク",
nodeTypeDistribution: "ノードタイプ分布",
@@ -73,6 +78,155 @@ export const ja = {
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: "エクスポートされる" },
@@ -110,6 +264,9 @@ export const ja = {
categorized_under: { forward: "カテゴリ化", backward: "カテゴリ化する" },
authored_by: { forward: "作成者", backward: "作成" },
},
pathFinder: {
title: "ノード間のパスを検索 (P)",
},
};
export default ja;
@@ -17,6 +17,10 @@ export const ko = {
truncated: "(생략)",
preview: "미리보기",
doubleClickToOpen: "두 번 클릭하여 열기",
appName: "Understand Anything",
pressKeyboard: "? 키를 눌러 키보드 단축키 보기",
path: "경로",
theme: "테마",
},
projectOverview: {
nodes: "노드",
@@ -30,6 +34,7 @@ export const ko = {
infra: "인프라",
data: "데이터",
domain: "도메인",
knowledge: "지식",
languages: "프로그래밍 언어",
frameworks: "프레임워크",
nodeTypeDistribution: "노드 타입 분포",
@@ -73,7 +78,156 @@ export const ko = {
deepDive: "심층",
deepDiveDesc: "코드 중심 채팅",
},
edgeLabels: {
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: "포함됨" },
@@ -98,7 +252,7 @@ export const ko = {
documents: { forward: "문서화", backward: "문서화됨" },
provisions: { forward: "제공", backward: "제공됨" },
routes: { forward: "라우팅", backward: "라우팅됨" },
defines_schema: { forward: "스키마 정의", backward: "스키마 정됨" },
defines_schema: { forward: "스키마 정의", backward: "스키마 정됨" },
triggers: { forward: "트리거", backward: "트리거됨" },
contains_flow: { forward: "플로우 포함", backward: "플로우 내" },
flow_step: { forward: "플로우 단계", backward: "단계의" },
@@ -110,6 +264,9 @@ export const ko = {
categorized_under: { forward: "카테고리화", backward: "카테고리화함" },
authored_by: { forward: "작성자", backward: "작성" },
},
pathFinder: {
title: "노드 간 경로 찾기 (P)",
},
};
export default ko;
@@ -17,6 +17,10 @@ export const zhTW = {
truncated: "(已截斷)",
preview: "預覽",
doubleClickToOpen: "雙擊開啟",
appName: "Understand Anything",
pressKeyboard: "按 ? 查看鍵盤快捷鍵",
path: "路徑",
theme: "主題",
},
projectOverview: {
nodes: "節點",
@@ -30,6 +34,7 @@ export const zhTW = {
infra: "基礎設施",
data: "資料",
domain: "領域",
knowledge: "知識",
languages: "程式語言",
frameworks: "框架",
nodeTypeDistribution: "節點類型分布",
@@ -73,6 +78,155 @@ export const zhTW = {
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: "被導出" },
@@ -110,6 +264,9 @@ export const zhTW = {
categorized_under: { forward: "归类於", backward: "归类" },
authored_by: { forward: "作者", backward: "著作" },
},
pathFinder: {
title: "尋找節點間路徑 (P)",
},
};
export default zhTW;
@@ -17,6 +17,10 @@ export const zh = {
truncated: "(已截断)",
preview: "预览",
doubleClickToOpen: "双击打开",
appName: "Understand Anything",
pressKeyboard: "按 ? 查看键盘快捷键",
path: "路径",
theme: "主题",
},
projectOverview: {
nodes: "节点",
@@ -30,6 +34,7 @@ export const zh = {
infra: "基础设施",
data: "数据",
domain: "领域",
knowledge: "知识",
languages: "编程语言",
frameworks: "框架",
nodeTypeDistribution: "节点类型分布",
@@ -73,6 +78,155 @@ export const zh = {
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: "被导出" },
@@ -110,6 +264,9 @@ export const zh = {
categorized_under: { forward: "归类于", backward: "归类" },
authored_by: { forward: "作者", backward: "著作" },
},
pathFinder: {
title: "查找节点间路径 (P)",
},
};
export default zh;