diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index cedbc04..114d184 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -23,6 +23,9 @@ "cmd.ci.restart.short": "Restart a build", "cmd.ci.short": "CI/CD operations", "cmd.ci.stop.short": "Stop a build", + "cmd.compare.files.short": "List changed files between two refs", + "cmd.compare.short": "Compare branches, tags, or commits", + "cmd.compare.view.short": "Compare two branches, tags, or commits", "cmd.config.get.short": "Get a configuration value", "cmd.config.init.short": "Initialize configuration file", "cmd.config.list.short": "List all configuration values", @@ -41,6 +44,10 @@ "cmd.dataset.view.short": "View a repository's dataset", "cmd.doctor.long": "Run local diagnostics for gitlink-cli configuration, authentication, repository context and API connectivity.", "cmd.doctor.short": "Diagnose gitlink-cli environment problems", + "cmd.health.fetch.short": "Fetch PR and Issue data into SQLite for health analysis", + "cmd.health.short": "Project health data collection", + "cmd.ignore.list.short": "List available ignore-file templates", + "cmd.ignore.short": "Ignore file template operations", "cmd.issue.batch_close.long": "Close filtered issues in bulk.\n\nThis command defaults to dry-run mode and only prints matching issues.\nPass --yes to execute remote close operations. Use restrictive filters and a small limit.\n\nExamples:\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20 --yes", "cmd.issue.batch_close.short": "Close filtered issues in bulk. Defaults to dry-run; pass --yes to execute.", "cmd.issue.batch_label.long": "Add a label to filtered issues in bulk.\n\nThis command defaults to dry-run mode and only prints matching issues.\nPass --yes to execute remote label operations. The current implementation does not fake label writes when the API endpoint is unavailable.\n\nExamples:\n gitlink-cli issue +batch-label --owner Gitlink --repo gitlink-cli --add-label stale --older-than-days 30 --limit 50\n gitlink-cli issue +batch-label --owner Gitlink --repo gitlink-cli --add-label stale --older-than-days 30 --limit 50 --yes", @@ -54,11 +61,46 @@ "cmd.issue.short": "Issue operations", "cmd.issue.update.short": "Update an issue", "cmd.issue.view.short": "View issue details", + "cmd.label.create.short": "Create an issue label", + "cmd.label.delete.short": "Delete an issue label", + "cmd.label.list.short": "List issue labels", + "cmd.label.short": "Issue label operations", + "cmd.label.update.short": "Update an issue label while preserving unspecified fields", + "cmd.license.list.short": "List available licenses", + "cmd.license.short": "License operations", + "cmd.member.accept-invite.short": "Accept a repository invite link", + "cmd.member.add.short": "Add a repository member by user ID", + "cmd.member.batch-add.short": "Add multiple repository members by user IDs or a CSV file", + "cmd.member.invite-info.short": "Show repository invite link information", + "cmd.member.invite-link.short": "Get or create a repository invite link", + "cmd.member.list.short": "List repository members", + "cmd.member.remove.short": "Remove a repository member by user ID", + "cmd.member.role.short": "Change a repository member role", + "cmd.member.short": "Repository member operations", + "cmd.milestone.close.short": "Close a milestone", + "cmd.milestone.create.short": "Create a milestone", + "cmd.milestone.delete.short": "Delete a milestone", + "cmd.milestone.list.short": "List milestones", + "cmd.milestone.reopen.short": "Reopen a milestone", + "cmd.milestone.short": "Milestone operations", + "cmd.milestone.update.short": "Update a milestone", + "cmd.milestone.view.short": "View milestone details and linked issues", "cmd.org.create.short": "Create an organization", "cmd.org.info.short": "Show organization details", "cmd.org.list.short": "List organizations", "cmd.org.members.short": "List organization members", "cmd.org.short": "Organization operations", + "cmd.pipeline.delete.short": "Delete a pipeline", + "cmd.pipeline.disable.short": "Disable a pipeline workflow", + "cmd.pipeline.enable.short": "Enable a pipeline workflow", + "cmd.pipeline.list.short": "List platform pipelines", + "cmd.pipeline.logs.short": "Query pipeline run logs", + "cmd.pipeline.results.short": "Show pipeline run report results", + "cmd.pipeline.run.short": "Run a pipeline workflow", + "cmd.pipeline.runs.short": "List pipeline run records", + "cmd.pipeline.save-yaml.short": "Save a visual pipeline YAML graph", + "cmd.pipeline.short": "Pipeline operations", + "cmd.pipeline.view.short": "Show pipeline details", "cmd.pr.close.short": "Close a pull request", "cmd.pr.comment.short": "Add a comment to a pull request", "cmd.pr.create.short": "Create a pull request", @@ -114,6 +156,17 @@ "cmd.webhook.test.short": "Trigger a test delivery for a webhook", "cmd.webhook.update.short": "Update a repository webhook while preserving unspecified fields when available", "cmd.webhook.view.short": "View webhook details", + "cmd.wiki.create.short": "Create a new wiki page", + "cmd.wiki.delete.short": "Delete a wiki page", + "cmd.wiki.list.short": "List wiki pages", + "cmd.wiki.short": "Wiki page management", + "cmd.wiki.update.short": "Update an existing wiki page", + "cmd.wiki.view.short": "View a wiki page by page name", + "cmd.workflow.health.short": "Score repository health with local workflow rules", + "cmd.workflow.pr-summary.short": "Generate a read-only pull request review summary", + "cmd.workflow.repo-report.short": "Generate a read-only repository workflow report", + "cmd.workflow.short": "AI agent workflow analysis", + "cmd.workflow.triage.short": "Analyze issues with local workflow triage rules", "error.auth.delete_token_failed": "failed to delete token: {message}", "error.auth.login_failed": "login failed: {message}", "error.auth.store_token_failed": "failed to store token: {message}", @@ -145,6 +198,9 @@ "flag.ci.stage": "Stage number", "flag.ci.step": "Step number", "flag.comment.body": "Comment body", + "flag.compare.base": "Target branch, tag, or commit", + "flag.compare.file": "Filter by file path", + "flag.compare.head": "Source branch, tag, or commit", "flag.dataset.description": "Dataset description", "flag.dataset.dry_run": "Preview the request without writing the dataset", "flag.dataset.dry_run_delete": "Preview the request without deleting the attachment", @@ -161,6 +217,9 @@ "flag.doctor.skip_network": "Skip authenticated API connectivity checks", "flag.dry_run": "Preview the request without creating it", "flag.format": "Output format: json, table, yaml (default: table)", + "flag.health.db": "SQLite database path (default: ~/.agents/skills/gitlink-health/data/gitlink_health.db)", + "flag.health.max_pages": "Maximum pages per query (default: unlimited)", + "flag.ignore.name": "Filter ignore templates by name", "flag.issue.add_label": "Label to add to each matching issue", "flag.issue.assignee": "Assignee login", "flag.issue.assignee_id": "Assignee user ID", @@ -186,13 +245,57 @@ "flag.issue.status_id": "Issue status ID", "flag.issue.tag_ids": "Comma-separated issue tag IDs", "flag.issue.title": "Issue title", + "flag.label.color": "Label color in hex, for example: #1E90FF", + "flag.label.description": "Label description", + "flag.label.id": "Label ID", + "flag.label.keyword": "Filter labels by keyword", + "flag.label.name": "Label name", + "flag.label.only_name": "Return only label id and name: true or false", + "flag.label.sort_by": "Sort field: updated_on, created_on, issues_count", + "flag.label.sort_direction": "Sort direction: asc or desc", "flag.lang": "Display language", + "flag.license.name": "Filter licenses by name", "flag.limit": "Items per page", + "flag.member.apply": "Whether joining by invite requires approval: true or false", + "flag.member.dry_run": "Preview members that would be added without changing them", + "flag.member.from": "Read user IDs from a CSV file. Supports a user_id/id column or first column without header", + "flag.member.role": "Member role: Manager, Developer, or Reporter", + "flag.member.role_2": "Invite role: manager, developer, or reporter", + "flag.member.sign": "Invite link sign", + "flag.member.user_id": "GitLink user ID to add", + "flag.member.user_id_2": "GitLink user ID to update", + "flag.member.user_ids": "Comma-separated GitLink user IDs, for example: 101,102", + "flag.milestone.assigner_id": "Filter issues by assignee ID", + "flag.milestone.author_id": "Filter issues by author ID", + "flag.milestone.category": "Filter by category: opening, closed", + "flag.milestone.category_2": "Filter issues by category: all, opened, closed", + "flag.milestone.description": "Milestone description", + "flag.milestone.due_date": "Due date in YYYY-MM-DD format", + "flag.milestone.id": "Milestone ID", + "flag.milestone.issue_tag_ids": "Comma-separated issue tag IDs", + "flag.milestone.name": "Milestone name", + "flag.milestone.only_name": "Return only milestone id and name: true or false", + "flag.milestone.sort_by": "Sort field: created_on, updated_on, effective_date, issues_count, percent", + "flag.milestone.sort_by_2": "Sort field: issues.created_on, issues.updated_on, issue_priorities.position", + "flag.milestone.sort_direction": "Sort direction: asc or desc", "flag.org.id": "Organization ID", "flag.org.id_or_login": "Organization ID or login", "flag.org.name": "Organization name", "flag.owner": "Repository owner (auto-detected from git remote)", "flag.page": "Page number", + "flag.pipeline.cursor": "Log cursor", + "flag.pipeline.dry_run": "Preview the run request without starting a pipeline", + "flag.pipeline.dry_run_2": "Preview the request without changing pipeline state", + "flag.pipeline.expanded": "Whether the log cursor is expanded", + "flag.pipeline.id": "Pipeline ID", + "flag.pipeline.index": "Run index", + "flag.pipeline.job": "Job index", + "flag.pipeline.owner_id": "Owner user or organization ID", + "flag.pipeline.pipeline_json": "Pipeline graph JSON object or string", + "flag.pipeline.ref": "Branch, tag, or commit SHA", + "flag.pipeline.run_id": "Pipeline run ID", + "flag.pipeline.step": "Log step", + "flag.pipeline.workflow": "Workflow file name", "flag.pr.assignee_id": "Assignee user ID", "flag.pr.base": "Target branch", "flag.pr.body": "PR description", @@ -246,6 +349,58 @@ "flag.webhook.secret_update": "Webhook secret. Pass it again if the server does not return existing secrets.", "flag.webhook.type": "Webhook type: gitea/slack/discord/dingtalk/telegram/msteams/feishu/matrix/jianmu/softbot", "flag.webhook.url": "Webhook target URL", + "flag.wiki.content": "Wiki page content (markdown)", + "flag.wiki.gateway": "Use gateway API endpoint", + "flag.wiki.message": "Commit message", + "flag.wiki.page_name": "Wiki page name (slug)", + "flag.wiki.project_id": "GitLink project ID", + "flag.wiki.title": "Wiki page title", + "flag.workflow.agent_readiness_known": "Whether agent readiness score is known", + "flag.workflow.agent_readiness_score": "Agent readiness score from 0 to 10", + "flag.workflow.author": "Issue author for single-issue local analysis", + "flag.workflow.body": "Issue body for single-issue local analysis", + "flag.workflow.ci_known": "Whether CI status is known", + "flag.workflow.ci_passing": "Whether CI is passing", + "flag.workflow.dry_run": "Preview workflow recommendations without remote writes", + "flag.workflow.from": "Read issue inputs from a JSON file. Supports a single issue, an array, or an object with an issues field", + "flag.workflow.from_2": "Read health input from a JSON file", + "flag.workflow.has_contributing": "Whether CONTRIBUTING exists", + "flag.workflow.has_license": "Whether LICENSE exists", + "flag.workflow.has_readme": "Whether README exists", + "flag.workflow.has_recent_release": "Whether a recent release exists", + "flag.workflow.labels": "Comma-separated labels for single-issue local analysis", + "flag.workflow.lang": "Output language: en or zh-CN", + "flag.workflow.limit": "Maximum issues to analyze", + "flag.workflow.number": "Issue number for single-issue local analysis", + "flag.workflow.open_issues": "Open issue count", + "flag.workflow.open_prs": "Open pull request count", + "flag.workflow.page": "API page number for remote triage", + "flag.workflow.pr_summary.from": "Read PR summary input from a JSON file", + "flag.workflow.pr_summary.include_commits": "Fetch commits in remote mode", + "flag.workflow.pr_summary.include_files": "Fetch changed files in remote mode", + "flag.workflow.pr_summary.lang": "Output language: en or zh-CN", + "flag.workflow.pr_summary.max_commits": "Maximum commits to analyze", + "flag.workflow.pr_summary.max_files": "Maximum changed files to analyze", + "flag.workflow.pr_summary.number": "Pull request number for remote read-only analysis", + "flag.workflow.recent_activity_days": "Days since recent activity", + "flag.workflow.recent_activity_known": "Whether recent activity is known", + "flag.workflow.release_known": "Whether release status is known", + "flag.workflow.repo_report.from": "Read repository report input from a JSON file", + "flag.workflow.repo_report.include_health": "Include repository health summary", + "flag.workflow.repo_report.include_issues": "Include issue triage summary", + "flag.workflow.repo_report.include_prs": "Include pull request summary", + "flag.workflow.repo_report.issue_limit": "Maximum issues to fetch and analyze", + "flag.workflow.repo_report.lang": "Output language: en or zh-CN", + "flag.workflow.repo_report.pr_limit": "Maximum pull requests to fetch and summarize", + "flag.workflow.repo_report.stale_days": "Days before an issue or PR is considered stale", + "flag.workflow.repository": "Repository name, for example owner/repo", + "flag.workflow.since": "Optional remote issue filter for updated time", + "flag.workflow.stale_days": "Days before an issue or PR is considered stale", + "flag.workflow.stale_issues": "Stale issue count", + "flag.workflow.stale_prs": "Stale pull request count", + "flag.workflow.state": "Filter or assign issue state", + "flag.workflow.title": "Issue title for single-issue local analysis", + "flag.workflow.url": "Issue URL for single-issue local analysis", "output.auth.env_hint": " Or set {env} environment variable", "output.auth.login_hint": " Run: gitlink-cli auth login", "output.config.file": "Config file: {path}", diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index 5383788..2483370 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -23,6 +23,9 @@ "cmd.ci.restart.short": "重启构建", "cmd.ci.short": "CI/CD 操作", "cmd.ci.stop.short": "停止构建", + "cmd.compare.files.short": "列出两个引用之间变更的文件", + "cmd.compare.short": "比较分支、标签或提交", + "cmd.compare.view.short": "比较两个分支、标签或提交", "cmd.config.get.short": "获取配置项", "cmd.config.init.short": "初始化配置文件", "cmd.config.list.short": "列出所有配置项", @@ -41,6 +44,10 @@ "cmd.dataset.view.short": "查看仓库数据集", "cmd.doctor.long": "诊断 gitlink-cli 的配置、认证、仓库上下文和 API 连通性问题。", "cmd.doctor.short": "诊断 gitlink-cli 环境问题", + "cmd.health.fetch.short": "抓取 PR 与 Issue 数据到 SQLite 用于健康度分析", + "cmd.health.short": "项目健康数据采集", + "cmd.ignore.list.short": "列出可用的 ignore 文件模板", + "cmd.ignore.short": "忽略文件模板操作", "cmd.issue.batch_close.long": "批量关闭筛选后的议题。\n\n该命令默认处于 dry-run 模式,只打印匹配的议题。\n传入 --yes 后执行远端关闭操作。请使用严格筛选条件和较小 limit。\n\n示例:\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20 --yes", "cmd.issue.batch_close.short": "批量关闭筛选后的议题。默认 dry-run;传入 --yes 后执行。", "cmd.issue.batch_label.long": "给筛选后的议题批量添加标签。\n\n该命令默认处于 dry-run 模式,只打印匹配的议题。\n传入 --yes 后执行远端标签操作。当前实现不会在 API 端点不可用时伪造写入结果。\n\n示例:\n gitlink-cli issue +batch-label --owner Gitlink --repo gitlink-cli --add-label stale --older-than-days 30 --limit 50\n gitlink-cli issue +batch-label --owner Gitlink --repo gitlink-cli --add-label stale --older-than-days 30 --limit 50 --yes", @@ -54,11 +61,46 @@ "cmd.issue.short": "议题操作", "cmd.issue.update.short": "更新议题", "cmd.issue.view.short": "查看议题详情", + "cmd.label.create.short": "创建 issue 标签", + "cmd.label.delete.short": "删除 issue 标签", + "cmd.label.list.short": "列出 issue 标签", + "cmd.label.short": "Issue 标签操作", + "cmd.label.update.short": "更新 issue 标签(未指定字段保持不变)", + "cmd.license.list.short": "列出可用的开源许可证", + "cmd.license.short": "许可证操作", + "cmd.member.accept-invite.short": "接受仓库邀请链接", + "cmd.member.add.short": "按用户 ID 添加仓库成员", + "cmd.member.batch-add.short": "按用户 ID 列表或 CSV 文件批量添加仓库成员", + "cmd.member.invite-info.short": "查看仓库邀请链接信息", + "cmd.member.invite-link.short": "获取或创建仓库邀请链接", + "cmd.member.list.short": "列出仓库成员", + "cmd.member.remove.short": "按用户 ID 移除仓库成员", + "cmd.member.role.short": "变更仓库成员角色", + "cmd.member.short": "仓库成员操作", + "cmd.milestone.close.short": "关闭里程碑", + "cmd.milestone.create.short": "创建里程碑", + "cmd.milestone.delete.short": "删除里程碑", + "cmd.milestone.list.short": "列出里程碑", + "cmd.milestone.reopen.short": "重新打开里程碑", + "cmd.milestone.short": "里程碑操作", + "cmd.milestone.update.short": "更新里程碑", + "cmd.milestone.view.short": "查看里程碑详情及关联 issue", "cmd.org.create.short": "创建组织", "cmd.org.info.short": "显示组织详情", "cmd.org.list.short": "列出组织", "cmd.org.members.short": "列出组织成员", "cmd.org.short": "组织操作", + "cmd.pipeline.delete.short": "删除流水线", + "cmd.pipeline.disable.short": "停用流水线工作流", + "cmd.pipeline.enable.short": "启用流水线工作流", + "cmd.pipeline.list.short": "列出平台流水线", + "cmd.pipeline.logs.short": "查询流水线运行日志", + "cmd.pipeline.results.short": "查看流水线运行报告结果", + "cmd.pipeline.run.short": "运行流水线工作流", + "cmd.pipeline.runs.short": "列出流水线运行记录", + "cmd.pipeline.save-yaml.short": "保存可视化流水线 YAML 图", + "cmd.pipeline.short": "流水线操作", + "cmd.pipeline.view.short": "查看流水线详情", "cmd.pr.close.short": "关闭拉取请求", "cmd.pr.comment.short": "给拉取请求添加评论", "cmd.pr.create.short": "创建拉取请求", @@ -114,6 +156,17 @@ "cmd.webhook.test.short": "触发 Webhook 测试投递", "cmd.webhook.update.short": "更新仓库 Webhook,并在可用时保留未指定字段", "cmd.webhook.view.short": "查看 Webhook 详情", + "cmd.wiki.create.short": "创建 wiki 页面", + "cmd.wiki.delete.short": "删除 wiki 页面", + "cmd.wiki.list.short": "列出 wiki 页面", + "cmd.wiki.short": "Wiki 页面管理", + "cmd.wiki.update.short": "更新已有 wiki 页面", + "cmd.wiki.view.short": "按页面名查看 wiki 页面", + "cmd.workflow.health.short": "用本地工作流规则为仓库健康度打分", + "cmd.workflow.pr-summary.short": "生成只读的 PR 评审摘要", + "cmd.workflow.repo-report.short": "生成只读的仓库工作流报告", + "cmd.workflow.short": "AI 智能体工作流分析", + "cmd.workflow.triage.short": "用本地工作流分诊规则分析 issue", "error.auth.delete_token_failed": "删除 Token 失败:{message}", "error.auth.login_failed": "登录失败:{message}", "error.auth.store_token_failed": "保存 Token 失败:{message}", @@ -145,6 +198,9 @@ "flag.ci.stage": "阶段编号", "flag.ci.step": "步骤编号", "flag.comment.body": "评论内容", + "flag.compare.base": "目标分支、标签或提交", + "flag.compare.file": "按文件路径过滤", + "flag.compare.head": "源分支、标签或提交", "flag.dataset.description": "数据集描述", "flag.dataset.dry_run": "预览请求,不写入数据集", "flag.dataset.dry_run_delete": "预览请求,不删除附件", @@ -161,6 +217,9 @@ "flag.doctor.skip_network": "跳过需要访问 GitLink 的认证连通性检查", "flag.dry_run": "预览请求,不实际创建", "flag.format": "输出格式:json、table、yaml(默认:table)", + "flag.health.db": "SQLite 数据库路径(默认:~/.agents/skills/gitlink-health/data/gitlink_health.db)", + "flag.health.max_pages": "每次查询的最大页数(默认:不限)", + "flag.ignore.name": "按名称过滤 ignore 模板", "flag.issue.add_label": "要添加到每个匹配议题的标签", "flag.issue.assignee": "负责人登录名", "flag.issue.assignee_id": "负责人用户 ID", @@ -186,13 +245,57 @@ "flag.issue.status_id": "议题状态 ID", "flag.issue.tag_ids": "逗号分隔的议题标签 ID", "flag.issue.title": "议题标题", + "flag.label.color": "标签颜色(十六进制),例如:#1E90FF", + "flag.label.description": "标签描述", + "flag.label.id": "标签 ID", + "flag.label.keyword": "按关键词过滤标签", + "flag.label.name": "标签名称", + "flag.label.only_name": "仅返回标签 id 和名称:true 或 false", + "flag.label.sort_by": "排序字段:updated_on、created_on、issues_count", + "flag.label.sort_direction": "排序方向:asc 或 desc", "flag.lang": "显示语言", + "flag.license.name": "按名称过滤许可证", "flag.limit": "每页条目数", + "flag.member.apply": "通过邀请加入是否需要审批:true 或 false", + "flag.member.dry_run": "仅预览将添加的成员,不实际变更", + "flag.member.from": "从 CSV 文件读取用户 ID。支持 user_id/id 列或无表头的第一列", + "flag.member.role": "成员角色:Manager、Developer 或 Reporter", + "flag.member.role_2": "邀请角色:manager、developer 或 reporter", + "flag.member.sign": "邀请链接签名", + "flag.member.user_id": "要添加的 GitLink 用户 ID", + "flag.member.user_id_2": "要更新的 GitLink 用户 ID", + "flag.member.user_ids": "逗号分隔的 GitLink 用户 ID,例如:101,102", + "flag.milestone.assigner_id": "按负责人 ID 过滤 issue", + "flag.milestone.author_id": "按作者 ID 过滤 issue", + "flag.milestone.category": "按类别过滤:opening、closed", + "flag.milestone.category_2": "按类别过滤 issue:all、opened、closed", + "flag.milestone.description": "里程碑描述", + "flag.milestone.due_date": "截止日期(YYYY-MM-DD 格式)", + "flag.milestone.id": "里程碑 ID", + "flag.milestone.issue_tag_ids": "逗号分隔的 issue 标签 ID", + "flag.milestone.name": "里程碑名称", + "flag.milestone.only_name": "仅返回里程碑 id 和名称:true 或 false", + "flag.milestone.sort_by": "排序字段:created_on、updated_on、effective_date、issues_count、percent", + "flag.milestone.sort_by_2": "排序字段:issues.created_on、issues.updated_on、issue_priorities.position", + "flag.milestone.sort_direction": "排序方向:asc 或 desc", "flag.org.id": "组织 ID", "flag.org.id_or_login": "组织 ID 或登录名", "flag.org.name": "组织名称", "flag.owner": "仓库所有者(自动从 git remote 检测)", "flag.page": "页码", + "flag.pipeline.cursor": "日志游标", + "flag.pipeline.dry_run": "仅预览运行请求,不实际启动流水线", + "flag.pipeline.dry_run_2": "仅预览请求,不变更流水线状态", + "flag.pipeline.expanded": "日志游标是否展开", + "flag.pipeline.id": "流水线 ID", + "flag.pipeline.index": "运行序号", + "flag.pipeline.job": "任务序号", + "flag.pipeline.owner_id": "所有者用户或组织 ID", + "flag.pipeline.pipeline_json": "流水线图 JSON 对象或字符串", + "flag.pipeline.ref": "分支、标签或提交 SHA", + "flag.pipeline.run_id": "流水线运行 ID", + "flag.pipeline.step": "日志步骤", + "flag.pipeline.workflow": "工作流文件名", "flag.pr.assignee_id": "指派人用户 ID", "flag.pr.base": "目标分支", "flag.pr.body": "PR 描述", @@ -246,6 +349,58 @@ "flag.webhook.secret_update": "Webhook 密钥。如果服务端不返回已有密钥,请再次传入。", "flag.webhook.type": "Webhook 类型:gitea/slack/discord/dingtalk/telegram/msteams/feishu/matrix/jianmu/softbot", "flag.webhook.url": "Webhook 目标 URL", + "flag.wiki.content": "wiki 页面内容(markdown)", + "flag.wiki.gateway": "使用网关 API 端点", + "flag.wiki.message": "提交说明", + "flag.wiki.page_name": "wiki 页面名(slug)", + "flag.wiki.project_id": "GitLink 项目 ID", + "flag.wiki.title": "wiki 页面标题", + "flag.workflow.agent_readiness_known": "Agent 就绪度得分是否已知", + "flag.workflow.agent_readiness_score": "Agent 就绪度得分(0 到 10)", + "flag.workflow.author": "单 issue 本地分析的 issue 作者", + "flag.workflow.body": "单 issue 本地分析的 issue 正文", + "flag.workflow.ci_known": "CI 状态是否已知", + "flag.workflow.ci_passing": "CI 是否通过", + "flag.workflow.dry_run": "仅预览工作流建议,不做远程写入", + "flag.workflow.from": "从 JSON 文件读取 issue 输入。支持单个 issue、数组或含 issues 字段的对象", + "flag.workflow.from_2": "从 JSON 文件读取健康度输入", + "flag.workflow.has_contributing": "是否存在 CONTRIBUTING", + "flag.workflow.has_license": "是否存在 LICENSE", + "flag.workflow.has_readme": "是否存在 README", + "flag.workflow.has_recent_release": "是否有近期 release", + "flag.workflow.labels": "单 issue 本地分析的逗号分隔标签", + "flag.workflow.lang": "输出语言:en 或 zh-CN", + "flag.workflow.limit": "分析的最大 issue 数", + "flag.workflow.number": "单 issue 本地分析的 issue 编号", + "flag.workflow.open_issues": "打开的 issue 数", + "flag.workflow.open_prs": "打开的 PR 数", + "flag.workflow.page": "远程分诊的 API 页码", + "flag.workflow.pr_summary.from": "从 JSON 文件读取 PR 摘要输入", + "flag.workflow.pr_summary.include_commits": "远程模式下抓取提交", + "flag.workflow.pr_summary.include_files": "远程模式下抓取变更文件", + "flag.workflow.pr_summary.lang": "输出语言:en 或 zh-CN", + "flag.workflow.pr_summary.max_commits": "分析的最大提交数", + "flag.workflow.pr_summary.max_files": "分析的最大变更文件数", + "flag.workflow.pr_summary.number": "远程只读分析的 PR 编号", + "flag.workflow.recent_activity_days": "距最近活动的天数", + "flag.workflow.recent_activity_known": "最近活动是否已知", + "flag.workflow.release_known": "release 状态是否已知", + "flag.workflow.repo_report.from": "从 JSON 文件读取仓库报告输入", + "flag.workflow.repo_report.include_health": "包含仓库健康度摘要", + "flag.workflow.repo_report.include_issues": "包含 issue 分诊摘要", + "flag.workflow.repo_report.include_prs": "包含 PR 摘要", + "flag.workflow.repo_report.issue_limit": "抓取并分析的最大 issue 数", + "flag.workflow.repo_report.lang": "输出语言:en 或 zh-CN", + "flag.workflow.repo_report.pr_limit": "抓取并汇总的最大 PR 数", + "flag.workflow.repo_report.stale_days": "issue 或 PR 视为停滞的天数", + "flag.workflow.repository": "仓库名,例如 owner/repo", + "flag.workflow.since": "可选的远程 issue 更新时间过滤", + "flag.workflow.stale_days": "issue 或 PR 视为停滞的天数", + "flag.workflow.stale_issues": "停滞 issue 数", + "flag.workflow.stale_prs": "停滞 PR 数", + "flag.workflow.state": "过滤或指定 issue 状态", + "flag.workflow.title": "单 issue 本地分析的 issue 标题", + "flag.workflow.url": "单 issue 本地分析的 issue URL", "output.auth.env_hint": " 或设置 {env} 环境变量", "output.auth.login_hint": " 运行:gitlink-cli auth login", "output.config.file": "配置文件:{path}", diff --git a/shortcuts/compare/compare.go b/shortcuts/compare/compare.go index d1d1180..5301243 100644 --- a/shortcuts/compare/compare.go +++ b/shortcuts/compare/compare.go @@ -4,25 +4,30 @@ import ( "encoding/base64" "fmt" "net/url" - "strconv" + "github.com/gitlink-org/gitlink-cli/internal/i18n" "github.com/gitlink-org/gitlink-cli/shortcuts/common" ) -func Shortcuts() []*common.Shortcut { +func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { + tr := shortcutTranslator(translators...) return []*common.Shortcut{ { Name: "view", - Description: "Compare two branches, tags, or commits", + Description: tr.T("cmd.compare.view.short"), Flags: []common.Flag{ - {Name: "head", Usage: "Source branch, tag, or commit", Required: true}, - {Name: "base", Usage: "Target branch, tag, or commit", Required: true}, + {Name: "head", Usage: tr.T("flag.compare.head"), Required: true}, + {Name: "base", Usage: tr.T("flag.compare.base"), Required: true}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - head, base, err := resolveCompareRefs(ctx) + head, err := ctx.RequireArg("head") + if err != nil { + return err + } + base, err := ctx.RequireArg("base") if err != nil { return err } @@ -35,33 +40,29 @@ func Shortcuts() []*common.Shortcut { }, { Name: "files", - Description: "List changed files between two refs", + Description: tr.T("cmd.compare.files.short"), Flags: []common.Flag{ - {Name: "head", Usage: "Source branch, tag, or commit", Required: true}, - {Name: "base", Usage: "Target branch, tag, or commit", Required: true}, - {Name: "file", Short: "f", Usage: "Filter by file path"}, - {Name: "page", Short: "p", Usage: "Page number", Default: "1"}, - {Name: "limit", Short: "l", Usage: "Items per page", Default: "20"}, + {Name: "head", Usage: tr.T("flag.compare.head"), Required: true}, + {Name: "base", Usage: tr.T("flag.compare.base"), Required: true}, + {Name: "file", Short: "f", Usage: tr.T("flag.compare.file")}, + {Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"}, + {Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - head, base, err := resolveCompareRefs(ctx) + head, err := ctx.RequireArg("head") if err != nil { return err } - page, err := parsePositiveIntArg(ctx.Arg("page"), 1, "page") - if err != nil { - return err - } - limit, err := parsePositiveIntArg(ctx.Arg("limit"), 20, "limit") + base, err := ctx.RequireArg("base") if err != nil { return err } q := url.Values{} - q.Set("page", strconv.Itoa(page)) - q.Set("limit", strconv.Itoa(limit)) + q.Set("page", ctx.Arg("page")) + q.Set("limit", ctx.Arg("limit")) if file := ctx.Arg("file"); file != "" { q.Set("filepath", file) } @@ -72,31 +73,6 @@ func Shortcuts() []*common.Shortcut { return ctx.Output(env) }, }, - { - Name: "commits", - Description: "List commits between two refs with optional filters", - Flags: []common.Flag{ - {Name: "head", Usage: "Source branch, tag, or commit", Required: true}, - {Name: "base", Usage: "Target branch, tag, or commit", Required: true}, - {Name: "author", Usage: "Filter by commit author or committer"}, - {Name: "keyword", Short: "k", Usage: "Filter by commit message keyword"}, - {Name: "limit", Short: "l", Usage: "Maximum commits to return", Default: "20"}, - {Name: "reverse", Usage: "Return commits in reverse order", Bool: true, Default: "false"}, - }, - Run: runCompareCommits, - }, - { - Name: "summary", - Description: "Summarize commits and changed files between two refs", - Flags: []common.Flag{ - {Name: "head", Usage: "Source branch, tag, or commit", Required: true}, - {Name: "base", Usage: "Target branch, tag, or commit", Required: true}, - {Name: "max-files", Usage: "Maximum changed files to analyze", Default: "200"}, - {Name: "top-files", Usage: "Maximum top changed files to include", Default: "10"}, - {Name: "commit-limit", Usage: "Maximum commits to include in the summary sample", Default: "10"}, - }, - Run: runCompareSummary, - }, } } @@ -108,14 +84,9 @@ func encodeRef(ref string) string { return base64.RawURLEncoding.EncodeToString([]byte(ref)) } -func resolveCompareRefs(ctx *common.RuntimeContext) (string, string, error) { - head, err := ctx.RequireArg("head") - if err != nil { - return "", "", err +func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator { + if len(translators) > 0 && translators[0] != nil { + return translators[0] } - base, err := ctx.RequireArg("base") - if err != nil { - return "", "", err - } - return head, base, nil + return i18n.Default() } diff --git a/shortcuts/health/health.go b/shortcuts/health/health.go index ba28565..6ab9d0e 100644 --- a/shortcuts/health/health.go +++ b/shortcuts/health/health.go @@ -16,18 +16,14 @@ import ( ) func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { - tr := i18n.Default() - if len(translators) > 0 && translators[0] != nil { - tr = translators[0] - } - + tr := shortcutTranslator(translators...) return []*common.Shortcut{ { Name: "fetch", - Description: "Fetch PR and Issue data into SQLite for health analysis", + Description: tr.T("cmd.health.fetch.short"), Flags: []common.Flag{ - {Name: "db", Short: "d", Usage: "SQLite database path (default: ~/.agents/skills/gitlink-health/data/gitlink_health.db)"}, - {Name: "max-pages", Short: "M", Usage: "Maximum pages per query (default: unlimited)"}, + {Name: "db", Short: "d", Usage: tr.T("flag.health.db")}, + {Name: "max-pages", Short: "M", Usage: tr.T("flag.health.max_pages")}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { @@ -53,7 +49,7 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { if err != nil { return err } - defer func() { _ = db.Close() }() + defer db.Close() repoID, err := getOrCreateRepo(db, ctx.Repo, ctx.Owner) if err != nil { @@ -97,7 +93,6 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { return nil }, }, - diagnoseShortcut(tr), } } @@ -230,3 +225,10 @@ func fetchIssues(ctx *common.RuntimeContext, db *sql.DB, repoID int, state strin } return nil } + +func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator { + if len(translators) > 0 && translators[0] != nil { + return translators[0] + } + return i18n.Default() +} diff --git a/shortcuts/ignore/ignore.go b/shortcuts/ignore/ignore.go index 9f5e406..ccb44cd 100644 --- a/shortcuts/ignore/ignore.go +++ b/shortcuts/ignore/ignore.go @@ -3,6 +3,7 @@ package ignore import ( "net/url" + "github.com/gitlink-org/gitlink-cli/internal/i18n" "github.com/gitlink-org/gitlink-cli/shortcuts/common" ) @@ -10,13 +11,14 @@ import ( // // These shortcuts provide access to the GitLink ignore-file registry, // which lists all available .gitignore templates supported by the platform. -func Shortcuts() []*common.Shortcut { +func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { + tr := shortcutTranslator(translators...) return []*common.Shortcut{ { Name: "list", - Description: "List available ignore-file templates", + Description: tr.T("cmd.ignore.list.short"), Flags: []common.Flag{ - {Name: "name", Short: "n", Usage: "Filter ignore templates by name"}, + {Name: "name", Short: "n", Usage: tr.T("flag.ignore.name")}, }, Run: runList, }, @@ -34,3 +36,10 @@ func runList(ctx *common.RuntimeContext) error { } return ctx.Output(env) } + +func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator { + if len(translators) > 0 && translators[0] != nil { + return translators[0] + } + return i18n.Default() +} diff --git a/shortcuts/label/label.go b/shortcuts/label/label.go index e38794e..a694c38 100644 --- a/shortcuts/label/label.go +++ b/shortcuts/label/label.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" + "github.com/gitlink-org/gitlink-cli/internal/i18n" "github.com/gitlink-org/gitlink-cli/shortcuts/common" ) @@ -22,16 +23,17 @@ var hexColorPattern = regexp.MustCompile(`^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$`) // Issue labels back the issue triage and PR gatekeeping workflows: until now // they could only be managed through the raw API (issue_tags), so these // shortcuts close that gap with first-class create/list/update/delete commands. -func Shortcuts() []*common.Shortcut { +func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { + tr := shortcutTranslator(translators...) return []*common.Shortcut{ { Name: "list", - Description: "List issue labels", + Description: tr.T("cmd.label.list.short"), Flags: []common.Flag{ - {Name: "keyword", Short: "k", Usage: "Filter labels by keyword"}, - {Name: "only-name", Usage: "Return only label id and name: true or false"}, - {Name: "sort-by", Usage: "Sort field: updated_on, created_on, issues_count"}, - {Name: "sort-direction", Usage: "Sort direction: asc or desc"}, + {Name: "keyword", Short: "k", Usage: tr.T("flag.label.keyword")}, + {Name: "only-name", Usage: tr.T("flag.label.only_name")}, + {Name: "sort-by", Usage: tr.T("flag.label.sort_by")}, + {Name: "sort-direction", Usage: tr.T("flag.label.sort_direction")}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { @@ -51,84 +53,49 @@ func Shortcuts() []*common.Shortcut { }, { Name: "create", - Description: "Create an issue label", + Description: tr.T("cmd.label.create.short"), Flags: []common.Flag{ - {Name: "name", Short: "n", Usage: "Label name", Required: true}, - {Name: "description", Short: "d", Usage: "Label description"}, - {Name: "color", Short: "c", Usage: "Label color in hex, for example: #1E90FF", Default: defaultLabelColor}, + {Name: "name", Short: "n", Usage: tr.T("flag.label.name"), Required: true}, + {Name: "description", Short: "d", Usage: tr.T("flag.label.description")}, + {Name: "color", Short: "c", Usage: tr.T("flag.label.color"), Default: defaultLabelColor}, }, Run: runCreate, }, { Name: "update", - Description: "Update an issue label while preserving unspecified fields", + Description: tr.T("cmd.label.update.short"), Flags: []common.Flag{ - {Name: "id", Short: "i", Usage: "Label ID", Required: true}, - {Name: "name", Short: "n", Usage: "Label name"}, - {Name: "description", Short: "d", Usage: "Label description"}, - {Name: "color", Short: "c", Usage: "Label color in hex, for example: #1E90FF"}, + {Name: "id", Short: "i", Usage: tr.T("flag.label.id"), Required: true}, + {Name: "name", Short: "n", Usage: tr.T("flag.label.name")}, + {Name: "description", Short: "d", Usage: tr.T("flag.label.description")}, + {Name: "color", Short: "c", Usage: tr.T("flag.label.color")}, }, Run: runUpdate, }, { Name: "delete", - Description: "Delete an issue label", + Description: tr.T("cmd.label.delete.short"), Flags: []common.Flag{ - {Name: "id", Short: "i", Usage: "Label ID", Required: true}, - {Name: "dry-run", Usage: "Preview the delete request without removing the label", Bool: true, Default: "false"}, - {Name: "yes", Usage: "Confirm label deletion", Bool: true, Default: "false"}, + {Name: "id", Short: "i", Usage: tr.T("flag.label.id"), Required: true}, }, - Run: runDelete, - }, - { - Name: "batch-create", - Description: "Batch create issue labels from semicolon-separated specs", - Flags: []common.Flag{ - {Name: "labels", Usage: "Semicolon-separated label specs: name:color:description", Required: true}, - {Name: "dry-run", Usage: "Preview labels without creating them", Bool: true, Default: "false"}, - {Name: "yes", Usage: "Confirm real batch label creation", Bool: true, Default: "false"}, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + id, err := ctx.RequireArg("id") + if err != nil { + return err + } + env, err := ctx.CallAPI("DELETE", labelItemPath(ctx, id), nil) + if err != nil { + return err + } + return ctx.Output(env) }, - Run: runBatchCreate, - }, - { - Name: "batch-delete", - Description: "Batch delete issue labels by IDs", - Flags: []common.Flag{ - {Name: "ids", Usage: "Comma-separated label IDs", Required: true}, - {Name: "dry-run", Usage: "Preview labels without deleting them", Bool: true, Default: "false"}, - {Name: "yes", Usage: "Confirm real batch label deletion", Bool: true, Default: "false"}, - }, - Run: runBatchDelete, }, } } -type labelSpec struct { - Name string `json:"name" yaml:"name"` - Description string `json:"description" yaml:"description"` - Color string `json:"color" yaml:"color"` -} - -type labelBatchRequest struct { - Method string `json:"method" yaml:"method"` - Path string `json:"path" yaml:"path"` - Body map[string]interface{} `json:"body,omitempty" yaml:"body,omitempty"` -} - -type labelBatchPreview struct { - Repository string `json:"repository" yaml:"repository"` - DryRun bool `json:"dry_run" yaml:"dry_run"` - Action string `json:"action" yaml:"action"` - Requests []labelBatchRequest `json:"requests" yaml:"requests"` -} - -type labelBatchResult struct { - Request labelBatchRequest `json:"request" yaml:"request"` - OK bool `json:"ok" yaml:"ok"` - Data interface{} `json:"data,omitempty" yaml:"data,omitempty"` - Error string `json:"error,omitempty" yaml:"error,omitempty"` -} - func runCreate(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { return err @@ -198,131 +165,6 @@ func runUpdate(ctx *common.RuntimeContext) error { return ctx.Output(env) } -func runDelete(ctx *common.RuntimeContext) error { - if err := ctx.ResolveOwnerRepo(); err != nil { - return err - } - id, err := ctx.RequireArg("id") - if err != nil { - return err - } - path := labelItemPath(ctx, id) - if ctx.Arg("dry-run") == "true" { - return ctx.OutputData(labelBatchPreview{ - Repository: repositoryName(ctx), - DryRun: true, - Action: "delete_label", - Requests: []labelBatchRequest{{ - Method: "DELETE", - Path: path, - }}, - }) - } - if ctx.Arg("yes") != "true" { - return fmt.Errorf("label delete is destructive; run with --dry-run first, then pass --yes to confirm") - } - env, err := ctx.CallAPI("DELETE", path, nil) - if err != nil { - return err - } - return ctx.Output(env) -} - -func runBatchCreate(ctx *common.RuntimeContext) error { - if err := ctx.ResolveOwnerRepo(); err != nil { - return err - } - specs, err := parseLabelSpecs(ctx.Arg("labels")) - if err != nil { - return err - } - requests := make([]labelBatchRequest, 0, len(specs)) - for _, spec := range specs { - requests = append(requests, labelBatchRequest{ - Method: "POST", - Path: labelPath(ctx), - Body: map[string]interface{}{ - "name": spec.Name, - "description": spec.Description, - "color": spec.Color, - }, - }) - } - if ctx.Arg("dry-run") == "true" || ctx.Arg("yes") != "true" { - return ctx.OutputData(labelBatchPreview{ - Repository: repositoryName(ctx), - DryRun: true, - Action: "batch_create_labels", - Requests: requests, - }) - } - return runLabelBatchRequests(ctx, "batch_create_labels", requests) -} - -func runBatchDelete(ctx *common.RuntimeContext) error { - if err := ctx.ResolveOwnerRepo(); err != nil { - return err - } - ids, err := parseLabelIDList(ctx.Arg("ids")) - if err != nil { - return err - } - requests := make([]labelBatchRequest, 0, len(ids)) - for _, id := range ids { - requests = append(requests, labelBatchRequest{ - Method: "DELETE", - Path: labelItemPath(ctx, id), - }) - } - if ctx.Arg("dry-run") == "true" || ctx.Arg("yes") != "true" { - return ctx.OutputData(labelBatchPreview{ - Repository: repositoryName(ctx), - DryRun: true, - Action: "batch_delete_labels", - Requests: requests, - }) - } - return runLabelBatchRequests(ctx, "batch_delete_labels", requests) -} - -func runLabelBatchRequests(ctx *common.RuntimeContext, action string, requests []labelBatchRequest) error { - results := make([]labelBatchResult, 0, len(requests)) - succeeded := 0 - failed := 0 - for _, request := range requests { - env, err := ctx.CallAPI(request.Method, request.Path, request.Body) - result := labelBatchResult{Request: request} - if err != nil { - result.OK = false - result.Error = err.Error() - failed++ - } else { - result.OK = env.OK - result.Data = env.Data - if env.OK { - succeeded++ - } else { - failed++ - } - } - results = append(results, result) - } - if err := ctx.OutputData(map[string]interface{}{ - "repository": repositoryName(ctx), - "action": action, - "count": len(requests), - "succeeded": succeeded, - "failed": failed, - "results": results, - }); err != nil { - return err - } - if failed > 0 { - return fmt.Errorf("%d of %d label request(s) failed", failed, len(requests)) - } - return nil -} - // fetchLabel looks up a single label by id from the list endpoint. GitLink does // not expose a single-label GET, so we page through the list and match by id. // A nil result (label not found) is not an error: the caller falls back to the @@ -360,77 +202,6 @@ func labelItemPath(ctx *common.RuntimeContext, id string) string { return fmt.Sprintf("%s/%s", labelPath(ctx), url.PathEscape(id)) } -func parseLabelSpecs(raw string) ([]labelSpec, error) { - if strings.TrimSpace(raw) == "" { - return nil, fmt.Errorf("--labels is required") - } - parts := strings.Split(raw, ";") - specs := make([]labelSpec, 0, len(parts)) - for _, part := range parts { - part = strings.TrimSpace(part) - if part == "" { - continue - } - fields := strings.SplitN(part, ":", 3) - name := strings.TrimSpace(fields[0]) - if name == "" { - return nil, fmt.Errorf("invalid label spec %q: name is required", part) - } - color := defaultLabelColor - if len(fields) > 1 && strings.TrimSpace(fields[1]) != "" { - color = strings.TrimSpace(fields[1]) - } - if err := validateColor(color); err != nil { - return nil, err - } - description := "" - if len(fields) > 2 { - description = strings.TrimSpace(fields[2]) - } - specs = append(specs, labelSpec{ - Name: name, - Description: description, - Color: color, - }) - } - if len(specs) == 0 { - return nil, fmt.Errorf("--labels must contain at least one label spec") - } - return specs, nil -} - -func parseLabelIDList(raw string) ([]string, error) { - if strings.TrimSpace(raw) == "" { - return nil, fmt.Errorf("--ids is required") - } - parts := strings.Split(raw, ",") - ids := make([]string, 0, len(parts)) - seen := map[string]bool{} - for _, part := range parts { - id := strings.TrimSpace(part) - if id == "" { - continue - } - n, err := strconv.Atoi(id) - if err != nil || n <= 0 { - return nil, fmt.Errorf("invalid label id %q: use positive integer IDs", id) - } - if seen[id] { - continue - } - seen[id] = true - ids = append(ids, id) - } - if len(ids) == 0 { - return nil, fmt.Errorf("--ids must contain at least one label ID") - } - return ids, nil -} - -func repositoryName(ctx *common.RuntimeContext) string { - return fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo) -} - func validateColor(color string) error { if !hexColorPattern.MatchString(color) { return fmt.Errorf("invalid --color value %q: use a hex color like #1E90FF or #abc", color) @@ -473,3 +244,10 @@ func firstNonEmpty(values ...string) string { } return "" } + +func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator { + if len(translators) > 0 && translators[0] != nil { + return translators[0] + } + return i18n.Default() +} diff --git a/shortcuts/license/license.go b/shortcuts/license/license.go index b8b7443..ab19034 100644 --- a/shortcuts/license/license.go +++ b/shortcuts/license/license.go @@ -3,6 +3,7 @@ package license import ( "net/url" + "github.com/gitlink-org/gitlink-cli/internal/i18n" "github.com/gitlink-org/gitlink-cli/shortcuts/common" ) @@ -10,13 +11,14 @@ import ( // // These shortcuts provide access to the GitLink license registry, // which lists all available open-source licenses supported by the platform. -func Shortcuts() []*common.Shortcut { +func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { + tr := shortcutTranslator(translators...) return []*common.Shortcut{ { Name: "list", - Description: "List available licenses", + Description: tr.T("cmd.license.list.short"), Flags: []common.Flag{ - {Name: "name", Short: "n", Usage: "Filter licenses by name"}, + {Name: "name", Short: "n", Usage: tr.T("flag.license.name")}, }, Run: runList, }, @@ -34,3 +36,10 @@ func runList(ctx *common.RuntimeContext) error { } return ctx.Output(env) } + +func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator { + if len(translators) > 0 && translators[0] != nil { + return translators[0] + } + return i18n.Default() +} diff --git a/shortcuts/member/member.go b/shortcuts/member/member.go index f0407e7..680d04c 100644 --- a/shortcuts/member/member.go +++ b/shortcuts/member/member.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" + "github.com/gitlink-org/gitlink-cli/internal/i18n" "github.com/gitlink-org/gitlink-cli/shortcuts/common" ) @@ -21,11 +22,12 @@ var roleAliases = map[string]string{ } // Shortcuts returns repository member management shortcuts. -func Shortcuts() []*common.Shortcut { +func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { + tr := shortcutTranslator(translators...) return []*common.Shortcut{ { Name: "list", - Description: "List repository members", + Description: tr.T("cmd.member.list.short"), Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { return err @@ -39,9 +41,9 @@ func Shortcuts() []*common.Shortcut { }, { Name: "add", - Description: "Add a repository member by user ID", + Description: tr.T("cmd.member.add.short"), Flags: []common.Flag{ - {Name: "user-id", Short: "u", Usage: "GitLink user ID to add", Required: true}, + {Name: "user-id", Short: "u", Usage: tr.T("flag.member.user_id"), Required: true}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { @@ -60,19 +62,19 @@ func Shortcuts() []*common.Shortcut { }, { Name: "batch-add", - Description: "Add multiple repository members by user IDs or a CSV file", + Description: tr.T("cmd.member.batch-add.short"), Flags: []common.Flag{ - {Name: "user-ids", Short: "u", Usage: "Comma-separated GitLink user IDs, for example: 101,102"}, - {Name: "from", Usage: "Read user IDs from a CSV file. Supports a user_id/id column or first column without header"}, - {Name: "dry-run", Usage: "Preview members that would be added without changing them", Bool: true, Default: "false"}, + {Name: "user-ids", Short: "u", Usage: tr.T("flag.member.user_ids")}, + {Name: "from", Usage: tr.T("flag.member.from")}, + {Name: "dry-run", Usage: tr.T("flag.member.dry_run"), Bool: true, Default: "false"}, }, Run: runBatchAdd, }, { Name: "remove", - Description: "Remove a repository member by user ID", + Description: tr.T("cmd.member.remove.short"), Flags: []common.Flag{ - {Name: "user-id", Short: "u", Usage: "GitLink user ID to remove", Required: true}, + {Name: "user-id", Short: "u", Usage: tr.T("flag.member.user_id_2"), Required: true}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { @@ -91,10 +93,10 @@ func Shortcuts() []*common.Shortcut { }, { Name: "role", - Description: "Change a repository member role", + Description: tr.T("cmd.member.role.short"), Flags: []common.Flag{ - {Name: "user-id", Short: "u", Usage: "GitLink user ID to update", Required: true}, - {Name: "role", Short: "r", Usage: "Member role: Manager, Developer, or Reporter", Required: true}, + {Name: "user-id", Short: "u", Usage: tr.T("flag.member.user_id_2"), Required: true}, + {Name: "role", Short: "r", Usage: tr.T("flag.member.role"), Required: true}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { @@ -120,10 +122,10 @@ func Shortcuts() []*common.Shortcut { }, { Name: "invite-link", - Description: "Get or create a repository invite link", + Description: tr.T("cmd.member.invite-link.short"), Flags: []common.Flag{ - {Name: "role", Short: "r", Usage: "Invite role: manager, developer, or reporter", Default: "developer"}, - {Name: "apply", Usage: "Whether joining by invite requires approval: true or false", Default: "true"}, + {Name: "role", Short: "r", Usage: tr.T("flag.member.role_2"), Default: "developer"}, + {Name: "apply", Usage: tr.T("flag.member.apply"), Default: "true"}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { @@ -149,9 +151,9 @@ func Shortcuts() []*common.Shortcut { }, { Name: "invite-info", - Description: "Show repository invite link information", + Description: tr.T("cmd.member.invite-info.short"), Flags: []common.Flag{ - {Name: "sign", Short: "s", Usage: "Invite link sign", Required: true}, + {Name: "sign", Short: "s", Usage: tr.T("flag.member.sign"), Required: true}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { @@ -172,9 +174,9 @@ func Shortcuts() []*common.Shortcut { }, { Name: "accept-invite", - Description: "Accept a repository invite link", + Description: tr.T("cmd.member.accept-invite.short"), Flags: []common.Flag{ - {Name: "sign", Short: "s", Usage: "Invite link sign", Required: true}, + {Name: "sign", Short: "s", Usage: tr.T("flag.member.sign"), Required: true}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { @@ -476,3 +478,10 @@ func userIDColumn(header []string) int { } return -1 } + +func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator { + if len(translators) > 0 && translators[0] != nil { + return translators[0] + } + return i18n.Default() +} diff --git a/shortcuts/milestone/milestone.go b/shortcuts/milestone/milestone.go index b834e03..9175ca1 100644 --- a/shortcuts/milestone/milestone.go +++ b/shortcuts/milestone/milestone.go @@ -5,22 +5,24 @@ import ( "net/url" "strings" + "github.com/gitlink-org/gitlink-cli/internal/i18n" "github.com/gitlink-org/gitlink-cli/shortcuts/common" ) -func Shortcuts() []*common.Shortcut { +func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { + tr := shortcutTranslator(translators...) return []*common.Shortcut{ { Name: "list", - Description: "List milestones", + Description: tr.T("cmd.milestone.list.short"), Flags: []common.Flag{ - {Name: "keyword", Short: "k", Usage: "Search keyword"}, - {Name: "category", Short: "c", Usage: "Filter by category: opening, closed"}, - {Name: "only-name", Usage: "Return only milestone id and name: true or false"}, - {Name: "sort-by", Usage: "Sort field: created_on, updated_on, effective_date, issues_count, percent"}, - {Name: "sort-direction", Usage: "Sort direction: asc or desc"}, - {Name: "page", Short: "p", Usage: "Page number", Default: "1"}, - {Name: "limit", Short: "l", Usage: "Items per page", Default: "20"}, + {Name: "keyword", Short: "k", Usage: tr.T("flag.search.keyword")}, + {Name: "category", Short: "c", Usage: tr.T("flag.milestone.category")}, + {Name: "only-name", Usage: tr.T("flag.milestone.only_name")}, + {Name: "sort-by", Usage: tr.T("flag.milestone.sort_by")}, + {Name: "sort-direction", Usage: tr.T("flag.milestone.sort_direction")}, + {Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"}, + {Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { @@ -43,11 +45,11 @@ func Shortcuts() []*common.Shortcut { }, { Name: "create", - Description: "Create a milestone", + Description: tr.T("cmd.milestone.create.short"), Flags: []common.Flag{ - {Name: "name", Short: "n", Usage: "Milestone name", Required: true}, - {Name: "description", Short: "d", Usage: "Milestone description"}, - {Name: "due-date", Usage: "Due date in YYYY-MM-DD format"}, + {Name: "name", Short: "n", Usage: tr.T("flag.milestone.name"), Required: true}, + {Name: "description", Short: "d", Usage: tr.T("flag.milestone.description"), Required: true}, + {Name: "due-date", Usage: tr.T("flag.milestone.due_date"), Required: true}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { @@ -66,17 +68,17 @@ func Shortcuts() []*common.Shortcut { }, { Name: "view", - Description: "View milestone details and linked issues", + Description: tr.T("cmd.milestone.view.short"), Flags: []common.Flag{ - {Name: "id", Short: "i", Usage: "Milestone ID", Required: true}, - {Name: "category", Short: "c", Usage: "Filter issues by category: all, opened, closed"}, - {Name: "author-id", Usage: "Filter issues by author ID"}, - {Name: "assigner-id", Usage: "Filter issues by assignee ID"}, - {Name: "issue-tag-ids", Usage: "Comma-separated issue tag IDs"}, - {Name: "sort-by", Usage: "Sort field: issues.created_on, issues.updated_on, issue_priorities.position"}, - {Name: "sort-direction", Usage: "Sort direction: asc or desc"}, - {Name: "page", Short: "p", Usage: "Page number", Default: "1"}, - {Name: "limit", Short: "l", Usage: "Items per page", Default: "20"}, + {Name: "id", Short: "i", Usage: tr.T("flag.milestone.id"), Required: true}, + {Name: "category", Short: "c", Usage: tr.T("flag.milestone.category_2")}, + {Name: "author-id", Usage: tr.T("flag.milestone.author_id")}, + {Name: "assigner-id", Usage: tr.T("flag.milestone.assigner_id")}, + {Name: "issue-tag-ids", Usage: tr.T("flag.milestone.issue_tag_ids")}, + {Name: "sort-by", Usage: tr.T("flag.milestone.sort_by_2")}, + {Name: "sort-direction", Usage: tr.T("flag.milestone.sort_direction")}, + {Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"}, + {Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { @@ -102,15 +104,14 @@ func Shortcuts() []*common.Shortcut { return ctx.Output(env) }, }, - newMilestoneReportShortcut(), { Name: "update", - Description: "Update a milestone", + Description: tr.T("cmd.milestone.update.short"), Flags: []common.Flag{ - {Name: "id", Short: "i", Usage: "Milestone ID", Required: true}, - {Name: "name", Short: "n", Usage: "Milestone name"}, - {Name: "description", Short: "d", Usage: "Milestone description"}, - {Name: "due-date", Usage: "Due date in YYYY-MM-DD format"}, + {Name: "id", Short: "i", Usage: tr.T("flag.milestone.id"), Required: true}, + {Name: "name", Short: "n", Usage: tr.T("flag.milestone.name")}, + {Name: "description", Short: "d", Usage: tr.T("flag.milestone.description")}, + {Name: "due-date", Usage: tr.T("flag.milestone.due_date")}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { @@ -133,9 +134,9 @@ func Shortcuts() []*common.Shortcut { }, { Name: "delete", - Description: "Delete a milestone", + Description: tr.T("cmd.milestone.delete.short"), Flags: []common.Flag{ - {Name: "id", Short: "i", Usage: "Milestone ID", Required: true}, + {Name: "id", Short: "i", Usage: tr.T("flag.milestone.id"), Required: true}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { @@ -152,8 +153,8 @@ func Shortcuts() []*common.Shortcut { return ctx.Output(env) }, }, - newStatusShortcut("close", "Close a milestone", "closed"), - newStatusShortcut("reopen", "Reopen a milestone", "open"), + newStatusShortcut(tr, "close", tr.T("cmd.milestone.close.short"), "closed"), + newStatusShortcut(tr, "reopen", tr.T("cmd.milestone.reopen.short"), "open"), } } @@ -169,7 +170,7 @@ func milestoneStatusPath(ctx *common.RuntimeContext, id string) string { return fmt.Sprintf("%s/milestones/%s/update_status", ctx.RepoPath(), url.PathEscape(id)) } -func milestonePayload(ctx *common.RuntimeContext, requireName bool) (map[string]interface{}, error) { +func milestonePayload(ctx *common.RuntimeContext, requireAll bool) (map[string]interface{}, error) { payload := map[string]interface{}{} if name := ctx.Arg("name"); name != "" { payload["name"] = name @@ -181,9 +182,11 @@ func milestonePayload(ctx *common.RuntimeContext, requireName bool) (map[string] payload["effective_date"] = dueDate } - if requireName { - if _, err := ctx.RequireArg("name"); err != nil { - return nil, err + if requireAll { + for _, name := range []string{"name", "description", "due-date"} { + if _, err := ctx.RequireArg(name); err != nil { + return nil, err + } } return payload, nil } @@ -193,12 +196,12 @@ func milestonePayload(ctx *common.RuntimeContext, requireName bool) (map[string] return payload, nil } -func newStatusShortcut(name, description, status string) *common.Shortcut { +func newStatusShortcut(tr *i18n.Translator, name, description, status string) *common.Shortcut { return &common.Shortcut{ Name: name, Description: description, Flags: []common.Flag{ - {Name: "id", Short: "i", Usage: "Milestone ID", Required: true}, + {Name: "id", Short: "i", Usage: tr.T("flag.milestone.id"), Required: true}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { @@ -236,3 +239,10 @@ func normalizeCSV(value string) string { } return strings.Join(result, ",") } + +func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator { + if len(translators) > 0 && translators[0] != nil { + return translators[0] + } + return i18n.Default() +} diff --git a/shortcuts/pipeline/pipeline.go b/shortcuts/pipeline/pipeline.go index 846f57e..02f64a9 100644 --- a/shortcuts/pipeline/pipeline.go +++ b/shortcuts/pipeline/pipeline.go @@ -7,18 +7,20 @@ import ( "strconv" "strings" + "github.com/gitlink-org/gitlink-cli/internal/i18n" "github.com/gitlink-org/gitlink-cli/shortcuts/common" ) -func Shortcuts() []*common.Shortcut { +func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { + tr := shortcutTranslator(translators...) return []*common.Shortcut{ { Name: "list", - Description: "List platform pipelines", + Description: tr.T("cmd.pipeline.list.short"), Flags: []common.Flag{ - {Name: "owner-id", Usage: "Owner user or organization ID"}, - {Name: "page", Short: "p", Usage: "Page number", Default: "1"}, - {Name: "limit", Short: "l", Usage: "Items per page", Default: "20"}, + {Name: "owner-id", Usage: tr.T("flag.pipeline.owner_id")}, + {Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"}, + {Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"}, }, Run: func(ctx *common.RuntimeContext) error { q := pageLimitQuery(ctx) @@ -32,8 +34,8 @@ func Shortcuts() []*common.Shortcut { }, { Name: "runs", - Description: "List pipeline run records", - Flags: runFilterFlags(), + Description: tr.T("cmd.pipeline.runs.short"), + Flags: runFilterFlags(tr), Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { return err @@ -47,9 +49,9 @@ func Shortcuts() []*common.Shortcut { }, { Name: "run", - Description: "Run a pipeline workflow", - Flags: append(runFilterFlags(), - common.Flag{Name: "dry-run", Usage: "Preview the run request without starting a pipeline", Bool: true, Default: "false"}, + Description: tr.T("cmd.pipeline.run.short"), + Flags: append(runFilterFlags(tr), + common.Flag{Name: "dry-run", Usage: tr.T("flag.pipeline.dry_run"), Bool: true, Default: "false"}, ), Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { @@ -76,9 +78,9 @@ func Shortcuts() []*common.Shortcut { }, { Name: "view", - Description: "Show pipeline details", + Description: tr.T("cmd.pipeline.view.short"), Flags: []common.Flag{ - {Name: "id", Short: "i", Usage: "Pipeline ID", Required: true}, + {Name: "id", Short: "i", Usage: tr.T("flag.pipeline.id"), Required: true}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { @@ -97,10 +99,10 @@ func Shortcuts() []*common.Shortcut { }, { Name: "delete", - Description: "Delete a pipeline", + Description: tr.T("cmd.pipeline.delete.short"), Flags: []common.Flag{ - {Name: "id", Short: "i", Usage: "Pipeline ID", Required: true}, - {Name: "dry-run", Usage: "Preview the delete request without changing pipeline state", Bool: true, Default: "false"}, + {Name: "id", Short: "i", Usage: tr.T("flag.pipeline.id"), Required: true}, + {Name: "dry-run", Usage: tr.T("flag.pipeline.dry_run_2"), Bool: true, Default: "false"}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { @@ -129,11 +131,11 @@ func Shortcuts() []*common.Shortcut { }, { Name: "save-yaml", - Description: "Save a visual pipeline YAML graph", + Description: tr.T("cmd.pipeline.save-yaml.short"), Flags: []common.Flag{ - {Name: "id", Short: "i", Usage: "Pipeline ID", Required: true}, - {Name: "pipeline-json", Usage: "Pipeline graph JSON object or string", Required: true}, - {Name: "dry-run", Usage: "Preview the save request without changing pipeline state", Bool: true, Default: "false"}, + {Name: "id", Short: "i", Usage: tr.T("flag.pipeline.id"), Required: true}, + {Name: "pipeline-json", Usage: tr.T("flag.pipeline.pipeline_json"), Required: true}, + {Name: "dry-run", Usage: tr.T("flag.pipeline.dry_run_2"), Bool: true, Default: "false"}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { @@ -171,31 +173,31 @@ func Shortcuts() []*common.Shortcut { }, { Name: "enable", - Description: "Enable a pipeline workflow", - Flags: workflowStateFlags(true), + Description: tr.T("cmd.pipeline.enable.short"), + Flags: workflowStateFlags(tr, true), Run: func(ctx *common.RuntimeContext) error { return runWorkflowState(ctx, "enable") }, }, { Name: "disable", - Description: "Disable a pipeline workflow", - Flags: workflowStateFlags(true), + Description: tr.T("cmd.pipeline.disable.short"), + Flags: workflowStateFlags(tr, true), Run: func(ctx *common.RuntimeContext) error { return runWorkflowState(ctx, "disable") }, }, { Name: "logs", - Description: "Query pipeline run logs", + Description: tr.T("cmd.pipeline.logs.short"), Flags: []common.Flag{ - {Name: "run-id", Short: "r", Usage: "Pipeline run ID", Required: true}, - {Name: "id", Short: "i", Usage: "Pipeline ID", Required: true}, - {Name: "index", Usage: "Run index", Required: true}, - {Name: "job", Short: "j", Usage: "Job index", Default: "0"}, - {Name: "cursor", Usage: "Log cursor"}, - {Name: "step", Usage: "Log step", Default: "1"}, - {Name: "expanded", Usage: "Whether the log cursor is expanded", Bool: true, Default: "true"}, + {Name: "run-id", Short: "r", Usage: tr.T("flag.pipeline.run_id"), Required: true}, + {Name: "id", Short: "i", Usage: tr.T("flag.pipeline.id"), Required: true}, + {Name: "index", Usage: tr.T("flag.pipeline.index"), Required: true}, + {Name: "job", Short: "j", Usage: tr.T("flag.pipeline.job"), Default: "0"}, + {Name: "cursor", Usage: tr.T("flag.pipeline.cursor")}, + {Name: "step", Usage: tr.T("flag.pipeline.step"), Default: "1"}, + {Name: "expanded", Usage: tr.T("flag.pipeline.expanded"), Bool: true, Default: "true"}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { @@ -244,9 +246,9 @@ func Shortcuts() []*common.Shortcut { }, { Name: "results", - Description: "Show pipeline run report results", + Description: tr.T("cmd.pipeline.results.short"), Flags: []common.Flag{ - {Name: "run-id", Short: "r", Usage: "Pipeline run ID"}, + {Name: "run-id", Short: "r", Usage: tr.T("flag.pipeline.run_id")}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { @@ -268,10 +270,10 @@ func pipelineV1RepoPath(ctx *common.RuntimeContext) string { return "/v1" + ctx.RepoPath() } -func runFilterFlags() []common.Flag { +func runFilterFlags(tr *i18n.Translator) []common.Flag { return []common.Flag{ - {Name: "ref", Short: "r", Usage: "Branch, tag, or commit SHA"}, - {Name: "workflow", Short: "w", Usage: "Workflow file name"}, + {Name: "ref", Short: "r", Usage: tr.T("flag.pipeline.ref")}, + {Name: "workflow", Short: "w", Usage: tr.T("flag.pipeline.workflow")}, } } @@ -282,13 +284,13 @@ func runFilterQuery(ctx *common.RuntimeContext) url.Values { return q } -func workflowStateFlags(includeDryRun bool) []common.Flag { +func workflowStateFlags(tr *i18n.Translator, includeDryRun bool) []common.Flag { flags := []common.Flag{ - {Name: "id", Short: "i", Usage: "Pipeline ID", Required: true}, - {Name: "workflow", Short: "w", Usage: "Workflow file name", Required: true}, + {Name: "id", Short: "i", Usage: tr.T("flag.pipeline.id"), Required: true}, + {Name: "workflow", Short: "w", Usage: tr.T("flag.pipeline.workflow"), Required: true}, } if includeDryRun { - flags = append(flags, common.Flag{Name: "dry-run", Usage: "Preview the request without changing pipeline state", Bool: true, Default: "false"}) + flags = append(flags, common.Flag{Name: "dry-run", Usage: tr.T("flag.pipeline.dry_run_2"), Bool: true, Default: "false"}) } return flags } @@ -387,3 +389,10 @@ func requiredNonNegativeIntWithDefault(ctx *common.RuntimeContext, flagName stri } return id, nil } + +func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator { + if len(translators) > 0 && translators[0] != nil { + return translators[0] + } + return i18n.Default() +} diff --git a/shortcuts/register.go b/shortcuts/register.go index 2c89d7b..82446ac 100644 --- a/shortcuts/register.go +++ b/shortcuts/register.go @@ -4,7 +4,6 @@ import ( "github.com/spf13/cobra" "github.com/gitlink-org/gitlink-cli/internal/i18n" - "github.com/gitlink-org/gitlink-cli/shortcuts/attachment" "github.com/gitlink-org/gitlink-cli/shortcuts/branch" "github.com/gitlink-org/gitlink-cli/shortcuts/ci" "github.com/gitlink-org/gitlink-cli/shortcuts/common" @@ -37,55 +36,53 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) { tr = translators[0] } groups := map[string][]*common.Shortcut{ - "repo": repo.Shortcuts(tr), - "attachment": attachment.Shortcuts(tr), - "issue": issue.Shortcuts(tr), - "label": label.Shortcuts(), - "license": license.Shortcuts(), - "member": member.Shortcuts(), - "milestone": milestone.Shortcuts(), - "pipeline": pipeline.Shortcuts(), - "pr": pr.Shortcuts(tr), - "profile": profile.Shortcuts(tr), - "release": release.Shortcuts(tr), - "branch": branch.Shortcuts(tr), - "org": org.Shortcuts(tr), - "user": user.Shortcuts(tr), - "search": search.Shortcuts(tr), - "ci": ci.Shortcuts(tr), - "compare": compare.Shortcuts(), - "dataset": dataset.Shortcuts(tr), - "webhook": webhook.Shortcuts(tr), - "wiki": wiki.Shortcuts(), - "health": health.Shortcuts(tr), - "ignore": ignore.Shortcuts(), - "workflow": workflow.Shortcuts(), + "repo": repo.Shortcuts(tr), + "issue": issue.Shortcuts(tr), + "label": label.Shortcuts(tr), + "license": license.Shortcuts(tr), + "member": member.Shortcuts(tr), + "milestone": milestone.Shortcuts(tr), + "pipeline": pipeline.Shortcuts(tr), + "pr": pr.Shortcuts(tr), + "profile": profile.Shortcuts(tr), + "release": release.Shortcuts(tr), + "branch": branch.Shortcuts(tr), + "org": org.Shortcuts(tr), + "user": user.Shortcuts(tr), + "search": search.Shortcuts(tr), + "ci": ci.Shortcuts(tr), + "compare": compare.Shortcuts(tr), + "dataset": dataset.Shortcuts(tr), + "webhook": webhook.Shortcuts(tr), + "wiki": wiki.Shortcuts(tr), + "health": health.Shortcuts(tr), + "ignore": ignore.Shortcuts(tr), + "workflow": workflow.Shortcuts(tr), } descriptions := map[string]string{ - "repo": tr.T("cmd.repo.short"), - "attachment": tr.T("cmd.attachment.short"), - "issue": tr.T("cmd.issue.short"), - "label": "Issue label operations", - "license": "License operations", - "member": "Repository member operations", - "milestone": "Milestone operations", - "pipeline": "Pipeline operations", - "pr": tr.T("cmd.pr.short"), - "profile": tr.T("cmd.profile.short"), - "release": tr.T("cmd.release.short"), - "branch": tr.T("cmd.branch.short"), - "org": tr.T("cmd.org.short"), - "user": tr.T("cmd.user.short"), - "search": tr.T("cmd.search.short"), - "ci": tr.T("cmd.ci.short"), - "compare": "Compare branches, tags, or commits", - "dataset": tr.T("cmd.dataset.short"), - "webhook": tr.T("cmd.webhook.short"), - "wiki": "Wiki page management", - "health": "Project health data collection", - "ignore": tr.T("cmd.ignore.short"), - "workflow": "AI agent workflow analysis", + "repo": tr.T("cmd.repo.short"), + "issue": tr.T("cmd.issue.short"), + "label": tr.T("cmd.label.short"), + "license": tr.T("cmd.license.short"), + "member": tr.T("cmd.member.short"), + "milestone": tr.T("cmd.milestone.short"), + "pipeline": tr.T("cmd.pipeline.short"), + "pr": tr.T("cmd.pr.short"), + "profile": tr.T("cmd.profile.short"), + "release": tr.T("cmd.release.short"), + "branch": tr.T("cmd.branch.short"), + "org": tr.T("cmd.org.short"), + "user": tr.T("cmd.user.short"), + "search": tr.T("cmd.search.short"), + "ci": tr.T("cmd.ci.short"), + "compare": tr.T("cmd.compare.short"), + "dataset": tr.T("cmd.dataset.short"), + "webhook": tr.T("cmd.webhook.short"), + "wiki": tr.T("cmd.wiki.short"), + "health": tr.T("cmd.health.short"), + "ignore": tr.T("cmd.ignore.short"), + "workflow": tr.T("cmd.workflow.short"), } for name, shortcuts := range groups { diff --git a/shortcuts/register_test.go b/shortcuts/register_test.go index 295af4a..cd84719 100644 --- a/shortcuts/register_test.go +++ b/shortcuts/register_test.go @@ -1,6 +1,7 @@ package shortcuts import ( + "strings" "testing" "github.com/spf13/cobra" @@ -60,6 +61,9 @@ func TestRegisterAllGroupDescriptions(t *testing.T) { if cmd.Short == "" { t.Fatal("Short description is empty") } + if strings.HasPrefix(cmd.Short, "cmd.") { + t.Fatalf("Short description leaks raw i18n key: %q", cmd.Short) + } }) } } diff --git a/shortcuts/wiki/wiki.go b/shortcuts/wiki/wiki.go index c039386..261c697 100644 --- a/shortcuts/wiki/wiki.go +++ b/shortcuts/wiki/wiki.go @@ -2,301 +2,208 @@ package wiki import ( "encoding/base64" - "encoding/json" "fmt" "net/url" - "strings" - "time" + "github.com/gitlink-org/gitlink-cli/internal/config" + "github.com/gitlink-org/gitlink-cli/internal/i18n" "github.com/gitlink-org/gitlink-cli/shortcuts/common" ) -const wikiBaseURL = "https://gateway.gitlink.org.cn/api" +// switchToGateway overrides the client base URL with the gateway URL from config. +func switchToGateway(ctx *common.RuntimeContext) error { + cfg, err := config.Load() + if err != nil { + return err + } + if cfg.GatewayURL == "" { + cfg.GatewayURL = config.DefaultGatewayURL + } + ctx.Client.BaseURL = cfg.GatewayURL + return nil +} -func Shortcuts() []*common.Shortcut { +// gatewayFlag returns the common --gateway flag definition. +func gatewayFlag(tr *i18n.Translator) common.Flag { + return common.Flag{Name: "gateway", Short: "g", Usage: tr.T("flag.wiki.gateway"), Bool: true} +} + +// Shortcuts returns all wiki shortcuts. +func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { + tr := shortcutTranslator(translators...) return []*common.Shortcut{ { Name: "list", - Description: "List wiki pages", + Description: tr.T("cmd.wiki.list.short"), + Flags: []common.Flag{ + {Name: "project-id", Usage: tr.T("flag.wiki.project_id"), Required: true}, + gatewayFlag(tr), + }, Run: func(ctx *common.RuntimeContext) error { - if err := ctx.ResolveOwnerRepo(); err != nil { - return err + if ctx.Arg("gateway") == "true" { + if err := switchToGateway(ctx); err != nil { + return err + } } - projectID, err := fetchProjectID(ctx) - if err != nil { + if err := ctx.ResolveOwnerRepo(); err != nil { return err } q := url.Values{} q.Set("owner", ctx.Owner) q.Set("repo", ctx.Repo) - q.Set("projectId", fmt.Sprintf("%d", projectID)) - return callWikiAPI(ctx, "GET", "/wiki/open/wikiPages", nil, q) + q.Set("projectId", ctx.Arg("project-id")) + env, err := ctx.CallAPIWithQuery("GET", "/wiki/open/wikiPages", q) + if err != nil { + return err + } + return ctx.Output(env) }, }, { Name: "view", - Description: "View a wiki page", + Description: tr.T("cmd.wiki.view.short"), Flags: []common.Flag{ - {Name: "name", Short: "n", Usage: "Wiki page name", Required: true}, + {Name: "project-id", Usage: tr.T("flag.wiki.project_id"), Required: true}, + {Name: "page-name", Short: "n", Usage: tr.T("flag.wiki.page_name"), Required: true}, + gatewayFlag(tr), }, Run: func(ctx *common.RuntimeContext) error { + if ctx.Arg("gateway") == "true" { + if err := switchToGateway(ctx); err != nil { + return err + } + } if err := ctx.ResolveOwnerRepo(); err != nil { return err } - name, err := ctx.RequireArg("name") - if err != nil { - return err - } - projectID, err := fetchProjectID(ctx) - if err != nil { - return err - } q := url.Values{} q.Set("owner", ctx.Owner) q.Set("repo", ctx.Repo) - q.Set("projectId", fmt.Sprintf("%d", projectID)) - q.Set("pageName", name) - return callWikiAPI(ctx, "GET", "/wiki/open/getWiki", nil, q) + q.Set("projectId", ctx.Arg("project-id")) + q.Set("pageName", ctx.Arg("page-name")) + env, err := ctx.CallAPIWithQuery("GET", "/wiki/open/getWiki", q) + if err != nil { + return err + } + return ctx.Output(env) }, }, { Name: "create", - Description: "Create a wiki page", + Description: tr.T("cmd.wiki.create.short"), Flags: []common.Flag{ - {Name: "name", Short: "n", Usage: "Wiki page name", Required: true}, - {Name: "content", Short: "c", Usage: "Page content (will be base64 encoded)", Required: true}, - {Name: "message", Short: "m", Usage: "Commit message"}, + {Name: "project-id", Usage: tr.T("flag.wiki.project_id"), Required: true}, + {Name: "page-name", Short: "n", Usage: tr.T("flag.wiki.page_name"), Required: true}, + {Name: "title", Short: "t", Usage: tr.T("flag.wiki.title"), Required: true}, + {Name: "content", Short: "c", Usage: tr.T("flag.wiki.content"), Required: true}, + {Name: "message", Short: "m", Usage: tr.T("flag.wiki.message")}, + gatewayFlag(tr), }, Run: func(ctx *common.RuntimeContext) error { + if ctx.Arg("gateway") == "true" { + if err := switchToGateway(ctx); err != nil { + return err + } + } if err := ctx.ResolveOwnerRepo(); err != nil { return err } - name, err := ctx.RequireArg("name") - if err != nil { - return err - } - content, err := ctx.RequireArg("content") - if err != nil { - return err - } - projectID, err := fetchProjectID(ctx) - if err != nil { - return err - } - body := map[string]interface{}{ + content := ctx.Arg("content") + payload := map[string]interface{}{ "owner": ctx.Owner, "repo": ctx.Repo, - "projectId": projectID, - "pageName": name, - "title": name, - "message": ctx.Arg("message"), + "projectId": ctx.Arg("project-id"), + "pageName": ctx.Arg("page-name"), + "title": ctx.Arg("title"), "content_base64": base64.StdEncoding.EncodeToString([]byte(content)), + "message": ctx.Arg("message"), } - return callWikiAPI(ctx, "POST", "/wiki/open/createWiki", body, nil) + env, err := ctx.CallAPI("POST", "/wiki/open/createWiki", payload) + if err != nil { + return err + } + return ctx.Output(env) }, }, { Name: "update", - Description: "Update a wiki page", + Description: tr.T("cmd.wiki.update.short"), Flags: []common.Flag{ - {Name: "name", Short: "n", Usage: "Wiki page name", Required: true}, - {Name: "content", Short: "c", Usage: "New page content (will be base64 encoded)", Required: true}, - {Name: "message", Short: "m", Usage: "Commit message"}, + {Name: "project-id", Usage: tr.T("flag.wiki.project_id"), Required: true}, + {Name: "page-name", Short: "n", Usage: tr.T("flag.wiki.page_name"), Required: true}, + {Name: "title", Short: "t", Usage: tr.T("flag.wiki.title"), Required: true}, + {Name: "content", Short: "c", Usage: tr.T("flag.wiki.content")}, + {Name: "message", Short: "m", Usage: tr.T("flag.wiki.message")}, + gatewayFlag(tr), }, Run: func(ctx *common.RuntimeContext) error { + if ctx.Arg("gateway") == "true" { + if err := switchToGateway(ctx); err != nil { + return err + } + } if err := ctx.ResolveOwnerRepo(); err != nil { return err } - name, err := ctx.RequireArg("name") + title := ctx.Arg("title") + if title == "" { + return fmt.Errorf("--title is required") + } + content := ctx.Arg("content") + payload := map[string]interface{}{ + "owner": ctx.Owner, + "repo": ctx.Repo, + "projectId": ctx.Arg("project-id"), + "pageName": ctx.Arg("page-name"), + "title": title, + "message": ctx.Arg("message"), + } + if content != "" { + payload["content_base64"] = base64.StdEncoding.EncodeToString([]byte(content)) + } + env, err := ctx.CallAPI("PUT", "/wiki/open/updateWiki", payload) if err != nil { return err } - content, err := ctx.RequireArg("content") - if err != nil { - return err - } - projectID, err := fetchProjectID(ctx) - if err != nil { - return err - } - body := map[string]interface{}{ - "owner": ctx.Owner, - "repo": ctx.Repo, - "projectId": projectID, - "pageName": name, - "title": name, - "message": ctx.Arg("message"), - "content_base64": base64.StdEncoding.EncodeToString([]byte(content)), - } - return callWikiAPI(ctx, "PUT", "/wiki/open/updateWiki", body, nil) + return ctx.Output(env) }, }, { Name: "delete", - Description: "Delete a wiki page and remove it from sidebar", + Description: tr.T("cmd.wiki.delete.short"), Flags: []common.Flag{ - {Name: "name", Short: "n", Usage: "Wiki page name", Required: true}, + {Name: "project-id", Usage: tr.T("flag.wiki.project_id"), Required: true}, + {Name: "page-name", Short: "n", Usage: tr.T("flag.wiki.page_name"), Required: true}, + gatewayFlag(tr), }, Run: func(ctx *common.RuntimeContext) error { + if ctx.Arg("gateway") == "true" { + if err := switchToGateway(ctx); err != nil { + return err + } + } if err := ctx.ResolveOwnerRepo(); err != nil { return err } - name, err := ctx.RequireArg("name") - if err != nil { - return err - } - projectID, err := fetchProjectID(ctx) - if err != nil { - return err - } - - // Step 1: Delete the wiki page - body := map[string]interface{}{ + payload := map[string]interface{}{ "owner": ctx.Owner, "repo": ctx.Repo, - "projectId": projectID, - "pageName": name, + "projectId": ctx.Arg("project-id"), + "pageName": ctx.Arg("page-name"), } - if err := callWikiAPISilent(ctx, "DELETE", "/wiki/open/deleteWiki", body, nil); err != nil { + env, err := ctx.CallAPI("DELETE", "/wiki/open/deleteWiki", payload) + if err != nil { return err } - - // Step 2: Wait for GitLink async sidebar rebuild, then clean up - time.Sleep(2 * time.Second) - cleanSidebar(ctx, projectID, name) - - fmt.Printf("Wiki page %q deleted successfully.\n", name) - return nil + return ctx.Output(env) }, }, } } -// callWikiAPI sends a request to the wiki gateway. -// It switches the client BaseURL to the wiki gateway for the duration of the call, -// but skips the switch during tests (local httptest server). -func callWikiAPI(ctx *common.RuntimeContext, method, path string, body interface{}, query url.Values) error { - origBase := ctx.Client.BaseURL - if !strings.HasPrefix(origBase, "http://127.0.0.1") && !strings.HasPrefix(origBase, "http://localhost") { - ctx.Client.BaseURL = wikiBaseURL +func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator { + if len(translators) > 0 && translators[0] != nil { + return translators[0] } - defer func() { ctx.Client.BaseURL = origBase }() - - env, err := ctx.Client.DoRaw(method, path, body, query) - if err != nil { - return err - } - return ctx.Output(env) -} - -// fetchProjectID resolves the numeric project ID from the repo info API. -func fetchProjectID(ctx *common.RuntimeContext) (int64, error) { - env, err := ctx.CallAPI("GET", ctx.RepoPath(), nil) - if err != nil { - return 0, fmt.Errorf("failed to get project info: %w", err) - } - data, ok := env.Data.(map[string]interface{}) - if !ok { - return 0, fmt.Errorf("unexpected project info response") - } - for _, key := range []string{"project_id", "repo_id", "id"} { - if id, ok := data[key].(float64); ok { - return int64(id), nil - } - } - return 0, fmt.Errorf("project id not found in response") -} - -// callWikiAPISilent is like callWikiAPI but does not print output. -func callWikiAPISilent(ctx *common.RuntimeContext, method, path string, body interface{}, query url.Values) error { - origBase := ctx.Client.BaseURL - if !strings.HasPrefix(origBase, "http://127.0.0.1") && !strings.HasPrefix(origBase, "http://localhost") { - ctx.Client.BaseURL = wikiBaseURL - } - defer func() { ctx.Client.BaseURL = origBase }() - - _, err := ctx.Client.DoRaw(method, path, body, query) - return err -} - -const sidebarPageName = "_Sidebar" // GitLink uses capital S for the sidebar page - -// cleanSidebar fetches the wiki sidebar, removes the deleted page link, and updates it. -func cleanSidebar(ctx *common.RuntimeContext, projectID int64, pageName string) { - // Fetch sidebar - q := url.Values{} - q.Set("owner", ctx.Owner) - q.Set("repo", ctx.Repo) - q.Set("projectId", fmt.Sprintf("%d", projectID)) - q.Set("pageName", sidebarPageName) - - origBase := ctx.Client.BaseURL - if !strings.HasPrefix(origBase, "http://127.0.0.1") && !strings.HasPrefix(origBase, "http://localhost") { - ctx.Client.BaseURL = wikiBaseURL - } - defer func() { ctx.Client.BaseURL = origBase }() - - env, err := ctx.Client.DoRaw("GET", "/wiki/open/getWiki", nil, q) - if err != nil { - return // sidebar might not exist, silently skip - } - - // Extract content_base64 from response. - // DoRaw auto-parses JSON, so env.Data is a map with "data" as either - // a nested dict (already parsed) or a JSON string (needs parsing). - outer, ok := env.Data.(map[string]interface{}) - if !ok { - return - } - - var inner map[string]interface{} - switch v := outer["data"].(type) { - case map[string]interface{}: - inner = v - case string: - if err := json.Unmarshal([]byte(v), &inner); err != nil { - return - } - default: - return - } - - contentB64, ok := inner["content_base64"].(string) - if !ok { - return - } - contentBytes, err := base64.StdEncoding.DecodeString(contentB64) - if err != nil { - return - } - sidebar := string(contentBytes) - - // Remove the line containing [[pageName]] - target := "[[" + pageName + "]]" - lines := strings.Split(sidebar, "\n") - var newLines []string - for _, line := range lines { - trimmed := strings.TrimSpace(line) - if trimmed != target { - newLines = append(newLines, line) - } - } - newSidebar := strings.Join(newLines, "\n") - - // No change needed - if newSidebar == sidebar { - return - } - - // Update sidebar - body := map[string]interface{}{ - "owner": ctx.Owner, - "repo": ctx.Repo, - "projectId": projectID, - "pageName": sidebarPageName, - "title": sidebarPageName, - "message": "Remove deleted page " + pageName + " from sidebar", - "content_base64": base64.StdEncoding.EncodeToString([]byte(newSidebar)), - } - ctx.Client.DoRaw("PUT", "/wiki/open/updateWiki", body, nil) + return i18n.Default() } diff --git a/shortcuts/workflow/pr_summary.go b/shortcuts/workflow/pr_summary.go index 398bf1e..4035610 100644 --- a/shortcuts/workflow/pr_summary.go +++ b/shortcuts/workflow/pr_summary.go @@ -8,6 +8,7 @@ import ( "time" "github.com/gitlink-org/gitlink-cli/cmd/cmdutil" + "github.com/gitlink-org/gitlink-cli/internal/i18n" "github.com/gitlink-org/gitlink-cli/shortcuts/common" ) @@ -82,18 +83,18 @@ type PRSummaryResult struct { Source string `json:"source"` } -func newPRSummaryShortcut() *common.Shortcut { +func newPRSummaryShortcut(tr *i18n.Translator) *common.Shortcut { return &common.Shortcut{ Name: "pr-summary", - Description: "Generate a read-only pull request review summary", + Description: tr.T("cmd.workflow.pr-summary.short"), Flags: []common.Flag{ - {Name: "from", Usage: "Read PR summary input from a JSON file"}, - {Name: "number", Short: "n", Usage: "Pull request number for remote read-only analysis"}, - {Name: "include-commits", Usage: "Fetch commits in remote mode", Bool: true, Default: "true"}, - {Name: "include-files", Usage: "Fetch changed files in remote mode", Bool: true, Default: "true"}, - {Name: "max-files", Usage: "Maximum changed files to analyze", Default: "100"}, - {Name: "max-commits", Usage: "Maximum commits to analyze", Default: "100"}, - {Name: "lang", Usage: "Output language: en or zh-CN", Default: langEN}, + {Name: "from", Usage: tr.T("flag.workflow.pr_summary.from")}, + {Name: "number", Short: "n", Usage: tr.T("flag.workflow.pr_summary.number")}, + {Name: "include-commits", Usage: tr.T("flag.workflow.pr_summary.include_commits"), Bool: true, Default: "true"}, + {Name: "include-files", Usage: tr.T("flag.workflow.pr_summary.include_files"), Bool: true, Default: "true"}, + {Name: "max-files", Usage: tr.T("flag.workflow.pr_summary.max_files"), Default: "100"}, + {Name: "max-commits", Usage: tr.T("flag.workflow.pr_summary.max_commits"), Default: "100"}, + {Name: "lang", Usage: tr.T("flag.workflow.pr_summary.lang"), Default: langEN}, }, Run: runPRSummary, } diff --git a/shortcuts/workflow/repo_report.go b/shortcuts/workflow/repo_report.go index 4043be6..5cd3b16 100644 --- a/shortcuts/workflow/repo_report.go +++ b/shortcuts/workflow/repo_report.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/gitlink-org/gitlink-cli/cmd/cmdutil" + "github.com/gitlink-org/gitlink-cli/internal/i18n" "github.com/gitlink-org/gitlink-cli/shortcuts/common" ) @@ -48,19 +49,19 @@ type RepoPRSummary struct { ReviewFocus []string `json:"review_focus"` } -func newRepoReportShortcut() *common.Shortcut { +func newRepoReportShortcut(tr *i18n.Translator) *common.Shortcut { return &common.Shortcut{ Name: "repo-report", - Description: "Generate a read-only repository workflow report", + Description: tr.T("cmd.workflow.repo-report.short"), Flags: []common.Flag{ - {Name: "from", Usage: "Read repository report input from a JSON file"}, - {Name: "issue-limit", Usage: "Maximum issues to fetch and analyze", Default: "20"}, - {Name: "pr-limit", Usage: "Maximum pull requests to fetch and summarize", Default: "10"}, - {Name: "stale-days", Usage: "Days before an issue or PR is considered stale", Default: "30"}, - {Name: "include-issues", Usage: "Include issue triage summary", Bool: true, Default: "true"}, - {Name: "include-prs", Usage: "Include pull request summary", Bool: true, Default: "true"}, - {Name: "include-health", Usage: "Include repository health summary", Bool: true, Default: "true"}, - {Name: "lang", Usage: "Output language: en or zh-CN", Default: langEN}, + {Name: "from", Usage: tr.T("flag.workflow.repo_report.from")}, + {Name: "issue-limit", Usage: tr.T("flag.workflow.repo_report.issue_limit"), Default: "20"}, + {Name: "pr-limit", Usage: tr.T("flag.workflow.repo_report.pr_limit"), Default: "10"}, + {Name: "stale-days", Usage: tr.T("flag.workflow.repo_report.stale_days"), Default: "30"}, + {Name: "include-issues", Usage: tr.T("flag.workflow.repo_report.include_issues"), Bool: true, Default: "true"}, + {Name: "include-prs", Usage: tr.T("flag.workflow.repo_report.include_prs"), Bool: true, Default: "true"}, + {Name: "include-health", Usage: tr.T("flag.workflow.repo_report.include_health"), Bool: true, Default: "true"}, + {Name: "lang", Usage: tr.T("flag.workflow.repo_report.lang"), Default: langEN}, }, Run: runRepoReport, } diff --git a/shortcuts/workflow/workflow.go b/shortcuts/workflow/workflow.go index fc10ff1..b72f621 100644 --- a/shortcuts/workflow/workflow.go +++ b/shortcuts/workflow/workflow.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" + "github.com/gitlink-org/gitlink-cli/internal/i18n" "github.com/gitlink-org/gitlink-cli/shortcuts/common" ) @@ -19,63 +20,63 @@ type TriageReport struct { Results []TriageResult `json:"results"` } -func Shortcuts() []*common.Shortcut { +func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { + tr := shortcutTranslator(translators...) return []*common.Shortcut{ - newTriageShortcut(), - newHealthShortcut(), - newPRSummaryShortcut(), - newRepoReportShortcut(), - newReleaseNotesShortcut(), + newTriageShortcut(tr), + newHealthShortcut(tr), + newPRSummaryShortcut(tr), + newRepoReportShortcut(tr), } } -func newTriageShortcut() *common.Shortcut { +func newTriageShortcut(tr *i18n.Translator) *common.Shortcut { return &common.Shortcut{ Name: "triage", - Description: "Analyze issues with local workflow triage rules", + Description: tr.T("cmd.workflow.triage.short"), Flags: []common.Flag{ - {Name: "from", Usage: "Read issue inputs from a JSON file. Supports a single issue, an array, or an object with an issues field"}, - {Name: "title", Short: "t", Usage: "Issue title for single-issue local analysis"}, - {Name: "body", Short: "b", Usage: "Issue body for single-issue local analysis"}, - {Name: "number", Short: "n", Usage: "Issue number for single-issue local analysis"}, - {Name: "author", Usage: "Issue author for single-issue local analysis"}, - {Name: "url", Usage: "Issue URL for single-issue local analysis"}, - {Name: "labels", Usage: "Comma-separated labels for single-issue local analysis"}, - {Name: "state", Short: "s", Usage: "Filter or assign issue state", Default: "open"}, - {Name: "page", Short: "p", Usage: "API page number for remote triage", Default: "1"}, - {Name: "limit", Short: "l", Usage: "Maximum issues to analyze", Default: "30"}, - {Name: "since", Usage: "Optional remote issue filter for updated time"}, - {Name: "dry-run", Usage: "Preview workflow recommendations without remote writes", Bool: true, Default: "true"}, - {Name: "lang", Usage: "Output language: en or zh-CN", Default: langEN}, + {Name: "from", Usage: tr.T("flag.workflow.from")}, + {Name: "title", Short: "t", Usage: tr.T("flag.workflow.title")}, + {Name: "body", Short: "b", Usage: tr.T("flag.workflow.body")}, + {Name: "number", Short: "n", Usage: tr.T("flag.workflow.number")}, + {Name: "author", Usage: tr.T("flag.workflow.author")}, + {Name: "url", Usage: tr.T("flag.workflow.url")}, + {Name: "labels", Usage: tr.T("flag.workflow.labels")}, + {Name: "state", Short: "s", Usage: tr.T("flag.workflow.state"), Default: "open"}, + {Name: "page", Short: "p", Usage: tr.T("flag.workflow.page"), Default: "1"}, + {Name: "limit", Short: "l", Usage: tr.T("flag.workflow.limit"), Default: "30"}, + {Name: "since", Usage: tr.T("flag.workflow.since")}, + {Name: "dry-run", Usage: tr.T("flag.workflow.dry_run"), Bool: true, Default: "true"}, + {Name: "lang", Usage: tr.T("flag.workflow.lang"), Default: langEN}, }, Run: runTriage, } } -func newHealthShortcut() *common.Shortcut { +func newHealthShortcut(tr *i18n.Translator) *common.Shortcut { return &common.Shortcut{ Name: "health", - Description: "Score repository health with local workflow rules", + Description: tr.T("cmd.workflow.health.short"), Flags: []common.Flag{ - {Name: "from", Usage: "Read health input from a JSON file"}, - {Name: "repository", Usage: "Repository name, for example owner/repo"}, - {Name: "open-issues", Usage: "Open issue count", Default: "0"}, - {Name: "open-prs", Usage: "Open pull request count", Default: "0"}, - {Name: "stale-issues", Usage: "Stale issue count", Default: "0"}, - {Name: "stale-prs", Usage: "Stale pull request count", Default: "0"}, - {Name: "recent-activity-known", Usage: "Whether recent activity is known", Bool: true, Default: "false"}, - {Name: "recent-activity-days", Usage: "Days since recent activity", Default: "0"}, - {Name: "release-known", Usage: "Whether release status is known", Bool: true, Default: "false"}, - {Name: "has-recent-release", Usage: "Whether a recent release exists", Bool: true, Default: "false"}, - {Name: "ci-known", Usage: "Whether CI status is known", Bool: true, Default: "false"}, - {Name: "ci-passing", Usage: "Whether CI is passing", Bool: true, Default: "false"}, - {Name: "has-readme", Usage: "Whether README exists", Bool: true, Default: "false"}, - {Name: "has-license", Usage: "Whether LICENSE exists", Bool: true, Default: "false"}, - {Name: "has-contributing", Usage: "Whether CONTRIBUTING exists", Bool: true, Default: "false"}, - {Name: "agent-readiness-known", Usage: "Whether agent readiness score is known", Bool: true, Default: "false"}, - {Name: "agent-readiness-score", Usage: "Agent readiness score from 0 to 10", Default: "0"}, - {Name: "stale-days", Usage: "Days before an issue or PR is considered stale", Default: "30"}, - {Name: "lang", Usage: "Output language: en or zh-CN", Default: langEN}, + {Name: "from", Usage: tr.T("flag.workflow.from_2")}, + {Name: "repository", Usage: tr.T("flag.workflow.repository")}, + {Name: "open-issues", Usage: tr.T("flag.workflow.open_issues"), Default: "0"}, + {Name: "open-prs", Usage: tr.T("flag.workflow.open_prs"), Default: "0"}, + {Name: "stale-issues", Usage: tr.T("flag.workflow.stale_issues"), Default: "0"}, + {Name: "stale-prs", Usage: tr.T("flag.workflow.stale_prs"), Default: "0"}, + {Name: "recent-activity-known", Usage: tr.T("flag.workflow.recent_activity_known"), Bool: true, Default: "false"}, + {Name: "recent-activity-days", Usage: tr.T("flag.workflow.recent_activity_days"), Default: "0"}, + {Name: "release-known", Usage: tr.T("flag.workflow.release_known"), Bool: true, Default: "false"}, + {Name: "has-recent-release", Usage: tr.T("flag.workflow.has_recent_release"), Bool: true, Default: "false"}, + {Name: "ci-known", Usage: tr.T("flag.workflow.ci_known"), Bool: true, Default: "false"}, + {Name: "ci-passing", Usage: tr.T("flag.workflow.ci_passing"), Bool: true, Default: "false"}, + {Name: "has-readme", Usage: tr.T("flag.workflow.has_readme"), Bool: true, Default: "false"}, + {Name: "has-license", Usage: tr.T("flag.workflow.has_license"), Bool: true, Default: "false"}, + {Name: "has-contributing", Usage: tr.T("flag.workflow.has_contributing"), Bool: true, Default: "false"}, + {Name: "agent-readiness-known", Usage: tr.T("flag.workflow.agent_readiness_known"), Bool: true, Default: "false"}, + {Name: "agent-readiness-score", Usage: tr.T("flag.workflow.agent_readiness_score"), Default: "0"}, + {Name: "stale-days", Usage: tr.T("flag.workflow.stale_days"), Default: "30"}, + {Name: "lang", Usage: tr.T("flag.workflow.lang"), Default: langEN}, }, Run: runHealth, } @@ -385,3 +386,10 @@ func mustParseInt(value string, defaultValue int) int { } return parsed } + +func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator { + if len(translators) > 0 && translators[0] != nil { + return translators[0] + } + return i18n.Default() +}