export type Locale = "en" | "zh"; const STORAGE_KEY = "wh-locale"; type Dict = Record; const en: Dict = { "app.tagline": "CONFIG CONSOLE", "app.newRoute": "+ New Route", "app.newGroup": "+ New Group", "app.signOut": "Sign out", "app.langToggle": "中文", "login.title": "Config Console", "login.forbidden": "Access denied — this GitHub account is not an admin or group admin.", "login.prompt": "Sign in with GitHub to manage routes.", "login.button": "Sign in with GitHub", "tab.overview": "Overview", "tab.groups": "Groups", "tab.logs": "Send Logs", "tab.audit": "Audit Log", "tab.metrics": "Stats", "metrics.title": "Delivery Stats", "metrics.successRate": "Success rate", "overview.routes": "Routes", "overview.recent": "Recent deliveries", "overview.viewAll": "View all", "overview.refreshed": "Refreshed", "metrics.refresh": "Refresh", "metrics.total": "Total", "metrics.ok": "OK", "metrics.failed": "Failed", "metrics.failureRate": "Failure rate", "metrics.avgDuration": "Avg duration", "metrics.avgAttempts": "Avg attempts", "metrics.byPlatform": "By platform", "metrics.byEvent": "By event", "metrics.byStatus": "By status", "metrics.recentFailures": "Recent failures", "metrics.empty": "No delivery data yet", "metrics.filterGroup": "Group", "metrics.allGroups": "All groups", "metrics.sortTotal": "By total", "metrics.sortFailed": "By failures", "metrics.sortRate": "By failure rate", "metrics.retryRate": "Retry rate", "event.push": "push", "event.pull_request": "pull_request", "event.pull_request_review": "pull_request_review", "event.pull_request_review_comment": "pull_request_review_comment", "event.issues": "issues", "event.issue_comment": "issue_comment", "event.workflow_run": "workflow_run", "event.workflow_job": "workflow_job", "event.status": "status", "event.deployment": "deployment", "event.deployment_status": "deployment_status", "event.ping": "ping", "event.release": "release", "event.create": "create", "event.delete": "delete", "event.star": "star", "event.fork": "fork", "event.check_run": "check_run", "event.check_suite": "check_suite", "event.commit_comment": "commit_comment", "event.member": "member", "event.label": "label", "event.milestone": "milestone", "event.discussion": "discussion", "event.discussion_comment": "discussion_comment", "event.repository": "repository", "event.code_scanning_alert": "code_scanning_alert", "event.dependabot_alert": "dependabot_alert", "event.custom": "custom", "status.loading": "loading", "status.connected": "connected", "kpi.groups": "GROUPS", "kpi.routes": "ROUTES", "role.owner": "Owner", "role.admin": "Admin", "role.viewer": "Viewer", "role.badge": "You: {role}", "roles.owner": "Owner", "roles.admin": "Admin", "roles.viewer": "Viewer", "members.title": "Members", "members.note": "(role: owner = full control · admin = edit routes · viewer = read-only)", "members.empty": "No members yet — add the first owner or share an invite link.", "members.add": "Add member", "members.addPlaceholder": "GitHub login", "members.addBtn": "Add", "members.role": "Role", "members.you": "(you)", "members.remove": "Remove", "members.invites": "Invite links", "members.inviteNote": "(single-use · expires in 7 days)", "members.inviteRole": "Invite as", "members.createInvite": "Create invite link", "members.copyLink": "Copy", "members.copied": "Copied!", "members.revoke": "Revoke", "members.noInvites": "No pending invites.", "members.errSelfOwner": "You must remain an owner of this group.", "members.errOwner": "A group needs at least one owner.", "members.errLastOwner": "Cannot remove the last owner.", "members.inviteOk": "Invite accepted — welcome!", "members.inviteBad": "This invite is invalid or expired.", "install.ok": "GitHub App installation registered — the group has been created and bound to this installation.", "webhook.title": "Webhook endpoint", "webhook.note": "(per-group URL + secret)", "webhook.empty": "Not configured. Generate a secret to enable the group's own webhook endpoint.", "webhook.url": "URL", "webhook.secret": "Secret", "webhook.noSecret": "— no secret configured —", "webhook.secretHidden": "The secret is only shown once after generating/regenerating. Regenerate to reveal a new one.", "webhook.copy": "Copy", "webhook.copied": "Copied!", "webhook.generate": "Generate secret", "webhook.regenerate": "Regenerate", "webhook.disable": "Disable", "webhook.usageTitle": "How to use", "webhook.usageGitHub": "GitHub: repo/org Settings → Webhooks → Add webhook → payload URL = the URL above, Content type = application/json, Secret = the secret above.", "webhook.usageGitea": "Gitea: repo Settings → Webhooks → Add Webhook → same URL + secret.", "webhook.usageCustom": "Custom: POST any JSON signed with X-WebHooker-Signature (sha256 HMAC). Create a route with event = custom to receive it:", "groups.empty": "No groups yet. Groups scope routes by org/user and delegate access.", "groups.createFirst": "Create your first group", "groups.admins": "ADMINS", "groups.owners": "OWNERS", "groups.members": "MEMBERS", "groups.any": "any", "groups.open": "Open →", "group.back": "← Groups", "group.routesIn": "Routes in {name}", "routes.emptyGroup": "No routes in this group yet.", "routes.createFirst": "Create your first route", "route.untitled": "(untitled)", "route.noFilters": "no filters", "route.channel": "CHANNEL", "route.thread": "THREAD", "route.chat": "CHAT", "route.topic": "TOPIC", "route.feishu": "Feishu", "route.fallback": "fallback", "route.stop": "stop", "route.moveUp": "Move up", "route.moveDown": "Move down", "filter.event": "Event", "filter.repo": "Repo", "filter.actor": "Actor", "filter.action": "Action", "filter.branch": "Branch", "filter.keyword": "Keyword", "filter.field": "Field", "filterOp.eq": "equals", "filterOp.ne": "not equals", "filterOp.contains": "contains", "filterOp.startsWith": "starts with", "filterOp.endsWith": "ends with", "filterOp.regex": "matches regex", "filterOp.gt": "greater than", "filterOp.gte": "greater or equal", "filterOp.lt": "less than", "filterOp.lte": "less or equal", "filterOp.in": "in", "filterOp.exists": "exists", "filterNode.all": "All of", "filterNode.any": "Any of", "filterNode.not": "not", "filterNode.is": "is", "filterNode.matches": "matches", "filterNode.and": "and", "filterNode.or": "or", "filterNode.unwrap": "unwrap", "filterNode.empty": "No conditions", "routeEditor.editTitle": "Edit route", "routeEditor.newTitle": "New route", "routeEditor.eyebrow": "Route", "routeEditor.close": "Close", "routeEditor.sectionBasic": "Basic", "routeEditor.sectionOptions": "Options", "routeEditor.sectionFilters": "Filters", "routeEditor.sectionTargets": "Targets", "routeEditor.templates": "Templates", "routeEditor.templatesNote": "(click to prefill filters)", "templates.push": "Push", "templates.pullRequest": "Pull Request", "templates.issues": "Issues", "templates.release": "Release", "templates.workflow": "Workflow", "templates.checks": "Check Suite", "templates.deployment": "Deployment", "templates.comments": "Comments", "templates.reviews": "Review", "templates.starFork": "Star / Fork", "templates.createDelete": "Create / Delete", "templates.member": "Member", "templates.commitComment": "Commit Comment", "templates.customWebhook": "Custom Webhook", "routeEditor.name": "Name", "routeEditor.namePlaceholder": "My Route", "routeEditor.id": "ID", "routeEditor.idHint": "Unique, use a-z / 0-9 / dashes", "routeEditor.enabled": "Route enabled", "routeEditor.fallback": "Fallback route", "routeEditor.fallbackHint": "Only fires when no other route matched; filters still apply", "routeEditor.stop": "Stop on match", "routeEditor.stopHint": "When matched, no further routes are evaluated for this event", "routeEditor.filters": "Filters", "routeEditor.filtersNote": "(all must match · AND)", "routeEditor.matchPlaceholder": "match value (* ? wildcards, /regex/)", "routeEditor.not": "NOT", "routeEditor.addFilter": "+ Add filter", "routeEditor.discordRoles": "Discord role mentions", "routeEditor.discordRolesNote": "(optional · comma-separated)", "routeEditor.discordRolesPlaceholder": "123456789012345678, 987654321098765432", "routeEditor.discordRolesHint": "When this route fires, the listed roles are pinged in Discord targets.", "routeEditor.channel": "Channel ID", "routeEditor.channelPlaceholder": "Discord channel ID", "routeEditor.thread": "Thread ID", "routeEditor.threadNote": "(optional)", "routeEditor.threadPlaceholder": "Optional thread ID", "routeEditor.platform": "Platform", "routeEditor.platformFeishu": "Feishu", "routeEditor.chat": "Chat ID", "routeEditor.chatPlaceholder": "Telegram group chat ID", "routeEditor.topic": "Topic ID", "routeEditor.topicNote": "(optional)", "routeEditor.topicPlaceholder": "Optional topic (message_thread_id)", "routeEditor.feishuChatPlaceholder": "Feishu chat ID (chat_id)", "routeEditor.errChat": "Target {n} chat ID is required", "routeEditor.targets": "Targets", "routeEditor.targetsNote": "(one or more destinations)", "routeEditor.addTarget": "+ Add target", "routeEditor.errTargets": "Add at least one target", "routeEditor.cancel": "Cancel", "routeEditor.save": "Save route", "routeEditor.errFilterMatch": "Filter {n} needs a match value", "routeEditor.errAddFilter": "Add at least one filter", "routeEditor.errIdFormat": "ID must be a-z / 0-9 / dashes", "routeEditor.errName": "Name is required", "routeEditor.errChannel": "Target {n} channel ID is required", "routeEditor.addLeaf": "Add condition", "routeEditor.addGroup": "Add group", "routeEditor.addNot": "Add not", "routeEditor.remove": "Remove", "routeEditor.op": "Operator", "routeEditor.pathPlaceholder": "payload.path", "routeEditor.valuesPlaceholder": "Type a value and press Enter", "routeEditor.testMatch": "Test match", "routeEditor.testEvent": "Event (optional)", "routeEditor.testPayloadPlaceholder": "Paste a JSON webhook payload", "routeEditor.testRun": "Run test", "routeEditor.testMatched": "Matches", "routeEditor.testNotMatched": "Does not match", "routeEditor.testInvalidJson": "Invalid JSON", "routeEditor.fragments": "Fragments", "routeEditor.fragmentsPresets": "Presets", "routeEditor.fragmentsEmpty": "No fragments yet", "routeEditor.fragmentsInsert": "Insert", "routeEditor.fragmentsDelete": "Delete", "routeEditor.fragmentsSave": "Save as fragment", "routeEditor.fragmentsNamePlaceholder": "Fragment name", "routeEditor.fragmentsNameRequired": "Fragment name is required", "routeEditor.errPath": "Field filters need a path", "routeEditor.errValues": "Add at least one value", "routeEditor.errGroupEmpty": "Group needs at least one condition", "fragmentPreset.prBot": "PR by a bot", "fragmentPreset.dependabot": "Dependabot PR", "fragmentPreset.releasePublished": "Release published", "fragmentPreset.pushMain": "Push to main", "fragmentPreset.issueBug": "Issue labeled bug", "groupEditor.editTitle": "Edit group", "groupEditor.newTitle": "New group", "groupEditor.eyebrow": "Group", "groupEditor.close": "Close", "groupEditor.sectionBasic": "Basic", "groupEditor.sectionPreferences": "Preferences", "groupEditor.sectionAccess": "Access & members", "groupEditor.sectionProviders": "Providers", "groupEditor.sectionLog": "Logging", "groupEditor.name": "Name", "groupEditor.namePlaceholder": "My Team", "groupEditor.id": "ID", "groupEditor.idPlaceholder": "my-team", "groupEditor.idHint": "Unique, use a-z / 0-9 / dashes", "groupEditor.renameHint": "Changing the id also updates routes, webhook secrets and invites that reference this group.", "groupEditor.language": "Message language", "groupEditor.langPlaceholder": "en", "groupEditor.langHint": "en or zh; custom via KV i18n:", "groupEditor.emojiNote": "(default on)", "groupEditor.emojiLabel": "Show emojis in messages", "groupEditor.admins": "Group admins", "groupEditor.adminsNote": "(GitHub login or user id, comma-separated)", "groupEditor.adminsPlaceholder": "octocat, 12345", "groupEditor.adminsHint": "These users can view logs and edit this group's routes.", "groupEditor.owners": "Owner scope", "groupEditor.ownersNote": "(GitHub org / user logins, comma-separated)", "groupEditor.ownersSuperOnly": "(only super admins may change)", "groupEditor.ownersPlaceholder": "my-org, some-user", "groupEditor.ownersHint": "Only webhook events from these orgs/users enter this group's routes. Leave empty for no restriction.", "groupEditor.membersNote": "Members", "groupEditor.membersHint": "(owner / admin / viewer)", "groupEditor.membersGoPanel": "Manage members and invite links from the group page.", "groupEditor.providers": "Source platforms", "groupEditor.providersNote": "(leave both unchecked = all)", "groupEditor.providersHint": "Only webhook events from the checked forges (GitHub / Gitea) enter this group's routes.", "groupEditor.installationId": "GitHub App installation ID", "groupEditor.installationIdNote": "(optional)", "groupEditor.installationIdPlaceholder": "e.g. 12345678", "groupEditor.installationIdHint": "Bind this group to one GitHub App installation (the org/user that installed the app). Only events from that installation enter this group's routes; leave empty to allow any installation (still filtered by Owner scope).", "groupEditor.errInstallationId": "Installation ID must be a positive integer", "groupEditor.emoji": "Show emojis in messages", "groupEditor.forgeSources": "Forge hosts", "groupEditor.forgeSourcesNote": "(optional — label events by their host)", "groupEditor.forgeSourcesHost": "git.example.com", "groupEditor.forgeSourcesName": "Optional display name", "groupEditor.forgeSourcesAdd": "+ Add host", "groupEditor.forgeSourcesHint": "Events whose repository host matches an entry are labeled in the message footer with its display name (or the host when none is set) — e.g. git1.example.com and git2.example.com for two Gitea instances. GitHub events match github.com.", "groupEditor.errForgeSourceHost": "Every forge host must be a valid hostname", "groupEditor.logTarget": "Webhook log channel", "groupEditor.logTargetNote": "(optional)", "groupEditor.logDisabled": "— Disabled —", "groupEditor.logFeishu": "Feishu", "groupEditor.logTargetHint": "A Discord channel/thread, Telegram chat/topic or Feishu chat that receives a summary of every webhook this group's routes dispatch.", "groupEditor.errLogChannel": "Log channel ID is required when Discord is selected", "groupEditor.errLogChat": "Log chat ID is required when Telegram is selected", "groupEditor.errLogFeishuChat": "Log chat ID is required when Feishu is selected", "groupEditor.cancel": "Cancel", "groupEditor.save": "Save group", "groupEditor.errIdFormat": "ID must be a-z / 0-9 / dashes", "groupEditor.errName": "Name is required", "logs.lastSends": "LAST {n} SENDS", "logs.refresh": "⟳ Refresh", "logs.empty": "No send records yet — trigger a webhook to see results.", "logs.repo": "REPO", "logs.target": "TARGET", "logs.detailTitle": "Send Detail", "logs.detailMissing": "Could not load this entry.", "logs.id": "ID", "logs.time": "Time", "logs.route": "Route", "logs.event": "Event", "logs.action": "Action", "logs.actor": "Actor", "logs.deliveryId": "Delivery ID", "logs.platform": "Platform", "logs.status": "HTTP Status", "logs.messageId": "Message ID", "logs.errorCode": "Error Code", "logs.duration": "Duration", "logs.attempts": "Attempts", "logs.ok": "Result", "logs.yes": "OK", "logs.no": "Failed", "logs.error": "Error", "logs.detail": "Extra Info", "logs.filterGroup": "Group", "logs.allGroups": "All groups", "audit.title": "AUDIT LOG", "audit.empty": "No audit entries yet.", "audit.time": "Time", "audit.actor": "Actor", "audit.action": "Action", "audit.target": "Target", "audit.group": "Group", "audit.detail": "Detail", "audit.filterGroup": "Group", "audit.allGroups": "All groups", "audit.refresh": "⟳ Refresh", "toast.routesSaved": "Routes saved", "toast.routeDeleted": "Route deleted", "toast.groupSaved": "Groups saved", "toast.groupDeleted": "Group deleted", "toast.routeIdExists": "Route ID already exists", "toast.groupIdExists": "Group ID already exists", "toast.saveFailed": "Save failed: {msg}", "toast.deleteFailed": "Delete failed: {msg}", "confirm.deleteRoute": 'Delete route "{name}"?', "confirm.deleteGroup": 'Delete group "{name}"?', "confirm.deleteGroupWarn": " {n} route(s) reference it and will lose their group.", }; const zh: Dict = { "app.tagline": "配置控制台", "app.newRoute": "+ 新建路由", "app.newGroup": "+ 新建分组", "app.signOut": "退出登录", "app.langToggle": "EN", "login.title": "配置控制台", "login.forbidden": "访问被拒绝 —— 该 GitHub 账号不是超级管理员或分组管理员。", "login.prompt": "使用 GitHub 登录以管理路由。", "login.button": "使用 GitHub 登录", "tab.overview": "概览", "tab.groups": "分组", "tab.logs": "发送日志", "tab.audit": "审计日志", "tab.metrics": "统计", "metrics.title": "投递统计", "metrics.successRate": "成功率", "overview.routes": "路由", "overview.recent": "最近投递", "overview.viewAll": "查看全部", "overview.refreshed": "已刷新", "metrics.refresh": "刷新", "metrics.total": "总计", "metrics.ok": "成功", "metrics.failed": "失败", "metrics.failureRate": "失败率", "metrics.avgDuration": "平均耗时", "metrics.avgAttempts": "平均尝试次数", "metrics.byPlatform": "按平台", "metrics.byEvent": "按事件", "metrics.byStatus": "按状态", "metrics.recentFailures": "最近失败", "metrics.empty": "暂无投递数据", "metrics.filterGroup": "分组", "metrics.allGroups": "全部分组", "metrics.sortTotal": "按总量", "metrics.sortFailed": "按失败", "metrics.sortRate": "按失败率", "metrics.retryRate": "重试率", "event.push": "推送", "event.pull_request": "拉取请求", "event.pull_request_review": "PR 审查", "event.pull_request_review_comment": "PR 审查评论", "event.issues": "议题", "event.issue_comment": "议题评论", "event.workflow_run": "工作流运行", "event.workflow_job": "工作流任务", "event.status": "提交状态", "event.deployment": "部署", "event.deployment_status": "部署状态", "event.ping": "心跳", "event.release": "发布", "event.create": "创建", "event.delete": "删除", "event.star": "星标", "event.fork": "复刻", "event.check_run": "检查运行", "event.check_suite": "检查套件", "event.commit_comment": "提交评论", "event.member": "成员", "event.label": "标签", "event.milestone": "里程碑", "event.discussion": "讨论", "event.discussion_comment": "讨论评论", "event.repository": "仓库", "event.code_scanning_alert": "代码扫描", "event.dependabot_alert": "依赖告警", "event.custom": "自定义", "status.loading": "加载中", "status.connected": "已连接", "kpi.groups": "分组", "kpi.routes": "路由", "role.owner": "拥有者", "role.admin": "管理员", "role.viewer": "只读", "role.badge": "我的角色:{role}", "roles.owner": "拥有者", "roles.admin": "管理员", "roles.viewer": "只读", "members.title": "成员", "members.note": "(角色:owner = 完全控制 · admin = 编辑路由 · viewer = 只读)", "members.empty": "还没有成员 —— 添加第一位 owner 或分享邀请链接。", "members.add": "添加成员", "members.addPlaceholder": "GitHub 登录名", "members.addBtn": "添加", "members.role": "角色", "members.you": "(我)", "members.remove": "移除", "members.invites": "邀请链接", "members.inviteNote": "(一次性 · 7 天内有效)", "members.inviteRole": "邀请为", "members.createInvite": "生成邀请链接", "members.copyLink": "复制", "members.copied": "已复制!", "members.revoke": "撤销", "members.noInvites": "暂无待接受的邀请。", "members.errSelfOwner": "你必须保留本组的 owner 身份。", "members.errOwner": "分组至少需要一位 owner。", "members.errLastOwner": "不能移除最后一位 owner。", "members.inviteOk": "邀请已接受 —— 欢迎!", "members.inviteBad": "该邀请无效或已过期。", "install.ok": "GitHub App 安装已注册 —— 分组已创建并绑定到该安装。", "webhook.title": "Webhook 入口", "webhook.note": "(分组独立的 URL + secret)", "webhook.empty": "未配置。生成 secret 即可启用该分组的独立 webhook 入口。", "webhook.url": "URL", "webhook.secret": "Secret", "webhook.noSecret": "—— 未配置 secret ——", "webhook.secretHidden": "secret 仅在生成/重新生成时显示一次。如需查看请重新生成。", "webhook.copy": "复制", "webhook.copied": "已复制!", "webhook.generate": "生成 secret", "webhook.regenerate": "重新生成", "webhook.disable": "停用", "webhook.usageTitle": "使用方法", "webhook.usageGitHub": "GitHub:仓库/组织 Settings → Webhooks → Add webhook → Payload URL 填上面的 URL,Content type 选 application/json,Secret 填上面的 secret。", "webhook.usageGitea": "Gitea:仓库 Settings → Webhooks → Add Webhook,填写同样的 URL 和 secret。", "webhook.usageCustom": "自定义:用 X-WebHooker-Signature(sha256 HMAC)签名任意 JSON 后 POST。创建一条 event = custom 的路由即可接收:", "groups.admins": "管理员", "groups.owners": "来源", "groups.members": "成员", "groups.any": "任意", "groups.open": "进入 →", "group.back": "← 返回分组", "group.routesIn": "{name} 的路由", "routes.emptyGroup": "该分组还没有路由。", "routes.createFirst": "创建第一个路由", "route.untitled": "(未命名)", "route.noFilters": "无过滤器", "route.channel": "频道", "route.thread": "子区", "route.chat": "群组", "route.topic": "话题", "route.feishu": "飞书", "route.fallback": "兜底", "route.stop": "停止", "route.moveUp": "上移", "route.moveDown": "下移", "filter.event": "事件", "filter.repo": "仓库", "filter.actor": "操作者", "filter.action": "动作", "filter.branch": "分支", "filter.keyword": "关键词", "filter.field": "字段", "filterOp.eq": "等于", "filterOp.ne": "不等于", "filterOp.contains": "包含", "filterOp.startsWith": "开头是", "filterOp.endsWith": "结尾是", "filterOp.regex": "正则匹配", "filterOp.gt": "大于", "filterOp.gte": "大于等于", "filterOp.lt": "小于", "filterOp.lte": "小于等于", "filterOp.in": "属于", "filterOp.exists": "存在", "filterNode.all": "全部满足", "filterNode.any": "任一满足", "filterNode.not": "非", "filterNode.is": "是", "filterNode.matches": "匹配", "filterNode.and": "且", "filterNode.or": "或", "filterNode.unwrap": "取消非", "filterNode.empty": "暂无条件", "routeEditor.editTitle": "编辑路由", "routeEditor.newTitle": "新建路由", "routeEditor.eyebrow": "路由", "routeEditor.close": "关闭", "routeEditor.sectionBasic": "基本信息", "routeEditor.sectionOptions": "选项", "routeEditor.sectionFilters": "过滤器", "routeEditor.sectionTargets": "目标", "routeEditor.templates": "模板", "routeEditor.templatesNote": "(点击可预填过滤器)", "templates.push": "推送", "templates.pullRequest": "拉取请求", "templates.issues": "议题", "templates.release": "发布", "templates.workflow": "工作流", "templates.checks": "检查套件", "templates.deployment": "部署", "templates.comments": "评论", "templates.reviews": "评审", "templates.starFork": "星标 / 复刻", "templates.createDelete": "创建 / 删除", "templates.member": "协作者", "templates.commitComment": "提交评论", "templates.customWebhook": "自定义 Webhook", "routeEditor.name": "名称", "routeEditor.namePlaceholder": "我的路由", "routeEditor.id": "ID", "routeEditor.idHint": "唯一,使用 a-z / 0-9 / 短横线", "routeEditor.enabled": "启用路由", "routeEditor.fallback": "兜底路由", "routeEditor.fallbackHint": "仅当没有其它路由匹配该事件时才发送,其过滤器仍会生效", "routeEditor.stop": "匹配后停止", "routeEditor.stopHint": "该路由匹配后,不再评估后续路由", "routeEditor.filters": "过滤器", "routeEditor.filtersNote": "(全部匹配 · AND)", "routeEditor.matchPlaceholder": "匹配值(支持 * ? 通配符、/正则/)", "routeEditor.not": "取反", "routeEditor.addFilter": "+ 添加过滤器", "routeEditor.discordRoles": "Discord 身份组提醒", "routeEditor.discordRolesNote": "(可选 · 逗号分隔)", "routeEditor.discordRolesPlaceholder": "123456789012345678, 987654321098765432", "routeEditor.discordRolesHint": "当该路由触发时,会在 Discord 目标中提醒(@)这些身份组。", "routeEditor.channel": "频道 ID", "routeEditor.channelPlaceholder": "Discord 频道 ID", "routeEditor.thread": "子区 ID", "routeEditor.threadNote": "(可选)", "routeEditor.threadPlaceholder": "可选的子区 ID", "routeEditor.platform": "平台", "routeEditor.platformFeishu": "飞书", "routeEditor.chat": "群组 ID", "routeEditor.chatPlaceholder": "Telegram 群组聊天 ID", "routeEditor.topic": "话题 ID", "routeEditor.topicNote": "(可选)", "routeEditor.topicPlaceholder": "可选的话题 (message_thread_id)", "routeEditor.feishuChatPlaceholder": "飞书群组 ID (chat_id)", "routeEditor.errChat": "第 {n} 个目标群组 ID 为必填项", "routeEditor.targets": "目标", "routeEditor.targetsNote": "(一个或多个推送目标)", "routeEditor.addTarget": "+ 添加目标", "routeEditor.errTargets": "至少添加一个目标", "routeEditor.cancel": "取消", "routeEditor.save": "保存路由", "routeEditor.errFilterMatch": "第 {n} 个过滤器需要匹配值", "routeEditor.errAddFilter": "至少添加一个过滤器", "routeEditor.errIdFormat": "ID 只能是 a-z / 0-9 / 短横线", "routeEditor.errName": "名称为必填项", "routeEditor.errChannel": "第 {n} 个目标频道 ID 为必填项", "routeEditor.addLeaf": "添加条件", "routeEditor.addGroup": "添加分组", "routeEditor.addNot": "添加非", "routeEditor.remove": "删除", "routeEditor.op": "操作符", "routeEditor.pathPlaceholder": "payload.path", "routeEditor.valuesPlaceholder": "输入值后回车", "routeEditor.testMatch": "测试匹配", "routeEditor.testEvent": "事件类型(可选)", "routeEditor.testPayloadPlaceholder": "粘贴 JSON webhook 事件样本", "routeEditor.testRun": "运行测试", "routeEditor.testMatched": "匹配", "routeEditor.testNotMatched": "不匹配", "routeEditor.testInvalidJson": "无效的 JSON", "routeEditor.fragments": "过滤器片段", "routeEditor.fragmentsPresets": "预设", "routeEditor.fragmentsEmpty": "暂无片段", "routeEditor.fragmentsInsert": "插入", "routeEditor.fragmentsDelete": "删除", "routeEditor.fragmentsSave": "存为片段", "routeEditor.fragmentsNamePlaceholder": "片段名称", "routeEditor.fragmentsNameRequired": "请填写片段名称", "routeEditor.errPath": "字段过滤需要路径", "routeEditor.errValues": "至少需要一个值", "routeEditor.errGroupEmpty": "分组至少需要一个条件", "fragmentPreset.prBot": "机器人提交的 PR", "fragmentPreset.dependabot": "Dependabot PR", "fragmentPreset.releasePublished": "发布 Release", "fragmentPreset.pushMain": "推送到 main 分支", "fragmentPreset.issueBug": "标记 bug 的 Issue", "groupEditor.editTitle": "编辑分组", "groupEditor.newTitle": "新建分组", "groupEditor.eyebrow": "分组", "groupEditor.close": "关闭", "groupEditor.sectionBasic": "基本信息", "groupEditor.sectionPreferences": "偏好", "groupEditor.sectionAccess": "访问与成员", "groupEditor.sectionProviders": "提供方", "groupEditor.sectionLog": "日志", "groupEditor.name": "名称", "groupEditor.namePlaceholder": "我的团队", "groupEditor.id": "ID", "groupEditor.idPlaceholder": "my-team", "groupEditor.idHint": "唯一,使用 a-z / 0-9 / 短横线", "groupEditor.renameHint": "修改 id 会同步更新引用该分组的路由、webhook secret 与邀请链接。", "groupEditor.language": "消息语言", "groupEditor.langPlaceholder": "zh", "groupEditor.langHint": "en 或 zh;可通过 KV i18n: 自定义", "groupEditor.emojiNote": "(默认开启)", "groupEditor.emojiLabel": "消息中显示表情符号", "groupEditor.admins": "分组管理员", "groupEditor.adminsNote": "(GitHub 登录名或用户 ID,逗号分隔)", "groupEditor.adminsPlaceholder": "octocat, 12345", "groupEditor.adminsHint": "这些用户可以查看日志并编辑本分组的路由。", "groupEditor.owners": "来源限定", "groupEditor.ownersNote": "(GitHub 组织 / 用户登录名,逗号分隔)", "groupEditor.ownersSuperOnly": "(仅超级管理员可修改)", "groupEditor.ownersPlaceholder": "my-org, some-user", "groupEditor.ownersHint": "只有来自这些组织/用户的 webhook 事件才会进入本分组的路由。留空表示不限制。", "groupEditor.membersNote": "成员", "groupEditor.membersHint": "(owner / admin / viewer)", "groupEditor.membersGoPanel": "请在分组页面管理成员与邀请链接。", "groupEditor.providers": "来源平台", "groupEditor.providersNote": "(都不勾选 = 全部来源)", "groupEditor.providersHint": "只有来自所勾选 forge(GitHub / Gitea)的 webhook 事件才会进入本分组的路由。", "groupEditor.installationId": "GitHub App 安装 ID", "groupEditor.installationIdNote": "(可选)", "groupEditor.installationIdPlaceholder": "如 12345678", "groupEditor.installationIdHint": "将本分组绑定到一个 GitHub App 安装(安装了 App 的组织/用户)。只有来自该安装的事件才会进入本分组的路由;留空表示接受任意安装(仍受来源限定过滤)。", "groupEditor.errInstallationId": "安装 ID 必须为正整数", "groupEditor.emoji": "消息中显示表情符号", "groupEditor.forgeSources": "来源主机", "groupEditor.forgeSourcesNote": "(可选 —— 按主机标识事件)", "groupEditor.forgeSourcesHost": "git.example.com", "groupEditor.forgeSourcesName": "可选显示名称", "groupEditor.forgeSourcesAdd": "+ 添加主机", "groupEditor.forgeSourcesHint": "仓库主机与条目匹配的事件,会在消息底部用该条目的显示名称标注(未填名称则用主机名)——例如两个 Gitea 实例可分别填 git1.example.com 和 git2.example.com。GitHub 事件匹配 github.com。", "groupEditor.errForgeSourceHost": "每个来源主机必须是有效的主机名", "groupEditor.logTarget": "Webhook 日志频道", "groupEditor.logTargetNote": "(可选)", "groupEditor.logDisabled": "— 未启用 —", "groupEditor.logFeishu": "飞书", "groupEditor.logTargetHint": "一个 Discord 频道/子区、Telegram 群组/话题或飞书群组,本分组路由分发(dispatch)的每个 webhook 都会向其发送摘要。", "groupEditor.errLogChannel": "选择 Discord 时必须填写日志频道 ID", "groupEditor.errLogChat": "选择 Telegram 时必须填写日志群组 ID", "groupEditor.errLogFeishuChat": "选择飞书时必须填写日志群组 ID", "groupEditor.cancel": "取消", "groupEditor.save": "保存分组", "groupEditor.errIdFormat": "ID 只能是 a-z / 0-9 / 短横线", "groupEditor.errName": "名称为必填项", "logs.lastSends": "最近 {n} 条发送", "logs.refresh": "⟳ 刷新", "logs.empty": "还没有发送记录 —— 触发一次 webhook 即可查看结果。", "logs.repo": "仓库", "logs.target": "目标", "logs.detailTitle": "发送详情", "logs.detailMissing": "无法加载该条记录。", "logs.id": "ID", "logs.time": "时间", "logs.route": "路由", "logs.event": "事件", "logs.action": "动作", "logs.actor": "操作者", "logs.deliveryId": "Delivery ID", "logs.platform": "平台", "logs.status": "HTTP 状态码", "logs.messageId": "消息 ID", "logs.errorCode": "错误码", "logs.duration": "耗时", "logs.attempts": "尝试次数", "logs.ok": "结果", "logs.yes": "成功", "logs.no": "失败", "logs.error": "错误信息", "logs.detail": "附加信息", "logs.filterGroup": "分组", "logs.allGroups": "全部分组", "audit.title": "审计日志", "audit.empty": "暂无审计记录。", "audit.time": "时间", "audit.actor": "操作者", "audit.action": "动作", "audit.target": "对象", "audit.group": "分组", "audit.detail": "详情", "audit.filterGroup": "分组", "audit.allGroups": "全部分组", "audit.refresh": "⟳ 刷新", "toast.routesSaved": "路由已保存", "toast.routeDeleted": "路由已删除", "toast.groupSaved": "分组已保存", "toast.groupDeleted": "分组已删除", "toast.routeIdExists": "路由 ID 已存在", "toast.groupIdExists": "分组 ID 已存在", "toast.saveFailed": "保存失败:{msg}", "toast.deleteFailed": "删除失败:{msg}", "confirm.deleteRoute": "确定删除路由“{name}”吗?", "confirm.deleteGroup": "确定删除分组“{name}”吗?", "confirm.deleteGroupWarn": " 有 {n} 条路由引用它,将失去所属分组。", }; const messages: Record = { en, zh }; function detectLocale(): Locale { if (typeof window === "undefined") return "zh"; const stored = window.localStorage.getItem(STORAGE_KEY); if (stored === "en" || stored === "zh") return stored; const nav = (window.navigator.language || "").toLowerCase(); return nav.startsWith("zh") ? "zh" : "en"; } export function useI18n() { const locale = useState("wh-locale", detectLocale); function setLocale(next: Locale): void { locale.value = next; if (typeof window !== "undefined") window.localStorage.setItem(STORAGE_KEY, next); } function toggle(): void { setLocale(locale.value === "zh" ? "en" : "zh"); } function t(key: string, params?: Record): string { const dict = messages[locale.value] ?? en; let str = dict[key] ?? en[key] ?? key; if (params) { for (const [k, v] of Object.entries(params)) { str = str.replace(`{${k}}`, String(v)); } } return str; } return { locale, setLocale, toggle, t }; }