mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-12 23:50:49 +08:00
0.3.16 mapped every post-scan failure to login_finalize_failed, hiding codes with their own message (bootstrap_required, account_disabled, registration_closed). Pass those through again and only fall back for unknown errors. The bootstrap_required message now includes the scanned employee number and how to proceed (UDS_AUTH_INITIAL_ADMIN or emergency account login). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
518 lines
26 KiB
JavaScript
518 lines
26 KiB
JavaScript
/**
|
||
* uds-auth i18n — single source for UI + API user-facing strings.
|
||
* Locales: zh (default), en. Keys are stable error/UI ids.
|
||
*
|
||
* Browser client.js embeds a copy of MESSAGES (ModuleLoader cannot import this file).
|
||
* When changing strings here, also refresh the `UDS_I18N_MESSAGES` block in lib/client.js
|
||
* (search for `uds-auth-i18n-begin`).
|
||
*/
|
||
|
||
export const DEFAULT_LOCALE = 'zh'
|
||
|
||
/** @type {Record<string, Record<string, string>>} */
|
||
export const MESSAGES = {
|
||
zh: {
|
||
// roles
|
||
'role.super_admin': '超级管理员',
|
||
'role.admin': '管理员',
|
||
'role.user': '普通用户',
|
||
'role.fallback_admin': '应急管理员',
|
||
'role.badge.super_admin': '超管',
|
||
'role.badge.admin': '管理员',
|
||
'role.badge.fallback_admin': '应急',
|
||
'role.badge.user': '',
|
||
|
||
// settings / users
|
||
'ui.settingsTitle': 'UAC 认证',
|
||
'ui.settingsIntro': '工号+token 双校验;UAC 挂死时用应急账号 administrator 密码登录。',
|
||
'ui.loginRequiredPage': '请先登录后查看此页',
|
||
'ui.roleHint': '当前角色:{role}。超管只是身份标签,与管理员权限相同;显式配置的初始管理员在首次登录时获得超管身份,也可在「用户管理」设为管理员。应急账号默认账号 administrator、密码 Admin@123,首次登录后请修改。',
|
||
'ui.deployConfig': '部署配置',
|
||
'ui.userSearchUrl': '用户搜索 URL(token 校验)',
|
||
'ui.workspaceRoot': '工作区根目录(空=$DSH_HOME/user-workspaces)',
|
||
'ui.allowRawAgentToken': '允许 Skill 获取原始 UDS token',
|
||
'ui.allowRawAgentTokenHint': '开启后,具备当前任务 credentials scope 的本机 Skill 可获取工号和 token。只在业务确实需要原始 token 时开启。',
|
||
'ui.saveConfig': '保存配置',
|
||
'ui.saving': '保存中...',
|
||
'ui.configSaved': '配置已保存',
|
||
'ui.saveFailed': '保存失败',
|
||
'ui.fallbackTitle': '应急登录(UAC 不可用)',
|
||
'ui.fallbackStatus': '状态:{status}。可在此改密或关闭。仅在扫码不可用时从登录面板切换。',
|
||
'ui.enabled': '已启用',
|
||
'ui.disabled': '未启用',
|
||
'ui.fallbackPassword': '应急密码(至少 10 位)',
|
||
'ui.saveFallbackPassword': '保存应急密码',
|
||
'ui.fallbackPasswordSet': '应急密码已设置',
|
||
'ui.confirmClearFallback': '确认清除应急密码?',
|
||
'ui.clear': '清除',
|
||
'ui.userManagement': '用户管理',
|
||
'ui.searchEmpNo': '搜索工号',
|
||
'ui.search': '搜索',
|
||
'ui.loading': '加载中...',
|
||
'ui.loadFailed': '加载失败',
|
||
'ui.empNo': '工号',
|
||
'ui.role': '角色',
|
||
'ui.actions': '操作',
|
||
'ui.noUsers': '暂无用户',
|
||
'ui.delete': '删除',
|
||
'ui.confirmDelete': '确认删除 {empNo}?',
|
||
'ui.pager': '共 {total} 人,第 {page} / {totalPages} 页',
|
||
'ui.prevPage': '上一页',
|
||
'ui.nextPage': '下一页',
|
||
'ui.add': '添加',
|
||
'ui.department': '部门',
|
||
|
||
// privacy / session visibility
|
||
'ui.viewAllSessionsTitle': '查看全部会话',
|
||
'ui.viewAllSessionsIntro': '默认可见全部会话(含渠道与共享工作区)。关闭后仅看自己名下的会话与个人工作区。',
|
||
'ui.viewAllSessionsToggle': '显示所有人的会话',
|
||
'ui.viewAllSessionsOn': '已开启:可见全部会话',
|
||
'ui.viewAllSessionsOff': '已关闭:仅可见自己的会话',
|
||
|
||
// login panel
|
||
'ui.notLoggedIn': '未登录',
|
||
'ui.pleaseScan': '请使用 iCenter 扫码登录',
|
||
'ui.refreshQr': '刷新二维码',
|
||
'ui.fallbackLink': 'UAC 不可用?应急账号登录',
|
||
'ui.fallbackLogin': '应急登录',
|
||
'ui.fallbackDetail': 'UAC / 扫码不可用时使用',
|
||
'ui.localKeyLink': '本机密钥解锁',
|
||
'ui.localKeyLogin': '本机密钥解锁',
|
||
'ui.localKeyDetail': '用密封盒口令解密后登录(仅设环境变量不会自动登录)',
|
||
'ui.localKey': '解密密钥',
|
||
'ui.username': '用户名',
|
||
'ui.password': '密码',
|
||
'ui.login': '登录',
|
||
'ui.loggingIn': '登录中...',
|
||
'ui.backToQr': '返回扫码登录',
|
||
'ui.logout': '退出登录',
|
||
'ui.userPrefix': '用户',
|
||
|
||
// QR status
|
||
'ui.qrGenerating': '正在生成二维码...',
|
||
'ui.qrScanPrompt': '请使用 iCenter 扫码登录...',
|
||
'ui.userInfoFailed': '用户信息查询失败',
|
||
'ui.loginSuccess': '登录成功!',
|
||
'ui.missingToken': '缺少 token,无法完成校验',
|
||
'ui.waitingScan': '等待扫码...',
|
||
'ui.qrExpired': '二维码已失效',
|
||
'ui.qrExpiredHint': '请刷新后重新扫描',
|
||
'ui.loginFailed': '登录失败',
|
||
'ui.networkError': '网络错误...',
|
||
'ui.qrGenerateFailed': '生成二维码失败',
|
||
|
||
// client gates
|
||
'ui.cronLoginRequired': '登录后才能使用定时任务',
|
||
'ui.workspaceCreateForbidden': '只有管理员可以创建工作区',
|
||
'ui.workspaceLoginRequired': '登录后才能使用工作区',
|
||
|
||
// API / ACL errors (stable codes)
|
||
'err.not_logged_in': '未登录',
|
||
'err.forbidden_settings': '当前账号无设置权限',
|
||
'err.forbidden_manage_users': '只有超级管理员可以管理用户',
|
||
'err.forbidden_list_users': '只有超级管理员可以查看用户列表',
|
||
'err.forbidden_set_role': '只有超级管理员可以修改角色',
|
||
'err.forbidden_add_user': '只有超级管理员可以添加用户',
|
||
'err.forbidden_remove_user': '只有超级管理员可以删除用户',
|
||
'err.forbidden_set_fallback': '只有超级管理员可以设置应急密码',
|
||
'err.forbidden_clear_fallback': '只有超级管理员可以清除应急密码',
|
||
'err.forbidden_view_all_sessions': '当前角色不能开启查看全部会话',
|
||
'err.invalid_role_params': '参数错误: empNo 和 role 必填',
|
||
'err.emp_no_required': 'empNo 必填',
|
||
'err.username_password_required': '用户名和密码必填',
|
||
'err.invalid_credentials': '用户名或密码错误',
|
||
'err.last_super_admin_demote': '系统至少需要 1 个超级管理员,不能降级最后一个',
|
||
'err.last_super_admin_delete': '系统至少需要 1 个超级管理员,不能删除最后一个',
|
||
'err.password_too_short': '密码至少 10 位',
|
||
'err.password_too_common': '不能使用已知默认口令',
|
||
'err.account_disabled': '账号已停用',
|
||
'err.qr_unknown': '二维码无效或已过期',
|
||
'err.qr_replay': '二维码已使用',
|
||
'err.qr_expired': '二维码已过期',
|
||
'err.qr_mismatch': '二维码校验失败',
|
||
'err.upstream_too_large': '上游响应过大',
|
||
'err.upstream_dns': '无法解析 {host}(DNS 失败),请确认本机已接入公司内网 / VPN',
|
||
'err.upstream_timeout': '连接 {host} 超时,请确认本机已接入公司内网 / VPN,且未被防火墙拦截',
|
||
'err.upstream_unreachable': '无法连接 {host}(连接被拒绝或中断),请检查网络、防火墙与代理',
|
||
'err.upstream_cert': '与 {host} 的 TLS 证书校验失败:Node 不读取系统证书库,请设置 NODE_OPTIONS=--use-system-ca 或 NODE_EXTRA_CA_CERTS 后重启',
|
||
'err.upstream_error': '访问 {host} 失败,详见服务端日志',
|
||
'err.upstream_http_status': '{host} 返回 HTTP {status}',
|
||
'err.invalid_upstream_response': '{host} 返回了无法识别的响应',
|
||
'err.profile_http_status': '用户信息服务 {host} 返回 HTTP {status}(该服务需内网直连,不能经代理)',
|
||
'err.profile_rejected': '扫码成功,但用户信息校验未通过({reason})',
|
||
'err.login_finalize_failed': '扫码成功,但本机创建会话失败({detail}),详见服务端日志',
|
||
'err.config_not_ready': '配置未初始化',
|
||
'err.request_failed': '请求失败',
|
||
'err.method_not_allowed': '方法不允许',
|
||
'err.missing_qr_params': '缺少 qrCodeKey 或 qrCodeValue',
|
||
'err.missing_emp_token': '缺少 empNo 或 token',
|
||
'err.token_in_query_forbidden': '禁止在 URL 传递 token,请改用 POST body 或请求头',
|
||
'err.user_search_failed': '用户搜索失败',
|
||
'err.not_found': '未找到',
|
||
'err.internal': '内部错误',
|
||
'err.login_required_cron': '登录后才能使用定时任务',
|
||
'err.login_required_session': '登录后才能访问会话',
|
||
'err.login_required_create_session': '登录后才能创建会话',
|
||
'err.login_required_create_workspace': '登录后才能创建工作区',
|
||
'err.login_required_workspace': '登录后才能使用工作区',
|
||
'err.workspace_provision_failed': '个人工作区初始化失败,请重试',
|
||
'err.plugin_unready': '认证插件未就绪',
|
||
'err.origin_forbidden': '请求来源不被允许',
|
||
'err.content_type': 'Content-Type 必须为 application/json',
|
||
'err.https_required': '出站代理仅允许 HTTPS',
|
||
'err.invalid_config': '配置无效',
|
||
'err.unknown_config_field': '未知配置字段',
|
||
'err.invalid_uac_url': 'uacBaseUrl 必须是 https URL',
|
||
'err.invalid_user_search_url': 'userSearchUrl 必须是 https URL',
|
||
'err.invalid_workspace_root': 'workspaceRoot 必须是绝对路径',
|
||
'err.invalid_ttl': 'TTL 超出允许范围',
|
||
'err.invalid_outbound_host': 'outbound 白名单 host 格式无效',
|
||
'err.bootstrap_required': '系统尚未初始化管理员(当前扫码工号 {empNo})。请设置环境变量 UDS_AUTH_INITIAL_ADMIN={empNo} 后重启 Harness;或先用「应急账号登录」进入,在设置中添加用户',
|
||
'err.registration_closed': '未开放自动注册,请联系管理员',
|
||
'err.raw_token_disabled': '原始凭证接口已关闭,请使用 outbound 代理',
|
||
'err.caller_unauthenticated': '调用方未通过本地会话认证',
|
||
'err.session_id_required': '需要 DSH sessionId',
|
||
'err.session_owner_mismatch': '会话归属与调用方不一致',
|
||
'err.session_forbidden': '无权访问该会话',
|
||
'err.session_revoked': '会话已失效,请重新登录',
|
||
'err.fallback_disabled': '应急账号已关闭',
|
||
'err.file_forbidden': '无权访问该文件',
|
||
'err.session_workspace_only': '只能在自己的工作区创建会话',
|
||
'err.workspace_path_only': '只能打开自己的工作区路径',
|
||
'err.workspace_create_forbidden': '只有管理员可以创建工作区',
|
||
'err.no_skill_credentials': '请先完成 UAC 扫码登录',
|
||
'err.loopback_only_credentials': 'agent-credentials 仅允许本机访问',
|
||
'err.loopback_only_outbound': 'outbound 仅允许本机访问',
|
||
'err.url_required': '缺少 url',
|
||
'err.invalid_url': '无效 url',
|
||
'err.unsupported_protocol': '不支持的协议',
|
||
'err.host_not_allowed': '主机不在白名单',
|
||
'err.upstream_failed': '上游请求失败',
|
||
'err.skill_credentials_not_ready': 'skill 凭证未就绪',
|
||
'err.outbound_not_ready': 'outbound 未就绪',
|
||
'err.local_admin_not_configured': '未配置本机管理员密封盒',
|
||
'err.key_required': '请输入解密密钥',
|
||
'err.decrypt_failed': '密钥无法解密,登录失败',
|
||
'err.rate_limited': '尝试过多,请稍后再试',
|
||
'err.payload_too_large': '请求体过大',
|
||
'err.invalid_json': 'JSON 无效',
|
||
|
||
// API success
|
||
'ok.logged_out': '已退出登录',
|
||
'ok.config_saved': '配置已保存',
|
||
'ok.role_updated': '{empNo} 角色已更新为 {role}',
|
||
'ok.user_added': '{empNo} 已添加为 {role}',
|
||
'ok.user_removed': '{empNo} 已删除',
|
||
'ok.fallback_password_set': '应急管理员密码已设置',
|
||
'ok.fallback_password_cleared': '应急管理员密码已清除',
|
||
'ok.fallback_login': '应急管理员登录成功',
|
||
'ok.local_admin_unlock': '本机密钥解锁成功',
|
||
'ok.view_all_sessions_on': '已开启查看全部会话',
|
||
'ok.view_all_sessions_off': '已关闭查看全部会话',
|
||
},
|
||
en: {
|
||
'role.super_admin': 'Super admin',
|
||
'role.admin': 'Admin',
|
||
'role.user': 'User',
|
||
'role.fallback_admin': 'Emergency admin',
|
||
'role.badge.super_admin': 'Super',
|
||
'role.badge.admin': 'Admin',
|
||
'role.badge.fallback_admin': 'Emergency',
|
||
'role.badge.user': '',
|
||
|
||
'ui.settingsTitle': 'UAC Auth',
|
||
'ui.settingsIntro': 'EmpNo + token verification; when UAC is down, sign in with emergency account administrator.',
|
||
'ui.loginRequiredPage': 'Sign in to view this page',
|
||
'ui.roleHint': 'Current role: {role}. Super admin is only an identity label with the same permissions as admin; the explicitly configured initial administrator receives that identity on first login, or grant admin in User management. The emergency account defaults to administrator / Admin@123; change it after first sign-in.',
|
||
'ui.deployConfig': 'Deploy config',
|
||
'ui.userSearchUrl': 'User search URL (token verify)',
|
||
'ui.workspaceRoot': 'Workspace root (empty=$DSH_HOME/user-workspaces)',
|
||
'ui.allowRawAgentToken': 'Allow Skills to read the raw UDS token',
|
||
'ui.allowRawAgentTokenHint': 'Enables local Skills with the current task credentials scope to read the employee number and token. Turn this on only when a workflow needs the raw token.',
|
||
'ui.saveConfig': 'Save config',
|
||
'ui.saving': 'Saving...',
|
||
'ui.configSaved': 'Config saved',
|
||
'ui.saveFailed': 'Save failed',
|
||
'ui.fallbackTitle': 'Emergency login (UAC unavailable)',
|
||
'ui.fallbackStatus': 'Status: {status}. Change or disable here. Switch from the login panel only when QR is unavailable.',
|
||
'ui.enabled': 'Enabled',
|
||
'ui.disabled': 'Disabled',
|
||
'ui.fallbackPassword': 'Emergency password (min 10 chars)',
|
||
'ui.saveFallbackPassword': 'Save emergency password',
|
||
'ui.fallbackPasswordSet': 'Emergency password set',
|
||
'ui.confirmClearFallback': 'Clear emergency password?',
|
||
'ui.clear': 'Clear',
|
||
'ui.userManagement': 'User management',
|
||
'ui.searchEmpNo': 'Search empNo',
|
||
'ui.search': 'Search',
|
||
'ui.loading': 'Loading...',
|
||
'ui.loadFailed': 'Load failed',
|
||
'ui.empNo': 'EmpNo',
|
||
'ui.role': 'Role',
|
||
'ui.actions': 'Actions',
|
||
'ui.noUsers': 'No users',
|
||
'ui.delete': 'Delete',
|
||
'ui.confirmDelete': 'Delete {empNo}?',
|
||
'ui.pager': '{total} users, page {page} / {totalPages}',
|
||
'ui.prevPage': 'Previous',
|
||
'ui.nextPage': 'Next',
|
||
'ui.add': 'Add',
|
||
'ui.department': 'Department',
|
||
|
||
'ui.viewAllSessionsTitle': 'View all sessions',
|
||
'ui.viewAllSessionsIntro': 'All sessions are visible by default (including channels and shared workspaces). Turn off to only see sessions you own and your personal workspace.',
|
||
'ui.viewAllSessionsToggle': 'Show everyone’s sessions',
|
||
'ui.viewAllSessionsOn': 'On: all sessions visible',
|
||
'ui.viewAllSessionsOff': 'Off: only your own sessions',
|
||
|
||
'ui.notLoggedIn': 'Not signed in',
|
||
'ui.pleaseScan': 'Scan with iCenter to sign in',
|
||
'ui.refreshQr': 'Refresh QR',
|
||
'ui.fallbackLink': 'UAC down? Emergency account',
|
||
'ui.fallbackLogin': 'Emergency login',
|
||
'ui.fallbackDetail': 'Use when UAC / QR is unavailable',
|
||
'ui.localKeyLink': 'Unlock with local key',
|
||
'ui.localKeyLogin': 'Local key unlock',
|
||
'ui.localKeyDetail': 'Decrypt the sealed box with your passphrase (env alone does nothing)',
|
||
'ui.localKey': 'Decryption key',
|
||
'ui.username': 'Username',
|
||
'ui.password': 'Password',
|
||
'ui.login': 'Sign in',
|
||
'ui.loggingIn': 'Signing in...',
|
||
'ui.backToQr': 'Back to QR login',
|
||
'ui.logout': 'Sign out',
|
||
'ui.userPrefix': 'User',
|
||
|
||
'ui.qrGenerating': 'Generating QR...',
|
||
'ui.qrScanPrompt': 'Scan with iCenter to sign in...',
|
||
'ui.userInfoFailed': 'User info lookup failed',
|
||
'ui.loginSuccess': 'Signed in!',
|
||
'ui.missingToken': 'Missing token; cannot verify',
|
||
'ui.waitingScan': 'Waiting for scan...',
|
||
'ui.qrExpired': 'QR code expired',
|
||
'ui.qrExpiredHint': 'Refresh and scan again',
|
||
'ui.loginFailed': 'Sign-in failed',
|
||
'ui.networkError': 'Network error...',
|
||
'ui.qrGenerateFailed': 'Failed to generate QR',
|
||
|
||
'ui.cronLoginRequired': 'Sign in to use scheduled tasks',
|
||
'ui.workspaceCreateForbidden': 'Only admins can create workspaces',
|
||
'ui.workspaceLoginRequired': 'Sign in to use workspaces',
|
||
|
||
'err.not_logged_in': 'Not signed in',
|
||
'err.forbidden_settings': 'No settings permission',
|
||
'err.forbidden_manage_users': 'Only super admins can manage users',
|
||
'err.forbidden_list_users': 'Only super admins can list users',
|
||
'err.forbidden_set_role': 'Only super admins can change roles',
|
||
'err.forbidden_add_user': 'Only super admins can add users',
|
||
'err.forbidden_remove_user': 'Only super admins can remove users',
|
||
'err.forbidden_set_fallback': 'Only super admins can set the emergency password',
|
||
'err.forbidden_clear_fallback': 'Only super admins can clear the emergency password',
|
||
'err.forbidden_view_all_sessions': 'Your role cannot enable view-all sessions',
|
||
'err.invalid_role_params': 'Invalid params: empNo and role required',
|
||
'err.emp_no_required': 'empNo required',
|
||
'err.username_password_required': 'Username and password required',
|
||
'err.invalid_credentials': 'Invalid username or password',
|
||
'err.last_super_admin_demote': 'At least one super admin is required; cannot demote the last one',
|
||
'err.last_super_admin_delete': 'At least one super admin is required; cannot delete the last one',
|
||
'err.password_too_short': 'Password must be at least 10 characters',
|
||
'err.password_too_common': 'Cannot use the known default password',
|
||
'err.account_disabled': 'Account is disabled',
|
||
'err.qr_unknown': 'QR challenge unknown or expired',
|
||
'err.qr_replay': 'QR challenge already used',
|
||
'err.qr_expired': 'QR challenge expired',
|
||
'err.qr_mismatch': 'QR challenge mismatch',
|
||
'err.upstream_too_large': 'Upstream response too large',
|
||
'err.upstream_dns': 'Cannot resolve {host} (DNS failed). Make sure this machine is on the corporate network / VPN',
|
||
'err.upstream_timeout': 'Connection to {host} timed out. Make sure this machine is on the corporate network / VPN and not firewalled',
|
||
'err.upstream_unreachable': 'Cannot connect to {host} (refused or reset). Check network, firewall and proxy',
|
||
'err.upstream_cert': 'TLS certificate check for {host} failed. Node ignores the OS cert store: set NODE_OPTIONS=--use-system-ca or NODE_EXTRA_CA_CERTS and restart',
|
||
'err.upstream_error': 'Request to {host} failed; see server logs',
|
||
'err.upstream_http_status': '{host} returned HTTP {status}',
|
||
'err.invalid_upstream_response': '{host} returned an unrecognized response',
|
||
'err.profile_http_status': 'User lookup service {host} returned HTTP {status} (needs direct intranet access, no proxy)',
|
||
'err.profile_rejected': 'Scan succeeded but user lookup was rejected ({reason})',
|
||
'err.login_finalize_failed': 'Scan succeeded but creating the local session failed ({detail}); see server logs',
|
||
'err.config_not_ready': 'Config not initialized',
|
||
'err.request_failed': 'Request failed',
|
||
'err.method_not_allowed': 'Method not allowed',
|
||
'err.missing_qr_params': 'Missing qrCodeKey or qrCodeValue',
|
||
'err.missing_emp_token': 'Missing empNo or token',
|
||
'err.token_in_query_forbidden': 'Do not put token in the URL; use POST body or headers',
|
||
'err.user_search_failed': 'User search failed',
|
||
'err.not_found': 'Not found',
|
||
'err.internal': 'Internal error',
|
||
'err.login_required_cron': 'Sign in to use scheduled tasks',
|
||
'err.login_required_session': 'Sign in to access sessions',
|
||
'err.login_required_create_session': 'Sign in to create a session',
|
||
'err.login_required_create_workspace': 'Sign in to create a workspace',
|
||
'err.login_required_workspace': 'Sign in to use workspaces',
|
||
'err.workspace_provision_failed': 'Personal workspace provisioning failed; retry',
|
||
'err.plugin_unready': 'Auth plugin not ready',
|
||
'err.origin_forbidden': 'Request origin not allowed',
|
||
'err.content_type': 'Content-Type must be application/json',
|
||
'err.https_required': 'Outbound proxy requires HTTPS',
|
||
'err.invalid_config': 'Invalid config',
|
||
'err.unknown_config_field': 'Unknown config field',
|
||
'err.invalid_uac_url': 'uacBaseUrl must be an https URL',
|
||
'err.invalid_user_search_url': 'userSearchUrl must be an https URL',
|
||
'err.invalid_workspace_root': 'workspaceRoot must be an absolute path',
|
||
'err.invalid_ttl': 'TTL out of allowed range',
|
||
'err.invalid_outbound_host': 'Invalid outbound host entry',
|
||
'err.bootstrap_required': 'Admin not initialized (scanned employee no. {empNo}). Set UDS_AUTH_INITIAL_ADMIN={empNo} and restart Harness, or sign in with the emergency account and add users in Settings',
|
||
'err.registration_closed': 'Open registration is disabled; contact an admin',
|
||
'err.raw_token_disabled': 'Raw credential API disabled; use outbound proxy',
|
||
'err.caller_unauthenticated': 'Caller lacks a verified local session',
|
||
'err.session_id_required': 'DSH sessionId required',
|
||
'err.session_owner_mismatch': 'Session owner does not match caller',
|
||
'err.session_forbidden': 'No access to this session',
|
||
'err.session_revoked': 'Session expired; please sign in again',
|
||
'err.fallback_disabled': 'Emergency account is disabled',
|
||
'err.file_forbidden': 'No access to this file',
|
||
'err.session_workspace_only': 'Sessions can only be created in your own workspace',
|
||
'err.workspace_path_only': 'You can only open your own workspace path',
|
||
'err.workspace_create_forbidden': 'Only admins can create workspaces',
|
||
'err.no_skill_credentials': 'Complete UAC QR sign-in first',
|
||
'err.loopback_only_credentials': 'agent-credentials is loopback-only',
|
||
'err.loopback_only_outbound': 'outbound is loopback-only',
|
||
'err.url_required': 'url required',
|
||
'err.invalid_url': 'invalid url',
|
||
'err.unsupported_protocol': 'unsupported protocol',
|
||
'err.host_not_allowed': 'host not allowed',
|
||
'err.upstream_failed': 'upstream failed',
|
||
'err.skill_credentials_not_ready': 'skill credentials not ready',
|
||
'err.outbound_not_ready': 'outbound not ready',
|
||
'err.local_admin_not_configured': 'Local admin sealed box is not configured',
|
||
'err.key_required': 'Decryption key required',
|
||
'err.decrypt_failed': 'Key could not decrypt — sign-in failed',
|
||
'err.rate_limited': 'Too many attempts, try later',
|
||
'err.payload_too_large': 'Payload too large',
|
||
'err.invalid_json': 'Invalid JSON',
|
||
|
||
'ok.logged_out': 'Signed out',
|
||
'ok.config_saved': 'Config saved',
|
||
'ok.role_updated': '{empNo} role updated to {role}',
|
||
'ok.user_added': '{empNo} added as {role}',
|
||
'ok.user_removed': '{empNo} removed',
|
||
'ok.fallback_password_set': 'Emergency admin password set',
|
||
'ok.fallback_password_cleared': 'Emergency admin password cleared',
|
||
'ok.fallback_login': 'Emergency admin signed in',
|
||
'ok.local_admin_unlock': 'Local admin unlocked',
|
||
'ok.view_all_sessions_on': 'View-all sessions enabled',
|
||
'ok.view_all_sessions_off': 'View-all sessions disabled',
|
||
},
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} lang
|
||
* @returns {'zh' | 'en'}
|
||
*/
|
||
export function normalizeLocale(lang) {
|
||
const raw = String(lang || '').trim().toLowerCase()
|
||
if (!raw) return DEFAULT_LOCALE
|
||
if (raw.startsWith('en')) return 'en'
|
||
if (raw.startsWith('zh')) return DEFAULT_LOCALE
|
||
return DEFAULT_LOCALE
|
||
}
|
||
|
||
/**
|
||
* @param {string} key
|
||
* @param {'zh' | 'en' | string} [locale]
|
||
* @param {Record<string, string | number>} [vars]
|
||
*/
|
||
export function t(key, locale = DEFAULT_LOCALE, vars = {}) {
|
||
const loc = normalizeLocale(locale)
|
||
const table = MESSAGES[loc] || MESSAGES[DEFAULT_LOCALE]
|
||
let text = table[key] || MESSAGES[DEFAULT_LOCALE][key] || key
|
||
for (const [k, v] of Object.entries(vars || {})) {
|
||
text = text.split('{' + k + '}').join(String(v))
|
||
}
|
||
return text
|
||
}
|
||
|
||
/** Role display label (full). */
|
||
export function roleLabel(role, locale = DEFAULT_LOCALE) {
|
||
return t('role.' + String(role || 'user'), locale)
|
||
}
|
||
|
||
/** Compact badge label. */
|
||
export function roleBadge(role, locale = DEFAULT_LOCALE) {
|
||
return t('role.badge.' + String(role || 'user'), locale)
|
||
}
|
||
|
||
/** Default zh labels for roles.json search / list (server-side). */
|
||
export const ROLE_LABELS_ZH = {
|
||
super_admin: MESSAGES.zh['role.super_admin'],
|
||
admin: MESSAGES.zh['role.admin'],
|
||
user: MESSAGES.zh['role.user'],
|
||
fallback_admin: MESSAGES.zh['role.fallback_admin'],
|
||
}
|
||
|
||
function parseCookie(header, name) {
|
||
if (!header) return null
|
||
const prefix = name + '='
|
||
for (const part of String(header).split(';')) {
|
||
const v = part.trim()
|
||
if (v.startsWith(prefix)) {
|
||
try { return decodeURIComponent(v.slice(prefix.length)) } catch { return v.slice(prefix.length) }
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* Resolve locale from HTTP request + optional userContext.lang.
|
||
* @param {import('node:http').IncomingMessage | null | undefined} req
|
||
* @param {{ lang?: string } | null | undefined} [userContext]
|
||
*/
|
||
export function resolveLocale(req, userContext) {
|
||
const headers = req?.headers || {}
|
||
const cookie = headers.cookie || ''
|
||
const fromCtx = userContext?.lang
|
||
const fromHeader = headers['x-lang-id'] || headers['X-Lang-Id']
|
||
const fromCookie = parseCookie(cookie, 'PORTALSSOLanguage')
|
||
|| parseCookie(cookie, 'ZTEDPGSSOLanguage')
|
||
const accept = String(headers['accept-language'] || '').split(',')[0]
|
||
return normalizeLocale(fromCtx || fromHeader || fromCookie || accept || DEFAULT_LOCALE)
|
||
}
|
||
|
||
/**
|
||
* API error payload: stable `error` code + localized `message`.
|
||
* @param {string} code
|
||
* @param {'zh' | 'en' | string} locale
|
||
* @param {Record<string, string | number>} [vars]
|
||
*/
|
||
export function apiError(code, locale, vars) {
|
||
const key = code.startsWith('err.') || code.startsWith('ok.') ? code : 'err.' + code
|
||
return {
|
||
error: code.startsWith('err.') ? code.slice(4) : code,
|
||
message: t(key, locale, vars),
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {string} code
|
||
* @param {'zh' | 'en' | string} locale
|
||
* @param {Record<string, string | number>} [vars]
|
||
*/
|
||
export function apiOk(code, locale, vars) {
|
||
const key = code.startsWith('ok.') ? code : 'ok.' + code
|
||
return {
|
||
message: t(key, locale, vars),
|
||
}
|
||
}
|
||
|
||
/** Host UI aria-labels (zh + en) for CSS / DOM gates — not translated UI of this plugin. */
|
||
export const HOST_ARIA = {
|
||
addWorkspace: ['添加工作区', 'Add workspace'],
|
||
chooseWorkspace: ['选择工作区', 'Choose workspace'],
|
||
sessions: ['会话', 'Sessions'],
|
||
}
|
||
|
||
export function ariaSelector(labels) {
|
||
return labels.map((l) => '[aria-label="' + l + '"]').join(',')
|
||
}
|
||
|
||
export function buttonAriaSelector(labels) {
|
||
return labels.map((l) => 'button[aria-label="' + l + '"]').join(',')
|
||
}
|