163 lines
8.9 KiB
TypeScript
163 lines
8.9 KiB
TypeScript
/**
|
||
* 后端的错误信息是英文(API 契约就是英文):前端在这里把常见的错误码、字段名和校验信息翻成中文。
|
||
* 没有收录的信息原样显示,不会丢。
|
||
*/
|
||
|
||
const CODES: Record<string, string> = {
|
||
validation_failed: '填写的内容有问题',
|
||
budget_exceeded: '超出了这台节点本地设定的带宽或包速率上限',
|
||
unauthorized: '用户名或密码不对,或者登录已经失效',
|
||
csrf_failed: '登录状态已经过期,请刷新页面重新登录',
|
||
forbidden: '没有权限做这件事',
|
||
site_private: '站点未公开:请管理员登录,或在管理端把站点设为公开',
|
||
not_found: '没有找到',
|
||
agent_not_found: '没有找到这个 Agent',
|
||
probe_not_found: '没有找到这个 Probe',
|
||
probe_revoked: '这个 Probe 已经被吊销,不能再换密钥,请新建一个 Probe',
|
||
probe_required: '有多个 Probe,请指定具体的线路',
|
||
task_not_found: '没有找到这个任务',
|
||
test_not_found: '没有找到这次测试',
|
||
chunk_not_found: '没有找到这个原始数据块',
|
||
token_not_found: '没有找到这个令牌',
|
||
unknown_kind: '不认识这种采集方法',
|
||
unknown_plan: '不认识这个测试方案',
|
||
name_conflict: '名称已经被使用,换一个',
|
||
probe_offline: '这个 Probe 现在没有连着这个 Agent',
|
||
test_busy: '这条线路上已经有测试在运行',
|
||
agent_not_active: '这个 Agent 还没有启用',
|
||
agent_offline: 'Agent 不在线',
|
||
raw_expired: '这份原始数据已经超过保留期限',
|
||
join_token_expired: '加入令牌已经过期',
|
||
precondition_failed: '这条记录刚被别人改过,请刷新后再改',
|
||
payload_too_large: '提交的内容太大',
|
||
range_too_large: '选的时间范围太大,请缩短或增大步长',
|
||
invalid_step: '步长不合法',
|
||
invalid_range: '时间范围不合法',
|
||
unknown_metric: '不认识这个指标',
|
||
unknown_preset: '不认识这个指标分组',
|
||
too_many_metrics: '一次选的指标太多',
|
||
too_many_requests: '请求太频繁,请稍后再试',
|
||
too_many_streams: '实时连接太多',
|
||
storage_unavailable: '存储暂时不可用,请稍后再试',
|
||
fleet_unsupported: '这个 Agent 不支持这项功能',
|
||
fleet_join_token_invalid: '加入令牌无效',
|
||
fleet_node_disabled: '这个节点已被停用',
|
||
internal_error: '服务器出错了,请稍后再试',
|
||
method_not_allowed: '不支持这个操作',
|
||
unsupported_media_type: '提交的格式不对',
|
||
}
|
||
|
||
const FIELDS: Record<string, string> = {
|
||
name: '名称',
|
||
username: '用户名',
|
||
password: '密码',
|
||
note: '备注',
|
||
labels: '标签',
|
||
public_host: '公开地址',
|
||
public_visible: '展示页可见',
|
||
max_uses: '可用次数',
|
||
expires_in_hours: '有效期',
|
||
scope: '类型',
|
||
kind: '采集方法',
|
||
target: '目标',
|
||
schedule: '调度',
|
||
agent_id: 'Agent',
|
||
base_task_id: '要覆盖的模板',
|
||
plan: '方案',
|
||
params: '参数',
|
||
range: '时间范围',
|
||
from: '开始时间',
|
||
to: '结束时间',
|
||
step: '步长',
|
||
lines: '线路',
|
||
metric: '指标',
|
||
metrics: '指标',
|
||
cursor: '分页游标',
|
||
anchor: '时间锚点',
|
||
tz: '时区',
|
||
}
|
||
|
||
const MESSAGES: [RegExp, (m: RegExpMatchArray) => string][] = [
|
||
[/^is required$/, () => '不能为空'],
|
||
[/^must be 1-64 characters( of letters, digits|: letters \(any language\), digits), '\.', '_' or '-'$/, () => '只能是 1–64 个字、字母、数字、点、下划线或连字符(可以用中文)'],
|
||
[/^must be 3-32 characters of letters, digits, '\.', '_' or '-'$/, () => '只能是 3–32 个字母、数字、点、下划线或连字符'],
|
||
[/^must be 1-64 characters$/, () => '长度需要 1–64 个字符'],
|
||
[/^must be (\d+)-(\d+) characters$/, (m) => `长度需要 ${m[1]}–${m[2]} 个字符`],
|
||
[/^must be at most (\d+) characters$/, (m) => `最多 ${m[1]} 个字符`],
|
||
[/^at most (\d+) labels of (\d+)-(\d+) characters$/, (m) => `最多 ${m[1]} 个标签,每个 ${m[2]}–${m[3]} 个字符`],
|
||
[/^must be a host name or IP address$/, () => '必须是域名或 IP 地址'],
|
||
[/^must be an IP address with an optional port$/, () => '必须是 IP 地址,端口可选'],
|
||
[/^must be between (-?[\d.]+) and (-?[\d.]+)$/, (m) => `必须在 ${m[1]} 到 ${m[2]} 之间`],
|
||
[/^must be an integer$/, () => '必须是整数'],
|
||
[/^must be a number$/, () => '必须是数字'],
|
||
[/^must be a boolean$/, () => '必须是 是 / 否'],
|
||
[/^must be one of: (.*)$/, (m) => `只能是:${m[1]}`],
|
||
[/^is not a parameter of this kind$/, () => '这种采集方法没有这个参数'],
|
||
[/^must be line or target$/, () => '只能是线路任务或目标任务'],
|
||
[/^must be read or admin$/, () => '只能是 read(只读)或 admin(全部权限)'],
|
||
[/^must be now or latest$/, () => '只能是 now 或 latest'],
|
||
[/^is not a valid cursor$/, () => '不是有效的分页游标'],
|
||
[/^is mutually exclusive with from and to$/, () => '不能和开始、结束时间同时使用'],
|
||
[/^only UTC is supported at the moment$/, () => '目前只支持 UTC'],
|
||
[/^line tasks have no target$/, () => '线路任务没有目标'],
|
||
[/^agent_id is required for target tasks$/, () => '目标任务必须指定 Agent'],
|
||
[/^per-agent line tasks must be overrides \(base_task_id\)$/, () => '针对某个 Agent 的线路任务必须是覆盖(要选一个模板)'],
|
||
[/^must reference a line template and requires agent_id$/, () => '必须引用一个线路模板,并且要指定 Agent'],
|
||
[/^an override must use the same kind as its template$/, () => '覆盖必须和它的模板是同一种采集方法'],
|
||
[/^once tasks are created through the tests API$/, () => '一次性测试请在「按需测试」里发起'],
|
||
[/^unknown kind (.+)$/, (m) => `不认识采集方法 ${m[1]}`],
|
||
[/^unknown plan (.+)$/, (m) => `不认识测试方案 ${m[1]}`],
|
||
[/^a probe named (.+) already exists$/, (m) => `已经有一个叫 ${m[1]} 的 Probe`],
|
||
[/^probe (\S+) has no session with this agent$/, () => '这个 Probe 现在没有连着这个 Agent'],
|
||
[/^probe (\S+) is not connected to this agent$/, () => '这个 Probe 现在没有连着这个 Agent'],
|
||
[/^this probe has no session with this agent$/, () => '这个 Probe 现在没有连着这个 Agent'],
|
||
[/^the probe has no session with this agent$/, () => '这个 Probe 现在没有连着这个 Agent'],
|
||
[/^another test is already running on this line$/, () => '这条线路上已经有测试在运行'],
|
||
[/^agent is offline$/, () => 'Agent 不在线'],
|
||
[/^agent is not active$/, () => 'Agent 还没有启用'],
|
||
[/^agent not found$/, () => '没有找到这个 Agent'],
|
||
[/^probe not found$/, () => '没有找到这个 Probe'],
|
||
[/^line not found$/, () => '没有找到这条线路'],
|
||
[/^there is no active probe$/, () => '现在没有可用的 Probe'],
|
||
[/^several probes exist.*$/, () => '有多个 Probe,请指定具体的线路'],
|
||
[/^a revoked probe cannot get a new key.*$/, () => '已吊销的 Probe 不能换密钥,请新建一个 Probe'],
|
||
[/^this agent does not support (\S+), which the (\S+) plan needs$/, (m) => `这个 Agent 不支持 ${m[1]},而 ${m[2]} 方案需要它`],
|
||
[/^the agent did not accept the test$/, () => 'Agent 没有接受这次测试'],
|
||
[/^a valid X-CSRF-Token header is required$/, () => '登录状态已经过期,请刷新页面重新登录'],
|
||
]
|
||
|
||
export function translateMessage(message: string): string {
|
||
const text = message.trim()
|
||
for (const [pattern, render] of MESSAGES) {
|
||
const match = text.match(pattern)
|
||
if (match) return render(match)
|
||
}
|
||
return text
|
||
}
|
||
|
||
export const translateField = (field: string): string => FIELDS[field] ?? field
|
||
|
||
export function translateCode(code: string): string | undefined {
|
||
return CODES[code]
|
||
}
|
||
|
||
const SETTINGS: Record<string, string> = {
|
||
'site.public': '匿名访客能不能读展示页的数据(关闭时只有登录的管理员能看)',
|
||
'retention.raw_days': '持续任务的原始数据块保留多少天',
|
||
'retention.test_raw_days': '按需测试的原始数据块保留多少天',
|
||
'retention.metric_10s_hours': '10 秒精度的指标保留多少小时',
|
||
'retention.metric_1m_days': '1 分钟精度的指标保留多少天',
|
||
'retention.metric_5m_days': '5 分钟精度的指标保留多少天',
|
||
'retention.events_days': '事件保留多少天',
|
||
'retention.host_days': 'Agent 主机资源占用(每分钟一个样本)保留多少天',
|
||
}
|
||
|
||
const PLANS: Record<string, { title: string; description: string }> = {
|
||
hy2_ladder: { title: 'Hysteria2 阶梯测试', description: 'UDP 灌包,依次以 10、25、50、100 Mbit/s 各跑 10 秒,用来判断 Brutal 固定码率拥塞控制能不能跑得起来。' },
|
||
reality_session: { title: 'REALITY 会话测试', description: '建连测试、4 秒 TLS 批量传输(量吞吐峰值)和一个长连接保持。' },
|
||
}
|
||
|
||
export const translateSetting = (key: string, fallback: string): string => SETTINGS[key] ?? fallback
|
||
|
||
export const translatePlan = (id: string, title: string, description: string): { title: string; description: string } => PLANS[id] ?? { title, description }
|