oclaw/uds-auth/lib/i18n.js
oliver e61f9c6336
Some checks are pending
ci / test (push) Waiting to run
ci / test-postgresql (push) Waiting to run
fix(uds-auth): keep login error codes on QR login, name the scanned empNo (0.3.17)
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>
2026-10-12 14:24:11 +08:00

518 lines
26 KiB
JavaScript
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.

/**
* 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(',')
}