mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
799 lines
34 KiB
TypeScript
799 lines
34 KiB
TypeScript
export type Locale = "en" | "zh";
|
||
|
||
const STORAGE_KEY = "wh-locale";
|
||
|
||
type Dict = Record<string, string>;
|
||
|
||
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.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.chat": "Chat ID",
|
||
"routeEditor.chatPlaceholder": "Telegram group chat ID",
|
||
"routeEditor.topic": "Topic ID",
|
||
"routeEditor.topicNote": "(optional)",
|
||
"routeEditor.topicPlaceholder": "Optional topic (message_thread_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:<lang>",
|
||
"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.logTargetHint":
|
||
"A Discord channel/thread or Telegram chat/topic 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.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.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.chat": "群组 ID",
|
||
"routeEditor.chatPlaceholder": "Telegram 群组聊天 ID",
|
||
"routeEditor.topic": "话题 ID",
|
||
"routeEditor.topicNote": "(可选)",
|
||
"routeEditor.topicPlaceholder": "可选的话题 (message_thread_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:<lang> 自定义",
|
||
"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.logTargetHint":
|
||
"一个 Discord 频道/子区或 Telegram 群组/话题,本分组路由分发(dispatch)的每个 webhook 都会向其发送摘要。",
|
||
"groupEditor.errLogChannel": "选择 Discord 时必须填写日志频道 ID",
|
||
"groupEditor.errLogChat": "选择 Telegram 时必须填写日志群组 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<Locale, Dict> = { 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<Locale>("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, string | number>): 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 };
|
||
}
|