WebHooker/app/composables/useI18n.ts

589 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 填上面的 URLContent type 选 application/jsonSecret 填上面的 secret。",
"webhook.usageGitea": "Gitea仓库 Settings → Webhooks → Add Webhook填写同样的 URL 和 secret。",
"webhook.usageCustom":
"自定义:用 X-WebHooker-Signaturesha256 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":
"只有来自所勾选 forgeGitHub / 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 };
}