Files
MoShiro/web/shared/i18n.ts
T
2026-10-04 21:46:39 +08:00

163 lines
8.9 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.
/**
* 后端的错误信息是英文(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 }