From 656289121c676e736b56d8c8c1db78d925578232 Mon Sep 17 00:00:00 2001 From: zhushen <2270364052@qq.com> Date: Mon, 11 May 2026 12:20:05 +0800 Subject: [PATCH 1/5] feat: Add --language parameter for localized content generation Adds --language parameter to /understand command to generate knowledge graph content in user-specified language. Changes: - Update argument-hint and Options documentation in SKILL.md - Add language parsing logic in Phase 0 (language normalization, config persistence, LANGUAGE_DIRECTIVE template) - Inject language directive into agent dispatch prompts for all content-generating phases (Phase 1-5) - Add language directive handling instructions in agent definitions - Create locales/ directory with template files for: - English (en.md) - default - Chinese Simplified (zh.md) - Chinese Traditional (zh-TW.md) - Japanese (ja.md) - Korean (ko.md) Locale files provide language-specific guidance for: - Tag naming conventions - Summary writing style - Technical term handling - Layer name translations Closes #141 --- .../agents/architecture-analyzer.md | 5 ++ .../agents/file-analyzer.md | 6 +++ .../agents/project-scanner.md | 2 + .../agents/tour-builder.md | 6 +++ .../skills/understand/SKILL.md | 42 +++++++++++++--- .../skills/understand/locales/en.md | 44 +++++++++++++++++ .../skills/understand/locales/ja.md | 49 +++++++++++++++++++ .../skills/understand/locales/ko.md | 49 +++++++++++++++++++ .../skills/understand/locales/zh-TW.md | 49 +++++++++++++++++++ .../skills/understand/locales/zh.md | 49 +++++++++++++++++++ 10 files changed, 293 insertions(+), 8 deletions(-) create mode 100644 understand-anything-plugin/skills/understand/locales/en.md create mode 100644 understand-anything-plugin/skills/understand/locales/ja.md create mode 100644 understand-anything-plugin/skills/understand/locales/ko.md create mode 100644 understand-anything-plugin/skills/understand/locales/zh-TW.md create mode 100644 understand-anything-plugin/skills/understand/locales/zh.md diff --git a/understand-anything-plugin/agents/architecture-analyzer.md b/understand-anything-plugin/agents/architecture-analyzer.md index b6dab09..58c005c 100644 --- a/understand-anything-plugin/agents/architecture-analyzer.md +++ b/understand-anything-plugin/agents/architecture-analyzer.md @@ -14,6 +14,11 @@ You are an expert software architect. Your job is to analyze a codebase's file s Given a list of file nodes (with paths, summaries, tags, and node types) and import edges, identify 3-10 logical architecture layers and assign every file node to exactly one layer. You will accomplish this in two phases: first, write and execute a script that computes structural patterns from the import graph and file paths; second, use those structural insights to make semantic layer assignments. +**Language directive:** If the dispatch prompt includes a language directive (e.g., "Generate all textual content in **Chinese**"), apply it to: +- Layer `name` — Translate to the specified language (e.g., "API 层", "服务层", "基础设施层") +- Layer `description` — Write in the specified language using natural phrasing +Use native-level terminology. Keep established English terms when appropriate (e.g., "CI/CD", "ORM", "REST API" may remain untranslated in some languages). + --- ## Phase 1 -- Structural Analysis Script diff --git a/understand-anything-plugin/agents/file-analyzer.md b/understand-anything-plugin/agents/file-analyzer.md index d7d2e69..6d0e506 100644 --- a/understand-anything-plugin/agents/file-analyzer.md +++ b/understand-anything-plugin/agents/file-analyzer.md @@ -17,6 +17,12 @@ For each file in the batch provided to you, extract structural data via a script **File categories in this batch:** Each file has a `fileCategory` field indicating its type: `code`, `config`, `docs`, `infra`, `data`, `script`, or `markup`. Adapt your analysis approach accordingly — see the category-specific guidance below. +**Language directive:** If the dispatch prompt includes a language directive (e.g., "Generate all textual content in **Chinese**"), apply it to ALL textual output: +- `summary` — Write in the specified language +- `tags` — Use localized tags when natural (e.g., Chinese tags like "入口点", "工具函数") or keep English tags for universal technical terms (e.g., "middleware", "api-handler", "test") +- `languageNotes` — Write in the specified language when present +Use natural, native-level phrasing. Keep technical terms in English when no standard translation exists. + --- ## Phase 1 -- Structural Extraction (Bundled Script) diff --git a/understand-anything-plugin/agents/project-scanner.md b/understand-anything-plugin/agents/project-scanner.md index ed84a80..2cedacc 100644 --- a/understand-anything-plugin/agents/project-scanner.md +++ b/understand-anything-plugin/agents/project-scanner.md @@ -14,6 +14,8 @@ You are a meticulous project inventory specialist. Your job is to scan a codebas Scan the project directory provided in the prompt and produce a JSON inventory. You will accomplish this in two phases: first, write and execute a discovery script that performs all deterministic file scanning; second, review the script's results and add a human-readable project description. +**Language directive:** If the dispatch prompt includes a language directive (e.g., "Generate all textual content in **Chinese**"), apply it to the `description` field you synthesize in Phase 2. Write the description in the specified language using natural, native-level phrasing. Keep technical terms in English when no standard translation exists (e.g., "middleware", "hook", "barrel"). + --- ## Phase 1 -- Discovery Script diff --git a/understand-anything-plugin/agents/tour-builder.md b/understand-anything-plugin/agents/tour-builder.md index 41f5ef9..ce12d1b 100644 --- a/understand-anything-plugin/agents/tour-builder.md +++ b/understand-anything-plugin/agents/tour-builder.md @@ -14,6 +14,12 @@ You are an expert technical educator who designs learning paths through codebase Given a codebase's nodes, edges, and layers, design a guided tour that teaches the project's architecture and key concepts. The tour must reference only real node IDs from the provided graph data. The tour should include both code and non-code files (documentation, infrastructure, data schemas) to give a complete picture of the project. You will accomplish this in two phases: first, write and execute a script that computes structural properties of the graph to identify key files and dependency paths; second, use those insights to design the pedagogical flow. +**Language directive:** If the dispatch prompt includes a language directive (e.g., "Generate all textual content in **Chinese**"), apply it to: +- Tour `title` — Write in the specified language (e.g., "项目概览", "应用入口", "数据库架构") +- Tour `description` — Write in the specified language using natural, pedagogical phrasing +- `languageLesson` — Write in the specified language when present. Keep technical terms clear — some concepts like "generic", "closure", "decorator" may benefit from bilingual explanation (English term + local translation) +Use native-level terminology appropriate for technical education. + --- ## Phase 1 -- Graph Topology Script diff --git a/understand-anything-plugin/skills/understand/SKILL.md b/understand-anything-plugin/skills/understand/SKILL.md index aa47a6c..22115df 100644 --- a/understand-anything-plugin/skills/understand/SKILL.md +++ b/understand-anything-plugin/skills/understand/SKILL.md @@ -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 ]"] --- # /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 ` — 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 ` 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 `language` 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 `{"language": ""}` 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 /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: `` — `` > Languages: `` +> +> $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/.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/.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/.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/.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/.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: diff --git a/understand-anything-plugin/skills/understand/locales/en.md b/understand-anything-plugin/skills/understand/locales/en.md new file mode 100644 index 0000000..f254e5a --- /dev/null +++ b/understand-anything-plugin/skills/understand/locales/en.md @@ -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` \ No newline at end of file diff --git a/understand-anything-plugin/skills/understand/locales/ja.md b/understand-anything-plugin/skills/understand/locales/ja.md new file mode 100644 index 0000000..30eac60 --- /dev/null +++ b/understand-anything-plugin/skills/understand/locales/ja.md @@ -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` \ No newline at end of file diff --git a/understand-anything-plugin/skills/understand/locales/ko.md b/understand-anything-plugin/skills/understand/locales/ko.md new file mode 100644 index 0000000..34d1736 --- /dev/null +++ b/understand-anything-plugin/skills/understand/locales/ko.md @@ -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` \ No newline at end of file diff --git a/understand-anything-plugin/skills/understand/locales/zh-TW.md b/understand-anything-plugin/skills/understand/locales/zh-TW.md new file mode 100644 index 0000000..65e220a --- /dev/null +++ b/understand-anything-plugin/skills/understand/locales/zh-TW.md @@ -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` \ No newline at end of file diff --git a/understand-anything-plugin/skills/understand/locales/zh.md b/understand-anything-plugin/skills/understand/locales/zh.md new file mode 100644 index 0000000..ef02b5b --- /dev/null +++ b/understand-anything-plugin/skills/understand/locales/zh.md @@ -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` \ No newline at end of file From 752fe59e0c90774a9bbf57b382fbbc8357e16068 Mon Sep 17 00:00:00 2001 From: zhushen <2270364052@qq.com> Date: Mon, 11 May 2026 19:00:05 +0800 Subject: [PATCH 2/5] feat(dashboard): Add i18n support for localized UI text - Add outputLanguage field to ProjectConfig type - Create /config.json endpoint in vite.config.ts - Build locale files for 5 languages (en, zh, zh-TW, ja, ko) - Add I18nProvider context and useI18n hook - Update 5 components (ProjectOverview, NodeInfo, FileExplorer, FilterPanel, PersonaSelector) - Dashboard reads language from config.json and displays localized UI All tests passed: - Core: 670 tests - Dashboard: 42 tests --- .../packages/core/src/persistence/index.ts | 2 +- .../core/src/persistence/persistence.test.ts | 4 +- .../packages/core/src/types.ts | 3 +- .../packages/dashboard/src/App.tsx | 13 +- .../dashboard/src/components/FileExplorer.tsx | 10 +- .../dashboard/src/components/FilterPanel.tsx | 14 ++- .../dashboard/src/components/NodeInfo.tsx | 99 +++++---------- .../src/components/PersonaSelector.tsx | 38 +++--- .../src/components/ProjectOverview.tsx | 54 ++++---- .../dashboard/src/contexts/I18nContext.tsx | 44 +++++++ .../packages/dashboard/src/locales/en.ts | 115 ++++++++++++++++++ .../packages/dashboard/src/locales/index.ts | 32 +++++ .../packages/dashboard/src/locales/ja.ts | 115 ++++++++++++++++++ .../packages/dashboard/src/locales/ko.ts | 115 ++++++++++++++++++ .../packages/dashboard/src/locales/zh-TW.ts | 115 ++++++++++++++++++ .../packages/dashboard/src/locales/zh.ts | 115 ++++++++++++++++++ .../packages/dashboard/vite.config.ts | 19 +++ .../pnpm-workspace.yaml | 13 ++ 18 files changed, 788 insertions(+), 132 deletions(-) create mode 100644 understand-anything-plugin/packages/dashboard/src/contexts/I18nContext.tsx create mode 100644 understand-anything-plugin/packages/dashboard/src/locales/en.ts create mode 100644 understand-anything-plugin/packages/dashboard/src/locales/index.ts create mode 100644 understand-anything-plugin/packages/dashboard/src/locales/ja.ts create mode 100644 understand-anything-plugin/packages/dashboard/src/locales/ko.ts create mode 100644 understand-anything-plugin/packages/dashboard/src/locales/zh-TW.ts create mode 100644 understand-anything-plugin/packages/dashboard/src/locales/zh.ts diff --git a/understand-anything-plugin/packages/core/src/persistence/index.ts b/understand-anything-plugin/packages/core/src/persistence/index.ts index f14b011..d69857b 100644 --- a/understand-anything-plugin/packages/core/src/persistence/index.ts +++ b/understand-anything-plugin/packages/core/src/persistence/index.ts @@ -130,7 +130,7 @@ export function loadFingerprints(projectRoot: string): FingerprintStore | null { } } -const DEFAULT_CONFIG: ProjectConfig = { autoUpdate: false }; +const DEFAULT_CONFIG: ProjectConfig = { autoUpdate: false, outputLanguage: "en" }; export function saveConfig(projectRoot: string, config: ProjectConfig): void { const dir = ensureDir(projectRoot); diff --git a/understand-anything-plugin/packages/core/src/persistence/persistence.test.ts b/understand-anything-plugin/packages/core/src/persistence/persistence.test.ts index 02e0a1c..3a694fb 100644 --- a/understand-anything-plugin/packages/core/src/persistence/persistence.test.ts +++ b/understand-anything-plugin/packages/core/src/persistence/persistence.test.ts @@ -189,7 +189,7 @@ describe("persistence", () => { it("should return default config when no file exists", () => { const loaded = loadConfig(tempDir); - expect(loaded).toEqual({ autoUpdate: false }); + expect(loaded).toEqual({ autoUpdate: false, outputLanguage: "en" }); }); it("should return default config when config.json is corrupted", () => { @@ -198,7 +198,7 @@ describe("persistence", () => { writeFileSync(join(dir, "config.json"), "not json!!", "utf-8"); const loaded = loadConfig(tempDir); - expect(loaded).toEqual({ autoUpdate: false }); + expect(loaded).toEqual({ autoUpdate: false, outputLanguage: "en" }); }); }); }); diff --git a/understand-anything-plugin/packages/core/src/types.ts b/understand-anything-plugin/packages/core/src/types.ts index 890a9f0..b7a0fa6 100644 --- a/understand-anything-plugin/packages/core/src/types.ts +++ b/understand-anything-plugin/packages/core/src/types.ts @@ -113,9 +113,10 @@ export interface AnalysisMeta { theme?: ThemeConfig; } -// Project config (for auto-update opt-in) +// Project config (for auto-update opt-in and language preference) export interface ProjectConfig { autoUpdate: boolean; + outputLanguage?: string; } // Non-code structural sub-interfaces diff --git a/understand-anything-plugin/packages/dashboard/src/App.tsx b/understand-anything-plugin/packages/dashboard/src/App.tsx index 4aed9bf..ef41f9b 100644 --- a/understand-anything-plugin/packages/dashboard/src/App.tsx +++ b/understand-anything-plugin/packages/dashboard/src/App.tsx @@ -23,6 +23,7 @@ import type { KeyboardShortcut } from "./hooks/useKeyboardShortcuts"; import { ThemeProvider } from "./themes/index.ts"; import { ThemePicker } from "./components/ThemePicker.tsx"; import type { ThemeConfig } from "./themes/index.ts"; +import { I18nProvider } from "./contexts/I18nContext.tsx"; // Lazy-load heavy / optional components so they ship in separate chunks. const CodeViewer = lazy(() => import("./components/CodeViewer")); @@ -44,6 +45,7 @@ function dataUrl(fileName: string, token: string | null): string { "domain-graph.json": import.meta.env.VITE_DOMAIN_GRAPH_URL, "meta.json": import.meta.env.VITE_META_URL, "diff-overlay.json": import.meta.env.VITE_DIFF_OVERLAY_URL, + "config.json": import.meta.env.VITE_CONFIG_URL, }; const url = envMap[fileName]; if (url) return url; @@ -118,6 +120,7 @@ function Dashboard({ accessToken }: { accessToken: string }) { const [showKeyboardHelp, setShowKeyboardHelp] = useState(false); const [metaTheme, setMetaTheme] = useState(null); const [sidebarTab, setSidebarTab] = useState("info"); + const [outputLanguage, setOutputLanguage] = useState(); const viewMode = useDashboardStore((s) => s.viewMode); const setViewMode = useDashboardStore((s) => s.setViewMode); const isKnowledgeGraph = useDashboardStore((s) => s.isKnowledgeGraph); @@ -139,6 +142,12 @@ function Dashboard({ accessToken }: { accessToken: string }) { if (meta?.theme) setMetaTheme(meta.theme); }) .catch(() => {}); + fetch(dataUrl("config.json", accessToken)) + .then((r) => (r.ok ? r.json() : null)) + .then((config) => { + if (config?.outputLanguage) setOutputLanguage(config.outputLanguage); + }) + .catch(() => {}); }, []); useEffect(() => { @@ -399,7 +408,8 @@ function Dashboard({ accessToken }: { accessToken: string }) { } return ( - + +
{/* Header */}
@@ -662,6 +672,7 @@ function Dashboard({ accessToken }: { accessToken: string }) { )}
+
); } diff --git a/understand-anything-plugin/packages/dashboard/src/components/FileExplorer.tsx b/understand-anything-plugin/packages/dashboard/src/components/FileExplorer.tsx index a0697d1..e4940f6 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/FileExplorer.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/FileExplorer.tsx @@ -1,6 +1,7 @@ import { useMemo, useState } from "react"; import type { GraphNode } from "@understand-anything/core/types"; import { useDashboardStore } from "../store"; +import { useI18n } from "../contexts/I18nContext"; interface FileEntry { name: string; @@ -142,6 +143,7 @@ export default function FileExplorer() { const graph = useDashboardStore((s) => s.graph); const openCodeViewer = useDashboardStore((s) => s.openCodeViewer); const navigateToNode = useDashboardStore((s) => s.navigateToNode); + const { t } = useI18n(); const entries = useMemo(() => buildFileTree(graph?.nodes ?? []), [graph]); const [expanded, setExpanded] = useState>(() => new Set()); @@ -176,7 +178,7 @@ export default function FileExplorer() { if (!graph) { return (
- No graph loaded + {t.common.noGraphLoaded}
); } @@ -185,15 +187,15 @@ export default function FileExplorer() {
- Analyzed Files + {t.fileExplorer.analyzedFiles}
- {totalFiles} files from the current knowledge graph + {totalFiles} {t.fileExplorer.filesFromGraph}
{entries.length === 0 ? ( -
No file paths found.
+
{t.fileExplorer.noFilePathsFound}
) : ( entries.map((entry) => ( s.graph); @@ -10,6 +11,7 @@ export default function FilterPanel() { const hasActiveFilters = useDashboardStore((s) => s.hasActiveFilters); const filterPanelOpen = useDashboardStore((s) => s.filterPanelOpen); const toggleFilterPanel = useDashboardStore((s) => s.toggleFilterPanel); + const { t } = useI18n(); const containerRef = useRef(null); @@ -97,7 +99,7 @@ export default function FilterPanel() { d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" /> - Filter + {t.common.filter} {filterPanelOpen && ( @@ -106,7 +108,7 @@ export default function FilterPanel() { {/* Node Types */}

- Node Types + {t.filterPanel.nodeTypes}

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

- Complexity + {t.filterPanel.complexity}

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

- Layers + {t.filterPanel.layers}

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

- Edge Categories + {t.filterPanel.edgeCategories}

{allEdgeCategories.map((category) => ( @@ -206,7 +208,7 @@ export default function FilterPanel() { onClick={resetFilters} className="w-full px-3 py-1.5 text-sm bg-elevated hover:bg-gold/20 text-text-secondary hover:text-gold rounded-lg transition-colors" > - Reset All + {t.common.resetAll} )}
diff --git a/understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx b/understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx index eb954f4..2af4993 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx @@ -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 = { 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 = { - 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)[edgeType]; +function getDirectionalLabel(edgeType: string, isSource: boolean, t: ReturnType["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
{categoryNode && (
-

Category

+

{t.nodeInfo.category}

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

- Tags + {t.common.tags}

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

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

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

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

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

Loading project...

+

{t.common.loading}

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

File Types

+

{t.projectOverview.fileTypes}

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

Languages

+

{t.projectOverview.languages}

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

Frameworks

+

{t.projectOverview.frameworks}

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

Node Type Distribution

+

{t.projectOverview.nodeTypeDistribution}

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

Complexity Distribution

+

{t.projectOverview.complexityDistribution}

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

Most Connected Nodes

+

{t.projectOverview.mostConnectedNodes}

{topNodes.map((node, idx) => (
- Avg Connections per Node + {t.projectOverview.avgConnectionsPerNode} {avgConnections}
{/* Analyzed at */}
- Analyzed: {new Date(project.analyzedAt).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })} + {t.common.analyzed}: {new Date(project.analyzedAt).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })}
{/* Start Tour button */} @@ -216,7 +214,7 @@ export default function ProjectOverview() { onClick={startTour} className="w-full bg-accent/10 border border-accent/30 text-accent text-sm font-medium py-2.5 px-4 rounded-lg hover:bg-accent/20 transition-all duration-200" > - Start Guided Tour + {t.common.startGuidedTour} )}
diff --git a/understand-anything-plugin/packages/dashboard/src/contexts/I18nContext.tsx b/understand-anything-plugin/packages/dashboard/src/contexts/I18nContext.tsx new file mode 100644 index 0000000..08adb7b --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/contexts/I18nContext.tsx @@ -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(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 ( + + {children} + + ); +} \ No newline at end of file diff --git a/understand-anything-plugin/packages/dashboard/src/locales/en.ts b/understand-anything-plugin/packages/dashboard/src/locales/en.ts new file mode 100644 index 0000000..0655e89 --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/locales/en.ts @@ -0,0 +1,115 @@ +export const en = { + common: { + loading: "Loading project...", + noGraphLoaded: "No graph loaded", + selectNode: "Select a node to see details", + back: "Back", + focus: "Focus", + unfocus: "Unfocus", + openCode: "Open code", + file: "File", + tags: "Tags", + connections: "Connections", + filter: "Filter", + resetAll: "Reset All", + analyzed: "Analyzed", + startGuidedTour: "Start Guided Tour", + truncated: "(truncated)", + preview: "Preview", + doubleClickToOpen: "double-click to open", + }, + projectOverview: { + nodes: "Nodes", + edges: "Edges", + layers: "Layers", + types: "Types", + fileTypes: "File Types", + code: "Code", + config: "Config", + docs: "Docs", + infra: "Infra", + data: "Data", + domain: "Domain", + languages: "Languages", + frameworks: "Frameworks", + nodeTypeDistribution: "Node Type Distribution", + complexityDistribution: "Complexity Distribution", + simple: "Simple", + moderate: "Moderate", + complex: "Complex", + mostConnectedNodes: "Most Connected Nodes", + avgConnectionsPerNode: "Avg Connections per Node", + }, + nodeInfo: { + definedInThisFile: "Defined in this file", + languageConcepts: "Language Concepts", + category: "Category", + wikilinks: "Wikilinks", + backlinks: "Backlinks", + entities: "Entities", + businessRules: "Business Rules", + crossDomain: "Cross-Domain", + flows: "Flows", + entryPoint: "Entry Point", + steps: "Steps", + implementation: "Implementation", + }, + fileExplorer: { + analyzedFiles: "Analyzed Files", + filesFromGraph: "files from the current knowledge graph", + noFilePathsFound: "No file paths found.", + }, + filterPanel: { + nodeTypes: "Node Types", + complexity: "Complexity", + layers: "Layers", + edgeCategories: "Edge Categories", + }, + personaSelector: { + overview: "Overview", + overviewDesc: "High-level architecture view", + learn: "Learn", + learnDesc: "Full dashboard with guided learning", + deepDive: "Deep Dive", + deepDiveDesc: "Code-focused with chat", + }, + edgeLabels: { + imports: { forward: "imports", backward: "imported by" }, + exports: { forward: "exports to", backward: "exported by" }, + contains: { forward: "contains", backward: "contained in" }, + inherits: { forward: "inherits from", backward: "inherited by" }, + implements: { forward: "implements", backward: "implemented by" }, + calls: { forward: "calls", backward: "called by" }, + subscribes: { forward: "subscribes to", backward: "subscribed by" }, + publishes: { forward: "publishes to", backward: "consumed by" }, + middleware: { forward: "middleware for", backward: "uses middleware" }, + reads_from: { forward: "reads from", backward: "read by" }, + writes_to: { forward: "writes to", backward: "written by" }, + transforms: { forward: "transforms", backward: "transformed by" }, + validates: { forward: "validates", backward: "validated by" }, + depends_on: { forward: "depends on", backward: "depended on by" }, + tested_by: { forward: "tested by", backward: "tests" }, + configures: { forward: "configures", backward: "configured by" }, + related: { forward: "related to", backward: "related to" }, + similar_to: { forward: "similar to", backward: "similar to" }, + deploys: { forward: "deploys", backward: "deployed by" }, + serves: { forward: "serves", backward: "served by" }, + migrates: { forward: "migrates", backward: "migrated by" }, + documents: { forward: "documents", backward: "documented by" }, + provisions: { forward: "provisions", backward: "provisioned by" }, + routes: { forward: "routes to", backward: "routed from" }, + defines_schema: { forward: "defines schema for", backward: "schema defined by" }, + triggers: { forward: "triggers", backward: "triggered by" }, + contains_flow: { forward: "contains flow", backward: "flow in" }, + flow_step: { forward: "flow step", backward: "step of" }, + cross_domain: { forward: "cross-domain to", backward: "cross-domain from" }, + cites: { forward: "cites", backward: "cited by" }, + contradicts: { forward: "contradicts", backward: "contradicted by" }, + builds_on: { forward: "builds on", backward: "built upon by" }, + exemplifies: { forward: "exemplifies", backward: "exemplified by" }, + categorized_under: { forward: "categorized under", backward: "categorizes" }, + authored_by: { forward: "authored by", backward: "authored" }, + }, +}; + +export default en; \ No newline at end of file diff --git a/understand-anything-plugin/packages/dashboard/src/locales/index.ts b/understand-anything-plugin/packages/dashboard/src/locales/index.ts new file mode 100644 index 0000000..911861b --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/locales/index.ts @@ -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 = { + 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 }; \ No newline at end of file diff --git a/understand-anything-plugin/packages/dashboard/src/locales/ja.ts b/understand-anything-plugin/packages/dashboard/src/locales/ja.ts new file mode 100644 index 0000000..01bb52e --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/locales/ja.ts @@ -0,0 +1,115 @@ +export const ja = { + common: { + loading: "プロジェクトを読み込み中...", + noGraphLoaded: "知識グラフが読み込まれていません", + selectNode: "ノードを選択して詳細を表示", + back: "戻る", + focus: "フォーカス", + unfocus: "フォーカス解除", + openCode: "コードを開く", + file: "ファイル", + tags: "タグ", + connections: "接続", + filter: "フィルター", + resetAll: "すべてリセット", + analyzed: "分析日時", + startGuidedTour: "ガイド付きツアーを開始", + truncated: "(省略)", + preview: "プレビュー", + doubleClickToOpen: "ダブルクリックで開く", + }, + projectOverview: { + nodes: "ノード", + edges: "エッジ", + layers: "レイヤー", + types: "タイプ", + fileTypes: "ファイルタイプ", + code: "コード", + config: "設定", + docs: "ドキュメント", + infra: "インフラ", + data: "データ", + domain: "ドメイン", + languages: "プログラミング言語", + frameworks: "フレームワーク", + nodeTypeDistribution: "ノードタイプ分布", + complexityDistribution: "複雑度分布", + simple: "単純", + moderate: "中程度", + complex: "複雑", + mostConnectedNodes: "最も接続されているノード", + avgConnectionsPerNode: "ノード平均接続数", + }, + nodeInfo: { + definedInThisFile: "このファイルで定義", + languageConcepts: "言語概念", + category: "カテゴリ", + wikilinks: "Wikilinks", + backlinks: "Backlinks", + entities: "エンティティ", + businessRules: "ビジネスルール", + crossDomain: "クロスドメイン", + flows: "フロー", + entryPoint: "エントリポイント", + steps: "ステップ", + implementation: "実装", + }, + fileExplorer: { + analyzedFiles: "分析済みファイル", + filesFromGraph: "現在の知識グラフからのファイル", + noFilePathsFound: "ファイルパスが見つかりません。", + }, + filterPanel: { + nodeTypes: "ノードタイプ", + complexity: "複雑度", + layers: "レイヤー", + edgeCategories: "エッジカテゴリ", + }, + personaSelector: { + overview: "概要", + overviewDesc: "高レベルアーキテクチャビュー", + learn: "学習", + learnDesc: "ガイド付き学習付き完全ダッシュボード", + deepDive: "詳細", + deepDiveDesc: "コード中心のチャット", + }, + edgeLabels: { + imports: { forward: "インポート", backward: "インポートされる" }, + exports: { forward: "エクスポート", backward: "エクスポートされる" }, + contains: { forward: "含む", backward: "含まれる" }, + inherits: { forward: "継承", backward: "継承される" }, + implements: { forward: "実装", backward: "実装される" }, + calls: { forward: "呼び出す", backward: "呼び出される" }, + subscribes: { forward: "購読", backward: "購読される" }, + publishes: { forward: "公開", backward: "消費される" }, + middleware: { forward: "ミドルウェア", backward: "ミドルウェアを使用" }, + reads_from: { forward: "読み取り", backward: "読み取られる" }, + writes_to: { forward: "書き込み", backward: "書き込まれる" }, + transforms: { forward: "変換", backward: "変換される" }, + validates: { forward: "検証", backward: "検証される" }, + depends_on: { forward: "依存", backward: "依存される" }, + tested_by: { forward: "テストされる", backward: "テスト" }, + configures: { forward: "設定", backward: "設定される" }, + related: { forward: "関連", backward: "関連" }, + similar_to: { forward: "類似", backward: "類似" }, + deploys: { forward: "デプロイ", backward: "デプロイされる" }, + serves: { forward: "提供", backward: "提供される" }, + migrates: { forward: "移行", backward: "移行される" }, + documents: { forward: "ドキュメント化", backward: "ドキュメント化される" }, + provisions: { forward: "提供", backward: "提供される" }, + routes: { forward: "ルーティング", backward: "ルーティングされる" }, + defines_schema: { forward: "スキーマ定義", backward: "スキーマ定義される" }, + triggers: { forward: "トリガー", backward: "トリガーされる" }, + contains_flow: { forward: "フローを含む", backward: "フロー内" }, + flow_step: { forward: "フローステップ", backward: "ステップの" }, + cross_domain: { forward: "クロスドメイン", backward: "クロスドメインから" }, + cites: { forward: "引用", backward: "引用される" }, + contradicts: { forward: "矛盾", backward: "矛盾される" }, + builds_on: { forward: "基礎", backward: "基礎となる" }, + exemplifies: { forward: "例示", backward: "例示される" }, + categorized_under: { forward: "カテゴリ化", backward: "カテゴリ化する" }, + authored_by: { forward: "作成者", backward: "作成" }, + }, +}; + +export default ja; \ No newline at end of file diff --git a/understand-anything-plugin/packages/dashboard/src/locales/ko.ts b/understand-anything-plugin/packages/dashboard/src/locales/ko.ts new file mode 100644 index 0000000..6e1dc88 --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/locales/ko.ts @@ -0,0 +1,115 @@ +export const ko = { + common: { + loading: "프로젝트 로딩 중...", + noGraphLoaded: "지식 그래프가 로드되지 않음", + selectNode: "노드를 선택하여 상세 정보 확인", + back: "뒤로", + focus: "포커스", + unfocus: "포커스 해제", + openCode: "코드 열기", + file: "파일", + tags: "태그", + connections: "연결", + filter: "필터", + resetAll: "모두 재설정", + analyzed: "분석 시간", + startGuidedTour: "가이드 투어 시작", + truncated: "(생략)", + preview: "미리보기", + doubleClickToOpen: "두 번 클릭하여 열기", + }, + projectOverview: { + nodes: "노드", + edges: "엣지", + layers: "레이어", + types: "타입", + fileTypes: "파일 타입", + code: "코드", + config: "설정", + docs: "문서", + infra: "인프라", + data: "데이터", + domain: "도메인", + languages: "프로그래밍 언어", + frameworks: "프레임워크", + nodeTypeDistribution: "노드 타입 분포", + complexityDistribution: "복잡도 분포", + simple: "단순", + moderate: "중간", + complex: "복잡", + mostConnectedNodes: "가장 많이 연결된 노드", + avgConnectionsPerNode: "노드 평균 연결 수", + }, + nodeInfo: { + definedInThisFile: "이 파일에 정義", + languageConcepts: "언어 개념", + category: "카테고리", + wikilinks: "Wikilinks", + backlinks: "Backlinks", + entities: "엔티티", + businessRules: "비즈니스 규칙", + crossDomain: "크로스 도메인", + flows: "플로우", + entryPoint: "진입점", + steps: "단계", + implementation: "구현", + }, + fileExplorer: { + analyzedFiles: "분석된 파일", + filesFromGraph: "현재 지식 그래프의 파일", + noFilePathsFound: "파일 경로를 찾을 수 없습니다.", + }, + filterPanel: { + nodeTypes: "노드 타입", + complexity: "복잡도", + layers: "레이어", + edgeCategories: "엣지 카테고리", + }, + personaSelector: { + overview: "개요", + overviewDesc: "고수준 아키텍처 뷰", + learn: "학습", + learnDesc: "가이드 학습 포함 완전 대시보드", + deepDive: "심층", + deepDiveDesc: "코드 중심 채팅", + }, + edgeLabels: { + imports: { forward: "임포트", backward: "임포트됨" }, + exports: { forward: "내보내기", backward: "내보내기됨" }, + contains: { forward: "포함", backward: "포함됨" }, + inherits: { forward: "상속", backward: "상속됨" }, + implements: { forward: "구현", backward: "구현됨" }, + calls: { forward: "호출", backward: "호출됨" }, + subscribes: { forward: "구독", backward: "구독됨" }, + publishes: { forward: "게시", backward: "소비됨" }, + middleware: { forward: "미들웨어", backward: "미들웨어 사용" }, + reads_from: { forward: "읽기", backward: "읽기됨" }, + writes_to: { forward: "쓰기", backward: "쓰기됨" }, + transforms: { forward: "변환", backward: "변환됨" }, + validates: { forward: "검증", backward: "검증됨" }, + depends_on: { forward: "종속", backward: "종속됨" }, + tested_by: { forward: "테스트됨", backward: "테스트" }, + configures: { forward: "설정", backward: "설정됨" }, + related: { forward: "관련", backward: "관련" }, + similar_to: { forward: "유사", backward: "유사" }, + deploys: { forward: "배포", backward: "배포됨" }, + serves: { forward: "서비스", backward: "서비스됨" }, + migrates: { forward: "마이그레이션", backward: "마이그레이션됨" }, + documents: { forward: "문서화", backward: "문서화됨" }, + provisions: { forward: "제공", backward: "제공됨" }, + routes: { forward: "라우팅", backward: "라우팅됨" }, + defines_schema: { forward: "스키마 정의", backward: "스키마 정義됨" }, + triggers: { forward: "트리거", backward: "트리거됨" }, + contains_flow: { forward: "플로우 포함", backward: "플로우 내" }, + flow_step: { forward: "플로우 단계", backward: "단계의" }, + cross_domain: { forward: "크로스 도메인", backward: "크로스 도메인에서" }, + cites: { forward: "인용", backward: "인용됨" }, + contradicts: { forward: "반박", backward: "반박됨" }, + builds_on: { forward: "기반", backward: "기반됨" }, + exemplifies: { forward: "예시", backward: "예시됨" }, + categorized_under: { forward: "카테고리화", backward: "카테고리화함" }, + authored_by: { forward: "작성자", backward: "작성" }, + }, +}; + +export default ko; \ No newline at end of file diff --git a/understand-anything-plugin/packages/dashboard/src/locales/zh-TW.ts b/understand-anything-plugin/packages/dashboard/src/locales/zh-TW.ts new file mode 100644 index 0000000..f8c6f01 --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/locales/zh-TW.ts @@ -0,0 +1,115 @@ +export const zhTW = { + common: { + loading: "載入專案...", + noGraphLoaded: "未載入知識圖谱", + selectNode: "選擇節點查看詳情", + back: "返回", + focus: "聚焦", + unfocus: "取消聚焦", + openCode: "開啟程式碼", + file: "檔案", + tags: "標籤", + connections: "連結", + filter: "篩選", + resetAll: "重置全部", + analyzed: "分析時間", + startGuidedTour: "開始導覽", + truncated: "(已截斷)", + preview: "預覽", + doubleClickToOpen: "雙擊開啟", + }, + projectOverview: { + nodes: "節點", + edges: "邊", + layers: "層級", + types: "類型", + fileTypes: "檔案類型", + code: "程式碼", + config: "配置", + docs: "文件", + infra: "基礎設施", + data: "資料", + domain: "領域", + languages: "程式語言", + frameworks: "框架", + nodeTypeDistribution: "節點類型分布", + complexityDistribution: "複雜度分布", + simple: "簡單", + moderate: "中等", + complex: "複雜", + mostConnectedNodes: "連結最多的節點", + avgConnectionsPerNode: "節點平均連結數", + }, + nodeInfo: { + definedInThisFile: "在此檔案中定義", + languageConcepts: "語言概念", + category: "分類", + wikilinks: "維基連結", + backlinks: "反向連結", + entities: "實體", + businessRules: "業務規則", + crossDomain: "跨領域", + flows: "流程", + entryPoint: "入口點", + steps: "步驟", + implementation: "實作", + }, + fileExplorer: { + analyzedFiles: "已分析檔案", + filesFromGraph: "來自目前知識圖谱的檔案", + noFilePathsFound: "未找到檔案路徑。", + }, + filterPanel: { + nodeTypes: "節點類型", + complexity: "複雜度", + layers: "層級", + edgeCategories: "邊類別", + }, + personaSelector: { + overview: "概覽", + overviewDesc: "高層次架構視圖", + learn: "學習", + learnDesc: "完整儀表板與導覽學習", + deepDive: "深入", + deepDiveDesc: "程式碼聚焦與對話", + }, + edgeLabels: { + imports: { forward: "導入", backward: "被導入" }, + exports: { forward: "導出到", backward: "被導出" }, + contains: { forward: "包含", backward: "被包含" }, + inherits: { forward: "繼承自", backward: "被繼承" }, + implements: { forward: "實作", backward: "被實作" }, + calls: { forward: "呼叫", backward: "被呼叫" }, + subscribes: { forward: "訂閱", backward: "被訂閱" }, + publishes: { forward: "發布到", backward: "被消費" }, + middleware: { forward: "中介軟體", backward: "使用中介軟體" }, + reads_from: { forward: "讀取", backward: "被讀取" }, + writes_to: { forward: "寫入", backward: "被寫入" }, + transforms: { forward: "轉換", backward: "被轉換" }, + validates: { forward: "驗證", backward: "被驗證" }, + depends_on: { forward: "依賴", backward: "被依賴" }, + tested_by: { forward: "被測試", backward: "測試" }, + configures: { forward: "配置", backward: "被配置" }, + related: { forward: "相關", backward: "相關" }, + similar_to: { forward: "相似", backward: "相似" }, + deploys: { forward: "部署", backward: "被部署" }, + serves: { forward: "服務", backward: "被服務" }, + migrates: { forward: "遷移", backward: "被遷移" }, + documents: { forward: "文件化", backward: "被文件化" }, + provisions: { forward: "提供", backward: "被提供" }, + routes: { forward: "路由到", backward: "被路由" }, + defines_schema: { forward: "定義架構", backward: "架構被定義" }, + triggers: { forward: "觸發", backward: "被觸發" }, + contains_flow: { forward: "包含流程", backward: "流程所在" }, + flow_step: { forward: "流程步驟", backward: "步驟所属" }, + cross_domain: { forward: "跨領域到", backward: "跨領域来自" }, + cites: { forward: "引用", backward: "被引用" }, + contradicts: { forward: "反駁", backward: "被反駁" }, + builds_on: { forward: "基於", backward: "作為基礎" }, + exemplifies: { forward: "例證", backward: "被例證" }, + categorized_under: { forward: "归类於", backward: "归类" }, + authored_by: { forward: "作者", backward: "著作" }, + }, +}; + +export default zhTW; \ No newline at end of file diff --git a/understand-anything-plugin/packages/dashboard/src/locales/zh.ts b/understand-anything-plugin/packages/dashboard/src/locales/zh.ts new file mode 100644 index 0000000..00df8ec --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/locales/zh.ts @@ -0,0 +1,115 @@ +export const zh = { + common: { + loading: "加载项目...", + noGraphLoaded: "未加载知识图谱", + selectNode: "选择节点查看详情", + back: "返回", + focus: "聚焦", + unfocus: "取消聚焦", + openCode: "打开代码", + file: "文件", + tags: "标签", + connections: "连接", + filter: "筛选", + resetAll: "重置全部", + analyzed: "分析时间", + startGuidedTour: "开始导览", + truncated: "(已截断)", + preview: "预览", + doubleClickToOpen: "双击打开", + }, + projectOverview: { + nodes: "节点", + edges: "边", + layers: "层级", + types: "类型", + fileTypes: "文件类型", + code: "代码", + config: "配置", + docs: "文档", + infra: "基础设施", + data: "数据", + domain: "领域", + languages: "编程语言", + frameworks: "框架", + nodeTypeDistribution: "节点类型分布", + complexityDistribution: "复杂度分布", + simple: "简单", + moderate: "中等", + complex: "复杂", + mostConnectedNodes: "连接最多的节点", + avgConnectionsPerNode: "节点平均连接数", + }, + nodeInfo: { + definedInThisFile: "在此文件中定义", + languageConcepts: "语言概念", + category: "分类", + wikilinks: "维基链接", + backlinks: "反向链接", + entities: "实体", + businessRules: "业务规则", + crossDomain: "跨领域", + flows: "流程", + entryPoint: "入口点", + steps: "步骤", + implementation: "实现", + }, + fileExplorer: { + analyzedFiles: "已分析文件", + filesFromGraph: "来自当前知识图谱的文件", + noFilePathsFound: "未找到文件路径。", + }, + filterPanel: { + nodeTypes: "节点类型", + complexity: "复杂度", + layers: "层级", + edgeCategories: "边类别", + }, + personaSelector: { + overview: "概览", + overviewDesc: "高层次架构视图", + learn: "学习", + learnDesc: "完整仪表盘与导览学习", + deepDive: "深入", + deepDiveDesc: "代码聚焦与对话", + }, + edgeLabels: { + imports: { forward: "导入", backward: "被导入" }, + exports: { forward: "导出到", backward: "被导出" }, + contains: { forward: "包含", backward: "被包含" }, + inherits: { forward: "继承自", backward: "被继承" }, + implements: { forward: "实现", backward: "被实现" }, + calls: { forward: "调用", backward: "被调用" }, + subscribes: { forward: "订阅", backward: "被订阅" }, + publishes: { forward: "发布到", backward: "被消费" }, + middleware: { forward: "中间件", backward: "使用中间件" }, + reads_from: { forward: "读取", backward: "被读取" }, + writes_to: { forward: "写入", backward: "被写入" }, + transforms: { forward: "转换", backward: "被转换" }, + validates: { forward: "验证", backward: "被验证" }, + depends_on: { forward: "依赖", backward: "被依赖" }, + tested_by: { forward: "被测试", backward: "测试" }, + configures: { forward: "配置", backward: "被配置" }, + related: { forward: "相关", backward: "相关" }, + similar_to: { forward: "相似", backward: "相似" }, + deploys: { forward: "部署", backward: "被部署" }, + serves: { forward: "服务", backward: "被服务" }, + migrates: { forward: "迁移", backward: "被迁移" }, + documents: { forward: "文档化", backward: "被文档化" }, + provisions: { forward: "提供", backward: "被提供" }, + routes: { forward: "路由到", backward: "被路由" }, + defines_schema: { forward: "定义架构", backward: "架构被定义" }, + triggers: { forward: "触发", backward: "被触发" }, + contains_flow: { forward: "包含流程", backward: "流程所在" }, + flow_step: { forward: "流程步骤", backward: "步骤所属" }, + cross_domain: { forward: "跨领域到", backward: "跨领域来自" }, + cites: { forward: "引用", backward: "被引用" }, + contradicts: { forward: "反驳", backward: "被反驳" }, + builds_on: { forward: "基于", backward: "作为基础" }, + exemplifies: { forward: "例证", backward: "被例证" }, + categorized_under: { forward: "归类于", backward: "归类" }, + authored_by: { forward: "作者", backward: "著作" }, + }, +}; + +export default zh; \ No newline at end of file diff --git a/understand-anything-plugin/packages/dashboard/vite.config.ts b/understand-anything-plugin/packages/dashboard/vite.config.ts index c28056b..3e097da 100644 --- a/understand-anything-plugin/packages/dashboard/vite.config.ts +++ b/understand-anything-plugin/packages/dashboard/vite.config.ts @@ -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" diff --git a/understand-anything-plugin/pnpm-workspace.yaml b/understand-anything-plugin/pnpm-workspace.yaml index dee51e9..901bea1 100644 --- a/understand-anything-plugin/pnpm-workspace.yaml +++ b/understand-anything-plugin/pnpm-workspace.yaml @@ -1,2 +1,15 @@ packages: - "packages/*" +allowBuilds: + esbuild: set this to true or false + tree-sitter-c: set this to true or false + tree-sitter-c-sharp: set this to true or false + tree-sitter-cpp: set this to true or false + tree-sitter-go: set this to true or false + tree-sitter-java: set this to true or false + tree-sitter-javascript: set this to true or false + tree-sitter-php: set this to true or false + tree-sitter-python: set this to true or false + tree-sitter-ruby: set this to true or false + tree-sitter-rust: set this to true or false + tree-sitter-typescript: set this to true or false From e1650f627cfb51317a73a87bd567c98f031ef1a5 Mon Sep 17 00:00:00 2001 From: zhushen <2270364052@qq.com> Date: Mon, 11 May 2026 20:30:50 +0800 Subject: [PATCH 3/5] fix: Wrap MobileLayout with I18nProvider; use outputLanguage key in config P1: MobileLayout was missing I18nProvider wrapper, causing useI18n to throw error on mobile devices. Now both desktop and mobile layouts are wrapped with I18nProvider. P2: SKILL.md used 'language' key but Dashboard reads 'outputLanguage'. Fixed config.json key name to match ProjectConfig type definition. All tests passed: - Core: 670 tests - Dashboard: 42 tests --- .../packages/dashboard/src/App.tsx | 22 ++++++++++--------- .../skills/understand/SKILL.md | 4 ++-- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/understand-anything-plugin/packages/dashboard/src/App.tsx b/understand-anything-plugin/packages/dashboard/src/App.tsx index ef41f9b..5a3bf84 100644 --- a/understand-anything-plugin/packages/dashboard/src/App.tsx +++ b/understand-anything-plugin/packages/dashboard/src/App.tsx @@ -394,16 +394,18 @@ function Dashboard({ accessToken }: { accessToken: string }) { if (isMobile) { return ( - - - + + + + + ); } diff --git a/understand-anything-plugin/skills/understand/SKILL.md b/understand-anything-plugin/skills/understand/SKILL.md index 22115df..5faf83e 100644 --- a/understand-anything-plugin/skills/understand/SKILL.md +++ b/understand-anything-plugin/skills/understand/SKILL.md @@ -121,10 +121,10 @@ Determine whether to run a full analysis or incremental update. - `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 `language` field. If present, use that. + - 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 `{"language": ""}` into existing config. + - Update `$PROJECT_ROOT/.understand-anything/config.json` with the new language: merge `{"outputLanguage": ""}` into existing config. - Store as `$OUTPUT_LANGUAGE` for use throughout all phases. - **Language directive template:** Store as `$LANGUAGE_DIRECTIVE`: ```markdown From a3ec91bf3974615ce3298fbb21c71ab8d91b7ce2 Mon Sep 17 00:00:00 2001 From: zhushen <2270364052@qq.com> Date: Tue, 12 May 2026 01:53:58 +0800 Subject: [PATCH 4/5] feat(dashboard): Complete i18n translation for all UI components --- .../packages/dashboard/src/App.tsx | 413 +++++++++--------- .../dashboard/src/components/Breadcrumb.tsx | 10 +- .../dashboard/src/components/CodeViewer.tsx | 22 +- .../dashboard/src/components/CustomNode.tsx | 6 +- .../dashboard/src/components/DiffToggle.tsx | 12 +- .../src/components/DomainGraphView.tsx | 4 +- .../dashboard/src/components/ExportMenu.tsx | 12 +- .../src/components/KeyboardShortcutsHelp.tsx | 19 +- .../dashboard/src/components/LayerLegend.tsx | 6 +- .../dashboard/src/components/LearnPanel.tsx | 22 +- .../src/components/MobileBottomNav.tsx | 84 ++-- .../dashboard/src/components/MobileDrawer.tsx | 56 +-- .../dashboard/src/components/MobileLayout.tsx | 4 +- .../dashboard/src/components/SearchBar.tsx | 10 +- .../dashboard/src/components/ThemePicker.tsx | 18 +- .../packages/dashboard/src/locales/en.ts | 157 +++++++ .../packages/dashboard/src/locales/ja.ts | 157 +++++++ .../packages/dashboard/src/locales/ko.ts | 161 ++++++- .../packages/dashboard/src/locales/zh-TW.ts | 157 +++++++ .../packages/dashboard/src/locales/zh.ts | 157 +++++++ 20 files changed, 1158 insertions(+), 329 deletions(-) diff --git a/understand-anything-plugin/packages/dashboard/src/App.tsx b/understand-anything-plugin/packages/dashboard/src/App.tsx index 5a3bf84..edbb7fc 100644 --- a/understand-anything-plugin/packages/dashboard/src/App.tsx +++ b/understand-anything-plugin/packages/dashboard/src/App.tsx @@ -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(null); const [graphIssues, setGraphIssues] = useState([]); - const [showKeyboardHelp, setShowKeyboardHelp] = useState(false); const [metaTheme, setMetaTheme] = useState(null); - const [sidebarTab, setSidebarTab] = useState("info"); const [outputLanguage, setOutputLanguage] = useState(); - 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( - () => [ - // Help - { - key: "?", - shiftKey: true, - description: "Show keyboard shortcuts", - action: () => setShowKeyboardHelp((prev) => !prev), - category: "General", - }, - // Navigation - { - key: "Escape", - description: "Close panels and modals / go back to overview", - action: () => { - // Read from store at invocation time to avoid stale closures - const state = useDashboardStore.getState(); - if (state.pathFinderOpen) { - state.togglePathFinder(); - } else if (state.filterPanelOpen) { - state.toggleFilterPanel(); - } else if (state.exportMenuOpen) { - state.toggleExportMenu(); - } else if (state.codeViewerExpanded) { - state.collapseCodeViewer(); - } else if (state.codeViewerOpen) { - state.closeCodeViewer(); - } else if (state.selectedNodeId) { - state.selectNode(null); - } else if (state.navigationLevel === "layer-detail") { - state.navigateToOverview(); - } else if (state.tourActive) { - state.stopTour(); - } else { - setShowKeyboardHelp(false); - } - }, - category: "Navigation", - }, - { - key: "/", - description: "Focus search bar", - action: () => { - const searchInput = document.querySelector( - 'input[placeholder*="Search"]' - ); - searchInput?.focus(); - }, - category: "Navigation", - }, - // Tour controls - { - key: "ArrowRight", - description: "Next tour step", - action: () => { - const state = useDashboardStore.getState(); - if (state.tourActive) { - state.nextTourStep(); - } - }, - category: "Tour", - }, - { - key: "ArrowLeft", - description: "Previous tour step", - action: () => { - const state = useDashboardStore.getState(); - if (state.tourActive) { - state.prevTourStep(); - } - }, - category: "Tour", - }, - // View toggles - { - key: "d", - description: "Toggle diff mode", - action: () => { - const state = useDashboardStore.getState(); - state.toggleDiffMode(); - }, - category: "View", - }, - { - key: "f", - description: "Toggle filter panel", - action: () => { - const state = useDashboardStore.getState(); - state.toggleFilterPanel(); - }, - category: "View", - }, - { - key: "e", - description: "Toggle export menu", - action: () => { - const state = useDashboardStore.getState(); - state.toggleExportMenu(); - }, - category: "View", - }, - { - key: "p", - description: "Open path finder", - action: () => { - const state = useDashboardStore.getState(); - state.togglePathFinder(); - }, - category: "View", - }, - ], - [] - ); - - // Register keyboard shortcuts - useKeyboardShortcuts(shortcuts); - useEffect(() => { fetch(dataUrl("knowledge-graph.json", accessToken)) .then((res) => res.json()) @@ -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).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 ( + + + + + + ); +} + +function DashboardContent({ + accessToken, + loadError, + graphIssues, +}: { + accessToken: string; + loadError: string | null; + graphIssues: GraphIssue[]; +}) { + const graph = useDashboardStore((s) => s.graph); + const selectedNodeId = useDashboardStore((s) => s.selectedNodeId); + const tourActive = useDashboardStore((s) => s.tourActive); + const persona = useDashboardStore((s) => s.persona); + const codeViewerOpen = useDashboardStore((s) => s.codeViewerOpen); + const codeViewerExpanded = useDashboardStore((s) => s.codeViewerExpanded); + const expandCodeViewer = useDashboardStore((s) => s.expandCodeViewer); + const collapseCodeViewer = useDashboardStore((s) => s.collapseCodeViewer); + const pathFinderOpen = useDashboardStore((s) => s.pathFinderOpen); + const togglePathFinder = useDashboardStore((s) => s.togglePathFinder); + const nodeTypeFilters = useDashboardStore((s) => s.nodeTypeFilters); + const toggleNodeTypeFilter = useDashboardStore((s) => s.toggleNodeTypeFilter); + const detailLevel = useDashboardStore((s) => s.detailLevel); + const setDetailLevel = useDashboardStore((s) => s.setDetailLevel); + const showFunctionsInClassView = useDashboardStore((s) => s.showFunctionsInClassView); + const toggleShowFunctionsInClassView = useDashboardStore((s) => s.toggleShowFunctionsInClassView); + const [showKeyboardHelp, setShowKeyboardHelp] = useState(false); + const [sidebarTab, setSidebarTab] = useState("info"); + const viewMode = useDashboardStore((s) => s.viewMode); + const setViewMode = useDashboardStore((s) => s.setViewMode); + const isKnowledgeGraph = useDashboardStore((s) => s.isKnowledgeGraph); + const domainGraph = useDashboardStore((s) => s.domainGraph); + const layoutIssues = useDashboardStore((s) => s.layoutIssues); + const isMobile = useIsMobile(); + const { t } = useI18n(); + const allIssues = useMemo( + () => [...graphIssues, ...layoutIssues], + [graphIssues, layoutIssues], + ); + + useEffect(() => { + if (selectedNodeId) setSidebarTab("info"); + }, [selectedNodeId]); + + // Define keyboard shortcuts + const shortcuts = useMemo( + () => [ + // Help + { + key: "?", + shiftKey: true, + description: t.keyboardShortcuts.showHelp, + action: () => setShowKeyboardHelp((prev) => !prev), + category: "General", + }, + // Navigation + { + key: "Escape", + description: t.keyboardShortcuts.escapeDesc, + action: () => { + // Read from store at invocation time to avoid stale closures + const state = useDashboardStore.getState(); + if (state.pathFinderOpen) { + state.togglePathFinder(); + } else if (state.filterPanelOpen) { + state.toggleFilterPanel(); + } else if (state.exportMenuOpen) { + state.toggleExportMenu(); + } else if (state.codeViewerExpanded) { + state.collapseCodeViewer(); + } else if (state.codeViewerOpen) { + state.closeCodeViewer(); + } else if (state.selectedNodeId) { + state.selectNode(null); + } else if (state.navigationLevel === "layer-detail") { + state.navigateToOverview(); + } else if (state.tourActive) { + state.stopTour(); + } else { + setShowKeyboardHelp(false); + } + }, + category: "Navigation", + }, + { + key: "/", + description: t.keyboardShortcuts.focusSearch, + action: () => { + const searchInput = document.querySelector( + '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} ))}
@@ -394,31 +411,25 @@ function Dashboard({ accessToken }: { accessToken: string }) { if (isMobile) { return ( - - - - - + ); } return ( - -
{/* Header */}
{/* Left — fixed */}

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

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

No file selected

+

{t.codeViewer.noFile}

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

{state.error}

@@ -209,7 +211,7 @@ export default function CodeViewer({ {source && ( <>
- {source.lineCount} lines + {source.lineCount} {t.codeViewer.linesLabel} {formatBytes(source.sizeBytes)}
diff --git a/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx b/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx index bfc50b4..5dffd2d 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx @@ -2,6 +2,7 @@ import { memo } from "react"; import { Handle, Position } from "@xyflow/react"; import type { NodeProps, Node } from "@xyflow/react"; import type { NodeType } from "@understand-anything/core/types"; +import { useI18n } from "../contexts/I18nContext"; // Color maps keyed by NodeType — must be kept in sync with core NodeType union. const typeColors: Record = { @@ -88,6 +89,7 @@ function CustomNodeComponent({ const barColor = typeColors[knownType] ?? typeColors.file; const textColor = typeTextColors[knownType] ?? typeTextColors.file; const complexityColor = complexityColors[data.complexity] ?? complexityColors.simple; + const { t } = useI18n(); if (import.meta.env.DEV && !(knownType in typeColors)) { console.warn(`[CustomNode] Unknown node type "${data.nodeType}" — using "file" colors`); @@ -159,8 +161,8 @@ function CustomNodeComponent({ )}
diff --git a/understand-anything-plugin/packages/dashboard/src/components/DiffToggle.tsx b/understand-anything-plugin/packages/dashboard/src/components/DiffToggle.tsx index f912d1a..a3dd2ff 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/DiffToggle.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/DiffToggle.tsx @@ -1,10 +1,12 @@ import { useDashboardStore } from "../store"; +import { useI18n } from "../contexts/I18nContext"; export default function DiffToggle() { const diffMode = useDashboardStore((s) => s.diffMode); const toggleDiffMode = useDashboardStore((s) => s.toggleDiffMode); const changedNodeIds = useDashboardStore((s) => s.changedNodeIds); const affectedNodeIds = useDashboardStore((s) => s.affectedNodeIds); + const { t } = useI18n(); const hasDiff = changedNodeIds.size > 0; @@ -23,9 +25,9 @@ export default function DiffToggle() { title={ hasDiff ? diffMode - ? "Hide diff overlay" - : "Show diff overlay" - : "No diff data loaded" + ? t.diffToggle.hideOverlay + : t.diffToggle.showOverlay + : t.diffToggle.noData } > Diff {diffMode && hasDiff ? "ON" : "OFF"} @@ -39,7 +41,7 @@ export default function DiffToggle() { style={{ backgroundColor: "var(--color-diff-changed)" }} /> - Changed + {t.diffToggle.changed} ({changedNodeIds.size}) @@ -51,7 +53,7 @@ export default function DiffToggle() { style={{ backgroundColor: "var(--color-diff-affected)" }} /> - Affected + {t.diffToggle.affected} ({affectedNodeIds.size}) diff --git a/understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx b/understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx index 6730000..ff15846 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/DomainGraphView.tsx @@ -17,6 +17,7 @@ import type { FlowFlowNode } from "./FlowNode"; import StepNode from "./StepNode"; import type { StepFlowNode } from "./StepNode"; import { useDashboardStore } from "../store"; +import { useI18n } from "../contexts/I18nContext"; import { mergeElkPositions, nodesToElkInput } from "../utils/layout"; import { applyElkLayout } from "../utils/elk-layout"; import type { KnowledgeGraph, GraphNode } from "@understand-anything/core/types"; @@ -167,6 +168,7 @@ function DomainGraphViewInner() { const domainGraph = useDashboardStore((s) => s.domainGraph); const activeDomainId = useDashboardStore((s) => s.activeDomainId); const clearActiveDomain = useDashboardStore((s) => s.clearActiveDomain); + const { t } = useI18n(); // Build structural nodes/edges/dims synchronously; only the layout call // itself is async, so we memo the structural pieces and run ELK in an @@ -237,7 +239,7 @@ function DomainGraphViewInner() { onClick={() => clearActiveDomain()} className="px-3 py-1.5 text-xs rounded-lg bg-elevated border border-border-subtle text-text-secondary hover:text-text-primary transition-colors" > - Back to domains + {t.domainView.backToDomains}
)} diff --git a/understand-anything-plugin/packages/dashboard/src/components/ExportMenu.tsx b/understand-anything-plugin/packages/dashboard/src/components/ExportMenu.tsx index e3ed709..2a35fc9 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/ExportMenu.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/ExportMenu.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef } from "react"; import { useDashboardStore } from "../store"; +import { useI18n } from "../contexts/I18nContext"; import type { KnowledgeGraph } from "@understand-anything/core/types"; import { filterNodes, filterEdges } from "../utils/filters"; @@ -26,6 +27,7 @@ export default function ExportMenu() { const toggleExportMenu = useDashboardStore((s) => s.toggleExportMenu); const reactFlowInstance = useDashboardStore((s) => s.reactFlowInstance); const persona = useDashboardStore((s) => s.persona); + const { t } = useI18n(); const containerRef = useRef(null); @@ -218,7 +220,7 @@ export default function ExportMenu() { {exportMenuOpen && ( @@ -247,7 +249,7 @@ export default function ExportMenu() { - Export as PNG + {t.export.asPNG}
diff --git a/understand-anything-plugin/packages/dashboard/src/components/KeyboardShortcutsHelp.tsx b/understand-anything-plugin/packages/dashboard/src/components/KeyboardShortcutsHelp.tsx index b05c65e..87d0381 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/KeyboardShortcutsHelp.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/KeyboardShortcutsHelp.tsx @@ -1,5 +1,6 @@ import type { KeyboardShortcut } from "../hooks/useKeyboardShortcuts"; import { formatShortcutKey } from "../hooks/useKeyboardShortcuts"; +import { useI18n } from "../contexts/I18nContext"; interface KeyboardShortcutsHelpProps { shortcuts: KeyboardShortcut[]; @@ -10,6 +11,8 @@ export default function KeyboardShortcutsHelp({ shortcuts, onClose, }: KeyboardShortcutsHelpProps) { + const { t } = useI18n(); + // Group shortcuts by category const groupedShortcuts = shortcuts.reduce((acc, shortcut) => { if (!acc[shortcut.category]) { @@ -19,6 +22,14 @@ export default function KeyboardShortcutsHelp({ return acc; }, {} as Record); + // Translate category names + const categoryTranslations: Record = { + "General": t.keyboardShortcuts.general, + "Navigation": t.keyboardShortcuts.navigation, + "Tour": t.keyboardShortcuts.tour, + "View": t.keyboardShortcuts.view, + }; + return (

- Keyboard Shortcuts + {t.keyboardShortcuts.title}

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

- Steps + {t.learnPanel.steps}

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

- Tour + {t.learnPanel.tour}

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

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

{searchQuery.trim() && ( - {searchResults.length} result{searchResults.length !== 1 ? "s" : ""}{" "} + {searchResults.length} {t.search.result}{searchResults.length !== 1 ? "s" : ""}{" "} ({searchMode}) )} diff --git a/understand-anything-plugin/packages/dashboard/src/components/ThemePicker.tsx b/understand-anything-plugin/packages/dashboard/src/components/ThemePicker.tsx index 49eea73..8c7e9e7 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/ThemePicker.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/ThemePicker.tsx @@ -1,11 +1,13 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useTheme, PRESETS } from "../themes/index.ts"; import type { HeadingFont } from "../themes/index.ts"; +import { useI18n } from "../contexts/I18nContext"; export function ThemePicker() { const { config, preset, setPreset, setAccent, setHeadingFont } = useTheme(); const [open, setOpen] = useState(false); const ref = useRef(null); + const { t } = useI18n(); // Close on outside click useEffect(() => { @@ -41,7 +43,7 @@ export function ThemePicker() { {open && ( @@ -67,7 +69,7 @@ export function ThemePicker() { {/* Presets */}
- Theme + {t.themePicker.theme}
{PRESETS.map((p) => ( @@ -119,7 +121,7 @@ export function ThemePicker() { {/* Accent swatches */}
- Accent Color + {t.themePicker.accentColor}
{preset.accentSwatches.map((swatch) => ( @@ -141,13 +143,13 @@ export function ThemePicker() { {/* Heading font */}
- Heading Font + {t.themePicker.headingFont}
{([ - { id: "serif" as HeadingFont, label: "Serif", sample: "Aa" }, - { id: "sans" as HeadingFont, label: "Sans", sample: "Aa" }, - { id: "mono" as HeadingFont, label: "Mono", sample: "Aa" }, + { id: "serif" as HeadingFont, label: t.themePicker.serif, sample: "Aa" }, + { id: "sans" as HeadingFont, label: t.themePicker.sans, sample: "Aa" }, + { id: "mono" as HeadingFont, label: t.themePicker.mono, sample: "Aa" }, ]).map((opt) => (