mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
589 lines
26 KiB
TypeScript
589 lines
26 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.groups": "Groups",
|
||
"tab.logs": "Send Logs",
|
||
"tab.audit": "Audit Log",
|
||
"tab.metrics": "Metrics",
|
||
"metrics.title": "Delivery Metrics",
|
||
"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.recentFailures": "Recent failures",
|
||
"metrics.empty": "No delivery data yet",
|
||
"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",
|
||
"routeEditor.editTitle": "Edit route",
|
||
"routeEditor.newTitle": "New route",
|
||
"routeEditor.close": "Close",
|
||
"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",
|
||
"groupEditor.editTitle": "Edit group",
|
||
"groupEditor.newTitle": "New group",
|
||
"groupEditor.close": "Close",
|
||
"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.groups": "分组",
|
||
"tab.logs": "发送日志",
|
||
"tab.audit": "审计日志",
|
||
"tab.metrics": "指标",
|
||
"metrics.title": "投递指标",
|
||
"metrics.refresh": "刷新",
|
||
"metrics.total": "总计",
|
||
"metrics.ok": "成功",
|
||
"metrics.failed": "失败",
|
||
"metrics.failureRate": "失败率",
|
||
"metrics.avgDuration": "平均耗时",
|
||
"metrics.avgAttempts": "平均尝试次数",
|
||
"metrics.byPlatform": "按平台",
|
||
"metrics.byEvent": "按事件",
|
||
"metrics.recentFailures": "最近失败",
|
||
"metrics.empty": "暂无投递数据",
|
||
"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": "关键词",
|
||
"routeEditor.editTitle": "编辑路由",
|
||
"routeEditor.newTitle": "新建路由",
|
||
"routeEditor.close": "关闭",
|
||
"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 为必填项",
|
||
"groupEditor.editTitle": "编辑分组",
|
||
"groupEditor.newTitle": "新建分组",
|
||
"groupEditor.close": "关闭",
|
||
"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 };
|
||
}
|