From 2a1c8ae49f7323e8159ec6c9a2939b0c91a02fa5 Mon Sep 17 00:00:00 2001 From: "Mr.Lee" Date: Sat, 4 Apr 2026 11:39:35 +0800 Subject: [PATCH 1/8] feat: add internationalization support #34 --- CHANGELOG.md | 5 + README.md | 1 + l10n/bundle.l10n.json | 119 +++++++ l10n/bundle.l10n.zh-cn.json | 119 +++++++ l10n/bundle.l10n.zh-tw.json | 119 +++++++ package.json | 59 ++-- package.nls.json | 29 ++ package.nls.zh-cn.json | 29 ++ package.nls.zh-tw.json | 29 ++ pnpm-lock.yaml | 309 ++++++++++++++++++ scripts/check-l10n.js | 264 +++++++++++++++ src/extension.ts | 5 +- src/extension/webviewHtml.ts | 36 +- src/extension/webviewL10n.ts | 142 ++++++++ src/l10n.ts | 92 ++++++ src/statusBarItem.ts | 5 +- src/webview/dropdown.ts | 4 +- src/webview/l10n.d.ts | 16 + src/webview/main.ts | 270 ++++++++------- src/webview/utils.ts | 33 +- .../backend/queries/loadBranches/list.test.ts | 1 + tests/webview/__mocks__/vscode.ts | 34 ++ tests/webview/setup.ts | 2 + vitest.config.ts | 10 +- 24 files changed, 1552 insertions(+), 180 deletions(-) create mode 100644 l10n/bundle.l10n.json create mode 100644 l10n/bundle.l10n.zh-cn.json create mode 100644 l10n/bundle.l10n.zh-tw.json create mode 100644 package.nls.json create mode 100644 package.nls.zh-cn.json create mode 100644 package.nls.zh-tw.json create mode 100755 scripts/check-l10n.js create mode 100644 src/extension/webviewL10n.ts create mode 100644 src/l10n.ts create mode 100644 src/webview/l10n.d.ts create mode 100644 tests/webview/__mocks__/vscode.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 70a6de02..efcebd82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## [Unreleased] +### Added + +- Full internationalization (i18n) support with multiple languages +- Language support: English (default), Simplified Chinese (简体中文), Traditional Chinese (繁體中文) + ## [0.3.0] - 2026-03-26 ### Added diff --git a/README.md b/README.md index 42eef323..d9b55d59 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ This fork is based on the last MIT commit and: - **Avatar support**: Optional avatars from GitHub, GitLab, or Gravatar - **Multi-repo**: Work with multiple repositories in one workspace - **Devcontainer ready**: Works in remote and container environments +- **Internationalization**: Full support for multiple languages ## Configuration diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json new file mode 100644 index 00000000..f46f4795 --- /dev/null +++ b/l10n/bundle.l10n.json @@ -0,0 +1,119 @@ +{ + "statusBar.text": "(neo) Git Graph", + "statusBar.tooltip": "View Git Graph", + "outputChannel.text": "Git Graph", + "ui.repo": "Repo", + "ui.branch": "Branch", + "ui.showRemoteBranches": "Show Remote Branches", + "ui.refresh": "Refresh", + "ui.locateHead": "Locate HEAD", + "ui.loading": "Loading ...", + "ui.loadMore": "Load More Commits", + "ui.showAll": "Show All", + "ui.filterPlaceholder": "Filter {0}...", + "ui.noResultsFound": "No results found.", + "ui.graph": "Graph", + "ui.description": "Description", + "ui.date": "Date", + "ui.author": "Author", + "ui.commit": "Commit", + "error.unableToLoadGitGraph": "Unable to load Git Graph", + "error.noGitRepository": "Either the current workspace does not contain a Git repository, or the Git repository is not configured correctly.", + "error.noGit": "If you are using a portable Git installation, make sure you have set the Visual Studio Code Setting \"git.path\" to the path of your portable installation (e.g. \"C:\\Program Files\\Git\\bin\\git.exe\" on Windows).", + "action.addTag": "Add Tag", + "action.createBranch": "Create Branch", + "action.checkout": "Checkout", + "action.cherryPick": "Cherry Pick", + "action.revert": "Revert", + "action.merge": "Merge into current branch", + "action.reset": "Reset current branch to this Commit", + "action.copyCommitHash": "Copy Commit Hash to Clipboard", + "action.copyCommitSubject": "Copy Commit Subject to Clipboard", + "action.deleteTag": "Delete Tag", + "action.pushTag": "Push Tag", + "action.checkoutBranch": "Checkout Branch", + "action.renameBranch": "Rename Branch", + "action.deleteBranch": "Delete Branch", + "action.mergeBranch": "Merge into current branch", + "label.tag": "the tag", + "label.branch": "the branch", + "label.currentBranch": "the current branch", + "dialog.addTag.title": "Add tag to commit {0}", + "dialog.addTag.name": "Name", + "dialog.addTag.type": "Type", + "dialog.addTag.message": "Message", + "dialog.addTag.typeAnnotated": "Annotated", + "dialog.addTag.typeLightweight": "Lightweight", + "dialog.addTag.optional": "Optional", + "dialog.addTag.submit": "Add Tag", + "dialog.createBranch.title": "Enter the name of the branch {0}", + "dialog.createBranch.submit": "Create Branch", + "dialog.checkout.confirm": "Are you sure you want to checkout commit {0}? This will result in a 'detached HEAD' state.", + "dialog.cherryPick.confirm": "Are you sure you want to cherry pick commit {0}?", + "dialog.revert.confirm": "Are you sure you want to revert commit {0}?", + "dialog.merge.confirm": "Are you sure you want to merge {0} into {1}?", + "dialog.merge.noFastForward": "Create a new commit even if fast-forward is possible", + "dialog.reset.confirm": "Are you sure you want to reset {0} to commit {1}?", + "dialog.reset.soft": "Soft - Keep all changes, but reset head", + "dialog.reset.mixed": "Mixed - Keep working tree, but reset index", + "dialog.reset.hard": "Hard - Discard all changes", + "dialog.delete.confirm": "Are you sure you want to delete {0} {1}?", + "dialog.delete.forceDelete": "Force Delete", + "dialog.renameBranch.title": "Enter the new name for the branch {0}:", + "dialog.renameBranch.submit": "Rename Branch", + "dialog.push.tag.confirm": "Are you sure you want to push the tag {0}?", + "dialog.yes": "Yes", + "dialog.yesCherryPick": "Yes, cherry pick commit", + "dialog.yesRevert": "Yes, revert commit", + "dialog.yesMerge": "Yes, merge", + "dialog.yesReset": "Yes, reset", + "dialog.cancel": "Cancel", + "dialog.dismiss": "Dismiss", + "error.unableToLoadCommitDetails": "Unable to load commit details", + "error.unableToCopyToClipboard": "Unable to Copy {0} to Clipboard", + "error.unableToViewDiff": "Unable to view diff of file", + "error.unableToAddTag": "Unable to Add Tag", + "error.unableToCheckoutBranch": "Unable to Checkout Branch", + "error.unableToCheckoutCommit": "Unable to Checkout Commit", + "error.unableToCherryPick": "Unable to Cherry Pick Commit", + "error.unableToCreateBranch": "Unable to Create Branch", + "error.unableToDeleteBranch": "Unable to Delete Branch", + "error.unableToDeleteTag": "Unable to Delete Tag", + "error.unableToMergeBranch": "Unable to Merge Branch", + "error.unableToMergeCommit": "Unable to Merge Commit", + "error.unableToPushTag": "Unable to Push Tag", + "error.unableToRenameBranch": "Unable to Rename Branch", + "error.unableToReset": "Unable to Reset to Commit", + "error.unableToRevert": "Unable to Revert Commit", + "error.invalidCharacters": "Unable to {0}, one or more invalid characters entered.", + "status.pushingTag": "Pushing Tag", + "time.needFormatMonth": "true", + "time.dateformat": "DD MM YYYY", + "time.second": "second", + "time.minute": "minute", + "time.hour": "hour", + "time.day": "day", + "time.week": "week", + "time.month": "month", + "time.year": "year", + "time.ago": "ago", + "time.seconds": "seconds", + "time.minutes": "minutes", + "time.hours": "hours", + "time.days": "days", + "time.weeks": "weeks", + "time.months": "months", + "time.years": "years", + "month.jan": "Jan", + "month.feb": "Feb", + "month.mar": "Mar", + "month.apr": "Apr", + "month.may": "May", + "month.jun": "Jun", + "month.jul": "Jul", + "month.aug": "Aug", + "month.sep": "Sep", + "month.oct": "Oct", + "month.nov": "Nov", + "month.dec": "Dec" +} diff --git a/l10n/bundle.l10n.zh-cn.json b/l10n/bundle.l10n.zh-cn.json new file mode 100644 index 00000000..bd97c2a0 --- /dev/null +++ b/l10n/bundle.l10n.zh-cn.json @@ -0,0 +1,119 @@ +{ + "statusBar.text": "(neo) Git Graph", + "statusBar.tooltip": "查看 Git 分支图", + "outputChannel.text": "Git 分支图", + "ui.repo": "仓库", + "ui.branch": "分支", + "ui.showRemoteBranches": "显示远程分支", + "ui.refresh": "刷新", + "ui.locateHead": "定位 HEAD", + "ui.loading": "加载中 ...", + "ui.loadMore": "加载更多提交", + "ui.showAll": "显示全部", + "ui.filterPlaceholder": "筛选{0}...", + "ui.noResultsFound": "未找到结果。", + "ui.graph": "分支图", + "ui.description": "描述", + "ui.date": "日期", + "ui.author": "作者", + "ui.commit": "提交", + "error.unableToLoadGitGraph": "无法加载 Git 图形", + "error.noGitRepository": "当前工作区不包含 Git 仓库,或者 Git 仓库配置不正确。", + "error.noGit": "如果您使用的是便携版 Git,请确保已将 Visual Studio Code 的设置 “git.path” 设置为便携版安装的路径(例如 Windows 上的 “C:\\Program Files\\Git\\bin\\git.exe”)。", + "action.addTag": "添加标签", + "action.createBranch": "创建分支", + "action.checkout": "检出", + "action.cherryPick": "拣选", + "action.revert": "还原", + "action.merge": "合并到当前分支", + "action.reset": "重置当前分支到此提交", + "action.copyCommitHash": "复制提交哈希到剪贴板", + "action.copyCommitSubject": "复制提交主题到剪贴板", + "action.deleteTag": "删除标签", + "action.pushTag": "推送标签", + "action.checkoutBranch": "检出分支", + "action.renameBranch": "重命名分支", + "action.deleteBranch": "删除分支", + "action.mergeBranch": "合并到当前分支", + "label.tag": "标签", + "label.branch": "分支", + "label.currentBranch": "当前分支", + "dialog.addTag.title": "为 {0} 添加标签", + "dialog.addTag.name": "名称", + "dialog.addTag.type": "类型", + "dialog.addTag.message": "消息", + "dialog.addTag.typeAnnotated": "附注标签", + "dialog.addTag.typeLightweight": "轻量标签", + "dialog.addTag.optional": "可选", + "dialog.addTag.submit": "添加标签", + "dialog.createBranch.title": "输入 {0} 的新分支名称", + "dialog.createBranch.submit": "创建分支", + "dialog.checkout.confirm": "确定要检出提交 {0} 吗?这将导致“分离 HEAD”状态。", + "dialog.cherryPick.confirm": "确定要拣选提交 {0} 吗?", + "dialog.revert.confirm": "确定要还原提交 {0} 吗?", + "dialog.merge.confirm": "确定要将 {0} 合并到 {1} 吗?", + "dialog.merge.noFastForward": "即使可以快进合并,也要创建新提交", + "dialog.reset.confirm": "确定要将 {0} 重置到 {1} 吗?", + "dialog.reset.soft": "软重置 - 保留所有更改,但重置 HEAD", + "dialog.reset.mixed": "混合重置 - 保留工作树,但重置索引", + "dialog.reset.hard": "硬重置 - 丢弃所有更改", + "dialog.delete.confirm": "确定要删除{0} {1} 吗?", + "dialog.delete.forceDelete": "强制删除", + "dialog.renameBranch.title": "输入 {0} 分支的新名称", + "dialog.renameBranch.submit": "重命名分支", + "dialog.push.tag.confirm": "你确定要推送 {0} 标签吗?", + "dialog.yes": "是", + "dialog.yesCherryPick": "是,拣选提交", + "dialog.yesRevert": "是,还原提交", + "dialog.yesMerge": "是,合并", + "dialog.yesReset": "是,重置", + "dialog.cancel": "取消", + "dialog.dismiss": "关闭", + "error.unableToLoadCommitDetails": "无法加载提交详情", + "error.unableToCopyToClipboard": "无法复制{0}到剪贴板", + "error.unableToViewDiff": "无法查看文件差异", + "error.unableToAddTag": "无法添加标签", + "error.unableToCheckoutBranch": "无法检出分支", + "error.unableToCheckoutCommit": "无法检出提交", + "error.unableToCherryPick": "无法拣选提交", + "error.unableToCreateBranch": "无法创建分支", + "error.unableToDeleteBranch": "无法删除分支", + "error.unableToDeleteTag": "无法删除标签", + "error.unableToMergeBranch": "无法合并分支", + "error.unableToMergeCommit": "无法合并提交", + "error.unableToPushTag": "无法推送标签", + "error.unableToRenameBranch": "无法重命名分支", + "error.unableToReset": "无法重置到提交", + "error.unableToRevert": "无法还原提交", + "error.invalidCharacters": "无法{0},输入了一个或多个无效字符。", + "status.pushingTag": "正在推送标签", + "time.needFormatMonth": "false", + "time.dateformat": "YYYY年MM月DD日", + "time.second": "秒", + "time.minute": "分钟", + "time.hour": "小时", + "time.day": "天", + "time.week": "周", + "time.month": "月", + "time.year": "年", + "time.ago": "前", + "time.seconds": "秒", + "time.minutes": "分钟", + "time.hours": "小时", + "time.days": "天", + "time.weeks": "周", + "time.months": "月", + "time.years": "年", + "month.jan": "", + "month.feb": "", + "month.mar": "", + "month.apr": "", + "month.may": "", + "month.jun": "", + "month.jul": "", + "month.aug": "", + "month.sep": "", + "month.oct": "", + "month.nov": "", + "month.dec": "" +} diff --git a/l10n/bundle.l10n.zh-tw.json b/l10n/bundle.l10n.zh-tw.json new file mode 100644 index 00000000..c59ec051 --- /dev/null +++ b/l10n/bundle.l10n.zh-tw.json @@ -0,0 +1,119 @@ +{ + "statusBar.text": "(neo) Git Graph", + "statusBar.tooltip": "檢視 Git 分支圖", + "outputChannel.text": "Git 分支圖", + "ui.repo": "存放庫", + "ui.branch": "分支", + "ui.showRemoteBranches": "顯示遠端分支", + "ui.refresh": "重新整理", + "ui.locateHead": "定位 HEAD", + "ui.loading": "載入中 ...", + "ui.loadMore": "載入更多提交", + "ui.showAll": "顯示全部", + "ui.filterPlaceholder": "篩選{0}...", + "ui.noResultsFound": "未找到結果。", + "ui.graph": "分支圖", + "ui.description": "描述", + "ui.date": "日期", + "ui.author": "作者", + "ui.commit": "提交", + "error.unableToLoadGitGraph": "無法載入 Git 圖形", + "error.noGitRepository": "目前工作區不包含 Git 存放庫,或者 Git 存放庫設定不正確。", + "error.noGit": "如果您使用的是可攜式 Git 安裝程式,請確保已將 Visual Studio Code 的設定 “git.path” 設定為該可攜式安裝程式的內容路徑(例如 Windows 上的 “C:\\Program Files\\Git\\bin\\git.exe”)。", + "action.addTag": "新增標籤", + "action.createBranch": "建立分支", + "action.checkout": "檢出", + "action.cherryPick": "揀選", + "action.revert": "還原", + "action.merge": "合併到目前分支", + "action.reset": "重設目前分支到此提交", + "action.copyCommitHash": "複製提交雜湊到剪貼簿", + "action.copyCommitSubject": "複製提交主旨到剪貼簿", + "action.deleteTag": "刪除標籤", + "action.pushTag": "推送標籤", + "action.checkoutBranch": "檢出分支", + "action.renameBranch": "重新命名分支", + "action.deleteBranch": "刪除分支", + "action.mergeBranch": "合併到目前分支", + "label.tag": "標籤", + "label.branch": "分支", + "label.currentBranch": "目前分支", + "dialog.addTag.title": "為 {0} 新增標籤", + "dialog.addTag.name": "名稱", + "dialog.addTag.type": "類型", + "dialog.addTag.message": "訊息", + "dialog.addTag.typeAnnotated": "附註標籤", + "dialog.addTag.typeLightweight": "輕量標籤", + "dialog.addTag.optional": "選擇性", + "dialog.addTag.submit": "新增標籤", + "dialog.createBranch.title": "輸入 {0} 的新分支名稱", + "dialog.createBranch.submit": "建立分支", + "dialog.checkout.confirm": "確定要檢出提交 {0} 嗎?這將導致「分離 HEAD」狀態。", + "dialog.cherryPick.confirm": "確定要揀選提交 {0} 嗎?", + "dialog.revert.confirm": "確定要還原提交 {0} 嗎?", + "dialog.merge.confirm": "確定要將 {0} 合併到 {1} 嗎?", + "dialog.merge.noFastForward": "即使可以快進也建立新提交", + "dialog.reset.confirm": "確定要將 {0} 重設到提交 {1} 嗎?", + "dialog.reset.soft": "軟重設 - 保留所有變更,但重設 HEAD", + "dialog.reset.mixed": "混合重設 - 保留工作樹,但重設索引", + "dialog.reset.hard": "硬重設 - 捨棄所有變更", + "dialog.delete.confirm": "確定要刪除{0} {1} 嗎?", + "dialog.delete.forceDelete": "強制刪除", + "dialog.renameBranch.title": "輸入 {0} 分支的新名稱", + "dialog.renameBranch.submit": "重新命名分支", + "dialog.push.tag.confirm": "你確定要推送標籤 {0} 嗎?", + "dialog.yes": "是", + "dialog.yesCherryPick": "是,揀選提交", + "dialog.yesRevert": "是,還原提交", + "dialog.yesMerge": "是,合併", + "dialog.yesReset": "是,重設", + "dialog.cancel": "取消", + "dialog.dismiss": "關閉", + "error.unableToLoadCommitDetails": "無法載入提交詳情", + "error.unableToCopyToClipboard": "無法複製{0}到剪貼簿", + "error.unableToViewDiff": "無法檢視檔案差異", + "error.unableToAddTag": "無法新增標籤", + "error.unableToCheckoutBranch": "無法檢出分支", + "error.unableToCheckoutCommit": "無法檢出提交", + "error.unableToCherryPick": "無法揀選提交", + "error.unableToCreateBranch": "無法建立分支", + "error.unableToDeleteBranch": "無法刪除分支", + "error.unableToDeleteTag": "無法刪除標籤", + "error.unableToMergeBranch": "無法合併分支", + "error.unableToMergeCommit": "無法合併提交", + "error.unableToPushTag": "無法推送標籤", + "error.unableToRenameBranch": "無法重新命名分支", + "error.unableToReset": "無法重設到提交", + "error.unableToRevert": "無法還原提交", + "error.invalidCharacters": "無法{0},輸入了一個或多個無效字元。", + "status.pushingTag": "正在推送標籤", + "time.needFormatMonth": "false", + "time.dateformat": "YYYY年MM月DD日", + "time.second": "秒", + "time.minute": "分鐘", + "time.hour": "小時", + "time.day": "天", + "time.week": "週", + "time.month": "月", + "time.year": "年", + "time.ago": "前", + "time.seconds": "秒", + "time.minutes": "分鐘", + "time.hours": "小時", + "time.days": "天", + "time.weeks": "週", + "time.months": "月", + "time.years": "年", + "month.jan": "", + "month.feb": "", + "month.mar": "", + "month.apr": "", + "month.may": "", + "month.jun": "", + "month.jul": "", + "month.aug": "", + "month.sep": "", + "month.oct": "", + "month.nov": "", + "month.dec": "" +} diff --git a/package.json b/package.json index 0eaa8336..271da9a9 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "neo-git-graph", - "displayName": "(neo) Git Graph", + "displayName": "%displayName%", "version": "0.3.0", - "description": "A clean MIT fork of Git Graph. Visual history, branch actions, and devcontainer support.", + "description": "%description%", "categories": [ "SCM Providers", "Visualization" @@ -49,7 +49,8 @@ "format": "oxfmt --check", "format:fix": "oxfmt .", "lint": "oxlint", - "lint:fix": "oxlint --fix" + "lint:fix": "oxlint --fix", + "l10n:check": "node ./scripts/check-l10n.js" }, "dependencies": { "simple-git": "^3.33.0" @@ -58,6 +59,7 @@ "@types/mocha": "^10.0.10", "@types/node": "^20.19.37", "@types/vscode": "~1.98.0", + "@vscode/l10n-dev": "^0.0.35", "@vscode/test-cli": "^0.0.12", "@vscode/test-electron": "^2.5.2", "esbuild": "^0.27.4", @@ -68,27 +70,28 @@ "typescript": "^5.9.3", "vitest": "^4.1.2" }, + "l10n": "./l10n", "contributes": { "commands": [ { "category": "(neo) Git Graph", "command": "neo-git-graph.view", - "title": "View Git Graph (git log)" + "title": "%command.view%" }, { "category": "(neo) Git Graph", "command": "neo-git-graph.clearAvatarCache", - "title": "Clear Avatar Cache" + "title": "%command.clearAvatarCache%" } ], "configuration": { "type": "object", - "title": "(neo) Git Graph", + "title": "%config.title%", "properties": { "neo-git-graph.autoCenterCommitDetailsView": { "type": "boolean", "default": true, - "description": "Automatically center the commit details view when it is opened." + "description": "%config.autoCenterCommitDetailsView%" }, "neo-git-graph.dateFormat": { "type": "string", @@ -98,12 +101,12 @@ "Relative" ], "enumDescriptions": [ - "Show the date and time, for example \"19 Mar 2019 21:34\"", - "Show the date only, for example \"19 Mar 2019\"", - "Show relative times, for example \"5 minutes ago\"" + "%config.dateFormat.dateTime%", + "%config.dateFormat.dateOnly%", + "%config.dateFormat.relative%" ], "default": "Date & Time", - "description": "Specifies the date format to be used in the date column of Git Graph." + "description": "%config.dateFormat%" }, "neo-git-graph.dateType": { "type": "string", @@ -112,16 +115,16 @@ "Commit Date" ], "enumDescriptions": [ - "Use the author date of a commit", - "Use the committer date of a commit" + "%config.dateType.authorDate%", + "%config.dateType.commitDate%" ], "default": "Author Date", - "description": "Specifies the date type to be displayed throughout Git Graph." + "description": "%config.dateType%" }, "neo-git-graph.fetchAvatars": { "type": "boolean", "default": false, - "description": "Fetch avatars of commit authors and committers. By enabling this setting, you consent to commit author and committer email addresses being sent GitHub, GitLab or Gravatar, depending on the repositories remote origin." + "description": "%config.fetchAvatars%" }, "neo-git-graph.graphColours": { "type": "array", @@ -144,7 +147,7 @@ "#6f24d6", "#ffcc00" ], - "description": "Specifies the colours used on the graph." + "description": "%config.graphColours%" }, "neo-git-graph.graphStyle": { "type": "string", @@ -153,41 +156,41 @@ "angular" ], "enumDescriptions": [ - "Use smooth curves when transitioning between branches on the graph", - "Use angular lines when transitioning between branches on the graph" + "%config.graphStyle.rounded%", + "%config.graphStyle.angular%" ], "default": "rounded", - "description": "Specifies the style of the graph." + "description": "%config.graphStyle%" }, "neo-git-graph.initialLoadCommits": { "type": "number", "default": 300, - "description": "Specifies the number of commits to initially load." + "description": "%config.initialLoadCommits%" }, "neo-git-graph.loadMoreCommits": { "type": "number", "default": 100, - "description": "Specifies the number of commits to load when the \"Load More Commits\" button is pressed (only shown when more commits are available)." + "description": "%config.loadMoreCommits%" }, "neo-git-graph.maxDepthOfRepoSearch": { "type": "number", "default": 0, - "description": "Specifies the maximum depth of subfolders to search when discovering repositories in the workspace." + "description": "%config.maxDepthOfRepoSearch%" }, "neo-git-graph.showCurrentBranchByDefault": { "type": "boolean", "default": false, - "description": "Show the current branch by default when Git Graph is opened. Default: false (show all branches)" + "description": "%config.showCurrentBranchByDefault%" }, "neo-git-graph.showStatusBarItem": { "type": "boolean", "default": true, - "description": "Show a Status Bar item which opens Git Graph when clicked." + "description": "%config.showStatusBarItem%" }, "neo-git-graph.showUncommittedChanges": { "type": "boolean", "default": true, - "description": "Show uncommitted changes (set to false to decrease load time on large repositories)." + "description": "%config.showUncommittedChanges%" }, "neo-git-graph.tabIconColourTheme": { "type": "string", @@ -196,11 +199,11 @@ "grey" ], "enumDescriptions": [ - "Show a colour icon which suits most Visual Studio Code colour themes", - "Show a grey icon which suits Visual Studio Code colour themes that are predominantly grayscale" + "%config.tabIconColourTheme.colour%", + "%config.tabIconColourTheme.grey%" ], "default": "colour", - "description": "Specifies the colour theme of the icon displayed on the Git Graph tab." + "description": "%config.tabIconColourTheme%" } } } diff --git a/package.nls.json b/package.nls.json new file mode 100644 index 00000000..7b16a778 --- /dev/null +++ b/package.nls.json @@ -0,0 +1,29 @@ +{ + "displayName": "(neo) Git Graph", + "description": "A clean MIT fork of Git Graph. Visual history, branch actions, and devcontainer support.", + "command.view": "View Git Graph (git log)", + "command.clearAvatarCache": "Clear Avatar Cache", + "config.title": "(neo) Git Graph", + "config.autoCenterCommitDetailsView": "Automatically center the commit details view when it is opened.", + "config.dateFormat": "Specifies the date format to be used in the date column of Git Graph.", + "config.dateFormat.dateTime": "Show the date and time, for example \"19 Mar 2019 21:34\"", + "config.dateFormat.dateOnly": "Show the date only, for example \"19 Mar 2019\"", + "config.dateFormat.relative": "Show relative times, for example \"5 minutes ago\"", + "config.dateType": "Specifies the date type to be displayed throughout Git Graph.", + "config.dateType.authorDate": "Use the author date of a commit", + "config.dateType.commitDate": "Use the committer date of a commit", + "config.fetchAvatars": "Fetch avatars of commit authors and committers. By enabling this setting, you consent to commit author and committer email addresses being sent GitHub, GitLab or Gravatar, depending on the repositories remote origin.", + "config.graphColours": "Specifies the colours used on the graph.", + "config.graphStyle": "Specifies the style of the graph.", + "config.graphStyle.rounded": "Use smooth curves when transitioning between branches on the graph", + "config.graphStyle.angular": "Use angular lines when transitioning between branches on the graph", + "config.initialLoadCommits": "Specifies the number of commits to initially load.", + "config.loadMoreCommits": "Specifies the number of commits to load when the \"Load More Commits\" button is pressed (only shown when more commits are available).", + "config.maxDepthOfRepoSearch": "Specifies the maximum depth of subfolders to search when discovering repositories in the workspace.", + "config.showCurrentBranchByDefault": "Show the current branch by default when Git Graph is opened. Default: false (show all branches)", + "config.showStatusBarItem": "Show a Status Bar item which opens Git Graph when clicked.", + "config.showUncommittedChanges": "Show uncommitted changes (set to false to decrease load time on large repositories).", + "config.tabIconColourTheme": "Specifies the colour theme of the icon displayed on the Git Graph tab.", + "config.tabIconColourTheme.colour": "Show a colour icon which suits most Visual Studio Code colour themes", + "config.tabIconColourTheme.grey": "Show a grey icon which suits Visual Studio Code colour themes that are predominantly grayscale" +} diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json new file mode 100644 index 00000000..bece8e3b --- /dev/null +++ b/package.nls.zh-cn.json @@ -0,0 +1,29 @@ +{ + "displayName": "(neo) Git Graph", + "description": "Git Graph 的纯净 MIT 分支。可视化历史记录、分支操作和开发容器支持。", + "command.view": "查看 Git 图形 (git log)", + "command.clearAvatarCache": "清除头像缓存", + "config.title": "(neo) Git Graph", + "config.autoCenterCommitDetailsView": "打开提交详情视图时自动居中显示。", + "config.dateFormat": "指定 Git 图形日期列中使用的日期格式。", + "config.dateFormat.dateTime": "显示日期和时间,例如 \"2019年3月19日 21:34\"", + "config.dateFormat.dateOnly": "仅显示日期,例如 \"2019年3月19日\"", + "config.dateFormat.relative": "显示相对时间,例如 \"5 分钟前\"", + "config.dateType": "指定在 Git 图形中显示的日期类型。", + "config.dateType.authorDate": "使用提交的作者日期", + "config.dateType.commitDate": "使用提交的提交者日期", + "config.fetchAvatars": "获取提交作者和提交者的头像。启用此设置即表示您同意将提交作者和提交者的电子邮件地址发送到 GitHub、GitLab 或 Gravatar(取决于仓库的远程源)。", + "config.graphColours": "指定图形上使用的颜色。", + "config.graphStyle": "指定图形的样式。", + "config.graphStyle.rounded": "在图形中的分支之间过渡时使用平滑曲线", + "config.graphStyle.angular": "在图形中的分支之间过渡时使用直角线条", + "config.initialLoadCommits": "指定初始加载的提交数量。", + "config.loadMoreCommits": "指定按下“加载更多提交”按钮时要加载的提交数量(仅在有更多提交可用时显示)。", + "config.maxDepthOfRepoSearch": "指定在工作区中发现仓库时搜索的子文件夹最大深度。", + "config.showCurrentBranchByDefault": "打开 Git 图形时默认显示当前分支。默认值:false(显示所有分支)", + "config.showStatusBarItem": "显示状态栏项,单击时打开 Git 图形。", + "config.showUncommittedChanges": "显示未提交的更改(在大型仓库上设置为 false 以减少加载时间)。", + "config.tabIconColourTheme": "指定 Git 图形选项卡上显示的图标的颜色主题。", + "config.tabIconColourTheme.colour": "显示适合大多数 Visual Studio Code 颜色主题的彩色图标", + "config.tabIconColourTheme.grey": "显示适合以灰度为主的 Visual Studio Code 颜色主题的灰色图标" +} diff --git a/package.nls.zh-tw.json b/package.nls.zh-tw.json new file mode 100644 index 00000000..4851d40f --- /dev/null +++ b/package.nls.zh-tw.json @@ -0,0 +1,29 @@ +{ + "displayName": "(neo) Git Graph", + "description": "Git Graph 的純淨 MIT 分支。視覺化歷史記錄、分支操作和開發容器支援。", + "command.view": "檢視 Git 圖形 (git log)", + "command.clearAvatarCache": "清除頭像快取", + "config.title": "(neo) Git Graph", + "config.autoCenterCommitDetailsView": "開啟提交詳情檢視時自動置中顯示。", + "config.dateFormat": "指定 Git 圖形日期欄中使用的日期格式。", + "config.dateFormat.dateTime": "顯示日期和時間,例如 \"2019年3月19日 21:34\"", + "config.dateFormat.dateOnly": "僅顯示日期,例如 \"2019年3月19日\"", + "config.dateFormat.relative": "顯示相對時間,例如 \"5 分鐘前\"", + "config.dateType": "指定在 Git 圖形中顯示的日期類型。", + "config.dateType.authorDate": "使用提交的作者日期", + "config.dateType.commitDate": "使用提交的提交者日期", + "config.fetchAvatars": "取得提交作者和提交者的頭像。啟用此設定即表示您同意將提交作者和提交者的電子郵件地址傳送到 GitHub、GitLab 或 Gravatar(取決於存放庫的遠端來源)。", + "config.graphColours": "指定圖形上使用的顏色。", + "config.graphStyle": "指定圖形的樣式。", + "config.graphStyle.rounded": "在圖形中的分支之間過渡時使用平滑曲線", + "config.graphStyle.angular": "在圖形中的分支之間過渡時使用直角線條", + "config.initialLoadCommits": "指定初始載入的提交數量。", + "config.loadMoreCommits": "指定按下「載入更多提交」按鈕時要載入的提交數量(僅在有更多提交可用時顯示)。", + "config.maxDepthOfRepoSearch": "指定在工作區中發現存放庫時搜尋的子資料夾最大深度。", + "config.showCurrentBranchByDefault": "開啟 Git 圖形時預設顯示目前分支。預設值:false(顯示所有分支)", + "config.showStatusBarItem": "顯示狀態列項目,按一下時開啟 Git 圖形。", + "config.showUncommittedChanges": "顯示未提交的變更(在大型存放庫上設定為 false 以減少載入時間)。", + "config.tabIconColourTheme": "指定 Git 圖形分頁上顯示的圖示的顏色主題。", + "config.tabIconColourTheme.colour": "顯示適合大多數 Visual Studio Code 顏色主題的彩色圖示", + "config.tabIconColourTheme.grey": "顯示適合以灰階為主的 Visual Studio Code 顏色主題的灰色圖示" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd5af491..dca847b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,9 @@ importers: '@types/vscode': specifier: ~1.98.0 version: 1.98.0 + '@vscode/l10n-dev': + specifier: ^0.0.35 + version: 0.0.35 '@vscode/test-cli': specifier: ^0.0.12 version: 0.0.12 @@ -62,6 +65,38 @@ packages: '@asamuzakjp/nwsapi@2.3.9': resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@azure-rest/ai-translation-text@1.0.1': + resolution: {integrity: sha512-lUs1FfBXjik6EReUEYP1ogkhaSPHZdUV+EB215y7uejuyHgG1RXD2aLsqXQrluZwXcLMdN+bTzxylKBc5xDhgQ==} + engines: {node: '>=18.0.0'} + + '@azure-rest/core-client@2.5.1': + resolution: {integrity: sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==} + engines: {node: '>=20.0.0'} + + '@azure/abort-controller@2.1.2': + resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} + engines: {node: '>=18.0.0'} + + '@azure/core-auth@1.10.1': + resolution: {integrity: sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==} + engines: {node: '>=20.0.0'} + + '@azure/core-rest-pipeline@1.23.0': + resolution: {integrity: sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==} + engines: {node: '>=20.0.0'} + + '@azure/core-tracing@1.3.1': + resolution: {integrity: sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==} + engines: {node: '>=20.0.0'} + + '@azure/core-util@1.13.1': + resolution: {integrity: sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==} + engines: {node: '>=20.0.0'} + + '@azure/logger@1.3.0': + resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==} + engines: {node: '>=20.0.0'} + '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} @@ -757,6 +792,10 @@ packages: '@types/vscode@1.98.0': resolution: {integrity: sha512-+KuiWhpbKBaG2egF+51KjbGWatTH5BbmWQjSLMDCssb4xF8FJnW4nGH4nuAdOOfMbpD0QlHtI+C3tPq+DoKElg==} + '@typespec/ts-http-runtime@0.3.4': + resolution: {integrity: sha512-CI0NhTrz4EBaa0U+HaaUZrJhPoso8sG7ZFya8uQoBA57fjzrjRSv87ekCjLZOFExN+gXE/z0xuN2QfH4H2HrLQ==} + engines: {node: '>=20.0.0'} + '@vitest/expect@4.1.2': resolution: {integrity: sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==} @@ -786,6 +825,10 @@ packages: '@vitest/utils@4.1.2': resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} + '@vscode/l10n-dev@0.0.35': + resolution: {integrity: sha512-s6uzBXsVDSL69Z85HSqpc5dfKswQkeucY8L00t1TWzGalw7wkLQUKMRwuzqTq+AMwQKrRd7Po14cMoTcd11iDw==} + hasBin: true + '@vscode/test-cli@0.0.12': resolution: {integrity: sha512-iYN0fDg29+a2Xelle/Y56Xvv7Nc8Thzq4VwpzAF/SIE6918rDicqfsQxV6w1ttr2+SOm+10laGuY9FG2ptEKsQ==} engines: {node: '>=18'} @@ -853,6 +896,9 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + brace-expansion@2.0.3: resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} @@ -930,10 +976,17 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + css-tree@3.2.1: resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + data-urls@7.0.0: resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -957,10 +1010,27 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge-json@1.5.0: + resolution: {integrity: sha512-jZRrDmBKjmGcqMFEUJ14FjMJwm05Qaked+1vxaALRtF0UAl7lPU8OLWXFxvoeg3jbQM249VPFVn8g2znaQkEtA==} + engines: {node: '>=4.0.0'} + diff@7.0.0: resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} engines: {node: '>=0.3.1'} + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -977,6 +1047,10 @@ packages: resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} engines: {node: '>=10.13.0'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + entities@6.0.1: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} @@ -1109,6 +1183,10 @@ packages: resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} engines: {node: '>=18'} + get-stdin@7.0.0: + resolution: {integrity: sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==} + engines: {node: '>=8'} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1265,6 +1343,9 @@ packages: lie@3.3.0: resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1291,9 +1372,16 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + markdown-it@14.1.1: + resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} + hasBin: true + mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -1326,10 +1414,20 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + node-html-markdown@1.3.0: + resolution: {integrity: sha512-OeFi3QwC/cPjvVKZ114tzzu+YoR+v9UXW5RwSXGUqGb0qCl0DvP406tzdL7SFn8pZrMyzXoisfG2zcuF9+zw4g==} + engines: {node: '>=10.0.0'} + + node-html-parser@6.1.13: + resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==} + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} @@ -1414,6 +1512,14 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + pseudo-localization@2.4.0: + resolution: {integrity: sha512-ISYMOKY8+f+PmiXMFw2y6KLY74LBrv/8ml/VjjoVEV2k+MS+OJZz7ydciK5ntJwxPrKQPTU1+oXq9Mx2b0zEzg==} + hasBin: true + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1455,6 +1561,10 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} @@ -1590,15 +1700,26 @@ packages: resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} engines: {node: '>=20'} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -1695,6 +1816,9 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} + web-tree-sitter@0.20.8: + resolution: {integrity: sha512-weOVgZ3aAARgdnb220GqYuh7+rZU0Ka9k9yfKtGAzEYMa6GgiCzW9JjQRJyCJakvibQW+dfjJdihjInKuuCAUQ==} + webidl-conversions@8.0.1: resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} engines: {node: '>=20'} @@ -1736,6 +1860,14 @@ packages: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} + xml2js@0.5.0: + resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} @@ -1779,6 +1911,70 @@ snapshots: '@asamuzakjp/nwsapi@2.3.9': {} + '@azure-rest/ai-translation-text@1.0.1': + dependencies: + '@azure-rest/core-client': 2.5.1 + '@azure/core-auth': 1.10.1 + '@azure/core-rest-pipeline': 1.23.0 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure-rest/core-client@2.5.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-rest-pipeline': 1.23.0 + '@azure/core-tracing': 1.3.1 + '@typespec/ts-http-runtime': 0.3.4 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/abort-controller@2.1.2': + dependencies: + tslib: 2.8.1 + + '@azure/core-auth@1.10.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-util': 1.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-rest-pipeline@1.23.0': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + '@typespec/ts-http-runtime': 0.3.4 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-tracing@1.3.1': + dependencies: + tslib: 2.8.1 + + '@azure/core-util@1.13.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@typespec/ts-http-runtime': 0.3.4 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/logger@1.3.0': + dependencies: + '@typespec/ts-http-runtime': 0.3.4 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + '@bcoe/v8-coverage@1.0.2': {} '@bramus/specificity@2.4.2': @@ -2175,6 +2371,14 @@ snapshots: '@types/vscode@1.98.0': {} + '@typespec/ts-http-runtime@0.3.4': + dependencies: + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + '@vitest/expect@4.1.2': dependencies: '@standard-schema/spec': 1.1.0 @@ -2216,6 +2420,21 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 + '@vscode/l10n-dev@0.0.35': + dependencies: + '@azure-rest/ai-translation-text': 1.0.1 + debug: 4.4.3(supports-color@8.1.1) + deepmerge-json: 1.5.0 + glob: 10.5.0 + markdown-it: 14.1.1 + node-html-markdown: 1.3.0 + pseudo-localization: 2.4.0 + web-tree-sitter: 0.20.8 + xml2js: 0.5.0 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + '@vscode/test-cli@0.0.12': dependencies: '@types/mocha': 10.0.10 @@ -2284,6 +2503,8 @@ snapshots: binary-extensions@2.3.0: {} + boolbase@1.0.0: {} + brace-expansion@2.0.3: dependencies: balanced-match: 1.0.2 @@ -2367,11 +2588,21 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + css-tree@3.2.1: dependencies: mdn-data: 2.27.1 source-map-js: 1.2.1 + css-what@6.2.2: {} + data-urls@7.0.0: dependencies: whatwg-mimetype: 5.0.0 @@ -2391,8 +2622,28 @@ snapshots: deep-is@0.1.4: {} + deepmerge-json@1.5.0: {} + diff@7.0.0: {} + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + eastasianwidth@0.2.0: {} emoji-regex@10.6.0: {} @@ -2406,6 +2657,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.2 + entities@4.5.0: {} + entities@6.0.1: {} es-module-lexer@2.0.0: {} @@ -2561,6 +2814,8 @@ snapshots: get-east-asian-width@1.5.0: {} + get-stdin@7.0.0: {} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -2721,6 +2976,10 @@ snapshots: dependencies: immediate: 3.0.6 + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -2747,8 +3006,19 @@ snapshots: dependencies: semver: 7.7.4 + markdown-it@14.1.1: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + mdn-data@2.27.1: {} + mdurl@2.0.0: {} + mimic-function@5.0.1: {} minimatch@10.2.4: @@ -2791,8 +3061,21 @@ snapshots: natural-compare@1.4.0: {} + node-html-markdown@1.3.0: + dependencies: + node-html-parser: 6.1.13 + + node-html-parser@6.1.13: + dependencies: + css-select: 5.2.2 + he: 1.2.0 + normalize-path@3.0.0: {} + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + obug@2.1.1: {} onetime@7.0.0: @@ -2909,6 +3192,15 @@ snapshots: process-nextick-args@2.0.1: {} + pseudo-localization@2.4.0: + dependencies: + flat: 5.0.2 + get-stdin: 7.0.0 + typescript: 4.9.5 + yargs: 17.7.2 + + punycode.js@2.3.1: {} + punycode@2.3.1: {} randombytes@2.1.0: @@ -2975,6 +3267,8 @@ snapshots: safe-buffer@5.2.1: {} + sax@1.6.0: {} + saxes@6.0.0: dependencies: xmlchars: 2.2.0 @@ -3096,12 +3390,18 @@ snapshots: dependencies: punycode: 2.3.1 + tslib@2.8.1: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 + typescript@4.9.5: {} + typescript@5.9.3: {} + uc.micro@2.1.0: {} + undici-types@6.21.0: {} undici@7.24.6: {} @@ -3162,6 +3462,8 @@ snapshots: dependencies: xml-name-validator: 5.0.0 + web-tree-sitter@0.20.8: {} + webidl-conversions@8.0.1: {} whatwg-mimetype@5.0.0: {} @@ -3201,6 +3503,13 @@ snapshots: xml-name-validator@5.0.0: {} + xml2js@0.5.0: + dependencies: + sax: 1.6.0 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + xmlchars@2.2.0: {} y18n@5.0.8: {} diff --git a/scripts/check-l10n.js b/scripts/check-l10n.js new file mode 100755 index 00000000..dba2dee3 --- /dev/null +++ b/scripts/check-l10n.js @@ -0,0 +1,264 @@ +/** + * Check l10n files for missing translations and parameter consistency + * Compares all translation files against the base bundle.l10n.json and package.nls.json + */ + +const fs = require("node:fs"); +const path = require("node:path"); + +const L10N_DIR = path.join(__dirname, "../l10n"); +const BASE_FILE = "bundle.l10n.json"; +const ROOT_DIR = path.join(__dirname, ".."); +const PACKAGE_NLS_BASE = "package.nls.json"; + +function loadJson(filePath) { + try { + const content = fs.readFileSync(filePath, "utf8"); + return JSON.parse(content); + } catch (error) { + console.error(`Failed to load ${filePath}:`, error.message); + return null; + } +} + +/** + * Extract placeholders from a translation string + * Supports formats: {0}, {1}, {variableName} + * @param {string} text - The translation text + * @returns {string[]} - Array of found placeholders (sorted for consistent comparison) + */ +function extractPlaceholders(text) { + if (typeof text !== "string") { + return []; + } + + // Match {0}, {1}, {variableName}, etc. + const placeholders = text.match(/\{[^}]+\}/g); + + if (!placeholders) { + return []; + } + + // Return sorted array for consistent comparison + return placeholders.sort(); +} + +/** + * Check if two placeholder arrays are equal + * @param {string[]} arr1 + * @param {string[]} arr2 + * @returns {boolean} + */ +function placeholdersMatch(arr1, arr2) { + if (arr1.length !== arr2.length) { + return false; + } + + for (let i = 0; i < arr1.length; i++) { + if (arr1[i] !== arr2[i]) { + return false; + } + } + + return true; +} + +function checkFileSet(baseDir, baseFileName, filePattern, sectionTitle) { + // Load base translation file + const basePath = path.join(baseDir, baseFileName); + const baseTranslations = loadJson(basePath); + + if (!baseTranslations) { + console.error(`Failed to load base file: ${baseFileName}`); + return { hasIssues: true, coverageStats: [] }; + } + + const baseKeys = Object.keys(baseTranslations); + console.log(`\n${sectionTitle}`); + console.log(`📚 Base file (${baseFileName}): ${baseKeys.length} keys\n`); + + // Get all translation files + let files; + if (baseDir === ROOT_DIR) { + // For package.nls files, look in root directory + files = fs + .readdirSync(baseDir) + .filter((file) => file.startsWith(filePattern) && file !== baseFileName) + .sort(); + } else { + // For bundle.l10n files, look in l10n directory + files = fs + .readdirSync(baseDir) + .filter((file) => file.startsWith(filePattern) && file !== baseFileName) + .sort(); + } + + if (files.length === 0) { + console.log("✅ No additional translation files found\n"); + return { hasIssues: false, coverageStats: [] }; + } + + let hasIssues = false; + const coverageStats = []; + + // Check each translation file + files.forEach((file) => { + const filePath = path.join(baseDir, file); + const translations = loadJson(filePath); + + if (!translations) { + return; + } + + const keys = Object.keys(translations); + const missing = baseKeys.filter((k) => !(k in translations)); + const extra = keys.filter((k) => !(k in baseTranslations)); + + // Extract locale name + let locale; + if (file.startsWith("bundle.l10n.")) { + locale = file.replace("bundle.l10n.", "").replace(".json", ""); + } else if (file.startsWith("package.nls.")) { + locale = file.replace("package.nls.", "").replace(".json", ""); + } + + const coverage = + baseKeys.length > 0 ? (((keys.length - extra.length) / baseKeys.length) * 100).toFixed(1) : 0; + coverageStats.push({ + locale, + coverage: parseFloat(coverage), + total: baseKeys.length, + translated: keys.length - extra.length + }); + + console.log(`🌍 ${locale} (${file}): ${keys.length} keys - Coverage: ${coverage}%`); + + if (missing.length > 0) { + hasIssues = true; + console.log(` ⚠️ Missing ${missing.length} translation(s):`); + missing.forEach((k) => { + console.log(` - ${k}: "${baseTranslations[k]}"`); + }); + } + + if (extra.length > 0) { + hasIssues = true; + console.log(` ⚠️ Extra ${extra.length} key(s) not in base:`); + extra.forEach((k) => console.log(` - ${k}`)); + } + + // Check parameter consistency + const paramIssues = []; + keys.forEach((key) => { + if (key in baseTranslations) { + const basePlaceholders = extractPlaceholders(baseTranslations[key]); + const translatedPlaceholders = extractPlaceholders(translations[key]); + + if (!placeholdersMatch(basePlaceholders, translatedPlaceholders)) { + paramIssues.push({ + key, + base: basePlaceholders, + translated: translatedPlaceholders, + baseText: baseTranslations[key], + translatedText: translations[key] + }); + } + } + }); + + if (paramIssues.length > 0) { + hasIssues = true; + console.log(` ⚠️ Parameter mismatch in ${paramIssues.length} translation(s):`); + paramIssues.forEach((issue) => { + console.log(` - ${issue.key}:`); + console.log(` Base: "${issue.baseText}" -> [${issue.base.join(", ")}]`); + console.log( + ` Translation: "${issue.translatedText}" -> [${issue.translated.join(", ")}]` + ); + }); + } + + if (missing.length === 0 && extra.length === 0 && paramIssues.length === 0) { + console.log(` ✅ Complete`); + } + + console.log(""); + }); + + return { hasIssues, coverageStats }; +} + +function printCoverageSummary(allStats) { + if (allStats.length === 0) { + return; + } + + console.log("\n📊 Coverage Summary by Language:\n"); + console.log("┌──────────────┬──────────────────┬──────────────────┐"); + console.log("│ Language │ bundle.l10n.*. │ package.nls.* │"); + console.log("├──────────────┼──────────────────┼──────────────────┤"); + + // Group stats by locale + const localeMap = {}; + allStats.forEach((stat) => { + if (!localeMap[stat.locale]) { + localeMap[stat.locale] = {}; + } + localeMap[stat.locale][stat.type] = stat.coverage; + }); + + Object.keys(localeMap) + .sort() + .forEach((locale) => { + const bundleCov = + localeMap[locale]["bundle"] !== undefined ? `${localeMap[locale]["bundle"]}%` : "N/A"; + const packageCov = + localeMap[locale]["package"] !== undefined ? `${localeMap[locale]["package"]}%` : "N/A"; + + console.log( + `│ ${locale.padEnd(12)} │ ${bundleCov.padStart(16)} │ ${packageCov.padStart(16)} │` + ); + }); + + console.log("└──────────────┴──────────────────┴──────────────────┘\n"); +} + +function checkTranslations() { + const allStats = []; + + // Check bundle.l10n.* files + const bundleResult = checkFileSet( + L10N_DIR, + BASE_FILE, + "bundle.l10n.", + "=== Checking bundle.l10n.* files ===" + ); + bundleResult.coverageStats.forEach((stat) => { + allStats.push({ ...stat, type: "bundle" }); + }); + + // Check package.nls.* files + const packageResult = checkFileSet( + ROOT_DIR, + PACKAGE_NLS_BASE, + "package.nls.", + "=== Checking package.nls.* files ===" + ); + packageResult.coverageStats.forEach((stat) => { + allStats.push({ ...stat, type: "package" }); + }); + + // Print coverage summary + printCoverageSummary(allStats); + + const hasIssues = bundleResult.hasIssues || packageResult.hasIssues; + + if (hasIssues) { + console.log("⚠️ Translation issues found\n"); + process.exit(1); + } else { + console.log("✅ All translations are complete\n"); + } +} + +checkTranslations(); diff --git a/src/extension.ts b/src/extension.ts index 1e43d632..938fd530 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -9,12 +9,13 @@ import { registerMessageHandlers } from "./extension/messageHandler"; import { WebviewBridge, webviewBridgeFactory } from "./extension/webviewBridge"; import { createWebviewPanel, WebviewPanel } from "./extension/webviewPanel"; import { ExtensionState } from "./extensionState"; +import * as l10n from "./l10n"; import { RepoFileWatcher } from "./repoFileWatcher"; import { RepoManager } from "./repoManager"; import { StatusBarItem } from "./statusBarItem"; export function activate(context: vscode.ExtensionContext) { - const outputChannel = vscode.window.createOutputChannel("(neo) Git Graph"); + const outputChannel = vscode.window.createOutputChannel(l10n.t("outputChannel.text")); const extensionState = new ExtensionState(context); const avatarManager = new AvatarManager(config.gitPath, extensionState); const statusBarItem = new StatusBarItem(context, config); @@ -32,7 +33,7 @@ export function activate(context: vscode.ExtensionContext) { } const panel = vscode.window.createWebviewPanel( "neo-git-graph", - "(neo) Git Graph", + l10n.t("outputChannel.text"), column ?? vscode.ViewColumn.One, { enableScripts: true, diff --git a/src/extension/webviewHtml.ts b/src/extension/webviewHtml.ts index f04727dc..5c26708f 100644 --- a/src/extension/webviewHtml.ts +++ b/src/extension/webviewHtml.ts @@ -7,6 +7,20 @@ import { ExtensionState } from "@/extensionState"; import { RepoManager } from "@/repoManager"; import { GitGraphViewState } from "@/types"; +import * as l10n from "../l10n"; +import { getWebviewLocalizedStrings } from "./webviewL10n"; + +/** + * Safely escape JSON for embedding in HTML script tags. + * Prevents XSS by escaping characters that could break out of script context. + */ +function escapeJsonForHtml(obj: object): string { + return JSON.stringify(obj) + .replace(//g, "\\u003e") + .replace(/&/g, "\\u0026"); +} + export function buildWebviewHtml(opts: { webview: vscode.Webview; config: Config; @@ -16,6 +30,7 @@ export function buildWebviewHtml(opts: { }): { html: string; isGraphLoaded: boolean } { const { webview, config, extensionPath, extensionState, repoManager } = opts; const nonce = getNonce(); + const l10nStrings = getWebviewLocalizedStrings(); const viewState: GitGraphViewState = { autoCenterCommitDetailsView: config.autoCenterCommitDetailsView(), dateFormat: config.dateFormat(), @@ -46,11 +61,11 @@ export function buildWebviewHtml(opts: { if (numRepos > 0) { body = `
- Repo: - Branch: - -
Refresh
-
Locate HEAD
+ ${l10nStrings.repo}: + ${l10nStrings.branch}: + +
${l10nStrings.refresh}
+
${l10nStrings.locateHead}
@@ -61,14 +76,15 @@ export function buildWebviewHtml(opts: {
- + + `; } else { body = ` -

Unable to load Git Graph

-

Either the current workspace does not contain a Git repository, or the Git executable could not be found.

-

If you are using a portable Git installation, make sure you have set the Visual Studio Code Setting "git.path" to the path of your portable installation (e.g. "C:\\Program Files\\Git\\bin\\git.exe" on Windows).

+

${l10nStrings.unableToLoadGitGraph}

+

${l10nStrings.noGitRepository}

+

${l10nStrings.noGit}

`; } @@ -80,7 +96,7 @@ export function buildWebviewHtml(opts: { - (neo) Git Graph + ${l10n.t("outputChannel.text")} ${body} diff --git a/src/extension/webviewL10n.ts b/src/extension/webviewL10n.ts new file mode 100644 index 00000000..dd81339a --- /dev/null +++ b/src/extension/webviewL10n.ts @@ -0,0 +1,142 @@ +import * as l10n from "../l10n"; + +/** + * Get all localized strings for the webview. + * Since webview cannot access vscode.l10n directly, we need to pass the strings + * from the extension context. + */ +export function getWebviewLocalizedStrings() { + return { + // UI labels + repo: l10n.t("ui.repo"), + branch: l10n.t("ui.branch"), + showRemoteBranches: l10n.t("ui.showRemoteBranches"), + refresh: l10n.t("ui.refresh"), + locateHead: l10n.t("ui.locateHead"), + loading: l10n.t("ui.loading"), + loadMore: l10n.t("ui.loadMore"), + showAll: l10n.t("ui.showAll"), + filterPlaceholder: l10n.t("ui.filterPlaceholder"), + noResultsFound: l10n.t("ui.noResultsFound"), + graph: l10n.t("ui.graph"), + description: l10n.t("ui.description"), + date: l10n.t("ui.date"), + author: l10n.t("ui.author"), + commit: l10n.t("ui.commit"), + + // Error messages + unableToLoadGitGraph: l10n.t("error.unableToLoadGitGraph"), + noGitRepository: l10n.t("error.noGitRepository"), + noGit: l10n.t("error.noGit"), + unableToLoadCommitDetails: l10n.t("error.unableToLoadCommitDetails"), + unableToCopyToClipboard: l10n.t("error.unableToCopyToClipboard"), + unableToViewDiff: l10n.t("error.unableToViewDiff"), + unableToAddTag: l10n.t("error.unableToAddTag"), + unableToCheckoutBranch: l10n.t("error.unableToCheckoutBranch"), + unableToCheckoutCommit: l10n.t("error.unableToCheckoutCommit"), + unableToCherryPick: l10n.t("error.unableToCherryPick"), + unableToCreateBranch: l10n.t("error.unableToCreateBranch"), + unableToDeleteBranch: l10n.t("error.unableToDeleteBranch"), + unableToDeleteTag: l10n.t("error.unableToDeleteTag"), + unableToMergeBranch: l10n.t("error.unableToMergeBranch"), + unableToMergeCommit: l10n.t("error.unableToMergeCommit"), + unableToPushTag: l10n.t("error.unableToPushTag"), + unableToRenameBranch: l10n.t("error.unableToRenameBranch"), + unableToReset: l10n.t("error.unableToReset"), + unableToRevert: l10n.t("error.unableToRevert"), + invalidCharacters: l10n.t("error.invalidCharacters"), + + // Actions + addTag: l10n.t("action.addTag"), + createBranch: l10n.t("action.createBranch"), + checkout: l10n.t("action.checkout"), + cherryPick: l10n.t("action.cherryPick"), + revert: l10n.t("action.revert"), + merge: l10n.t("action.merge"), + reset: l10n.t("action.reset"), + copyCommitHash: l10n.t("action.copyCommitHash"), + copyCommitSubject: l10n.t("action.copyCommitSubject"), + deleteTag: l10n.t("action.deleteTag"), + pushTag: l10n.t("action.pushTag"), + checkoutBranch: l10n.t("action.checkoutBranch"), + renameBranch: l10n.t("action.renameBranch"), + deleteBranch: l10n.t("action.deleteBranch"), + mergeBranch: l10n.t("action.mergeBranch"), + + // label + labelTag: l10n.t("label.tag"), + labelBranch: l10n.t("label.branch"), + labelCurrentBranch: l10n.t("label.currentBranch"), + + // Dialog + dialogAddTagTitle: l10n.t("dialog.addTag.title"), + dialogAddTagName: l10n.t("dialog.addTag.name"), + dialogAddTagType: l10n.t("dialog.addTag.type"), + dialogAddTagMessage: l10n.t("dialog.addTag.message"), + dialogAddTagTypeAnnotated: l10n.t("dialog.addTag.typeAnnotated"), + dialogAddTagTypeLightweight: l10n.t("dialog.addTag.typeLightweight"), + dialogAddTagOptional: l10n.t("dialog.addTag.optional"), + dialogAddTagSubmit: l10n.t("dialog.addTag.submit"), + dialogCreateBranchTitle: l10n.t("dialog.createBranch.title"), + dialogCreateBranchSubmit: l10n.t("dialog.createBranch.submit"), + dialogCheckoutConfirm: l10n.t("dialog.checkout.confirm"), + dialogCherryPickConfirm: l10n.t("dialog.cherryPick.confirm"), + dialogRevertConfirm: l10n.t("dialog.revert.confirm"), + dialogMergeConfirm: l10n.t("dialog.merge.confirm"), + dialogMergeNoFastForward: l10n.t("dialog.merge.noFastForward"), + dialogResetConfirm: l10n.t("dialog.reset.confirm"), + dialogResetSoft: l10n.t("dialog.reset.soft"), + dialogResetMixed: l10n.t("dialog.reset.mixed"), + dialogResetHard: l10n.t("dialog.reset.hard"), + dialogDeleteConfirm: l10n.t("dialog.delete.confirm"), + dialogDeleteForceDelete: l10n.t("dialog.delete.forceDelete"), + dialogRenameBranchTitle: l10n.t("dialog.renameBranch.title"), + dialogRenameBranchSubmit: l10n.t("dialog.renameBranch.submit"), + dialogPushTagConfirm: l10n.t("dialog.push.tag.confirm"), + dialogYes: l10n.t("dialog.yes"), + dialogYesCherryPick: l10n.t("dialog.yesCherryPick"), + dialogYesRevert: l10n.t("dialog.yesRevert"), + dialogYesMerge: l10n.t("dialog.yesMerge"), + dialogYesReset: l10n.t("dialog.yesReset"), + dialogCancel: l10n.t("dialog.cancel"), + dialogDismiss: l10n.t("dialog.dismiss"), + + // Status + pushingTag: l10n.t("status.pushingTag"), + + // Time + timeNeedFormatMonth: l10n.t("time.needFormatMonth"), + timeDateFormat: l10n.t("time.dateformat"), + timeSecond: l10n.t("time.second"), + timeMinute: l10n.t("time.minute"), + timeHour: l10n.t("time.hour"), + timeDay: l10n.t("time.day"), + timeWeek: l10n.t("time.week"), + timeMonth: l10n.t("time.month"), + timeYear: l10n.t("time.year"), + timeAgo: l10n.t("time.ago"), + timeSeconds: l10n.t("time.seconds"), + timeMinutes: l10n.t("time.minutes"), + timeHours: l10n.t("time.hours"), + timeDays: l10n.t("time.days"), + timeWeeks: l10n.t("time.weeks"), + timeMonths: l10n.t("time.months"), + timeYears: l10n.t("time.years"), + + // Months + monthJan: l10n.t("month.jan"), + monthFeb: l10n.t("month.feb"), + monthMar: l10n.t("month.mar"), + monthApr: l10n.t("month.apr"), + monthMay: l10n.t("month.may"), + monthJun: l10n.t("month.jun"), + monthJul: l10n.t("month.jul"), + monthAug: l10n.t("month.aug"), + monthSep: l10n.t("month.sep"), + monthOct: l10n.t("month.oct"), + monthNov: l10n.t("month.nov"), + monthDec: l10n.t("month.dec") + }; +} + +export type LocalizedStrings = ReturnType; diff --git a/src/l10n.ts b/src/l10n.ts new file mode 100644 index 00000000..22bf1b4d --- /dev/null +++ b/src/l10n.ts @@ -0,0 +1,92 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; + +import { l10n } from "vscode"; + +/** + * Cache for loaded translation files + */ +let englishTranslations: Record | null = null; + +/** + * Load English (base) translations from bundle.l10n.json + */ +function loadEnglishTranslations(translationPath: string): Record { + if (englishTranslations !== null) { + return englishTranslations; + } + + try { + const l10nPath = path.join(translationPath, "bundle.l10n.json"); + const content = fs.readFileSync(l10nPath, "utf8"); + englishTranslations = JSON.parse(content); + return englishTranslations as Record; + } catch (error) { + console.error("Failed to load English translations:", error); + englishTranslations = {}; + return englishTranslations; + } +} + +/** + * Translate with fallback to English + * + * If the translation is missing in the current language, it will fall back to English. + * + * @param key - Translation key + * @param args - Optional arguments for string interpolation + * @returns Translated string + */ +export function t(key: string, ...args: Array): string; +export function t(key: string, args: Record): string; +export function t( + key: string, + ...args: Array> +): string { + // Try to get translation from current locale + let result: string; + + if (args.length === 1 && typeof args[0] === "object" && !Array.isArray(args[0])) { + result = l10n.t(key, args[0]); + } else { + result = l10n.t(key, ...(args as Array)); + } + + if (result !== key) return result; + + const translationPath = l10n.uri?.fsPath ? path.dirname(l10n.uri.fsPath) : undefined; + if (!translationPath) return result; + const enTranslations = loadEnglishTranslations(translationPath); + const fallback = enTranslations[key]; + if (!fallback) return result; + + if (args.length === 1 && typeof args[0] === "object") { + return interpolate(fallback, args[0]); + } else if (args.length > 0) { + return interpolate(fallback, args as Array); + } + return fallback; +} + +/** + * Simple string interpolation for fallback translations + * Supports both positional ({0}, {1}) and named ({name}) placeholders + */ +function interpolate( + template: string, + args: Record | Array +): string { + if (Array.isArray(args)) { + // Positional arguments: {0}, {1}, etc. + return template.replace(/\{(\d+)\}/g, (_, index) => { + const value = args[parseInt(index, 10)]; + return value !== undefined ? String(value) : `{${index}}`; + }); + } else { + // Named arguments: {name}, {value}, etc. + return template.replace(/\{(\w+)\}/g, (_, key) => { + const value = args[key]; + return value !== undefined ? String(value) : `{${key}}`; + }); + } +} diff --git a/src/statusBarItem.ts b/src/statusBarItem.ts index 349b85bb..c5974aad 100644 --- a/src/statusBarItem.ts +++ b/src/statusBarItem.ts @@ -1,6 +1,7 @@ import * as vscode from "vscode"; import { Config } from "./config"; +import * as l10n from "./l10n"; export class StatusBarItem { private statusBarItem: vscode.StatusBarItem; @@ -10,8 +11,8 @@ export class StatusBarItem { constructor(context: vscode.ExtensionContext, config: Config) { this.config = config; this.statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1); - this.statusBarItem.text = "(neo) Git Graph"; - this.statusBarItem.tooltip = "View Git Graph"; + this.statusBarItem.text = l10n.t("statusBar.text"); + this.statusBarItem.tooltip = l10n.t("statusBar.tooltip"); this.statusBarItem.command = "neo-git-graph.view"; context.subscriptions.push(this.statusBarItem); } diff --git a/src/webview/dropdown.ts b/src/webview/dropdown.ts index 921859b6..9dfef623 100644 --- a/src/webview/dropdown.ts +++ b/src/webview/dropdown.ts @@ -33,7 +33,7 @@ export class Dropdown { filter.className = "dropdownFilter"; this.filterInput = document.createElement("input"); this.filterInput.className = "dropdownFilterInput"; - this.filterInput.placeholder = "Filter " + dropdownType + "..."; + this.filterInput.placeholder = l10n.filterPlaceholder.replace("{0}", dropdownType); filter.appendChild(this.filterInput); this.menuElem = document.createElement("div"); this.menuElem.className = "dropdownMenu"; @@ -43,7 +43,7 @@ export class Dropdown { this.menuElem.appendChild(this.optionsElem); this.noResultsElem = document.createElement("div"); this.noResultsElem.className = "dropdownNoResults"; - this.noResultsElem.innerHTML = "No results found."; + this.noResultsElem.innerHTML = l10n.noResultsFound; this.menuElem.appendChild(this.noResultsElem); this.currentValueElem = document.createElement("div"); this.currentValueElem.className = "dropdownCurrentValue"; diff --git a/src/webview/l10n.d.ts b/src/webview/l10n.d.ts new file mode 100644 index 00000000..5a18022c --- /dev/null +++ b/src/webview/l10n.d.ts @@ -0,0 +1,16 @@ +/** + * Type definitions for localized strings in webview. + * This file is generated based on webviewL10n.ts + */ + +import type { LocalizedStrings } from "@/extension/webviewL10n"; + +declare global { + /** + * Localized strings provided by the extension context. + * Available in the webview via a global variable injected in the HTML. + */ + var l10n: LocalizedStrings; +} + +export {}; diff --git a/src/webview/main.ts b/src/webview/main.ts index 65ffb7fd..4c4cfeb2 100644 --- a/src/webview/main.ts +++ b/src/webview/main.ts @@ -15,9 +15,9 @@ import { blinkHeadRow, ELLIPSIS, escapeHtml, + getMonth, getVSCodeStyle, insertAfter, - months, pad2, refInvalid, sendMessage, @@ -66,7 +66,7 @@ class GitGraphView { this.graph = new Graph("commitGraph", this.config); this.tableElem = document.getElementById("commitTable")!; this.footerElem = document.getElementById("footer")!; - this.repoDropdown = new Dropdown("repoSelect", true, "Repos", (value) => { + this.repoDropdown = new Dropdown("repoSelect", true, l10n.repo, (value) => { this.currentRepo = value; this.maxCommits = this.config.initialLoadCommits; this.expandedCommit = null; @@ -75,7 +75,7 @@ class GitGraphView { sendMessage({ command: "selectRepo", repo: value }); this.refresh(true); }); - this.branchDropdown = new Dropdown("branchSelect", false, "Branches", (value) => { + this.branchDropdown = new Dropdown("branchSelect", false, l10n.branch, (value) => { this.currentBranch = value; this.maxCommits = this.config.initialLoadCommits; this.expandedCommit = null; @@ -192,7 +192,7 @@ class GitGraphView { } this.saveState(); - let options = [{ name: "Show All", value: "" }]; + let options = [{ name: l10n.showAll, value: "" }]; for (let i = 0; i < this.gitBranches.length; i++) { options.push({ name: @@ -404,8 +404,7 @@ class GitGraphView { this.graph.render(this.expandedCommit); } private renderTable() { - let html = - 'GraphDescriptionDateAuthorCommit', + let html = `${l10n.graph}${l10n.description}${l10n.date}${l10n.author}${l10n.commit}`, i, currentHash = this.commits.length > 0 && this.commits[0].hash === "*" ? "*" : this.commitHead; for (i = 0; i < this.commits.length; i++) { @@ -471,14 +470,14 @@ class GitGraphView { } this.tableElem.innerHTML = "" + html + "
"; this.footerElem.innerHTML = this.moreCommitsAvailable - ? '
Load More Commits
' + ? '
' + l10n.loadMore + "
" : ""; this.makeTableResizable(); if (this.moreCommitsAvailable) { document.getElementById("loadMoreCommitsBtn")!.addEventListener("click", () => { (document.getElementById("loadMoreCommitsBtn")!.parentNode!).innerHTML = - '

' + svgIcons.loading + "Loading ...

"; + '

' + svgIcons.loading + l10n.loading + "

"; this.maxCommits += this.config.loadMoreCommits; this.hideCommitDetails(); this.saveState(); @@ -518,29 +517,29 @@ class GitGraphView { e, [ { - title: "Add Tag" + ELLIPSIS, + title: l10n.addTag + ELLIPSIS, onClick: () => { showFormDialog( - "Add tag to commit " + abbrevCommit(hash) + ":", + l10n.dialogAddTagTitle.replace("{0}", "" + abbrevCommit(hash) + ""), [ - { type: "text-ref" as const, name: "Name: ", default: "" }, + { type: "text-ref" as const, name: l10n.dialogAddTagName, default: "" }, { type: "select" as const, - name: "Type: ", + name: l10n.dialogAddTagType, default: "annotated", options: [ - { name: "Annotated", value: "annotated" }, - { name: "Lightweight", value: "lightweight" } + { name: l10n.dialogAddTagTypeAnnotated, value: "annotated" }, + { name: l10n.dialogAddTagTypeLightweight, value: "lightweight" } ] }, { type: "text" as const, - name: "Message: ", + name: l10n.dialogAddTagMessage, default: "", - placeholder: "Optional" + placeholder: l10n.dialogAddTagOptional } ], - "Add Tag", + l10n.dialogAddTagSubmit, (values) => { sendMessage({ command: "addTag", @@ -556,14 +555,15 @@ class GitGraphView { } }, { - title: "Create Branch" + ELLIPSIS, + title: l10n.createBranch + ELLIPSIS, onClick: () => { showRefInputDialog( - "Enter the name of the branch you would like to create from commit " + - abbrevCommit(hash) + - ":", + l10n.dialogCreateBranchTitle.replace( + "{0}", + "" + abbrevCommit(hash) + "" + ), "", - "Create Branch", + l10n.dialogCreateBranchSubmit, (name) => { sendMessage({ command: "createBranch", @@ -578,12 +578,13 @@ class GitGraphView { }, null, { - title: "Checkout" + ELLIPSIS, + title: l10n.checkout + ELLIPSIS, onClick: () => { showConfirmationDialog( - "Are you sure you want to checkout commit " + - abbrevCommit(hash) + - "? This will result in a 'detached HEAD' state.", + l10n.dialogCheckoutConfirm.replace( + "{0}", + "" + abbrevCommit(hash) + "" + ), () => { sendMessage({ command: "checkoutCommit", @@ -596,13 +597,14 @@ class GitGraphView { } }, { - title: "Cherry Pick" + ELLIPSIS, + title: l10n.cherryPick + ELLIPSIS, onClick: () => { if (this.commits[this.commitLookup[hash]].parentHashes.length === 1) { showConfirmationDialog( - "Are you sure you want to cherry pick commit " + - abbrevCommit(hash) + - "?", + l10n.dialogCherryPickConfirm.replace( + "{0}", + "" + abbrevCommit(hash) + "" + ), () => { sendMessage({ command: "cherrypickCommit", @@ -625,12 +627,13 @@ class GitGraphView { }) ); showSelectDialog( - "Are you sure you want to cherry pick merge commit " + - abbrevCommit(hash) + - "? Choose the parent hash on the main branch, to cherry pick the commit relative to:", + l10n.dialogCherryPickConfirm.replace( + "{0}", + "" + abbrevCommit(hash) + "" + ), "1", options, - "Yes, cherry pick commit", + l10n.dialogYesCherryPick, (parentIndex) => { sendMessage({ command: "cherrypickCommit", @@ -645,13 +648,14 @@ class GitGraphView { } }, { - title: "Revert" + ELLIPSIS, + title: l10n.revert + ELLIPSIS, onClick: () => { if (this.commits[this.commitLookup[hash]].parentHashes.length === 1) { showConfirmationDialog( - "Are you sure you want to revert commit " + - abbrevCommit(hash) + - "?", + l10n.dialogRevertConfirm.replace( + "{0}", + "" + abbrevCommit(hash) + "" + ), () => { sendMessage({ command: "revertCommit", @@ -674,12 +678,13 @@ class GitGraphView { }) ); showSelectDialog( - "Are you sure you want to revert merge commit " + - abbrevCommit(hash) + - "? Choose the parent hash on the main branch, to revert the commit relative to:", + l10n.dialogRevertConfirm.replace( + "{0}", + "" + abbrevCommit(hash) + "" + ), "1", options, - "Yes, revert commit", + l10n.dialogYesRevert, (parentIndex) => { sendMessage({ command: "revertCommit", @@ -695,15 +700,15 @@ class GitGraphView { }, null, { - title: "Merge into current branch" + ELLIPSIS, + title: l10n.merge + ELLIPSIS, onClick: () => { showCheckboxDialog( - "Are you sure you want to merge commit " + - abbrevCommit(hash) + - " into the current branch?", - "Create a new commit even if fast-forward is possible", + l10n.dialogMergeConfirm + .replace("{0}", `${abbrevCommit(hash)}`) + .replace("{1}", `${l10n.labelCurrentBranch}`), + l10n.dialogMergeNoFastForward, true, - "Yes, merge", + l10n.dialogYesMerge, (createNewCommit) => { sendMessage({ command: "mergeCommit", @@ -717,19 +722,19 @@ class GitGraphView { } }, { - title: "Reset current branch to this Commit" + ELLIPSIS, + title: l10n.reset + ELLIPSIS, onClick: () => { showSelectDialog( - "Are you sure you want to reset the current branch to commit " + - abbrevCommit(hash) + - "?", + l10n.dialogResetConfirm + .replace("{0}", `${l10n.labelCurrentBranch}`) + .replace("{1}", "" + abbrevCommit(hash) + ""), "mixed", [ - { name: "Soft - Keep all changes, but reset head", value: "soft" }, - { name: "Mixed - Keep working tree, but reset index", value: "mixed" }, - { name: "Hard - Discard all changes", value: "hard" } + { name: l10n.dialogResetSoft, value: "soft" }, + { name: l10n.dialogResetMixed, value: "mixed" }, + { name: l10n.dialogResetHard, value: "hard" } ], - "Yes, reset", + l10n.dialogYesReset, (mode) => { sendMessage({ command: "resetToCommit", @@ -744,7 +749,7 @@ class GitGraphView { }, null, { - title: "Copy Commit Hash to Clipboard", + title: l10n.copyCommitHash, onClick: () => { sendMessage({ command: "copyToClipboard", type: "Commit Hash", data: hash }); } @@ -770,12 +775,12 @@ class GitGraphView { if (sourceElem.classList.contains("tag")) { menu = [ { - title: "Delete Tag" + ELLIPSIS, + title: l10n.deleteTag + ELLIPSIS, onClick: () => { showConfirmationDialog( - "Are you sure you want to delete the tag " + - escapeHtml(refName) + - "?", + l10n.dialogDeleteConfirm + .replace("{0}", l10n.labelTag) + .replace("{1}", "" + escapeHtml(refName) + ""), () => { sendMessage({ command: "deleteTag", repo: this.currentRepo!, tagName: refName }); }, @@ -784,13 +789,16 @@ class GitGraphView { } }, { - title: "Push Tag" + ELLIPSIS, + title: l10n.pushTag + ELLIPSIS, onClick: () => { showConfirmationDialog( - "Are you sure you want to push the tag " + escapeHtml(refName) + "?", + l10n.dialogPushTagConfirm.replace( + "{0}", + "" + escapeHtml(refName) + "" + ), () => { sendMessage({ command: "pushTag", repo: this.currentRepo!, tagName: refName }); - showActionRunningDialog("Pushing Tag"); + showActionRunningDialog(l10n.pushingTag); }, null ); @@ -803,17 +811,20 @@ class GitGraphView { menu = []; if (this.gitBranchHead !== refName) { menu.push({ - title: "Checkout Branch", + title: l10n.checkoutBranch, onClick: () => this.checkoutBranchAction(sourceElem, refName) }); } menu.push({ - title: "Rename Branch" + ELLIPSIS, + title: l10n.renameBranch + ELLIPSIS, onClick: () => { showRefInputDialog( - "Enter the new name for branch " + escapeHtml(refName) + ":", + l10n.dialogRenameBranchTitle.replace( + "{0}", + "" + escapeHtml(refName) + "" + ), refName, - "Rename Branch", + l10n.dialogRenameBranchSubmit, (newName) => { sendMessage({ command: "renameBranch", @@ -829,15 +840,15 @@ class GitGraphView { if (this.gitBranchHead !== refName) { menu.push( { - title: "Delete Branch" + ELLIPSIS, + title: l10n.deleteBranch + ELLIPSIS, onClick: () => { showCheckboxDialog( - "Are you sure you want to delete the branch " + - escapeHtml(refName) + - "?", - "Force Delete", + l10n.dialogDeleteConfirm + .replace("{0}", l10n.labelBranch) + .replace("{1}", "" + escapeHtml(refName) + ""), + l10n.dialogDeleteForceDelete, false, - "Delete Branch", + l10n.deleteBranch, (forceDelete) => { sendMessage({ command: "deleteBranch", @@ -851,15 +862,16 @@ class GitGraphView { } }, { - title: "Merge into current branch" + ELLIPSIS, + title: l10n.merge + ELLIPSIS, onClick: () => { showCheckboxDialog( - "Are you sure you want to merge branch " + - escapeHtml(refName) + - " into the current branch?", - "Create a new commit even if fast-forward is possible", + l10n.dialogMergeConfirm.replace( + "{0}", + "" + escapeHtml(refName) + "" + ), + l10n.dialogMergeNoFastForward, true, - "Yes, merge", + l10n.dialogYesMerge, (createNewCommit) => { sendMessage({ command: "mergeBranch", @@ -877,7 +889,7 @@ class GitGraphView { } else { menu = [ { - title: "Checkout Branch" + ELLIPSIS, + title: l10n.checkoutBranch + ELLIPSIS, onClick: () => this.checkoutBranchAction(sourceElem, refName) } ]; @@ -914,7 +926,8 @@ class GitGraphView { private renderShowLoading() { hideDialogAndContextMenu(); this.graph.clear(); - this.tableElem.innerHTML = '

' + svgIcons.loading + "Loading ...

"; + this.tableElem.innerHTML = + '

' + svgIcons.loading + l10n.loading + "

"; this.footerElem.innerHTML = ""; } private checkoutBranchAction(sourceElem: HTMLElement, refName: string) { @@ -928,11 +941,12 @@ class GitGraphView { } else if (sourceElem.classList.contains("remote")) { let refNameComps = refName.split("/"); showRefInputDialog( - "Enter the name of the new branch you would like to create when checking out " + - escapeHtml(sourceElem.dataset.name!) + - ":", + l10n.dialogCreateBranchTitle.replace( + "{0}", + "" + escapeHtml(sourceElem.dataset.name!) + "" + ), refNameComps[refNameComps.length - 1], - "Checkout Branch", + l10n.checkoutBranch, (newBranch) => { sendMessage({ command: "checkoutBranch", @@ -1233,21 +1247,21 @@ window.addEventListener("message", (event) => { const msg: GG.ResponseMessage = event.data; switch (msg.command) { case "addTag": - refreshGraphOrDisplayError(msg.status, "Unable to Add Tag"); + refreshGraphOrDisplayError(msg.status, l10n.unableToAddTag); break; case "checkoutBranch": - refreshGraphOrDisplayError(msg.status, "Unable to Checkout Branch"); + refreshGraphOrDisplayError(msg.status, l10n.unableToCheckoutBranch); break; case "checkoutCommit": - refreshGraphOrDisplayError(msg.status, "Unable to Checkout Commit"); + refreshGraphOrDisplayError(msg.status, l10n.unableToCheckoutCommit); break; case "cherrypickCommit": - refreshGraphOrDisplayError(msg.status, "Unable to Cherry Pick Commit"); + refreshGraphOrDisplayError(msg.status, l10n.unableToCherryPick); break; case "commitDetails": if (msg.commitDetails === null) { gitGraph.hideCommitDetails(); - showErrorDialog("Unable to load commit details", null, null); + showErrorDialog(l10n.unableToLoadCommitDetails, null, null); } else { gitGraph.showCommitDetails( msg.commitDetails, @@ -1257,16 +1271,16 @@ window.addEventListener("message", (event) => { break; case "copyToClipboard": if (msg.success === false) - showErrorDialog("Unable to Copy " + msg.type + " to Clipboard", null, null); + showErrorDialog(l10n.unableToCopyToClipboard.replace("{0}", msg.type), null, null); break; case "createBranch": - refreshGraphOrDisplayError(msg.status, "Unable to Create Branch"); + refreshGraphOrDisplayError(msg.status, l10n.unableToCreateBranch); break; case "deleteBranch": - refreshGraphOrDisplayError(msg.status, "Unable to Delete Branch"); + refreshGraphOrDisplayError(msg.status, l10n.unableToDeleteBranch); break; case "deleteTag": - refreshGraphOrDisplayError(msg.status, "Unable to Delete Tag"); + refreshGraphOrDisplayError(msg.status, l10n.unableToDeleteTag); break; case "fetchAvatar": gitGraph.loadAvatar(msg.email, msg.image); @@ -1281,28 +1295,28 @@ window.addEventListener("message", (event) => { gitGraph.loadRepos(msg.repos, msg.lastActiveRepo); break; case "mergeBranch": - refreshGraphOrDisplayError(msg.status, "Unable to Merge Branch"); + refreshGraphOrDisplayError(msg.status, l10n.unableToMergeBranch); break; case "mergeCommit": - refreshGraphOrDisplayError(msg.status, "Unable to Merge Commit"); + refreshGraphOrDisplayError(msg.status, l10n.unableToMergeCommit); break; case "pushTag": - refreshGraphOrDisplayError(msg.status, "Unable to Push Tag"); + refreshGraphOrDisplayError(msg.status, l10n.unableToPushTag); break; case "renameBranch": - refreshGraphOrDisplayError(msg.status, "Unable to Rename Branch"); + refreshGraphOrDisplayError(msg.status, l10n.unableToRenameBranch); break; case "refresh": gitGraph.refresh(false); break; case "resetToCommit": - refreshGraphOrDisplayError(msg.status, "Unable to Reset to Commit"); + refreshGraphOrDisplayError(msg.status, l10n.unableToReset); break; case "revertCommit": - refreshGraphOrDisplayError(msg.status, "Unable to Revert Commit"); + refreshGraphOrDisplayError(msg.status, l10n.unableToRevert); break; case "viewDiff": - if (msg.success === false) showErrorDialog("Unable to view diff of file", null, null); + if (msg.success === false) showErrorDialog(l10n.unableToViewDiff, null, null); break; } }); @@ -1318,7 +1332,16 @@ function refreshGraphOrDisplayError(status: GitCommandStatus, errorMessage: stri function getCommitDate(dateVal: number) { let date = new Date(dateVal * 1000), value; - let dateStr = date.getDate() + " " + months[date.getMonth()] + " " + date.getFullYear(); + + let dateStr = l10n.timeDateFormat + .replace("DD", String(date.getDate())) + .replace( + "MM", + l10n.timeNeedFormatMonth === "true" + ? getMonth()[date.getMonth()] + : String(date.getMonth() + 1) + ) + .replace("YYYY", String(date.getFullYear())); let timeStr = pad2(date.getHours()) + ":" + pad2(date.getMinutes()); switch (viewState.dateFormat) { @@ -1327,30 +1350,38 @@ function getCommitDate(dateVal: number) { break; case "Relative": let diff = Math.round(new Date().getTime() / 1000) - dateVal, - unit; + unit, + unitPlural; if (diff < 60) { - unit = "second"; + unit = l10n.timeSecond; + unitPlural = l10n.timeSeconds; } else if (diff < 3600) { - unit = "minute"; + unit = l10n.timeMinute; + unitPlural = l10n.timeMinutes; diff /= 60; } else if (diff < 86400) { - unit = "hour"; + unit = l10n.timeHour; + unitPlural = l10n.timeHours; diff /= 3600; } else if (diff < 604800) { - unit = "day"; + unit = l10n.timeDay; + unitPlural = l10n.timeDays; diff /= 86400; } else if (diff < 2629800) { - unit = "week"; + unit = l10n.timeWeek; + unitPlural = l10n.timeWeeks; diff /= 604800; } else if (diff < 31557600) { - unit = "month"; + unit = l10n.timeMonth; + unitPlural = l10n.timeMonths; diff /= 2629800; } else { - unit = "year"; + unit = l10n.timeYear; + unitPlural = l10n.timeYears; diff /= 31557600; } diff = Math.round(diff); - value = diff + " " + unit + (diff !== 1 ? "s" : "") + " ago"; + value = diff + " " + (diff !== 1 ? unitPlural : unit) + " " + l10n.timeAgo; break; default: value = dateStr + " " + timeStr; @@ -1556,8 +1587,8 @@ function showConfirmationDialog( ) { showDialog( message, - "Yes", - "No", + l10n.dialogYes, + l10n.dialogCancel, () => { hideDialog(); confirmed(); @@ -1667,7 +1698,7 @@ function showFormDialog( showDialog( html, actionName, - "Cancel", + l10n.dialogCancel, () => { if (dialog.className === "active noInput" || dialog.className === "active inputInvalid") return; @@ -1700,9 +1731,7 @@ function showFormDialog( let newClassName = "active" + (noInput ? " noInput" : invalidInput ? " inputInvalid" : ""); if (dialog.className !== newClassName) { dialog.className = newClassName; - dialogAction.title = invalidInput - ? "Unable to " + actionName + ", one or more invalid characters entered." - : ""; + dialogAction.title = invalidInput ? l10n.invalidCharacters.replace("{0}", actionName) : ""; } }); } @@ -1710,13 +1739,12 @@ function showFormDialog( function showErrorDialog(message: string, reason: string | null, sourceElem: HTMLElement | null) { showDialog( svgIcons.alert + - "Error: " + message + (reason !== null ? '
' + escapeHtml(reason).split("\n").join("
") + "
" : ""), null, - "Dismiss", + l10n.dialogDismiss, null, sourceElem ); @@ -1725,7 +1753,7 @@ function showActionRunningDialog(command: string) { showDialog( '' + svgIcons.loading + command + " ...", null, - "Dismiss", + l10n.dialogDismiss, null, null ); diff --git a/src/webview/utils.ts b/src/webview/utils.ts index 5f2989c7..b925d38e 100644 --- a/src/webview/utils.ts +++ b/src/webview/utils.ts @@ -20,20 +20,25 @@ export const svgIcons = { '', file: '' }; -export const months = [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec" -]; +let getMonthCache: string[] | null = null; +export function getMonth(): string[] { + if (getMonthCache) return getMonthCache; + getMonthCache = [ + l10n.monthJan, + l10n.monthFeb, + l10n.monthMar, + l10n.monthApr, + l10n.monthMay, + l10n.monthJun, + l10n.monthJul, + l10n.monthAug, + l10n.monthSep, + l10n.monthOct, + l10n.monthNov, + l10n.monthDec + ]; + return getMonthCache; +} const htmlEscapes: { [key: string]: string } = { "&": "&", "<": "<", diff --git a/tests/backend/queries/loadBranches/list.test.ts b/tests/backend/queries/loadBranches/list.test.ts index fb499741..716dcff0 100644 --- a/tests/backend/queries/loadBranches/list.test.ts +++ b/tests/backend/queries/loadBranches/list.test.ts @@ -14,6 +14,7 @@ let detachedRepo: string; let repoWithRemote: string; beforeAll(() => { + process.env["LANG"] = "en_US.UTF-8"; simpleRepo = makeRepo(); git(["branch", "feature/foo"], simpleRepo); diff --git a/tests/webview/__mocks__/vscode.ts b/tests/webview/__mocks__/vscode.ts new file mode 100644 index 00000000..829100d8 --- /dev/null +++ b/tests/webview/__mocks__/vscode.ts @@ -0,0 +1,34 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; + +// Load English translations from bundle.l10n.json +const l10nPath = path.resolve(__dirname, "../../../l10n/bundle.l10n.json"); +const translations: Record = JSON.parse(fs.readFileSync(l10nPath, "utf8")); + +export const l10n = { + t: ( + key: string, + ...args: Array> + ): string => { + const template = translations[key] || key; + + // Handle object arguments (named parameters) + if (args.length === 1 && typeof args[0] === "object" && !Array.isArray(args[0])) { + return template.replace(/\{(\w+)\}/g, (_, name: string) => { + const value = (args[0] as Record)[name]; + return value !== undefined ? String(value) : `{${name}}`; + }); + } + + // Handle positional arguments {0}, {1}, etc. + if (args.length > 0) { + return template.replace(/\{(\d+)\}/g, (_, index) => { + const value = args[parseInt(index, 10)]; + return value !== undefined ? String(value) : `{${index}}`; + }); + } + + return template; + }, + uri: undefined +}; diff --git a/tests/webview/setup.ts b/tests/webview/setup.ts index 6f09d02d..85934830 100644 --- a/tests/webview/setup.ts +++ b/tests/webview/setup.ts @@ -1,3 +1,4 @@ +import { getWebviewLocalizedStrings } from "@/extension/webviewL10n"; import type * as GG from "@/types"; export function createVscodeMock() { @@ -45,6 +46,7 @@ export function setupHtml(viewState: GG.GitGraphViewState) { `; (global as unknown as { viewState: GG.GitGraphViewState }).viewState = viewState; + global["l10n"] = getWebviewLocalizedStrings(); } export function receive(msg: GG.ResponseMessage) { diff --git a/vitest.config.ts b/vitest.config.ts index 9ee656f6..ee81e2f4 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -15,7 +15,15 @@ export default defineConfig({ } }, { - resolve: { alias }, + resolve: { + alias: [ + ...alias, + { + find: "vscode", + replacement: path.resolve(__dirname, "tests/webview/__mocks__/vscode.ts") + } + ] + }, test: { name: "webview", environment: "jsdom", From 1d9060554ad7d3f3f8b4e7ad5e65232045fd77f3 Mon Sep 17 00:00:00 2001 From: "Mr.Lee" Date: Mon, 6 Apr 2026 21:41:03 +0800 Subject: [PATCH 2/8] fix: outputChannel name error --- l10n/bundle.l10n.json | 2 +- l10n/bundle.l10n.zh-cn.json | 2 +- l10n/bundle.l10n.zh-tw.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index f46f4795..55758429 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -1,7 +1,7 @@ { "statusBar.text": "(neo) Git Graph", "statusBar.tooltip": "View Git Graph", - "outputChannel.text": "Git Graph", + "outputChannel.text": "(neo) Git Graph", "ui.repo": "Repo", "ui.branch": "Branch", "ui.showRemoteBranches": "Show Remote Branches", diff --git a/l10n/bundle.l10n.zh-cn.json b/l10n/bundle.l10n.zh-cn.json index bd97c2a0..b8233050 100644 --- a/l10n/bundle.l10n.zh-cn.json +++ b/l10n/bundle.l10n.zh-cn.json @@ -1,7 +1,7 @@ { "statusBar.text": "(neo) Git Graph", "statusBar.tooltip": "查看 Git 分支图", - "outputChannel.text": "Git 分支图", + "outputChannel.text": "(neo) Git Graph", "ui.repo": "仓库", "ui.branch": "分支", "ui.showRemoteBranches": "显示远程分支", diff --git a/l10n/bundle.l10n.zh-tw.json b/l10n/bundle.l10n.zh-tw.json index c59ec051..1ab3c0ba 100644 --- a/l10n/bundle.l10n.zh-tw.json +++ b/l10n/bundle.l10n.zh-tw.json @@ -1,7 +1,7 @@ { "statusBar.text": "(neo) Git Graph", "statusBar.tooltip": "檢視 Git 分支圖", - "outputChannel.text": "Git 分支圖", + "outputChannel.text": "(neo) Git Graph", "ui.repo": "存放庫", "ui.branch": "分支", "ui.showRemoteBranches": "顯示遠端分支", From 9a4858c81aede2c3d00ff5cf000875d1e081d8fb Mon Sep 17 00:00:00 2001 From: "Mr.Lee" Date: Mon, 6 Apr 2026 21:48:20 +0800 Subject: [PATCH 3/8] fix: remove the needless translate key --- l10n/bundle.l10n.json | 2 -- l10n/bundle.l10n.zh-cn.json | 2 -- l10n/bundle.l10n.zh-tw.json | 2 -- src/extension/webviewL10n.ts | 2 -- 4 files changed, 8 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 55758429..8fd94011 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -28,13 +28,11 @@ "action.merge": "Merge into current branch", "action.reset": "Reset current branch to this Commit", "action.copyCommitHash": "Copy Commit Hash to Clipboard", - "action.copyCommitSubject": "Copy Commit Subject to Clipboard", "action.deleteTag": "Delete Tag", "action.pushTag": "Push Tag", "action.checkoutBranch": "Checkout Branch", "action.renameBranch": "Rename Branch", "action.deleteBranch": "Delete Branch", - "action.mergeBranch": "Merge into current branch", "label.tag": "the tag", "label.branch": "the branch", "label.currentBranch": "the current branch", diff --git a/l10n/bundle.l10n.zh-cn.json b/l10n/bundle.l10n.zh-cn.json index b8233050..0730287b 100644 --- a/l10n/bundle.l10n.zh-cn.json +++ b/l10n/bundle.l10n.zh-cn.json @@ -28,13 +28,11 @@ "action.merge": "合并到当前分支", "action.reset": "重置当前分支到此提交", "action.copyCommitHash": "复制提交哈希到剪贴板", - "action.copyCommitSubject": "复制提交主题到剪贴板", "action.deleteTag": "删除标签", "action.pushTag": "推送标签", "action.checkoutBranch": "检出分支", "action.renameBranch": "重命名分支", "action.deleteBranch": "删除分支", - "action.mergeBranch": "合并到当前分支", "label.tag": "标签", "label.branch": "分支", "label.currentBranch": "当前分支", diff --git a/l10n/bundle.l10n.zh-tw.json b/l10n/bundle.l10n.zh-tw.json index 1ab3c0ba..5656ccdd 100644 --- a/l10n/bundle.l10n.zh-tw.json +++ b/l10n/bundle.l10n.zh-tw.json @@ -28,13 +28,11 @@ "action.merge": "合併到目前分支", "action.reset": "重設目前分支到此提交", "action.copyCommitHash": "複製提交雜湊到剪貼簿", - "action.copyCommitSubject": "複製提交主旨到剪貼簿", "action.deleteTag": "刪除標籤", "action.pushTag": "推送標籤", "action.checkoutBranch": "檢出分支", "action.renameBranch": "重新命名分支", "action.deleteBranch": "刪除分支", - "action.mergeBranch": "合併到目前分支", "label.tag": "標籤", "label.branch": "分支", "label.currentBranch": "目前分支", diff --git a/src/extension/webviewL10n.ts b/src/extension/webviewL10n.ts index dd81339a..779fa753 100644 --- a/src/extension/webviewL10n.ts +++ b/src/extension/webviewL10n.ts @@ -55,13 +55,11 @@ export function getWebviewLocalizedStrings() { merge: l10n.t("action.merge"), reset: l10n.t("action.reset"), copyCommitHash: l10n.t("action.copyCommitHash"), - copyCommitSubject: l10n.t("action.copyCommitSubject"), deleteTag: l10n.t("action.deleteTag"), pushTag: l10n.t("action.pushTag"), checkoutBranch: l10n.t("action.checkoutBranch"), renameBranch: l10n.t("action.renameBranch"), deleteBranch: l10n.t("action.deleteBranch"), - mergeBranch: l10n.t("action.mergeBranch"), // label labelTag: l10n.t("label.tag"), From 298c4958b46154dddb4fcfa85d1eab9d8a67d04f Mon Sep 17 00:00:00 2001 From: "Mr.Lee" Date: Tue, 7 Apr 2026 10:38:23 +0800 Subject: [PATCH 4/8] fix: some bug --- src/webview/main.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/webview/main.ts b/src/webview/main.ts index 4c4cfeb2..5e45a1b5 100644 --- a/src/webview/main.ts +++ b/src/webview/main.ts @@ -865,10 +865,9 @@ class GitGraphView { title: l10n.merge + ELLIPSIS, onClick: () => { showCheckboxDialog( - l10n.dialogMergeConfirm.replace( - "{0}", - "" + escapeHtml(refName) + "" - ), + l10n.dialogMergeConfirm + .replace("{0}", "" + escapeHtml(refName) + "") + .replace("{1}", l10n.labelCurrentBranch), l10n.dialogMergeNoFastForward, true, l10n.dialogYesMerge, From 9b2e6dc6d1138f8245542f1ace2d8b03d2b8023e Mon Sep 17 00:00:00 2001 From: "Mr.Lee" Date: Thu, 9 Apr 2026 20:42:59 +0800 Subject: [PATCH 5/8] feat: add button at scm title --- package.json | 15 ++++++++++++++- src/extension.ts | 29 +++++++++++++++++++++++++++-- src/extension/webviewPanel.ts | 8 +++++++- src/webview/main.ts | 10 +++++++++- 4 files changed, 57 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 271da9a9..9b8c0e46 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,11 @@ { "category": "(neo) Git Graph", "command": "neo-git-graph.view", - "title": "%command.view%" + "title": "%command.view%", + "icon": { + "light": "resources/webview-icon-dark.svg", + "dark": "resources/webview-icon-dark.svg" + } }, { "category": "(neo) Git Graph", @@ -84,6 +88,15 @@ "title": "%command.clearAvatarCache%" } ], + "menus": { + "scm/title": [ + { + "command": "neo-git-graph.view", + "group": "navigation", + "when": "scmProvider == git" + } + ] + }, "configuration": { "type": "object", "title": "%config.title%", diff --git a/src/extension.ts b/src/extension.ts index b77797ff..582a9cd9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -24,12 +24,34 @@ export function activate(context: vscode.ExtensionContext) { const gitClient = gitClientFactory(extensionState.getLastActiveRepo() ?? "", config.gitPath()); const repoManager = new RepoManager(extensionState, statusBarItem, config); let currentPanel: WebviewPanel | undefined; + let currentBridge: WebviewBridge | undefined; context.subscriptions.push( outputChannel, - vscode.commands.registerCommand("neo-git-graph.view", () => { + vscode.commands.registerCommand("neo-git-graph.view", (resource) => { + let repoPath: string | undefined; + + if (resource && typeof resource === "object") { + if ("rootUri" in resource) { + repoPath = resource.rootUri.fsPath; + } else if ("uri" in resource) { + repoPath = resource.uri.fsPath; + } + } + const column = vscode.window.activeTextEditor?.viewColumn; if (currentPanel) { + if (repoPath) { + gitClient.setRepo(repoPath); + extensionState.setLastActiveRepo(repoPath); + if (currentBridge) { + currentBridge.post({ + command: "loadRepos", + repos: repoManager.getRepos(), + lastActiveRepo: repoPath + }); + } + } currentPanel.reveal(column); return; } @@ -50,6 +72,7 @@ export function activate(context: vscode.ExtensionContext) { if (panel.visible) bridge.post({ command: "refresh" }); }); bridge = webviewBridgeFactory(panel.webview, repoFileWatcher); + currentBridge = bridge; avatarManager.registerBridge(bridge.post.bind(bridge)); const { onPanelShown } = registerMessageHandlers(bridge, { config, @@ -70,8 +93,10 @@ export function activate(context: vscode.ExtensionContext) { repoManager, onDispose: () => { currentPanel = undefined; + currentBridge = undefined; }, - onPanelShown + onPanelShown, + initialRepo: repoPath }); }), vscode.commands.registerCommand("neo-git-graph.clearAvatarCache", () => { diff --git a/src/extension/webviewPanel.ts b/src/extension/webviewPanel.ts index 197cf2a0..fc66a5f5 100644 --- a/src/extension/webviewPanel.ts +++ b/src/extension/webviewPanel.ts @@ -22,6 +22,7 @@ export function createWebviewPanel(opts: { repoManager: RepoManager; onDispose: () => void; onPanelShown: () => void; + initialRepo?: string; }) { const { panel, @@ -33,9 +34,14 @@ export function createWebviewPanel(opts: { avatarManager, repoManager, onDispose, - onPanelShown + onPanelShown, + initialRepo } = opts; + if (initialRepo) { + extensionState.setLastActiveRepo(initialRepo); + } + const disposables: vscode.Disposable[] = []; let isGraphViewLoaded = false; let isPanelVisible = true; diff --git a/src/webview/main.ts b/src/webview/main.ts index 5e45a1b5..68dc8a00 100644 --- a/src/webview/main.ts +++ b/src/webview/main.ts @@ -135,7 +135,15 @@ class GitGraphView { let repoPaths = Object.keys(repos), changedRepo = false; - if (typeof repos[this.currentRepo] === "undefined") { + + // Check if we need to update the current repo + if (lastActiveRepo !== null && typeof repos[lastActiveRepo] !== "undefined" && lastActiveRepo !== this.currentRepo) { + // Explicitly switching to a different repo + this.currentRepo = lastActiveRepo; + this.saveState(); + changedRepo = true; + } else if (typeof repos[this.currentRepo] === "undefined") { + // Current repo no longer exists this.currentRepo = lastActiveRepo !== null && typeof repos[lastActiveRepo] !== "undefined" ? lastActiveRepo From 88cb99ac857631e0cf5d3a8db09107f0f2e7ac27 Mon Sep 17 00:00:00 2001 From: "Mr.Lee" Date: Thu, 9 Apr 2026 21:33:49 +0800 Subject: [PATCH 6/8] fix: forget format --- src/extension.ts | 4 ++-- src/webview/main.ts | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 582a9cd9..b8673ed9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -30,7 +30,7 @@ export function activate(context: vscode.ExtensionContext) { outputChannel, vscode.commands.registerCommand("neo-git-graph.view", (resource) => { let repoPath: string | undefined; - + if (resource && typeof resource === "object") { if ("rootUri" in resource) { repoPath = resource.rootUri.fsPath; @@ -38,7 +38,7 @@ export function activate(context: vscode.ExtensionContext) { repoPath = resource.uri.fsPath; } } - + const column = vscode.window.activeTextEditor?.viewColumn; if (currentPanel) { if (repoPath) { diff --git a/src/webview/main.ts b/src/webview/main.ts index 68dc8a00..97a76422 100644 --- a/src/webview/main.ts +++ b/src/webview/main.ts @@ -135,9 +135,13 @@ class GitGraphView { let repoPaths = Object.keys(repos), changedRepo = false; - + // Check if we need to update the current repo - if (lastActiveRepo !== null && typeof repos[lastActiveRepo] !== "undefined" && lastActiveRepo !== this.currentRepo) { + if ( + lastActiveRepo !== null && + typeof repos[lastActiveRepo] !== "undefined" && + lastActiveRepo !== this.currentRepo + ) { // Explicitly switching to a different repo this.currentRepo = lastActiveRepo; this.saveState(); From d80302e38f2588b22b335ab6680d873076880e3e Mon Sep 17 00:00:00 2001 From: "Mr.Lee" Date: Thu, 9 Apr 2026 22:09:13 +0800 Subject: [PATCH 7/8] fix: some bug feedback by AI --- package.json | 2 +- src/extension.ts | 35 ++++++++++++++++++++++------------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 9b8c0e46..a497ab5c 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "command": "neo-git-graph.view", "title": "%command.view%", "icon": { - "light": "resources/webview-icon-dark.svg", + "light": "resources/webview-icon-light.svg", "dark": "resources/webview-icon-dark.svg" } }, diff --git a/src/extension.ts b/src/extension.ts index b8673ed9..1f04c3ae 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -17,11 +17,16 @@ import { StatusBarItem } from "./statusBarItem"; export function activate(context: vscode.ExtensionContext) { initL10n(context.extensionPath); - const outputChannel = vscode.window.createOutputChannel(l10n.t("outputChannel.text")); + const outputChannel = vscode.window.createOutputChannel( + l10n.t("outputChannel.text"), + ); const extensionState = new ExtensionState(context); const avatarManager = new AvatarManager(config.gitPath, extensionState); const statusBarItem = new StatusBarItem(context, config); - const gitClient = gitClientFactory(extensionState.getLastActiveRepo() ?? "", config.gitPath()); + const gitClient = gitClientFactory( + extensionState.getLastActiveRepo() ?? "", + config.gitPath(), + ); const repoManager = new RepoManager(extensionState, statusBarItem, config); let currentPanel: WebviewPanel | undefined; let currentBridge: WebviewBridge | undefined; @@ -32,13 +37,17 @@ export function activate(context: vscode.ExtensionContext) { let repoPath: string | undefined; if (resource && typeof resource === "object") { - if ("rootUri" in resource) { + if (typeof resource?.rootUri?.fsPath === "string") { repoPath = resource.rootUri.fsPath; - } else if ("uri" in resource) { + } else if (typeof resource?.uri?.fsPath === "string") { repoPath = resource.uri.fsPath; } } + const repos = repoManager.getRepos(); + + if (repoPath && !repos[repoPath]) repoPath = undefined; + const column = vscode.window.activeTextEditor?.viewColumn; if (currentPanel) { if (repoPath) { @@ -47,8 +56,8 @@ export function activate(context: vscode.ExtensionContext) { if (currentBridge) { currentBridge.post({ command: "loadRepos", - repos: repoManager.getRepos(), - lastActiveRepo: repoPath + repos: repos, + lastActiveRepo: repoPath, }); } } @@ -63,9 +72,9 @@ export function activate(context: vscode.ExtensionContext) { enableScripts: true, localResourceRoots: [ buildExtensionUri(context.extensionPath, "media"), - buildExtensionUri(context.extensionPath, "out") - ] - } + buildExtensionUri(context.extensionPath, "out"), + ], + }, ); let bridge!: WebviewBridge; const repoFileWatcher = new RepoFileWatcher(() => { @@ -80,7 +89,7 @@ export function activate(context: vscode.ExtensionContext) { repoManager, extensionState, avatarManager, - repoFileWatcher + repoFileWatcher, }); currentPanel = createWebviewPanel({ panel, @@ -96,7 +105,7 @@ export function activate(context: vscode.ExtensionContext) { currentBridge = undefined; }, onPanelShown, - initialRepo: repoPath + initialRepo: repoPath, }); }), vscode.commands.registerCommand("neo-git-graph.clearAvatarCache", () => { @@ -104,7 +113,7 @@ export function activate(context: vscode.ExtensionContext) { }), vscode.workspace.registerTextDocumentContentProvider( DiffDocProvider.scheme, - new DiffDocProvider(gitClient.getInstance) + new DiffDocProvider(gitClient.getInstance), ), vscode.workspace.onDidChangeConfiguration((e) => { if (e.affectsConfiguration("neo-git-graph.showStatusBarItem")) { @@ -115,7 +124,7 @@ export function activate(context: vscode.ExtensionContext) { gitClient.setGitPath(config.gitPath()); } }), - repoManager + repoManager, ); outputChannel.appendLine("Extension activated successfully"); From 994dddf845ed2223dcca0ba4e844340effb4d350 Mon Sep 17 00:00:00 2001 From: "Mr.Lee" Date: Thu, 9 Apr 2026 22:16:10 +0800 Subject: [PATCH 8/8] fix: forget format --- src/extension.ts | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 1a7c83ad..bb837de8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,7 +2,7 @@ import * as vscode from "vscode"; import { AvatarManager } from "./avatarManager"; import { gitClientFactory } from "./backend/gitClient"; -import { buildExtensionUri } from "./backend/utils/path.util"; +import { buildExtensionUri, getPathFromStr } from "./backend/utils/path.util"; import { config } from "./config"; import { DiffDocProvider } from "./diffDocProvider"; import { registerMessageHandlers } from "./extension/messageHandler"; @@ -19,9 +19,7 @@ import { StatusBarItem } from "./statusBarItem"; export function activate(context: vscode.ExtensionContext) { initL10n(context.extensionPath); - const outputChannel = vscode.window.createOutputChannel( - l10n.t("outputChannel.text"), - ); + const outputChannel = vscode.window.createOutputChannel(l10n.t("outputChannel.text")); const extensionState = new ExtensionState(context); const avatarManager = new AvatarManager(config.gitPath, extensionState); const statusBarItem = new StatusBarItem(context, config); @@ -46,9 +44,9 @@ export function activate(context: vscode.ExtensionContext) { if (resource && typeof resource === "object") { if (typeof resource?.rootUri?.fsPath === "string") { - repoPath = resource.rootUri.fsPath; + repoPath = getPathFromStr(resource.rootUri.fsPath); } else if (typeof resource?.uri?.fsPath === "string") { - repoPath = resource.uri.fsPath; + repoPath = getPathFromStr(resource.uri.fsPath); } } @@ -65,7 +63,7 @@ export function activate(context: vscode.ExtensionContext) { currentBridge.post({ command: "loadRepos", repos: repos, - lastActiveRepo: repoPath, + lastActiveRepo: repoPath }); } } @@ -80,9 +78,9 @@ export function activate(context: vscode.ExtensionContext) { enableScripts: true, localResourceRoots: [ buildExtensionUri(context.extensionPath, "media"), - buildExtensionUri(context.extensionPath, "out"), - ], - }, + buildExtensionUri(context.extensionPath, "out") + ] + } ); let bridge!: WebviewBridge; const repoFileWatcher = new RepoFileWatcher(() => { @@ -97,7 +95,7 @@ export function activate(context: vscode.ExtensionContext) { repoManager, extensionState, avatarManager, - repoFileWatcher, + repoFileWatcher }); currentPanel = createWebviewPanel({ panel, @@ -113,7 +111,7 @@ export function activate(context: vscode.ExtensionContext) { currentBridge = undefined; }, onPanelShown, - initialRepo: repoPath, + initialRepo: repoPath }); }), vscode.commands.registerCommand("neo-git-graph.clearAvatarCache", () => { @@ -121,7 +119,7 @@ export function activate(context: vscode.ExtensionContext) { }), vscode.workspace.registerTextDocumentContentProvider( DiffDocProvider.scheme, - new DiffDocProvider(gitClient.getInstance), + new DiffDocProvider(gitClient.getInstance) ), vscode.workspace.onDidChangeConfiguration((e) => { if (e.affectsConfiguration("neo-git-graph.showStatusBarItem")) {