From d82665baba80db2fbf508e93b6687611b12fc207 Mon Sep 17 00:00:00 2001 From: vsxd Date: Fri, 13 Mar 2026 14:23:20 +0800 Subject: [PATCH 1/4] feat(review): allow admins to review their own submissions --- .../review/ReviewPermissionChecker.java | 5 +- .../review/ReviewPermissionCheckerTest.java | 18 +- web/src/i18n/locales/zh.json | 426 +++++++++++++----- 3 files changed, 331 insertions(+), 118 deletions(-) diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewPermissionChecker.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewPermissionChecker.java index e2f09e8b..ffd44a05 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewPermissionChecker.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewPermissionChecker.java @@ -26,9 +26,10 @@ public class ReviewPermissionChecker { NamespaceType namespaceType, Map userNamespaceRoles, Set platformRoles) { - // Cannot review own submission + // Admins can review their own submissions if (task.getSubmittedBy().equals(userId)) { - return false; + return platformRoles.contains("SKILL_ADMIN") + || platformRoles.contains("SUPER_ADMIN"); } return canReviewNamespace(task.getNamespaceId(), namespaceType, userNamespaceRoles, platformRoles); diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/ReviewPermissionCheckerTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/ReviewPermissionCheckerTest.java index 45a5a924..ce6c094a 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/ReviewPermissionCheckerTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/ReviewPermissionCheckerTest.java @@ -16,13 +16,29 @@ class ReviewPermissionCheckerTest { // --- canReview tests --- @Test - void cannotReviewOwnSubmission() { + void regularUserCannotReviewOwnSubmission() { String userId = "user-1"; ReviewTask task = new ReviewTask(1L, 10L, userId); assertFalse(checker.canReview(task, userId, NamespaceType.TEAM, Map.of(), Set.of())); } + @Test + void skillAdminCanReviewOwnSubmission() { + String userId = "user-1"; + ReviewTask task = new ReviewTask(1L, 10L, userId); + assertTrue(checker.canReview(task, userId, + NamespaceType.TEAM, Map.of(), Set.of("SKILL_ADMIN"))); + } + + @Test + void superAdminCanReviewOwnSubmission() { + String userId = "user-1"; + ReviewTask task = new ReviewTask(1L, 10L, userId); + assertTrue(checker.canReview(task, userId, + NamespaceType.TEAM, Map.of(), Set.of("SUPER_ADMIN"))); + } + @Test void teamAdminCanReviewTeamSkill() { ReviewTask task = new ReviewTask(1L, 10L, "user-2"); diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 9425b890..4d3290f0 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -25,138 +25,334 @@ "title": "轻松集成", "description": "无缝集成到您现有的工具中" } - } + }, + "badge": "企业级技能注册中心", + "tagline": "发布、发现、管理", + "taglineHighlight": " Agent 技能包", + "description": "自托管的私有技能注册平台,为团队提供安全、高效的技能共享与协作空间", + "publishSkill": "发布技能", + "statsSkills": "技能包", + "statsDownloads": "下载量", + "statsTeams": "团队", + "whyTitle": "为什么选择", + "whyDescription": "专为企业打造的技能管理平台,提供完整的发布、发现、治理能力", + "featuresList": { + "privateDeploy": { + "title": "私有部署", + "description": "完全自托管,数据主权在您手中。一键部署到您的基础设施,防火墙后安全运行。" + }, + "versionControl": { + "title": "版本管理", + "description": "语义化版本控制,自定义标签(beta、stable),自动追踪 latest 版本。" + }, + "smartSearch": { + "title": "智能搜索", + "description": "全文搜索,支持命名空间、下载量、评分、时间等多维度筛选。" + }, + "teamwork": { + "title": "团队协作", + "description": "命名空间管理,角色权限控制(Owner/Admin/Member),发布策略配置。" + }, + "governance": { + "title": "审核治理", + "description": "团队内审核,平台级审批,全流程审计日志,满足合规要求。" + }, + "cliFirst": { + "title": "CLI 优先", + "description": "原生 REST API,兼容现有 ClawHub CLI 工具,无需客户端改动。" + } + }, + "ctaTitle": "准备好开始了吗?", + "ctaDescription": "一条命令即可启动本地开发环境,体验完整的技能管理流程", + "ctaButton": "立即体验", + "footerDocs": "文档", + "footerGithub": "GitHub", + "footerCommunity": "社区" + }, + "home": { + "subtitle": "现代化的技能注册中心", + "description": "为开发者提供高效的技能管理、分发和协作平台", + "browseSkills": "浏览技能", + "publishSkill": "发布技能", + "popularTitle": "热门下载", + "popularDescription": "社区最受欢迎的技能", + "latestTitle": "最新发布", + "latestDescription": "刚刚发布的新技能", + "viewAll": "查看全部 →" }, "search": { "title": "搜索技能", "placeholder": "搜索技能...", "sort": { + "label": "排序:", "relevance": "相关性", "downloads": "下载量", "stars": "星标数", "newest": "最新" }, - "noResults": "未找到技能", - "results": "找到 {{count}} 个技能" + "noResults": "未找到结果", + "noResultsFor": "没有找到与 \"{{q}}\" 相关的技能", + "enterKeyword": "请输入搜索关键词", + "results": "找到 {{count}} 个技能", + "resultCount": "找到 <1>{{count}} 个结果" }, - "user": { - "menu": { - "dashboard": "控制台", - "mySkills": "我的技能", - "myNamespaces": "我的命名空间", - "stars": "我的星标", - "reviews": "审核管理", - "promotions": "推广管理", - "users": "用户管理", - "auditLog": "审计日志", - "security": "安全设置", - "accounts": "账号合并", - "logout": "退出登录" - } + "searchBar": { + "placeholder": "搜索技能...", + "button": "搜索" }, - "footer": { - "resources": "资源", - "docs": "文档", - "api": "API", - "community": "社区", - "privacy": "隐私政策", + "login": { + "title": "登录 SkillHub", + "subtitle": "选择一个方式登录以继续", + "tabPassword": "账号密码", + "tabOAuth": "GitHub", + "username": "用户名", + "password": "密码", + "usernamePlaceholder": "输入用户名", + "passwordPlaceholder": "输入密码", + "submitting": "登录中...", + "submit": "登录", + "noAccount": "还没有账号?", + "register": "立即注册", + "oauthHint": "使用 GitHub 登录时,认证完成后会自动返回当前站点。", + "agreementPrefix": "登录即表示你同意我们的", "terms": "服务条款", - "copyright": "© 2024 SkillHub. 保留所有权利。" + "and": "和", + "privacy": "隐私政策" }, - "publish": { - "title": "发布技能", - "subtitle": "上传技能包到 SkillHub", - "reviewNotice": { - "title": "发布审核说明", - "description": "技能包提交后需要经过管理员审核才能正式发布。审核通过后,您的技能将对所有用户可见。" - }, - "namespace": "命名空间", - "selectNamespace": "选择命名空间", - "visibility": "可见性", - "visibilityOptions": { - "public": "公开", - "namespaceOnly": "仅命名空间", - "private": "私有" - }, - "file": "技能包文件", - "publishing": "发布中...", - "confirm": "确认发布", - "success": "发布成功", - "successDescription": "{{skill}} 已提交审核,等待管理员批准后即可使用", - "error": "发布失败", - "selectRequired": "请选择命名空间和文件" + "register": { + "title": "创建账号", + "subtitle": "支持本地注册,也可以直接使用 OAuth 登录进入平台。", + "tabLocal": "本地账号", + "tabOAuth": "OAuth", + "username": "用户名", + "email": "邮箱", + "password": "密码", + "usernamePlaceholder": "3-64 位字母、数字或下划线", + "emailPlaceholder": "可选,用于后续账号识别", + "passwordPlaceholder": "至少 8 位,包含 3 种字符类型", + "submitting": "注册中...", + "submit": "注册并登录", + "hasAccount": "已有账号?", + "login": "返回登录", + "oauthHint": "直接使用现有 OAuth 账户进入平台,无需再创建本地密码。" }, - "toast": { - "success": "成功", - "error": "错误", - "warning": "警告", - "info": "信息" + "device": { + "title": "设备授权", + "subtitle": "请输入设备上显示的 8 位用户码", + "codeLabel": "用户码", + "codeHint": "格式: XXXX-XXXX (支持粘贴)", + "incompleteCode": "请输入完整的 8 位用户码", + "success": "设备授权成功!", + "defaultError": "授权失败,请检查用户码是否正确", + "submitting": "授权中...", + "submit": "授权设备", + "notice": "授权后,设备将可以访问你的账户" }, - "dialog": { - "confirm": "确认", - "cancel": "取消", - "delete": "删除", - "close": "关闭" + "dashboard": { + "title": "Dashboard", + "subtitle": "管理你的账户和 API Tokens", + "userInfo": "用户信息", + "userInfoDesc": "你的账户详情", + "loginVia": "通过 {{provider}} 登录", + "platformRoles": "平台角色", + "starsAndRatings": "收藏与评分", + "viewStars": "查看我的收藏", + "credentials": "访问凭证", + "openTokens": "打开 Token 页面", + "governanceTitle": "审核与治理", + "viewPromotions": "查看提升审核" }, - "review": { - "detail": "审核详情", - "id": "审核 ID", - "backToList": "返回列表", - "namespace": "命名空间/标识", + "mySkills": { + "title": "我的技能", + "subtitle": "管理你发布的技能", + "publishNew": "发布新技能", + "emptyTitle": "还没有技能", + "emptyDescription": "开始发布你的第一个技能吧", + "publishSkill": "发布技能" + }, + "myNamespaces": { + "title": "我的命名空间", + "subtitle": "管理你的命名空间和团队", + "create": "创建命名空间", + "typeGlobal": "全局", + "typeTeam": "团队", + "manageMembers": "管理成员", + "reviewTasks": "审核任务", + "emptyTitle": "还没有命名空间", + "emptyDescription": "创建一个命名空间来组织你的技能" + }, + "tokens": { + "pageTitle": "Token 管理", + "pageSubtitle": "管理 CLI 和 API 使用的访问凭证" + }, + "reviews": { + "title": "审核中心", + "subtitle": "管理技能版本审核", + "tabPending": "待审核", + "tabApproved": "已通过", + "tabRejected": "已拒绝", + "empty": "暂无审核任务", + "colSkill": "技能", + "colVersion": "版本", + "colSubmitter": "提交者", + "colSubmitTime": "提交时间", + "colReviewer": "审核者", + "colReviewTime": "审核时间" + }, + "stars": { + "title": "我的收藏", + "subtitle": "查看你标记过的技能", + "empty": "还没有收藏任何技能" + }, + "promotions": { + "title": "提升审核", + "subtitle": "审核团队技能提升到全局空间的申请", + "tabPending": "待审核", + "tabApproved": "已通过", + "tabRejected": "已拒绝", + "commentPlaceholder": "审核意见(可选)", + "approve": "通过", + "reject": "拒绝", + "empty": "暂无提升申请" + }, + "adminUsers": { + "title": "用户管理", + "subtitle": "管理平台用户和权限", + "searchPlaceholder": "搜索用户名或邮箱...", + "filterAll": "全部", + "filterActive": "活跃", + "filterPending": "待审批", + "filterDisabled": "已禁用", + "empty": "暂无用户数据", + "colUsername": "用户名", + "colEmail": "邮箱", + "colStatus": "状态", + "colRole": "角色", + "colCreatedAt": "创建时间", + "colActions": "操作", + "statusActive": "活跃", + "statusPending": "待审批", + "statusDisabled": "已禁用", + "changeRole": "修改角色", + "approveUser": "审批通过", + "disable": "禁用", + "enable": "启用", + "totalRecords": "共 {{total}} 条记录,第 {{page}} 页", + "prevPage": "上一页", + "nextPage": "下一页", + "changeRoleTitle": "修改用户角色", + "changeRoleDesc": "为用户 {{username}} 分配新角色", + "roleLabel": "角色", + "selectRole": "选择角色", + "roleUser": "普通用户", + "roleReviewer": "审核员", + "roleUserAdmin": "用户管理员", + "roleAuditor": "审计员", + "roleSuperAdmin": "超级管理员", + "confirmAction": "确认操作", + "confirmDisable": "确定要禁用用户 {{username}} 吗?", + "confirmEnable": "确定要启用用户 {{username}} 吗?" + }, + "auditLog": { + "title": "审计日志", + "subtitle": "查看系统操作记录", + "filterAll": "全部", + "filterCliPublish": "CLI 发布", + "filterCompatPublish": "Compat 发布", + "filterReviewApprove": "审核通过", + "filterReviewReject": "审核拒绝", + "filterPromotionApprove": "提升通过", + "filterYankVersion": "版本撤回", + "userIdPlaceholder": "用户 ID...", + "empty": "暂无审计日志", + "colTime": "时间", + "colAction": "操作", + "colUserId": "用户 ID", + "colUsername": "用户名", + "colIp": "IP 地址", + "colDetail": "详情", + "totalRecords": "共 {{total}} 条记录,第 {{page}} 页", + "prevPage": "上一页", + "nextPage": "下一页" + }, + "security": { + "title": "安全设置", + "subtitle": "已启用本地账号密码登录时,可以在这里更新密码。", + "currentPassword": "当前密码", + "newPassword": "新密码", + "success": "密码修改成功", + "defaultError": "修改密码失败", + "submitting": "提交中...", + "submit": "更新密码" + }, + "accounts": { + "initiateTitle": "发起账号合并", + "initiateDesc": "输入 secondary 账号标识。支持本地用户名,或 `provider:subject` 格式的外部身份。", + "secondaryLabel": "Secondary 标识", + "secondaryPlaceholder": "例如:other_user 或 github:123456", + "initiating": "发起中...", + "initiate": "发起合并", + "initiateSuccess": "已创建合并请求,secondary={{secondaryUserId}}", + "initiateError": "发起合并失败", + "verifyTitle": "验证并完成合并", + "verifyDesc": "先完成 token 验证,再单独确认执行数据迁移。", + "mergeRequestId": "Merge Request ID", + "verificationToken": "Verification Token", + "verifying": "验证中...", + "verify": "完成合并", + "verifySuccess": "验证成功,确认后将执行正式合并", + "verifyError": "验证合并失败", + "confirming": "确认中...", + "confirm": "确认并完成合并", + "confirmSuccess": "账号合并已完成", + "confirmError": "确认合并失败" + }, + "namespace": { + "notFound": "命名空间不存在", + "skillList": "技能列表", + "emptyTitle": "暂无技能", + "emptyDescription": "该命名空间下还没有发布任何技能" + }, + "skillDetail": { + "notFound": "技能不存在", + "notFoundDesc": "该技能可能已被删除或从未存在", + "tabReadme": "README", + "tabFiles": "文件", + "tabVersions": "版本", + "noReadme": "暂无 README", + "noFiles": "暂无文件", + "noVersions": "暂无版本", + "fileCount": "{{count}} 个文件", "version": "版本", - "status": "状态", - "statusPending": "待审核", - "statusApproved": "已通过", - "statusRejected": "已拒绝", - "submitter": "提交者", - "submitTime": "提交时间", - "reviewer": "审核者", - "reviewTime": "审核时间", - "reviewComment": "审核意见", - "actions": "审核操作", - "commentLabel": "审核意见(可选)", - "commentPlaceholder": "填写审核意见...", - "approve": "通过审核", - "reject": "拒绝审核", - "confirmReject": "确认拒绝", - "cancelReject": "取消", - "rejectReasonRequired": "拒绝审核时必须填写原因", - "approveTitle": "通过审核", - "approveDescription": "确定要通过这个审核吗?", - "approveConfirm": "通过", - "rejectTitle": "拒绝审核", - "rejectDescription": "确定要拒绝这个审核吗?", - "rejectConfirm": "拒绝", - "approveSuccess": "审核已通过", - "approveFailed": "审核失败", - "rejectSuccess": "审核已拒绝", - "rejectFailed": "拒绝失败", - "notFound": "审核任务不存在" + "downloads": "下载量", + "rating": "评分", + "ratingNone": "暂无", + "namespaceLabel": "命名空间", + "loginToRate": "登录后可以收藏和评分", + "install": "安装", + "download": "下载", + "governance": "治理操作", + "processing": "处理中...", + "hideSkill": "隐藏技能", + "unhideSkill": "恢复技能", + "yankVersion": "撤回当前版本" }, - "token": { - "title": "API Tokens", - "createNew": "创建新 Token", - "empty": "还没有创建任何 Token", - "emptyHint": "点击上方按钮创建第一个 Token", - "name": "名称", - "prefix": "Token 前缀", - "createdAt": "创建时间", - "lastUsed": "最后使用", - "expiresAt": "过期时间", - "actions": "操作", - "delete": "删除", - "deleteTitle": "删除 Token", - "deleteDescription": "确定要删除 Token \"{{name}}\" 吗?此操作无法撤销。", - "deleteSuccess": "Token 已删除", - "deleteFailed": "删除失败", - "loading": "加载中..." + "members": { + "title": "成员管理", + "addMember": "添加成员", + "colUserId": "用户 ID", + "colRole": "角色", + "colJoinedAt": "加入时间", + "colActions": "操作", + "remove": "移除", + "empty": "暂无成员", + "namespaceNotFound": "命名空间不存在" + }, + "upload": { + "dropHint": "放开以上传文件...", + "dragHint": "拖拽 ZIP 文件到此处,或点击选择", + "formatHint": "仅支持 .zip 格式" + }, + "layout": { + "footerDescription": "现代化的技能注册中心,为开发者提供高效的技能管理和分发平台。" }, - "apiError": { - "unauthorized": "登录已过期,请重新登录", - "forbidden": "没有权限执行此操作", - "notFound": "请求的资源不存在", - "serverError": "服务器错误,请稍后重试", - "networkError": "网络连接失败,请检查网络", - "unknown": "操作失败" - } -} From 2170412bdabf002c54ad33bf04808aff7ce9f984 Mon Sep 17 00:00:00 2001 From: vsxd Date: Fri, 13 Mar 2026 14:43:26 +0800 Subject: [PATCH 2/4] feat(web): complete i18n coverage for all pages and components Replace hardcoded Chinese strings with t() calls across 36 files. Expand translation keys from ~160 to 412 covering all pages, features, and shared components. Fix date formatting to use dynamic i18n locale. --- web/src/app/layout.tsx | 2 +- web/src/features/auth/login-button.tsx | 6 +- .../features/namespace/namespace-header.tsx | 4 +- web/src/features/publish/upload-zone.tsx | 8 +- web/src/features/search/search-bar.tsx | 8 +- web/src/features/skill/file-tree.tsx | 4 +- web/src/features/skill/markdown-renderer.tsx | 38 +- web/src/features/social/rating-input.tsx | 4 +- web/src/features/social/star-button.tsx | 4 +- .../features/token/create-token-dialog.tsx | 26 +- web/src/i18n/locales/en.json | 370 +++++++++++++++++- web/src/i18n/locales/zh.json | 168 ++++++++ web/src/pages/admin/audit-log.tsx | 44 ++- web/src/pages/admin/users.tsx | 78 ++-- web/src/pages/dashboard.tsx | 26 +- web/src/pages/dashboard/my-namespaces.tsx | 20 +- web/src/pages/dashboard/my-skills.tsx | 14 +- web/src/pages/dashboard/namespace-members.tsx | 27 +- web/src/pages/dashboard/namespace-reviews.tsx | 17 +- web/src/pages/dashboard/promotions.tsx | 23 +- web/src/pages/dashboard/review-detail.tsx | 4 +- web/src/pages/dashboard/reviews.tsx | 28 +- web/src/pages/dashboard/stars.tsx | 8 +- web/src/pages/dashboard/tokens.tsx | 6 +- web/src/pages/device.tsx | 20 +- web/src/pages/home.tsx | 22 +- web/src/pages/landing.tsx | 60 +-- web/src/pages/login.tsx | 34 +- web/src/pages/namespace.tsx | 10 +- web/src/pages/register.tsx | 30 +- web/src/pages/search.tsx | 16 +- web/src/pages/settings/accounts.tsx | 36 +- web/src/pages/settings/security.tsx | 16 +- web/src/pages/skill-detail.tsx | 46 +-- web/src/shared/components/copy-button.tsx | 4 +- web/src/shared/components/pagination.tsx | 10 +- 36 files changed, 934 insertions(+), 307 deletions(-) diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index 50e7610c..69d0472e 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -78,7 +78,7 @@ export function Layout() { SkillHub

- 现代化的技能注册中心,为开发者提供高效的技能管理和分发平台。 + {t('layout.footerDescription')}

diff --git a/web/src/features/auth/login-button.tsx b/web/src/features/auth/login-button.tsx index a6f4c52e..d075842d 100644 --- a/web/src/features/auth/login-button.tsx +++ b/web/src/features/auth/login-button.tsx @@ -1,4 +1,5 @@ import { useQuery } from '@tanstack/react-query' +import { useTranslation } from 'react-i18next' import { authApi } from '@/api/client' import { Button } from '@/shared/ui/button' import type { OAuthProvider } from '@/api/types' @@ -8,6 +9,7 @@ interface LoginButtonProps { } export function LoginButton({ returnTo }: LoginButtonProps) { + const { t } = useTranslation() const { data, isLoading } = useQuery({ queryKey: ['auth', 'providers', returnTo ?? ''], queryFn: () => authApi.getProviders(returnTo), @@ -20,7 +22,7 @@ export function LoginButton({ returnTo }: LoginButtonProps) {
) @@ -40,7 +42,7 @@ export function LoginButton({ returnTo }: LoginButtonProps) { - 使用 {provider.name} 登录 + {t('loginButton.loginWith', { name: provider.name })} ))} diff --git a/web/src/features/namespace/namespace-header.tsx b/web/src/features/namespace/namespace-header.tsx index 8b7b8dd0..dea9ad27 100644 --- a/web/src/features/namespace/namespace-header.tsx +++ b/web/src/features/namespace/namespace-header.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next' import type { Namespace } from '@/api/types' import { NamespaceBadge } from '@/shared/components/namespace-badge' @@ -6,6 +7,7 @@ interface NamespaceHeaderProps { } export function NamespaceHeader({ namespace }: NamespaceHeaderProps) { + const { t } = useTranslation() return (
{namespace.avatarUrl && ( @@ -18,7 +20,7 @@ export function NamespaceHeader({ namespace }: NamespaceHeaderProps) {

{namespace.displayName}

- +
{namespace.description && (

{namespace.description}

diff --git a/web/src/features/publish/upload-zone.tsx b/web/src/features/publish/upload-zone.tsx index f9ba9093..aec52e81 100644 --- a/web/src/features/publish/upload-zone.tsx +++ b/web/src/features/publish/upload-zone.tsx @@ -1,4 +1,5 @@ import { useCallback } from 'react' +import { useTranslation } from 'react-i18next' import { useDropzone } from 'react-dropzone' import { cn } from '@/shared/lib/utils' @@ -8,6 +9,7 @@ interface UploadZoneProps { } export function UploadZone({ onFileSelect, disabled }: UploadZoneProps) { + const { t } = useTranslation() const onDrop = useCallback( (acceptedFiles: File[]) => { if (acceptedFiles.length > 0) { @@ -57,11 +59,11 @@ export function UploadZone({ onFileSelect, disabled }: UploadZoneProps) {
{isDragActive ? ( -

放开以上传文件...

+

{t('upload.dropHint')}

) : ( <> -

拖拽 ZIP 文件到此处,或点击选择

-

仅支持 .zip 格式

+

{t('upload.dragHint')}

+

{t('upload.formatHint')}

)}
diff --git a/web/src/features/search/search-bar.tsx b/web/src/features/search/search-bar.tsx index a5c07ee3..60c045f1 100644 --- a/web/src/features/search/search-bar.tsx +++ b/web/src/features/search/search-bar.tsx @@ -1,4 +1,5 @@ import { useState } from 'react' +import { useTranslation } from 'react-i18next' import { Input } from '@/shared/ui/input' import { Button } from '@/shared/ui/button' @@ -8,7 +9,8 @@ interface SearchBarProps { onSearch?: (query: string) => void } -export function SearchBar({ defaultValue = '', placeholder = '搜索技能...', onSearch }: SearchBarProps) { +export function SearchBar({ defaultValue = '', placeholder, onSearch }: SearchBarProps) { + const { t } = useTranslation() const [query, setQuery] = useState(defaultValue) const handleSubmit = (e: React.FormEvent) => { @@ -38,12 +40,12 @@ export function SearchBar({ defaultValue = '', placeholder = '搜索技能...', type="text" value={query} onChange={(e) => setQuery(e.target.value)} - placeholder={placeholder} + placeholder={placeholder || t('searchBar.placeholder')} className="pl-10 border-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 h-12" /> ) diff --git a/web/src/features/skill/file-tree.tsx b/web/src/features/skill/file-tree.tsx index d0b28212..44742e9d 100644 --- a/web/src/features/skill/file-tree.tsx +++ b/web/src/features/skill/file-tree.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next' import type { SkillFile } from '@/api/types' interface FileTreeProps { @@ -12,10 +13,11 @@ function formatFileSize(bytes: number): string { } export function FileTree({ files, onFileClick }: FileTreeProps) { + const { t } = useTranslation() return (
- 文件列表 ({files.length}) + {t('fileTree.title', { count: files.length })}
{files.map((file) => ( diff --git a/web/src/features/skill/markdown-renderer.tsx b/web/src/features/skill/markdown-renderer.tsx index b4b409a3..53227547 100644 --- a/web/src/features/skill/markdown-renderer.tsx +++ b/web/src/features/skill/markdown-renderer.tsx @@ -2,23 +2,51 @@ import ReactMarkdown from 'react-markdown' import rehypeHighlight from 'rehype-highlight' import rehypeSanitize from 'rehype-sanitize' import remarkGfm from 'remark-gfm' +import { cn } from '@/shared/lib/utils' interface MarkdownRendererProps { content: string className?: string } +function stripFrontmatter(content: string) { + return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '') +} + export function MarkdownRenderer({ content, className }: MarkdownRendererProps) { + const markdown = stripFrontmatter(content).trim() + return ( -
+
, - }} > - {content} + {markdown}
) diff --git a/web/src/features/social/rating-input.tsx b/web/src/features/social/rating-input.tsx index 4239a40b..7895f6d5 100644 --- a/web/src/features/social/rating-input.tsx +++ b/web/src/features/social/rating-input.tsx @@ -1,4 +1,5 @@ import { useState } from 'react' +import { useTranslation } from 'react-i18next' import { Star } from 'lucide-react' import { useUserRating, useRate } from './use-rating' import { useAuth } from '@/features/auth/use-auth' @@ -9,6 +10,7 @@ interface RatingInputProps { } export function RatingInput({ skillId, onRequireLogin }: RatingInputProps) { + const { t } = useTranslation() const { data: userRating, isLoading } = useUserRating(skillId) const rateMutation = useRate(skillId) const { isAuthenticated } = useAuth() @@ -56,7 +58,7 @@ export function RatingInput({ skillId, onRequireLogin }: RatingInputProps) {
{currentRating > 0 && ( - 你的评分: {currentRating} 星 + {t('ratingInput.yourRating', { score: currentRating })} )}
diff --git a/web/src/features/social/star-button.tsx b/web/src/features/social/star-button.tsx index e6767d0a..0d6c06fd 100644 --- a/web/src/features/social/star-button.tsx +++ b/web/src/features/social/star-button.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next' import { Button } from '@/shared/ui/button' import { useStar, useToggleStar } from './use-star' import { Star } from 'lucide-react' @@ -10,6 +11,7 @@ interface StarButtonProps { } export function StarButton({ skillId, starCount, onRequireLogin }: StarButtonProps) { + const { t } = useTranslation() const { data: starStatus, isLoading } = useStar(skillId) const toggleMutation = useToggleStar(skillId) const { isAuthenticated } = useAuth() @@ -36,7 +38,7 @@ export function StarButton({ skillId, starCount, onRequireLogin }: StarButtonPro disabled={toggleMutation.isPending} > - {starStatus.starred ? '已收藏' : '收藏'} ({starCount}) + {starStatus.starred ? t('starButton.starred') : t('starButton.star')} ({starCount}) ) } diff --git a/web/src/features/token/create-token-dialog.tsx b/web/src/features/token/create-token-dialog.tsx index 884db442..cb3d2abd 100644 --- a/web/src/features/token/create-token-dialog.tsx +++ b/web/src/features/token/create-token-dialog.tsx @@ -1,4 +1,5 @@ import { useState } from 'react' +import { useTranslation } from 'react-i18next' import { useMutation, useQueryClient } from '@tanstack/react-query' import { tokenApi } from '@/api/client' import { @@ -20,6 +21,7 @@ interface CreateTokenDialogProps { } export function CreateTokenDialog({ children }: CreateTokenDialogProps) { + const { t } = useTranslation() const [open, setOpen] = useState(false) const [name, setName] = useState('') const [createdToken, setCreatedToken] = useState(null) @@ -53,17 +55,17 @@ export function CreateTokenDialog({ children }: CreateTokenDialogProps) { {!createdToken ? ( <> - 创建 API Token + {t('createToken.title')} - 创建一个新的 API Token 用于 CLI 或 API 访问 + {t('createToken.description')}
- + setName(e.target.value)} onKeyDown={(e) => { @@ -76,33 +78,33 @@ export function CreateTokenDialog({ children }: CreateTokenDialogProps) {
) : ( <> - Token 创建成功 + {t('createToken.successTitle')} - 请立即复制并保存此 Token,它只会显示一次 + {t('createToken.successDescription')}
- +
{createdToken.token}
- +
{createdToken.name}
@@ -112,10 +114,10 @@ export function CreateTokenDialog({ children }: CreateTokenDialogProps) { navigator.clipboard.writeText(createdToken.token) }} > - 复制 Token + {t('createToken.copyToken')} diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 8bf86003..0db36ce8 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -25,19 +25,383 @@ "title": "Easy Integration", "description": "Seamlessly integrate with your existing tools" } - } + }, + "badge": "Enterprise Skill Registry", + "tagline": "Publish, Discover, Manage", + "taglineHighlight": " Agent Skills", + "description": "Self-hosted private skill registry, providing secure and efficient skill sharing and collaboration for teams", + "publishSkill": "Publish Skill", + "statsSkills": "Skills", + "statsDownloads": "Downloads", + "statsTeams": "Teams", + "whyTitle": "Why Choose", + "whyDescription": "Enterprise-grade skill management platform with complete publishing, discovery, and governance capabilities", + "featuresList": { + "privateDeploy": { + "title": "Private Deployment", + "description": "Fully self-hosted with data sovereignty. One-click deploy to your infrastructure, running securely behind your firewall." + }, + "versionControl": { + "title": "Version Control", + "description": "Semantic versioning, custom tags (beta, stable), automatic latest version tracking." + }, + "smartSearch": { + "title": "Smart Search", + "description": "Full-text search with multi-dimensional filtering by namespace, downloads, ratings, and time." + }, + "teamwork": { + "title": "Team Collaboration", + "description": "Namespace management, role-based access control (Owner/Admin/Member), publishing policy configuration." + }, + "governance": { + "title": "Review & Governance", + "description": "Team-level review, platform-level approval, full audit trail, meeting compliance requirements." + }, + "cliFirst": { + "title": "CLI First", + "description": "Native REST API, compatible with existing ClawHub CLI tools, no client-side changes needed." + } + }, + "ctaTitle": "Ready to Get Started?", + "ctaDescription": "Start your local development environment with a single command and experience the complete skill management workflow", + "ctaButton": "Try Now", + "footerDocs": "Docs", + "footerGithub": "GitHub", + "footerCommunity": "Community" + }, + "home": { + "subtitle": "Modern Skill Registry", + "description": "Efficient skill management, distribution, and collaboration platform for developers", + "browseSkills": "Browse Skills", + "publishSkill": "Publish Skill", + "popularTitle": "Popular Downloads", + "popularDescription": "Most popular skills in the community", + "latestTitle": "Latest Releases", + "latestDescription": "Freshly published skills", + "viewAll": "View All →" }, "search": { "title": "Search Skills", "placeholder": "Search skills...", "sort": { + "label": "Sort:", "relevance": "Relevance", "downloads": "Downloads", "stars": "Stars", "newest": "Newest" }, - "noResults": "No skills found", - "results": "{{count}} skills found" + "noResults": "No results found", + "noResultsFor": "No skills found matching \"{{q}}\"", + "enterKeyword": "Please enter a search keyword", + "results": "{{count}} skills found", + "resultCount": "Found <1>{{count}} results" + }, + "searchBar": { + "placeholder": "Search skills...", + "button": "Search" + }, + "login": { + "title": "Login to SkillHub", + "subtitle": "Choose a method to continue", + "tabPassword": "Password", + "tabOAuth": "GitHub", + "username": "Username", + "password": "Password", + "usernamePlaceholder": "Enter username", + "passwordPlaceholder": "Enter password", + "submitting": "Logging in...", + "submit": "Login", + "noAccount": "Don't have an account?", + "register": "Sign up now", + "oauthHint": "After GitHub authentication, you will be automatically redirected back to this site.", + "agreementPrefix": "By logging in, you agree to our", + "terms": "Terms of Service", + "and": "and", + "privacy": "Privacy Policy" + }, + "register": { + "title": "Create Account", + "subtitle": "Register locally or sign in directly with OAuth.", + "tabLocal": "Local Account", + "tabOAuth": "OAuth", + "username": "Username", + "email": "Email", + "password": "Password", + "usernamePlaceholder": "3-64 characters: letters, numbers, or underscores", + "emailPlaceholder": "Optional, for account identification", + "passwordPlaceholder": "At least 8 characters with 3 character types", + "submitting": "Registering...", + "submit": "Register & Login", + "hasAccount": "Already have an account?", + "login": "Back to login", + "oauthHint": "Sign in directly with your existing OAuth account, no local password needed." + }, + "device": { + "title": "Device Authorization", + "subtitle": "Enter the 8-digit user code shown on your device", + "codeLabel": "User Code", + "codeHint": "Format: XXXX-XXXX (paste supported)", + "incompleteCode": "Please enter the complete 8-digit user code", + "success": "Device authorized successfully!", + "defaultError": "Authorization failed, please check the user code", + "submitting": "Authorizing...", + "submit": "Authorize Device", + "notice": "After authorization, the device will have access to your account" + }, + "dashboard": { + "title": "Dashboard", + "subtitle": "Manage your account and API Tokens", + "userInfo": "User Info", + "userInfoDesc": "Your account details", + "loginVia": "Logged in via {{provider}}", + "platformRoles": "Platform Roles", + "starsAndRatings": "Stars & Ratings", + "viewStars": "View My Stars", + "credentials": "Credentials", + "openTokens": "Open Token Page", + "governanceTitle": "Review & Governance", + "viewPromotions": "View Promotions" + }, + "mySkills": { + "title": "My Skills", + "subtitle": "Manage your published skills", + "publishNew": "Publish New Skill", + "emptyTitle": "No skills yet", + "emptyDescription": "Start publishing your first skill", + "publishSkill": "Publish Skill" + }, + "myNamespaces": { + "title": "My Namespaces", + "subtitle": "Manage your namespaces and teams", + "create": "Create Namespace", + "typeGlobal": "Global", + "typeTeam": "Team", + "manageMembers": "Manage Members", + "reviewTasks": "Review Tasks", + "emptyTitle": "No namespaces yet", + "emptyDescription": "Create a namespace to organize your skills" + }, + "tokens": { + "pageTitle": "Token Management", + "pageSubtitle": "Manage access credentials for CLI and API" + }, + "reviews": { + "title": "Review Center", + "subtitle": "Manage skill version reviews", + "tabPending": "Pending", + "tabApproved": "Approved", + "tabRejected": "Rejected", + "empty": "No review tasks", + "colSkill": "Skill", + "colVersion": "Version", + "colSubmitter": "Submitted By", + "colSubmitTime": "Submitted At", + "colReviewer": "Reviewed By", + "colReviewTime": "Reviewed At" + }, + "stars": { + "title": "My Stars", + "subtitle": "View your starred skills", + "empty": "No starred skills yet" + }, + "promotions": { + "title": "Promotion Review", + "subtitle": "Review team skill promotion requests to global namespace", + "tabPending": "Pending", + "tabApproved": "Approved", + "tabRejected": "Rejected", + "commentPlaceholder": "Review comment (optional)", + "approve": "Approve", + "reject": "Reject", + "empty": "No promotion requests" + }, + "adminUsers": { + "title": "User Management", + "subtitle": "Manage platform users and permissions", + "searchPlaceholder": "Search username or email...", + "filterAll": "All", + "filterActive": "Active", + "filterPending": "Pending", + "filterDisabled": "Disabled", + "empty": "No user data", + "colUsername": "Username", + "colEmail": "Email", + "colStatus": "Status", + "colRole": "Role", + "colCreatedAt": "Created At", + "colActions": "Actions", + "statusActive": "Active", + "statusPending": "Pending", + "statusDisabled": "Disabled", + "changeRole": "Change Role", + "approveUser": "Approve", + "disable": "Disable", + "enable": "Enable", + "totalRecords": "Total {{total}} records, page {{page}}", + "prevPage": "Previous", + "nextPage": "Next", + "changeRoleTitle": "Change User Role", + "changeRoleDesc": "Assign a new role to user {{username}}", + "roleLabel": "Role", + "selectRole": "Select role", + "roleUser": "User", + "roleReviewer": "Reviewer", + "roleUserAdmin": "User Admin", + "roleAuditor": "Auditor", + "roleSuperAdmin": "Super Admin", + "confirmAction": "Confirm Action", + "confirmDisable": "Are you sure you want to disable user {{username}}?", + "confirmEnable": "Are you sure you want to enable user {{username}}?" + }, + "auditLog": { + "title": "Audit Log", + "subtitle": "View system operation records", + "filterAll": "All", + "filterCliPublish": "CLI Publish", + "filterCompatPublish": "Compat Publish", + "filterReviewApprove": "Review Approved", + "filterReviewReject": "Review Rejected", + "filterPromotionApprove": "Promotion Approved", + "filterYankVersion": "Version Yanked", + "userIdPlaceholder": "User ID...", + "empty": "No audit logs", + "colTime": "Time", + "colAction": "Action", + "colUserId": "User ID", + "colUsername": "Username", + "colIp": "IP Address", + "colDetail": "Details", + "totalRecords": "Total {{total}} records, page {{page}}", + "prevPage": "Previous", + "nextPage": "Next" + }, + "security": { + "title": "Security Settings", + "subtitle": "Update your password when local account login is enabled.", + "currentPassword": "Current Password", + "newPassword": "New Password", + "success": "Password changed successfully", + "defaultError": "Failed to change password", + "submitting": "Submitting...", + "submit": "Update Password" + }, + "accounts": { + "initiateTitle": "Initiate Account Merge", + "initiateDesc": "Enter the secondary account identifier. Supports local username or `provider:subject` format for external identities.", + "secondaryLabel": "Secondary Identifier", + "secondaryPlaceholder": "e.g.: other_user or github:123456", + "initiating": "Initiating...", + "initiate": "Initiate Merge", + "initiateSuccess": "Merge request created, secondary={{secondaryUserId}}", + "initiateError": "Failed to initiate merge", + "verifyTitle": "Verify & Complete Merge", + "verifyDesc": "Complete token verification first, then confirm to execute data migration.", + "mergeRequestId": "Merge Request ID", + "verificationToken": "Verification Token", + "verifying": "Verifying...", + "verify": "Complete Merge", + "verifySuccess": "Verification successful, confirm to execute the merge", + "verifyError": "Merge verification failed", + "confirming": "Confirming...", + "confirm": "Confirm & Complete Merge", + "confirmSuccess": "Account merge completed", + "confirmError": "Merge confirmation failed" + }, + "namespace": { + "notFound": "Namespace not found", + "skillList": "Skills", + "emptyTitle": "No skills", + "emptyDescription": "No skills have been published in this namespace yet" + }, + "skillDetail": { + "notFound": "Skill not found", + "notFoundDesc": "This skill may have been deleted or never existed", + "tabReadme": "README", + "tabFiles": "Files", + "tabVersions": "Versions", + "noReadme": "No README", + "noFiles": "No files", + "noVersions": "No versions", + "fileCount": "{{count}} files", + "version": "Version", + "downloads": "Downloads", + "rating": "Rating", + "ratingNone": "None", + "namespaceLabel": "Namespace", + "loginToRate": "Login to star and rate", + "install": "Install", + "download": "Download", + "governance": "Governance", + "processing": "Processing...", + "hideSkill": "Hide Skill", + "unhideSkill": "Unhide Skill", + "yankVersion": "Yank Current Version" + }, + "members": { + "title": "Member Management", + "addMember": "Add Member", + "colUserId": "User ID", + "colRole": "Role", + "colJoinedAt": "Joined At", + "colActions": "Actions", + "remove": "Remove", + "empty": "No members", + "namespaceNotFound": "Namespace not found" + }, + "upload": { + "dropHint": "Drop to upload...", + "dragHint": "Drag a ZIP file here, or click to select", + "formatHint": "Only .zip format supported" + }, + "layout": { + "footerDescription": "Modern skill registry, providing efficient skill management and distribution for developers." + }, + "nsReviews": { + "title": "Namespace Reviews", + "loadingNamespace": "Loading namespace info", + "reviewsFor": "Review tasks for {{name}}", + "empty": "No review records", + "version": "Version {{version}}", + "tabPending": "Pending", + "tabApproved": "Approved", + "tabRejected": "Rejected" + }, + "fileTree": { + "title": "Files ({{count}})" + }, + "loginButton": { + "loading": "Loading...", + "loginWith": "Login with {{name}}" + }, + "ratingInput": { + "yourRating": "Your rating: {{score}} stars" + }, + "createToken": { + "title": "Create API Token", + "description": "Create a new API Token for CLI or API access", + "nameLabel": "Token Name", + "namePlaceholder": "e.g.: my-cli-token", + "creating": "Creating...", + "create": "Create", + "successTitle": "Token Created", + "successDescription": "Copy and save this token now. It will only be shown once.", + "tokenLabel": "Token", + "nameDisplay": "Name", + "copyToken": "Copy Token" + }, + "starButton": { + "starred": "Starred", + "star": "Star" + }, + "copyButton": { + "copied": "Copied", + "copy": "Copy" + }, + "pagination": { + "prev": "Previous", + "next": "Next", + "pagePrefix": "Page", + "pageSuffix": "" }, "user": { "menu": { diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 4d3290f0..51b92ecd 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -356,3 +356,171 @@ "layout": { "footerDescription": "现代化的技能注册中心,为开发者提供高效的技能管理和分发平台。" }, + "nsReviews": { + "title": "命名空间审核", + "loadingNamespace": "加载命名空间信息中", + "reviewsFor": "{{name}} 的审核任务", + "empty": "暂无审核记录", + "version": "版本 {{version}}", + "tabPending": "待审核", + "tabApproved": "已通过", + "tabRejected": "已拒绝" + }, + "fileTree": { + "title": "文件列表 ({{count}})" + }, + "loginButton": { + "loading": "加载中...", + "loginWith": "使用 {{name}} 登录" + }, + "ratingInput": { + "yourRating": "你的评分: {{score}} 星" + }, + "createToken": { + "title": "创建 API Token", + "description": "创建一个新的 API Token 用于 CLI 或 API 访问", + "nameLabel": "Token 名称", + "namePlaceholder": "例如: my-cli-token", + "creating": "创建中...", + "create": "创建", + "successTitle": "Token 创建成功", + "successDescription": "请立即复制并保存此 Token,它只会显示一次", + "tokenLabel": "Token", + "nameDisplay": "名称", + "copyToken": "复制 Token" + }, + "starButton": { + "starred": "已收藏", + "star": "收藏" + }, + "copyButton": { + "copied": "已复制", + "copy": "复制" + }, + "pagination": { + "prev": "上一页", + "next": "下一页", + "pagePrefix": "第", + "pageSuffix": "页" + }, + "user": { + "menu": { + "dashboard": "控制台", + "mySkills": "我的技能", + "myNamespaces": "我的命名空间", + "stars": "我的星标", + "reviews": "审核管理", + "promotions": "推广管理", + "users": "用户管理", + "auditLog": "审计日志", + "security": "安全设置", + "accounts": "账号合并", + "logout": "退出登录" + } + }, + "footer": { + "resources": "资源", + "docs": "文档", + "api": "API", + "community": "社区", + "privacy": "隐私政策", + "terms": "服务条款", + "copyright": "© 2024 SkillHub. 保留所有权利。" + }, + "publish": { + "title": "发布技能", + "subtitle": "上传技能包到 SkillHub", + "reviewNotice": { + "title": "发布审核说明", + "description": "技能包提交后需要经过管理员审核才能正式发布。审核通过后,您的技能将对所有用户可见。" + }, + "namespace": "命名空间", + "selectNamespace": "选择命名空间", + "visibility": "可见性", + "visibilityOptions": { + "public": "公开", + "namespaceOnly": "仅命名空间", + "private": "私有" + }, + "file": "技能包文件", + "publishing": "发布中...", + "confirm": "确认发布", + "success": "发布成功", + "successDescription": "{{skill}} 已提交审核,等待管理员批准后即可使用", + "error": "发布失败", + "selectRequired": "请选择命名空间和文件" + }, + "toast": { + "success": "成功", + "error": "错误", + "warning": "警告", + "info": "信息" + }, + "dialog": { + "confirm": "确认", + "cancel": "取消", + "delete": "删除", + "close": "关闭" + }, + "review": { + "detail": "审核详情", + "id": "审核 ID", + "backToList": "返回列表", + "namespace": "命名空间/标识", + "version": "版本", + "status": "状态", + "statusPending": "待审核", + "statusApproved": "已通过", + "statusRejected": "已拒绝", + "submitter": "提交者", + "submitTime": "提交时间", + "reviewer": "审核者", + "reviewTime": "审核时间", + "reviewComment": "审核意见", + "actions": "审核操作", + "commentLabel": "审核意见(可选)", + "commentPlaceholder": "填写审核意见...", + "approve": "通过审核", + "reject": "拒绝审核", + "confirmReject": "确认拒绝", + "cancelReject": "取消", + "rejectReasonRequired": "拒绝审核时必须填写原因", + "approveTitle": "通过审核", + "approveDescription": "确定要通过这个审核吗?", + "approveConfirm": "通过", + "rejectTitle": "拒绝审核", + "rejectDescription": "确定要拒绝这个审核吗?", + "rejectConfirm": "拒绝", + "approveSuccess": "审核已通过", + "approveFailed": "审核失败", + "rejectSuccess": "审核已拒绝", + "rejectFailed": "拒绝失败", + "notFound": "审核任务不存在" + }, + "token": { + "title": "API Tokens", + "createNew": "创建新 Token", + "empty": "还没有创建任何 Token", + "emptyHint": "点击上方按钮创建第一个 Token", + "name": "名称", + "prefix": "Token 前缀", + "createdAt": "创建时间", + "lastUsed": "最后使用", + "expiresAt": "过期时间", + "actions": "操作", + "delete": "删除", + "deleteTitle": "删除 Token", + "deleteDescription": "确定要删除 Token \"{{name}}\" 吗?此操作无法撤销。", + "deleteSuccess": "Token 已删除", + "deleteFailed": "删除失败", + "loading": "加载中..." + }, + "apiError": { + "unauthorized": "登录已过期,请重新登录", + "forbidden": "没有权限执行此操作", + "notFound": "请求的资源不存在", + "serverError": "服务器错误,请稍后重试", + "networkError": "网络连接失败,请检查网络", + "unknown": "操作失败" + } +} diff --git a/web/src/pages/admin/audit-log.tsx b/web/src/pages/admin/audit-log.tsx index 07fe92ef..881c4909 100644 --- a/web/src/pages/admin/audit-log.tsx +++ b/web/src/pages/admin/audit-log.tsx @@ -1,4 +1,5 @@ import { useState } from 'react' +import { useTranslation } from 'react-i18next' import { Card } from '@/shared/ui/card' import { Input } from '@/shared/ui/input' import { Button } from '@/shared/ui/button' @@ -14,6 +15,7 @@ import { import { useAuditLog } from '@/features/admin/use-audit-log' export function AuditLogPage() { + const { t, i18n } = useTranslation() const [actionFilter, setActionFilter] = useState('') const [userIdFilter, setUserIdFilter] = useState('') const [page, setPage] = useState(0) @@ -26,29 +28,29 @@ export function AuditLogPage() { }) const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleString('zh-CN') + return new Date(dateString).toLocaleString(i18n.language) } return (
-

审计日志

-

查看系统操作记录

+

{t('auditLog.title')}

+

{t('auditLog.subtitle')}

setUserIdFilter(e.target.value)} className="w-[200px]" @@ -64,7 +66,7 @@ export function AuditLogPage() {
) : !data || data.items.length === 0 ? ( -

暂无审计日志

+

{t('auditLog.empty')}

) : ( <> @@ -72,12 +74,12 @@ export function AuditLogPage() { - 时间 - 操作 - 用户 ID - 用户名 - IP 地址 - 详情 + {t('auditLog.colTime')} + {t('auditLog.colAction')} + {t('auditLog.colUserId')} + {t('auditLog.colUsername')} + {t('auditLog.colIp')} + {t('auditLog.colDetail')} @@ -99,7 +101,7 @@ export function AuditLogPage() {

- 共 {data.total} 条记录,第 {page + 1} 页 + {t('auditLog.totalRecords', { total: data.total, page: page + 1 })}

diff --git a/web/src/pages/admin/users.tsx b/web/src/pages/admin/users.tsx index 77dcee1c..a2c51263 100644 --- a/web/src/pages/admin/users.tsx +++ b/web/src/pages/admin/users.tsx @@ -1,4 +1,5 @@ import { useState } from 'react' +import { useTranslation } from 'react-i18next' import { Card } from '@/shared/ui/card' import { Input } from '@/shared/ui/input' import { Button } from '@/shared/ui/button' @@ -24,6 +25,7 @@ import { useAdminUsers, useApproveUser, useDisableUser, useEnableUser, useUpdate import type { AdminUser } from '@/features/admin/use-admin-users' export function AdminUsersPage() { + const { t, i18n } = useTranslation() const [search, setSearch] = useState('') const [statusFilter, setStatusFilter] = useState('') const [page, setPage] = useState(0) @@ -46,7 +48,7 @@ export function AdminUsersPage() { const enableUserMutation = useEnableUser() const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleString('zh-CN') + return new Date(dateString).toLocaleString(i18n.language) } const handleChangeRole = (user: AdminUser) => { @@ -90,23 +92,23 @@ export function AdminUsersPage() { return (
-

用户管理

-

管理平台用户和权限

+

{t('adminUsers.title')}

+

{t('adminUsers.subtitle')}

setSearch(e.target.value)} className="flex-1" />
@@ -119,7 +121,7 @@ export function AdminUsersPage() {
) : !data || data.items.length === 0 ? ( -

暂无用户数据

+

{t('adminUsers.empty')}

) : ( <> @@ -127,12 +129,12 @@ export function AdminUsersPage() {
- 用户名 - 邮箱 - 状态 - 角色 - 创建时间 - 操作 + {t('adminUsers.colUsername')} + {t('adminUsers.colEmail')} + {t('adminUsers.colStatus')} + {t('adminUsers.colRole')} + {t('adminUsers.colCreatedAt')} + {t('adminUsers.colActions')} @@ -150,7 +152,7 @@ export function AdminUsersPage() { : 'bg-red-500/10 text-red-400 border-red-500/20' }`} > - {user.status === 'ACTIVE' ? '活跃' : user.status === 'PENDING' ? '待审批' : '已禁用'} + {user.status === 'ACTIVE' ? t('adminUsers.statusActive') : user.status === 'PENDING' ? t('adminUsers.statusPending') : t('adminUsers.statusDisabled')} {user.platformRoles.join(', ')} @@ -162,7 +164,7 @@ export function AdminUsersPage() { size="sm" onClick={() => handleChangeRole(user)} > - 修改角色 + {t('adminUsers.changeRole')} {user.status === 'PENDING' && ( )} {user.status === 'ACTIVE' ? ( @@ -179,7 +181,7 @@ export function AdminUsersPage() { size="sm" onClick={() => handleToggleStatus(user, 'ban')} > - 禁用 + {t('adminUsers.disable')} ) : ( )} @@ -200,7 +202,7 @@ export function AdminUsersPage() {

- 共 {data.total} 条记录,第 {page + 1} 页 + {t('adminUsers.totalRecords', { total: data.total, page: page + 1 })}

@@ -227,30 +229,30 @@ export function AdminUsersPage() { - 修改用户角色 + {t('adminUsers.changeRoleTitle')} - 为用户 {selectedUser?.username} 分配新角色 + {t('adminUsers.changeRoleDesc', { username: selectedUser?.username })}
- +
@@ -259,17 +261,17 @@ export function AdminUsersPage() { - 确认操作 + {t('adminUsers.confirmAction')} - 确定要{actionType === 'ban' ? '禁用' : '启用'}用户 {selectedUser?.username} 吗? + {actionType === 'ban' ? t('adminUsers.confirmDisable', { username: selectedUser?.username }) : t('adminUsers.confirmEnable', { username: selectedUser?.username })} diff --git a/web/src/pages/dashboard.tsx b/web/src/pages/dashboard.tsx index 768b413f..7fc83e9b 100644 --- a/web/src/pages/dashboard.tsx +++ b/web/src/pages/dashboard.tsx @@ -1,24 +1,26 @@ import { Link } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next' import { useAuth } from '@/features/auth/use-auth' import { TokenList } from '@/features/token/token-list' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/ui/card' export function DashboardPage() { + const { t } = useTranslation() const { user } = useAuth() return (
-

Dashboard

+

{t('dashboard.title')}

- 管理你的账户和 API Tokens + {t('dashboard.subtitle')}

- 用户信息 - 你的账户详情 + {t('dashboard.userInfo')} + {t('dashboard.userInfoDesc')}
@@ -34,13 +36,13 @@ export function DashboardPage() {
{user?.email}
- 通过 {user?.oauthProvider} 登录 + {t('dashboard.loginVia', { provider: user?.oauthProvider })}
{user?.platformRoles && user.platformRoles.length > 0 && (
-
平台角色
+
{t('dashboard.platformRoles')}
{user.platformRoles.map((role: string) => ( -
收藏与评分
+
{t('dashboard.starsAndRatings')}
- 查看我的收藏 + {t('dashboard.viewStars')}
-
访问凭证
+
{t('dashboard.credentials')}
- 打开 Token 页面 + {t('dashboard.openTokens')}
-
审核与治理
+
{t('dashboard.governanceTitle')}
- 查看提升审核 + {t('dashboard.viewPromotions')}
diff --git a/web/src/pages/dashboard/my-namespaces.tsx b/web/src/pages/dashboard/my-namespaces.tsx index fd0dc01a..46210b30 100644 --- a/web/src/pages/dashboard/my-namespaces.tsx +++ b/web/src/pages/dashboard/my-namespaces.tsx @@ -1,4 +1,5 @@ import { useNavigate } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next' import { Button } from '@/shared/ui/button' import { Card } from '@/shared/ui/card' import { NamespaceBadge } from '@/shared/components/namespace-badge' @@ -7,6 +8,7 @@ import { useMyNamespaces } from '@/shared/hooks/use-skill-queries' export function MyNamespacesPage() { const navigate = useNavigate() + const { t } = useTranslation() const { data: namespaces, isLoading } = useMyNamespaces() const handleNamespaceClick = (slug: string) => { @@ -37,10 +39,10 @@ export function MyNamespacesPage() {
-

我的命名空间

-

管理你的命名空间和团队

+

{t('myNamespaces.title')}

+

{t('myNamespaces.subtitle')}

- +
{namespaces && namespaces.length > 0 ? ( @@ -60,7 +62,7 @@ export function MyNamespacesPage() {
{namespace.description && ( @@ -78,7 +80,7 @@ export function MyNamespacesPage() { size="sm" onClick={(e) => handleMembersClick(namespace.slug, e)} > - 管理成员 + {t('myNamespaces.manageMembers')} )}
@@ -95,9 +97,9 @@ export function MyNamespacesPage() { ) : ( 创建命名空间} + title={t('myNamespaces.emptyTitle')} + description={t('myNamespaces.emptyDescription')} + action={} /> )} diff --git a/web/src/pages/dashboard/my-skills.tsx b/web/src/pages/dashboard/my-skills.tsx index dd567009..c2f6d98b 100644 --- a/web/src/pages/dashboard/my-skills.tsx +++ b/web/src/pages/dashboard/my-skills.tsx @@ -1,4 +1,5 @@ import { useNavigate } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next' import { Button } from '@/shared/ui/button' import { Card } from '@/shared/ui/card' import { EmptyState } from '@/shared/components/empty-state' @@ -6,6 +7,7 @@ import { useMySkills } from '@/shared/hooks/use-skill-queries' export function MySkillsPage() { const navigate = useNavigate() + const { t } = useTranslation() const { data: skills, isLoading } = useMySkills() const handleSkillClick = (namespace: string, slug: string) => { @@ -26,11 +28,11 @@ export function MySkillsPage() {
-

我的技能

-

管理你发布的技能

+

{t('mySkills.title')}

+

{t('mySkills.subtitle')}

@@ -72,11 +74,11 @@ export function MySkillsPage() {
) : ( navigate({ to: '/dashboard/publish' })}> - 发布技能 + {t('mySkills.publishSkill')} } /> diff --git a/web/src/pages/dashboard/namespace-members.tsx b/web/src/pages/dashboard/namespace-members.tsx index b764761b..6081a7f1 100644 --- a/web/src/pages/dashboard/namespace-members.tsx +++ b/web/src/pages/dashboard/namespace-members.tsx @@ -1,11 +1,16 @@ import { useParams } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next' import { NamespaceHeader } from '@/features/namespace/namespace-header' import { Button } from '@/shared/ui/button' import { Card } from '@/shared/ui/card' import { useNamespaceDetail, useNamespaceMembers } from '@/shared/hooks/use-skill-queries' export function NamespaceMembersPage() { - const { slug } = useParams({ from: '/dashboard/namespaces/$slug/members' }) + const translation = useTranslation() + const t = translation.t + const language = translation.i18n.language + const params = useParams({ from: '/dashboard/namespaces/$slug/members' }) + const slug = params.slug const { data: namespace, isLoading: isLoadingNamespace } = useNamespaceDetail(slug) const { data: members, isLoading: isLoadingMembers } = useNamespaceMembers(slug) @@ -22,7 +27,7 @@ export function NamespaceMembersPage() { if (!namespace) { return (
-

命名空间不存在

+

{t('members.namespaceNotFound')}

) } @@ -33,8 +38,8 @@ export function NamespaceMembersPage() {
-

成员管理

- +

{t('members.title')}

+
{isLoadingMembers ? ( @@ -49,10 +54,10 @@ export function NamespaceMembersPage() {
- - - - + + + + @@ -65,11 +70,11 @@ export function NamespaceMembersPage() { @@ -80,7 +85,7 @@ export function NamespaceMembersPage() { ) : ( - 暂无成员 + {t('members.empty')} )} diff --git a/web/src/pages/dashboard/namespace-reviews.tsx b/web/src/pages/dashboard/namespace-reviews.tsx index c420b6c6..250e8471 100644 --- a/web/src/pages/dashboard/namespace-reviews.tsx +++ b/web/src/pages/dashboard/namespace-reviews.tsx @@ -1,17 +1,19 @@ import { useParams } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next' import { Card } from '@/shared/ui/card' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/ui/tabs' import { useNamespaceDetail } from '@/shared/hooks/use-skill-queries' import { useReviewList } from '@/features/review/use-review-list' function ReviewListSection({ namespaceId }: { namespaceId?: number }) { + const { t } = useTranslation() const { data: pending } = useReviewList('PENDING', namespaceId) const { data: approved } = useReviewList('APPROVED', namespaceId) const { data: rejected } = useReviewList('REJECTED', namespaceId) const renderItems = (items?: typeof pending) => { if (!items || items.length === 0) { - return 暂无审核记录 + return {t('nsReviews.empty')} } return ( @@ -20,7 +22,7 @@ function ReviewListSection({ namespaceId }: { namespaceId?: number }) {
{review.namespace}/{review.skillSlug}
-
版本 {review.version}
+
{t('nsReviews.version', { version: review.version })}
{new Date(review.submittedAt).toLocaleString('zh-CN')}
@@ -36,9 +38,9 @@ function ReviewListSection({ namespaceId }: { namespaceId?: number }) { return ( - 待审核 - 已通过 - 已拒绝 + {t('nsReviews.tabPending')} + {t('nsReviews.tabApproved')} + {t('nsReviews.tabRejected')} {renderItems(pending)} {renderItems(approved)} @@ -48,15 +50,16 @@ function ReviewListSection({ namespaceId }: { namespaceId?: number }) { } export function NamespaceReviewsPage() { + const { t } = useTranslation() const { slug } = useParams({ from: '/dashboard/namespaces/$slug/reviews' }) const { data: namespace } = useNamespaceDetail(slug) return (
-

命名空间审核

+

{t('nsReviews.title')}

- {namespace ? `${namespace.displayName} 的审核任务` : '加载命名空间信息中'} + {namespace ? t('nsReviews.reviewsFor', { name: namespace.displayName }) : t('nsReviews.loadingNamespace')}

diff --git a/web/src/pages/dashboard/promotions.tsx b/web/src/pages/dashboard/promotions.tsx index 469047de..446ad898 100644 --- a/web/src/pages/dashboard/promotions.tsx +++ b/web/src/pages/dashboard/promotions.tsx @@ -1,4 +1,5 @@ import { useState } from 'react' +import { useTranslation } from 'react-i18next' import { useApprovePromotion, usePromotionList, useRejectPromotion } from '@/features/promotion/use-promotion-list' import { Button } from '@/shared/ui/button' import { Card } from '@/shared/ui/card' @@ -6,6 +7,7 @@ import { Input } from '@/shared/ui/input' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/ui/tabs' function PromotionSection({ status }: { status: 'PENDING' | 'APPROVED' | 'REJECTED' }) { + const { t, i18n } = useTranslation() const { data: items, isLoading } = usePromotionList(status) const approveMutation = useApprovePromotion() const rejectMutation = useRejectPromotion() @@ -16,7 +18,7 @@ function PromotionSection({ status }: { status: 'PENDING' | 'APPROVED' | 'REJECT } if (!items || items.length === 0) { - return 暂无提升申请 + return {t('promotions.empty')} } return ( @@ -30,12 +32,12 @@ function PromotionSection({ status }: { status: 'PENDING' | 'APPROVED' | 'REJECT {item.sourceVersion} {'->'} @{item.targetNamespace}
-
{new Date(item.submittedAt).toLocaleString('zh-CN')}
+
{new Date(item.submittedAt).toLocaleString(i18n.language)}
{status === 'PENDING' ? ( <> setCommentById((prev) => ({ ...prev, [item.id]: event.target.value }))} /> @@ -44,14 +46,14 @@ function PromotionSection({ status }: { status: 'PENDING' | 'APPROVED' | 'REJECT onClick={() => approveMutation.mutate({ id: item.id, comment: commentById[item.id] })} disabled={approveMutation.isPending || rejectMutation.isPending} > - 通过 + {t('promotions.approve')} @@ -65,17 +67,18 @@ function PromotionSection({ status }: { status: 'PENDING' | 'APPROVED' | 'REJECT } export function PromotionsPage() { + const { t } = useTranslation() return (
-

提升审核

-

审核团队技能提升到全局空间的申请

+

{t('promotions.title')}

+

{t('promotions.subtitle')}

- 待审核 - 已通过 - 已拒绝 + {t('promotions.tabPending')} + {t('promotions.tabApproved')} + {t('promotions.tabRejected')} diff --git a/web/src/pages/dashboard/review-detail.tsx b/web/src/pages/dashboard/review-detail.tsx index 2a2031a2..83441597 100644 --- a/web/src/pages/dashboard/review-detail.tsx +++ b/web/src/pages/dashboard/review-detail.tsx @@ -12,7 +12,7 @@ import { useReviewDetail, useApproveReview, useRejectReview } from '@/features/r export function ReviewDetailPage() { const { id } = useParams({ from: '/dashboard/reviews/$id' }) const navigate = useNavigate() - const { t } = useTranslation() + const { t, i18n } = useTranslation() const taskId = Number(id) const { data: review, isLoading } = useReviewDetail(taskId) @@ -25,7 +25,7 @@ export function ReviewDetailPage() { const [rejectDialog, setRejectDialog] = useState(false) const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleString('zh-CN') + return new Date(dateString).toLocaleString(i18n.language) } const handleApprove = async () => { diff --git a/web/src/pages/dashboard/reviews.tsx b/web/src/pages/dashboard/reviews.tsx index aa62b711..5ce4225b 100644 --- a/web/src/pages/dashboard/reviews.tsx +++ b/web/src/pages/dashboard/reviews.tsx @@ -1,4 +1,5 @@ import { useNavigate } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next' import { Card } from '@/shared/ui/card' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/ui/tabs' import { @@ -12,13 +13,14 @@ import { import { useReviewList } from '@/features/review/use-review-list' export function ReviewsPage() { + const { t, i18n } = useTranslation() const navigate = useNavigate() const { data: pendingReviews, isLoading: isPendingLoading } = useReviewList('PENDING') const { data: approvedReviews, isLoading: isApprovedLoading } = useReviewList('APPROVED') const { data: rejectedReviews, isLoading: isRejectedLoading } = useReviewList('REJECTED') const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleString('zh-CN') + return new Date(dateString).toLocaleString(i18n.language) } const handleRowClick = (reviewId: number) => { @@ -39,7 +41,7 @@ export function ReviewsPage() { if (!reviews || reviews.length === 0) { return ( -

暂无审核任务

+

{t('reviews.empty')}

) } @@ -49,12 +51,12 @@ export function ReviewsPage() {
用户 ID角色加入时间操作{t('members.colUserId')}{t('members.colRole')}{t('members.colJoinedAt')}{t('members.colActions')}
- {new Date(member.createdAt).toLocaleDateString('zh-CN')} + {new Date(member.createdAt).toLocaleDateString(language)}
- 技能 - 版本 - 提交者 - 提交时间 - {status !== 'PENDING' && 审核者} - {status !== 'PENDING' && 审核时间} + {t('reviews.colSkill')} + {t('reviews.colVersion')} + {t('reviews.colSubmitter')} + {t('reviews.colSubmitTime')} + {status !== 'PENDING' && {t('reviews.colReviewer')}} + {status !== 'PENDING' && {t('reviews.colReviewTime')}} @@ -93,15 +95,15 @@ export function ReviewsPage() { return (
-

审核中心

-

管理技能版本审核

+

{t('reviews.title')}

+

{t('reviews.subtitle')}

- 待审核 - 已通过 - 已拒绝 + {t('reviews.tabPending')} + {t('reviews.tabApproved')} + {t('reviews.tabRejected')} diff --git a/web/src/pages/dashboard/stars.tsx b/web/src/pages/dashboard/stars.tsx index 9854e850..5a570dc1 100644 --- a/web/src/pages/dashboard/stars.tsx +++ b/web/src/pages/dashboard/stars.tsx @@ -1,9 +1,11 @@ import { useNavigate } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next' import { SkillCard } from '@/features/skill/skill-card' import { useMyStars } from '@/shared/hooks/use-skill-queries' import { Card } from '@/shared/ui/card' export function MyStarsPage() { + const { t } = useTranslation() const navigate = useNavigate() const { data: skills, isLoading } = useMyStars() @@ -20,12 +22,12 @@ export function MyStarsPage() { return (
-

我的收藏

-

查看你标记过的技能

+

{t('stars.title')}

+

{t('stars.subtitle')}

{!skills || skills.length === 0 ? ( - 还没有收藏任何技能 + {t('stars.empty')} ) : (
{skills.map((skill) => ( diff --git a/web/src/pages/dashboard/tokens.tsx b/web/src/pages/dashboard/tokens.tsx index dea5f9ca..034e0f96 100644 --- a/web/src/pages/dashboard/tokens.tsx +++ b/web/src/pages/dashboard/tokens.tsx @@ -1,11 +1,13 @@ +import { useTranslation } from 'react-i18next' import { TokenList } from '@/features/token/token-list' export function TokensPage() { + const { t } = useTranslation() return (
-

Token 管理

-

管理 CLI 和 API 使用的访问凭证

+

{t('tokens.pageTitle')}

+

{t('tokens.pageSubtitle')}

diff --git a/web/src/pages/device.tsx b/web/src/pages/device.tsx index abe16e3c..a67e189d 100644 --- a/web/src/pages/device.tsx +++ b/web/src/pages/device.tsx @@ -1,4 +1,5 @@ import { useState, useRef, useEffect } from 'react' +import { useTranslation } from 'react-i18next' import { Card } from '@/shared/ui/card' import { Button } from '@/shared/ui/button' import { Input } from '@/shared/ui/input' @@ -16,6 +17,7 @@ async function authorizeDevice(userCode: string): Promise { } export function DeviceAuthPage() { + const { t } = useTranslation() const [part1, setPart1] = useState('') const [part2, setPart2] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) @@ -61,7 +63,7 @@ export function DeviceAuthPage() { e.preventDefault() if (part1.length !== 4 || part2.length !== 4) { - setMessage({ type: 'error', text: '请输入完整的 8 位用户码' }) + setMessage({ type: 'error', text: t('device.incompleteCode') }) return } @@ -71,14 +73,14 @@ export function DeviceAuthPage() { try { await authorizeDevice(userCode) - setMessage({ type: 'success', text: '设备授权成功!' }) + setMessage({ type: 'success', text: t('device.success') }) setPart1('') setPart2('') input1Ref.current?.focus() } catch (error) { setMessage({ type: 'error', - text: error instanceof Error ? error.message : '授权失败,请检查用户码是否正确' + text: error instanceof Error ? error.message : t('device.defaultError') }) } finally { setIsSubmitting(false) @@ -94,15 +96,15 @@ export function DeviceAuthPage() {
-

设备授权

+

{t('device.title')}

- 请输入设备上显示的 8 位用户码 + {t('device.subtitle')}

- +

- 格式: XXXX-XXXX (支持粘贴) + {t('device.codeHint')}

@@ -148,12 +150,12 @@ export function DeviceAuthPage() { className="w-full" disabled={isSubmitting || part1.length !== 4 || part2.length !== 4} > - {isSubmitting ? '授权中...' : '授权设备'} + {isSubmitting ? t('device.submitting') : t('device.submit')}
-

授权后,设备将可以访问你的账户

+

{t('device.notice')}

diff --git a/web/src/pages/home.tsx b/web/src/pages/home.tsx index c42b7794..b8e34921 100644 --- a/web/src/pages/home.tsx +++ b/web/src/pages/home.tsx @@ -1,4 +1,5 @@ import { useNavigate } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next' import { SearchBar } from '@/features/search/search-bar' import { SkillCard } from '@/features/skill/skill-card' import { SkeletonList } from '@/shared/components/skeleton-loader' @@ -6,6 +7,7 @@ import { useSearchSkills } from '@/shared/hooks/use-skill-queries' import { Button } from '@/shared/ui/button' export function HomePage() { + const { t } = useTranslation() const navigate = useNavigate() const { data: popularSkills, isLoading: isLoadingPopular } = useSearchSkills({ @@ -35,10 +37,10 @@ export function HomePage() { SkillHub

- 现代化的技能注册中心 + {t('home.subtitle')}

- 为开发者提供高效的技能管理、分发和协作平台 + {t('home.description')}

@@ -48,10 +50,10 @@ export function HomePage() {
@@ -60,14 +62,14 @@ export function HomePage() {
-

热门下载

-

社区最受欢迎的技能

+

{t('home.popularTitle')}

+

{t('home.popularDescription')}

{isLoadingPopular ? ( @@ -90,14 +92,14 @@ export function HomePage() {
-

最新发布

-

刚刚发布的新技能

+

{t('home.latestTitle')}

+

{t('home.latestDescription')}

{isLoadingLatest ? ( diff --git a/web/src/pages/landing.tsx b/web/src/pages/landing.tsx index dc5078b2..7ad5a05c 100644 --- a/web/src/pages/landing.tsx +++ b/web/src/pages/landing.tsx @@ -1,9 +1,11 @@ import { useNavigate } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next' import { SearchBar } from '@/features/search/search-bar' import { Button } from '@/shared/ui/button' import { useEffect, useRef, useState } from 'react' export function LandingPage() { + const { t } = useTranslation() const navigate = useNavigate() const canvasRef = useRef(null) const [stats] = useState({ @@ -112,33 +114,33 @@ export function LandingPage() { const features = [ { icon: '🔒', - title: '私有部署', - description: '完全自托管,数据主权在您手中。一键部署到您的基础设施,防火墙后安全运行。', + title: t('landing.featuresList.privateDeploy.title'), + description: t('landing.featuresList.privateDeploy.description'), }, { icon: '📦', - title: '版本管理', - description: '语义化版本控制,自定义标签(beta、stable),自动追踪 latest 版本。', + title: t('landing.featuresList.versionControl.title'), + description: t('landing.featuresList.versionControl.description'), }, { icon: '🔍', - title: '智能搜索', - description: '全文搜索,支持命名空间、下载量、评分、时间等多维度筛选。', + title: t('landing.featuresList.smartSearch.title'), + description: t('landing.featuresList.smartSearch.description'), }, { icon: '👥', - title: '团队协作', - description: '命名空间管理,角色权限控制(Owner/Admin/Member),发布策略配置。', + title: t('landing.featuresList.teamwork.title'), + description: t('landing.featuresList.teamwork.description'), }, { icon: '✅', - title: '审核治理', - description: '团队内审核,平台级审批,全流程审计日志,满足合规要求。', + title: t('landing.featuresList.governance.title'), + description: t('landing.featuresList.governance.description'), }, { icon: '⚡', - title: 'CLI 优先', - description: '原生 REST API,兼容现有 ClawHub CLI 工具,无需客户端改动。', + title: t('landing.featuresList.cliFirst.title'), + description: t('landing.featuresList.cliFirst.description'), }, ] @@ -157,7 +159,7 @@ export function LandingPage() {
- 企业级技能注册中心 + {t('landing.badge')}
@@ -167,11 +169,11 @@ export function LandingPage() {

- 发布、发现、管理 - Agent 技能包 + {t('landing.tagline')} + {t('landing.taglineHighlight')}

- 自托管的私有技能注册平台,为团队提供安全、高效的技能共享与协作空间 + {t('landing.description')}

@@ -190,7 +192,7 @@ export function LandingPage() { className="bg-gradient-to-r from-cyan-500 to-blue-500 hover:from-cyan-400 hover:to-blue-400 text-white font-semibold px-8 py-6 text-lg rounded-xl shadow-lg shadow-cyan-500/25 hover:shadow-cyan-500/40 transition-all duration-300 hover:scale-105" onClick={() => navigate({ to: '/search', search: { q: '', sort: 'relevance', page: 0 } })} > - 探索技能 + {t('landing.hero.exploreSkills')}
@@ -209,9 +211,9 @@ export function LandingPage() { {value}
- {key === 'skills' && '技能包'} - {key === 'downloads' && '下载量'} - {key === 'teams' && '团队'} + {key === 'skills' && t('landing.statsSkills')} + {key === 'downloads' && t('landing.statsDownloads')} + {key === 'teams' && t('landing.statsTeams')}
))} @@ -221,10 +223,10 @@ export function LandingPage() {

- 为什么选择 SkillHub + {t('landing.whyTitle')} SkillHub

- 专为企业打造的技能管理平台,提供完整的发布、发现、治理能力 + {t('landing.whyDescription')}

@@ -255,10 +257,10 @@ export function LandingPage() {

- 准备好开始了吗? + {t('landing.ctaTitle')}

- 一条命令即可启动本地开发环境,体验完整的技能管理流程 + {t('landing.ctaDescription')}

make dev-all @@ -269,7 +271,7 @@ export function LandingPage() { className="bg-gradient-to-r from-cyan-500 to-blue-500 hover:from-cyan-400 hover:to-blue-400 text-white font-semibold px-8 py-6 text-lg rounded-xl shadow-lg shadow-cyan-500/25 hover:shadow-cyan-500/40 transition-all duration-300 hover:scale-105" onClick={() => navigate({ to: '/search', search: { q: '', sort: 'relevance', page: 0 } })} > - 立即体验 + {t('landing.ctaButton')}
@@ -282,9 +284,9 @@ export function LandingPage() {
diff --git a/web/src/pages/login.tsx b/web/src/pages/login.tsx index 2d766b1b..44be9590 100644 --- a/web/src/pages/login.tsx +++ b/web/src/pages/login.tsx @@ -1,5 +1,6 @@ import { Link, useNavigate, useSearch } from '@tanstack/react-router' import { useState } from 'react' +import { useTranslation } from 'react-i18next' import { LoginButton } from '@/features/auth/login-button' import { useLocalLogin } from '@/features/auth/use-local-auth' import { Button } from '@/shared/ui/button' @@ -7,6 +8,7 @@ import { Input } from '@/shared/ui/input' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/ui/tabs' export function LoginPage() { + const { t } = useTranslation() const navigate = useNavigate() const search = useSearch({ from: '/login' }) const loginMutation = useLocalLogin() @@ -32,57 +34,57 @@ export function LoginPage() {
S
-

登录 SkillHub

+

{t('login.title')}

- 选择一个方式登录以继续 + {t('login.subtitle')}

- 账号密码 - GitHub + {t('login.tabPassword')} + {t('login.tabOAuth')}
- + setUsername(event.target.value)} - placeholder="输入用户名" + placeholder={t('login.usernamePlaceholder')} />
- + setPassword(event.target.value)} - placeholder="输入密码" + placeholder={t('login.passwordPlaceholder')} />
{loginMutation.error ? (

{loginMutation.error.message}

) : null}

- 还没有账号? + {t('login.noAccount')} {' '} - 立即注册 + {t('login.register')}

@@ -90,7 +92,7 @@ export function LoginPage() {

- 使用 GitHub 登录时,认证完成后会自动返回当前站点。 + {t('login.oauthHint')}

@@ -98,10 +100,10 @@ export function LoginPage() {

- 登录即表示你同意我们的 - 服务条款 - 和 - 隐私政策 + {t('login.agreementPrefix')} + {t('login.terms')} + {t('login.and')} + {t('login.privacy')}

diff --git a/web/src/pages/namespace.tsx b/web/src/pages/namespace.tsx index e0929bf4..207234ba 100644 --- a/web/src/pages/namespace.tsx +++ b/web/src/pages/namespace.tsx @@ -1,4 +1,5 @@ import { useNavigate, useParams } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next' import { NamespaceHeader } from '@/features/namespace/namespace-header' import { SkillCard } from '@/features/skill/skill-card' import { SkeletonList } from '@/shared/components/skeleton-loader' @@ -6,6 +7,7 @@ import { EmptyState } from '@/shared/components/empty-state' import { useNamespaceDetail, useSearchSkills } from '@/shared/hooks/use-skill-queries' export function NamespacePage() { + const { t } = useTranslation() const navigate = useNavigate() const { namespace } = useParams({ from: '/space/$namespace' }) @@ -29,7 +31,7 @@ export function NamespacePage() { } if (!namespaceData) { - return + return } return ( @@ -37,7 +39,7 @@ export function NamespacePage() {
-

技能列表

+

{t('namespace.skillList')}

{isLoadingSkills ? ( ) : skillsData && skillsData.items.length > 0 ? ( @@ -53,8 +55,8 @@ export function NamespacePage() {
) : ( )} diff --git a/web/src/pages/register.tsx b/web/src/pages/register.tsx index e56645e8..0075fe8b 100644 --- a/web/src/pages/register.tsx +++ b/web/src/pages/register.tsx @@ -1,5 +1,6 @@ import { Link, useNavigate, useSearch } from '@tanstack/react-router' import { useState } from 'react' +import { useTranslation } from 'react-i18next' import { LoginButton } from '@/features/auth/login-button' import { useLocalRegister } from '@/features/auth/use-local-auth' import { Button } from '@/shared/ui/button' @@ -8,6 +9,7 @@ import { Input } from '@/shared/ui/input' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/ui/tabs' export function RegisterPage() { + const { t } = useTranslation() const navigate = useNavigate() const search = useSearch({ from: '/register' }) const registerMutation = useLocalRegister() @@ -31,65 +33,65 @@ export function RegisterPage() {
- 创建账号 - 支持本地注册,也可以直接使用 OAuth 登录进入平台。 + {t('register.title')} + {t('register.subtitle')} - 本地账号 - OAuth + {t('register.tabLocal')} + {t('register.tabOAuth')}
- + setUsername(event.target.value)} - placeholder="3-64 位字母、数字或下划线" + placeholder={t('register.usernamePlaceholder')} />
- + setEmail(event.target.value)} - placeholder="可选,用于后续账号识别" + placeholder={t('register.emailPlaceholder')} />
- + setPassword(event.target.value)} - placeholder="至少 8 位,包含 3 种字符类型" + placeholder={t('register.passwordPlaceholder')} />
{registerMutation.error ? (

{registerMutation.error.message}

) : null}

- 已有账号? + {t('register.hasAccount')} {' '} - 返回登录 + {t('register.login')}

@@ -97,7 +99,7 @@ export function RegisterPage() {

- 直接使用现有 OAuth 账户进入平台,无需再创建本地密码。 + {t('register.oauthHint')}

diff --git a/web/src/pages/search.tsx b/web/src/pages/search.tsx index db8b29ab..f2fda1ed 100644 --- a/web/src/pages/search.tsx +++ b/web/src/pages/search.tsx @@ -1,4 +1,5 @@ import { useNavigate, useSearch } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next' import { SearchBar } from '@/features/search/search-bar' import { SkillCard } from '@/features/skill/skill-card' import { SkeletonList } from '@/shared/components/skeleton-loader' @@ -8,6 +9,7 @@ import { useSearchSkills } from '@/shared/hooks/use-skill-queries' import { Button } from '@/shared/ui/button' export function SearchPage() { + const { t } = useTranslation() const navigate = useNavigate() const searchParams = useSearch({ from: '/search' }) @@ -50,35 +52,35 @@ export function SearchPage() { {/* Sort Selector */}
- 排序: + {t('search.sort.label')}
{data && data.total > 0 && (
- 找到 {data.total} 个结果 + {t('search.results', { count: data.total })}
)}
@@ -108,8 +110,8 @@ export function SearchPage() { ) : ( )}
diff --git a/web/src/pages/settings/accounts.tsx b/web/src/pages/settings/accounts.tsx index 41f711ce..222149f9 100644 --- a/web/src/pages/settings/accounts.tsx +++ b/web/src/pages/settings/accounts.tsx @@ -1,10 +1,12 @@ import { useState } from 'react' +import { useTranslation } from 'react-i18next' import { useConfirmAccountMerge, useInitiateAccountMerge, useVerifyAccountMerge } from '@/features/auth/use-account-merge' import { Button } from '@/shared/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/ui/card' import { Input } from '@/shared/ui/input' export function AccountSettingsPage() { + const { t } = useTranslation() const [secondaryIdentifier, setSecondaryIdentifier] = useState('') const [mergeRequestId, setMergeRequestId] = useState('') const [verificationToken, setVerificationToken] = useState('') @@ -21,9 +23,9 @@ export function AccountSettingsPage() { const result = await initiateMutation.mutateAsync({ secondaryIdentifier }) setMergeRequestId(String(result.mergeRequestId)) setVerificationToken(result.verificationToken) - setStatusMessage(`已创建合并请求,secondary=${result.secondaryUserId}`) + setStatusMessage(t('accounts.initiateSuccess', { secondaryUserId: result.secondaryUserId })) } catch (error) { - setStatusMessage(error instanceof Error ? error.message : '发起合并失败') + setStatusMessage(error instanceof Error ? error.message : t('accounts.initiateError')) } } @@ -35,9 +37,9 @@ export function AccountSettingsPage() { mergeRequestId: Number(mergeRequestId), verificationToken, }) - setStatusMessage('验证成功,确认后将执行正式合并') + setStatusMessage(t('accounts.verifySuccess')) } catch (error) { - setStatusMessage(error instanceof Error ? error.message : '验证合并失败') + setStatusMessage(error instanceof Error ? error.message : t('accounts.verifyError')) } } @@ -45,9 +47,9 @@ export function AccountSettingsPage() { setStatusMessage('') try { await confirmMutation.mutateAsync({ mergeRequestId: Number(mergeRequestId) }) - setStatusMessage('账号合并已完成') + setStatusMessage(t('accounts.confirmSuccess')) } catch (error) { - setStatusMessage(error instanceof Error ? error.message : '确认合并失败') + setStatusMessage(error instanceof Error ? error.message : t('accounts.confirmError')) } } @@ -55,22 +57,22 @@ export function AccountSettingsPage() {
- 发起账号合并 - 输入 secondary 账号标识。支持本地用户名,或 `provider:subject` 格式的外部身份。 + {t('accounts.initiateTitle')} + {t('accounts.initiateDesc')}
- + setSecondaryIdentifier(event.target.value)} - placeholder="例如:other_user 或 github:123456" + placeholder={t('accounts.secondaryPlaceholder')} />
@@ -78,13 +80,13 @@ export function AccountSettingsPage() { - 验证并完成合并 - 先完成 token 验证,再单独确认执行数据迁移。 + {t('accounts.verifyTitle')} + {t('accounts.verifyDesc')}
- +
- +
{statusMessage ?

{statusMessage}

: null} diff --git a/web/src/pages/settings/security.tsx b/web/src/pages/settings/security.tsx index b7267280..714e8ce3 100644 --- a/web/src/pages/settings/security.tsx +++ b/web/src/pages/settings/security.tsx @@ -1,10 +1,12 @@ import { useState } from 'react' +import { useTranslation } from 'react-i18next' import { authApi } from '@/api/client' import { Button } from '@/shared/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/ui/card' import { Input } from '@/shared/ui/input' export function SecuritySettingsPage() { + const { t } = useTranslation() const [currentPassword, setCurrentPassword] = useState('') const [newPassword, setNewPassword] = useState('') const [statusMessage, setStatusMessage] = useState('') @@ -18,11 +20,11 @@ export function SecuritySettingsPage() { setIsSubmitting(true) try { await authApi.changePassword({ currentPassword, newPassword }) - setStatusMessage('密码修改成功') + setStatusMessage(t('security.success')) setCurrentPassword('') setNewPassword('') } catch (error) { - setErrorMessage(error instanceof Error ? error.message : '修改密码失败') + setErrorMessage(error instanceof Error ? error.message : t('security.defaultError')) } finally { setIsSubmitting(false) } @@ -32,13 +34,13 @@ export function SecuritySettingsPage() {
- 安全设置 - 已启用本地账号密码登录时,可以在这里更新密码。 + {t('security.title')} + {t('security.subtitle')}
- +
- + {statusMessage}

: null} {errorMessage ?

{errorMessage}

: null} diff --git a/web/src/pages/skill-detail.tsx b/web/src/pages/skill-detail.tsx index 714f1e25..ec2a7523 100644 --- a/web/src/pages/skill-detail.tsx +++ b/web/src/pages/skill-detail.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next' import { useParams, useNavigate, useRouterState } from '@tanstack/react-router' import { useMutation, useQueryClient } from '@tanstack/react-query' import { MarkdownRenderer } from '@/features/skill/markdown-renderer' @@ -19,6 +20,7 @@ import { } from '@/shared/hooks/use-skill-queries' export function SkillDetailPage() { + const { t, i18n } = useTranslation() const navigate = useNavigate() const location = useRouterState({ select: (s) => s.location }) const queryClient = useQueryClient() @@ -84,8 +86,8 @@ export function SkillDetailPage() { if (!skill) { return (
-

技能不存在

-

该技能可能已被删除或从未存在

+

{t('skillDetail.notFound')}

+

{t('skillDetail.notFoundDesc')}

) } @@ -106,9 +108,9 @@ export function SkillDetailPage() { - README - 文件 - 版本 + {t('skillDetail.tabReadme')} + {t('skillDetail.tabFiles')} + {t('skillDetail.tabVersions')} @@ -118,7 +120,7 @@ export function SkillDetailPage() { ) : ( - 暂无 README + {t('skillDetail.noReadme')} )} @@ -128,7 +130,7 @@ export function SkillDetailPage() { ) : ( - 暂无文件 + {t('skillDetail.noFiles')} )} @@ -146,14 +148,14 @@ export function SkillDetailPage() { - {new Date(version.publishedAt).toLocaleDateString('zh-CN')} + {new Date(version.publishedAt).toLocaleDateString(i18n.language)}
{version.changelog && (

{version.changelog}

)}
- {version.fileCount} 个文件 + {t('skillDetail.fileCount', { count: version.fileCount })} {(version.totalSize / 1024).toFixed(1)} KB
@@ -161,7 +163,7 @@ export function SkillDetailPage() { ))}
) : ( -
暂无版本
+
{t('skillDetail.noVersions')}
)}
@@ -172,7 +174,7 @@ export function SkillDetailPage() {
-
版本
+
{t('skillDetail.version')}
{skill.latestVersion ? `v${skill.latestVersion}` : '—'}
@@ -181,23 +183,23 @@ export function SkillDetailPage() {
-
下载量
+
{t('skillDetail.downloads')}
{skill.downloadCount.toLocaleString()}
-
评分
+
{t('skillDetail.rating')}
- {skill.ratingCount > 0 && skill.ratingAvg !== undefined ? `${skill.ratingAvg.toFixed(1)} / 5` : '暂无'} + {skill.ratingCount > 0 && skill.ratingAvg !== undefined ? `${skill.ratingAvg.toFixed(1)} / 5` : t('skillDetail.ratingNone')}
-
命名空间
+
{t('skillDetail.namespaceLabel')}
@@ -207,14 +209,14 @@ export function SkillDetailPage() { {!user && ( -

登录后可以收藏和评分

+

{t('skillDetail.loginToRate')}

)}
{skill.latestVersion && ( -
安装
+
{t('skillDetail.install')}
- 下载 + {t('skillDetail.download')} {governanceVisible && ( -
治理操作
+
{t('skillDetail.governance')}
{!skill.hidden ? ( ) : ( )} {latestVersion && ( )}
diff --git a/web/src/shared/components/copy-button.tsx b/web/src/shared/components/copy-button.tsx index 6ec6dde1..3744f978 100644 --- a/web/src/shared/components/copy-button.tsx +++ b/web/src/shared/components/copy-button.tsx @@ -1,4 +1,5 @@ import { useState } from 'react' +import { useTranslation } from 'react-i18next' import { Button } from '@/shared/ui/button' interface CopyButtonProps { @@ -7,6 +8,7 @@ interface CopyButtonProps { } export function CopyButton({ text, className }: CopyButtonProps) { + const { t } = useTranslation() const [copied, setCopied] = useState(false) const handleCopy = async () => { @@ -26,7 +28,7 @@ export function CopyButton({ text, className }: CopyButtonProps) { onClick={handleCopy} className={className} > - {copied ? '已复制' : '复制'} + {copied ? t('copyButton.copied') : t('copyButton.copy')} ) } diff --git a/web/src/shared/components/pagination.tsx b/web/src/shared/components/pagination.tsx index 23e52fad..5f54d904 100644 --- a/web/src/shared/components/pagination.tsx +++ b/web/src/shared/components/pagination.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next' import { Button } from '@/shared/ui/button' interface PaginationProps { @@ -7,6 +8,7 @@ interface PaginationProps { } export function Pagination({ page, totalPages, onPageChange }: PaginationProps) { + const { t } = useTranslation() return (
- + {t('pagination.pagePrefix')} {page + 1} / {totalPages} - + {t('pagination.pageSuffix') && {t('pagination.pageSuffix')}}
) From c2b75bf96121d8fc83f762c451f07aedb94f0ff2 Mon Sep 17 00:00:00 2001 From: vsxd Date: Fri, 13 Mar 2026 14:44:28 +0800 Subject: [PATCH 3/4] Refine skill install command layout --- web/src/features/skill/install-command.tsx | 37 +++++++++++++++++++--- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/web/src/features/skill/install-command.tsx b/web/src/features/skill/install-command.tsx index 2c2157d4..12a18a32 100644 --- a/web/src/features/skill/install-command.tsx +++ b/web/src/features/skill/install-command.tsx @@ -1,4 +1,7 @@ -import { CopyButton } from '@/shared/components/copy-button' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Check, Copy } from 'lucide-react' +import { Button } from '@/shared/ui/button' interface InstallCommandProps { namespace: string @@ -7,14 +10,40 @@ interface InstallCommandProps { } export function InstallCommand({ namespace, slug, version }: InstallCommandProps) { + const { t } = useTranslation() + const [copied, setCopied] = useState(false) const command = version ? `skillhub install ${namespace}/${slug}@${version}` : `skillhub install ${namespace}/${slug}` + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(command) + setCopied(true) + window.setTimeout(() => setCopied(false), 2000) + } catch (err) { + console.error('Failed to copy:', err) + } + } + return ( -
- {command} - +
+ +
+        
+          {command}
+        
+      
) } From de5956476b26b65256f74b6379deae6c9fc7b752 Mon Sep 17 00:00:00 2001 From: vsxd Date: Fri, 13 Mar 2026 14:48:01 +0800 Subject: [PATCH 4/4] Sync header auth state after login --- web/src/features/auth/use-local-auth.ts | 14 ++++++++++++-- web/src/shared/components/user-menu.tsx | 7 +++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/web/src/features/auth/use-local-auth.ts b/web/src/features/auth/use-local-auth.ts index c17c5bd6..f92e7906 100644 --- a/web/src/features/auth/use-local-auth.ts +++ b/web/src/features/auth/use-local-auth.ts @@ -1,15 +1,25 @@ -import { useMutation } from '@tanstack/react-query' +import { useMutation, useQueryClient } from '@tanstack/react-query' import { authApi } from '@/api/client' -import type { LocalLoginRequest, LocalRegisterRequest } from '@/api/types' +import type { LocalLoginRequest, LocalRegisterRequest, User } from '@/api/types' export function useLocalLogin() { + const queryClient = useQueryClient() + return useMutation({ mutationFn: (request: LocalLoginRequest) => authApi.localLogin(request), + onSuccess: (user) => { + queryClient.setQueryData(['auth', 'me'], user) + }, }) } export function useLocalRegister() { + const queryClient = useQueryClient() + return useMutation({ mutationFn: (request: LocalRegisterRequest) => authApi.localRegister(request), + onSuccess: (user) => { + queryClient.setQueryData(['auth', 'me'], user) + }, }) } diff --git a/web/src/shared/components/user-menu.tsx b/web/src/shared/components/user-menu.tsx index 05fbbb4c..9ad5f534 100644 --- a/web/src/shared/components/user-menu.tsx +++ b/web/src/shared/components/user-menu.tsx @@ -1,5 +1,7 @@ import { useTranslation } from 'react-i18next' import { Link, useNavigate } from '@tanstack/react-router' +import { useQueryClient } from '@tanstack/react-query' +import { authApi } from '@/api/client' import { DropdownMenu, DropdownMenuContent, @@ -21,6 +23,7 @@ interface UserMenuProps { export function UserMenu({ user }: UserMenuProps) { const { t } = useTranslation() const navigate = useNavigate() + const queryClient = useQueryClient() const hasRole = (role: string) => user.platformRoles?.includes(role) ?? false const isReviewer = hasRole('SKILL_ADMIN') || hasRole('NAMESPACE_ADMIN') || hasRole('SUPER_ADMIN') @@ -30,9 +33,9 @@ export function UserMenu({ user }: UserMenuProps) { const handleLogout = async () => { try { - await fetch('/api/auth/logout', { method: 'POST' }) + await authApi.logout() + queryClient.setQueryData(['auth', 'me'], null) navigate({ to: '/' }) - window.location.reload() } catch (error) { console.error('Logout failed:', error) }