From 24a5148951052d81c45a481c69cdc41ab35fd301 Mon Sep 17 00:00:00 2001 From: argszero Date: Sat, 12 Sep 2026 01:22:01 +0800 Subject: [PATCH] fix(ui): drive table card-mode labels from i18n instead of hardcoded zh strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under `@media (max-width:560px)` the table thead is hidden and each row is rendered as a card, where `td::before { content: attr(data-label) }` prints "label: value". Every `data-label` was a hardcoded Chinese literal, and `applyStatic()` only rewrites `data-i18n` / `-ph` / `-title`, so those labels had no i18n path at all: with the language set to English the card headers still rendered in Chinese. Measured on the real UI in English mode at card-mode width before this change: 27 Chinese labels across the tables that had rows (`#mk-body` 7/7, `#api-keys` 5/6, `#model-body` 8/8, `#emp-body` 7/7). All 53 `data-label` sites now build their label from a translation key at render time, following the pattern the transaction table already used (`title: () => T("tx.col.…")` then `esc(col.title())`). The labels reuse the column-header keys that already existed as `data-i18n` on the matching ``, so no new vocabulary was invented; the 7 keys that were missing in both packs, plus 7 mirror keys for the empty-state cell, are added (769 keys in each pack, still exactly equal). Because rows are re-rendered per request and the `atp:langchange` handler calls `renderView(activeView)`, switching language now updates the card labels live. Note this deliberately does not hand the attributes to `applyStatic()`: that function assigns with `innerHTML`, which would re-enter the raw-markup path. Verified with a headless-Chrome A/B harness in which both trees are produced by the same generator and the baseline is rebuilt from git objects. At card-mode width: 27 Chinese labels in English mode before, 0 after, with the same cell counts and the same 27 labels still rendered in Chinese mode (control), so the fix is language-correct rather than labels simply disappearing. A static audit confirms all 53 sites are i18n-driven and no Chinese literal remains, which also covers the ops table that an admin session cannot reach (the ops view is role-gated). Desktop is unaffected: at 1400px no table enters card mode. Bumped the asset cache-bust to 20260911-8. --- ui/index.html | 10 ++--- ui/js/app.js | 104 +++++++++++++++++++++++++------------------------- ui/js/i18n.js | 14 +++++++ 3 files changed, 71 insertions(+), 57 deletions(-) diff --git a/ui/index.html b/ui/index.html index bf82a35..35aad0e 100644 --- a/ui/index.html +++ b/ui/index.html @@ -5,7 +5,7 @@ AITokenPool - + @@ -842,9 +842,9 @@

使用模型

- - - - + + + + diff --git a/ui/js/app.js b/ui/js/app.js index 288aaaa..293e693 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -797,21 +797,21 @@ $("#mk-count").textContent = T("cnt.on", { n: list.length }); $("#mk-body").innerHTML = guestHint + (list.length ? list.map((m) => - "" + + "" + '
' + '" + '' + '' + hl(m.provider, rawQ) + "" + '' + hl(m.model, rawQ) + "
" + - '' + D.fmt(m.in) + " " + T("common.points") + + '' + D.fmt(m.in) + " " + T("common.points") + (m.peak ? ' ' + esc(T("mk.peak.badge", { n: m.peakMult })) + "" : "") + "" + - '' + D.fmt(m.out) + " " + T("common.points") + "" + - '' + D.ctxFmt(m.ctx) + "" + + '' + D.fmt(m.out) + " " + T("common.points") + "" + + '' + D.ctxFmt(m.ctx) + "" + // 能力标签(rant 第 4 节:旗舰/推理 tag-accent,读图另加 tag)——仅渲染后端真实字段 - '' + capabilityTags(m) + "" + + '' + capabilityTags(m) + "" + // 可用性 pill(rant 第 4 节:keys>=2 可用·N key / keys==1 紧张 / 无 key) - "" + availPill(m) + "" + - "" + + "" + availPill(m) + "" + + "" + // 零 mock:成功率后端暂无字段 → 仅当有真实值时展示(multi/success 已从 data.js 移除) (m.success != null ? "
" + T("mk.success", { p: m.success }) + "
" : "") + "" + (mkExpanded === m.id ? '' + mkDetailHtml(m) + "" : "") @@ -941,16 +941,16 @@ $("#share-body").innerHTML = list.length ? list.map((s, i) => { // 已用 / 额度 进度条(rant 2026-09-11T16:23:43 第 5 节:进度条 + 数字) const pct = s.quota > 0 ? Math.min(100, Math.round((s.used / s.quota) * 100)) : 0; - return "" + esc(provLabel(s.provider)) + " · " + esc(s.plan || "API") + + return "" + esc(provLabel(s.provider)) + " · " + esc(s.plan || "API") + "
" + esc(s.model) + "
" + - "" + esc(maskKey(s.key)) + "" + - "" + D.fmt(s.used) + " / " + D.fmt(s.quota) + + "" + esc(maskKey(s.key)) + "" + + "" + D.fmt(s.used) + " / " + D.fmt(s.quota) + '
' + - '' + D.fmt(s.price) + " " + T("share.priceUnit") + "" + - '+' + D.fmt(s.earned) + " " + T("common.points") + "" + - "" + esc(fmtAvailable(s)) + "" + - "" + badge(s.status, SHARE_STATUS) + "" + - " " + ""; }).join("") : emptyRow(8, T("share.empty"), T("share.empty.sub"), @@ -1245,11 +1245,11 @@ } el.innerHTML = (list.length ? '
' + list.map((r, i) => - "" + - '" + - "" + - "" + - "" + + '" + + "" + + "" + + "" @@ -1935,12 +1935,12 @@ } list = list.filter((k) => !q || k.name.toLowerCase().includes(q)); $("#api-keys").innerHTML = list.length ? list.map((k, i) => - "" + - "" + - "" + - "" + - "" + - "" + + "" + + "" + + "" + + "" + + "" ).join("") : emptyRow(6, T("settings.ak.empty"), T("settings.ak.empty.sub"), @@ -2178,14 +2178,14 @@ (u.dept_name || "").toLowerCase().includes(empQ)); $("#emp-body").innerHTML = shown.length ? shown.map((u) => { const i = users.indexOf(u); - return "" + - "" + - "" + - '" + - '" + - '" + - "" + + "" + + '" + + '" + + '" + + ""; }).join("") : emptyRow(7, T("admin.emp.empty"), T("admin.emp.empty.sub")); pulseTbody($("#emp-body")); @@ -2241,14 +2241,14 @@ const list = Live.adminModels.filter((m) => !q || (m.provider || "").toLowerCase().includes(q) || (m.model || "").toLowerCase().includes(q)); $("#model-body").innerHTML = list.length ? list.map((m, i) => - "" + - "" + - '" + - '" + - '" + - '" + - "" + - "" + + "" + + '" + + '" + + '" + + '" + + "" + + "" ).join("") : emptyRow(8, T("admin.models.empty"), T("admin.models.empty.sub")); pulseTbody($("#model-body")); @@ -2439,13 +2439,13 @@ const members = d.member_count || 0; const pct = d.quota > 0 ? used / d.quota : 0; const st = pct >= 1 ? '' + T("common.exhausted") + "" : pct > 0.9 ? '' + T("common.nearLimit") + "" : '' + T("common.normal") + ""; - return "" + - '" + - '" + - '" + - '" + - "" + - "" + + '" + + '" + + '" + + '" + + "" + + ""; }).join("") : emptyRow(7, T("admin.org.empty"), T("admin.org.empty.sub"), '"); @@ -2583,10 +2583,10 @@ const q = rawQ.toLowerCase(); const list = src.filter((u) => !q || u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q)); $("#ops-body").innerHTML = list.length ? list.map((u) => - "" + - "" + - '" + - "" + "" + + "" + + '" + + "" ).join("") : emptyRow(4, T("ops.users.empty"), T("ops.users.empty.sub")); pulseTbody($("#ops-body")); } diff --git a/ui/js/i18n.js b/ui/js/i18n.js index 64f009c..ebe8136 100644 --- a/ui/js/i18n.js +++ b/ui/js/i18n.js @@ -278,6 +278,7 @@ "share.col.earn": "收益", "share.col.status": "状态", "share.col.action": "操作", + "share.col.empty": "共享", "share.status.on": "上架中", "share.status.paused": "已暂停", "share.status.off": "已下线", @@ -436,6 +437,7 @@ "settings.ak.col.last": "最近使用", "settings.ak.col.status": "状态", "settings.ak.col.action": "操作", + "settings.ak.col.empty": "API Key", "settings.ak.new": "生成新 Key", "settings.ak.name.ph": "key 名字(留空默认「未命名」)", "settings.ak.ok": "确认", @@ -498,6 +500,7 @@ "admin.models.col.ctx": "上下文", "admin.models.col.outmax": "最大输出", "admin.models.col.vision": "读图", + "admin.models.col.action": "操作", "admin.models.edit": "编辑", "admin.models.del": "删除", "admin.models.del.confirm": "确认删除?", @@ -549,6 +552,7 @@ "admin.emp.col.remain": "剩余", "admin.emp.col.status": "状态", "admin.emp.col.action": "操作", + "admin.emp.col.empty": "成员", "admin.emp.col.perm": "永久点数", "admin.emp.col.gift": "赠送点数", "admin.emp.col.avail": "可用", @@ -590,6 +594,7 @@ "admin.raise.col.reason": "原因", "admin.raise.col.status": "状态", "admin.raise.col.action": "操作", + "admin.raise.col.empty": "加额申请", "admin.raise.approve": "批准", "admin.raise.reject": "驳回", "admin.raise.status.pending": "待审批", @@ -626,6 +631,7 @@ "admin.org.col.remain": "剩余", "admin.org.col.status": "状态", "admin.org.col.action": "操作", + "admin.org.col.empty": "部门", "admin.org.add": "添加部门", "admin.org.search.ph": "搜索部门…", "admin.org.list": "部门列表", @@ -694,6 +700,7 @@ "ops.users.col.email": "邮箱", "ops.users.col.balance": "余额(点数)", "ops.users.col.action": "操作", + "ops.users.col.empty": "用户", "ops.users.topup": "充值点数", "ops.users.topup.ph": "点数(可为小数)", "ops.users.empty": "没有匹配的用户", @@ -1070,6 +1077,7 @@ "share.col.earn": "Earnings", "share.col.status": "Status", "share.col.action": "Action", + "share.col.empty": "Sharing", "share.status.on": "Listed", "share.status.paused": "Paused", "share.status.off": "Offline", @@ -1225,6 +1233,7 @@ "settings.ak.col.last": "Last used", "settings.ak.col.status": "Status", "settings.ak.col.action": "Action", + "settings.ak.col.empty": "API Key", "settings.ak.new": "Generate new key", "settings.ak.name.ph": "Key name (empty = “Unnamed”)", "settings.ak.ok": "OK", @@ -1286,6 +1295,7 @@ "admin.models.col.ctx": "Context", "admin.models.col.outmax": "Max output", "admin.models.col.vision": "Vision", + "admin.models.col.action": "Action", "admin.models.edit": "Edit", "admin.models.del": "Delete", "admin.models.del.confirm": "Delete?", @@ -1337,6 +1347,7 @@ "admin.emp.col.remain": "Remaining", "admin.emp.col.status": "Status", "admin.emp.col.action": "Action", + "admin.emp.col.empty": "Member", "admin.emp.col.perm": "Permanent pts", "admin.emp.col.gift": "Gift pts", "admin.emp.col.avail": "Available", @@ -1378,6 +1389,7 @@ "admin.raise.col.reason": "Reason", "admin.raise.col.status": "Status", "admin.raise.col.action": "Action", + "admin.raise.col.empty": "Top-up requests", "admin.raise.approve": "Approve", "admin.raise.reject": "Reject", "admin.raise.status.pending": "Pending", @@ -1414,6 +1426,7 @@ "admin.org.col.remain": "Remaining", "admin.org.col.status": "Status", "admin.org.col.action": "Action", + "admin.org.col.empty": "Department", "admin.org.add": "+ Add dept", "admin.org.search.ph": "Search department…", "admin.org.list": "Departments", @@ -1481,6 +1494,7 @@ "ops.users.col.email": "Email", "ops.users.col.balance": "Balance (pts)", "ops.users.col.action": "Action", + "ops.users.col.empty": "User", "ops.users.topup": "Top up points", "ops.users.topup.ph": "points (may be decimal)", "ops.users.empty": "No matching users",
成员申请点数原因状态
" + esc(r.name || r.user) + "
" + esc(r.email) + "
+' + D.fmt(r.amount) + " " + T("common.points") + "" + esc(r.reason) + "" + badge(r.status, RAISE_STATUS) + "" + (r.status === "pending" + "
" + esc(r.name || r.user) + "
" + esc(r.email) + "
+' + D.fmt(r.amount) + " " + T("common.points") + "" + esc(r.reason) + "" + badge(r.status, RAISE_STATUS) + "" + (r.status === "pending" ? " " + "" : '' + esc((r.created_at || "").slice(5, 16)) + "") + "
" + hl(k.name, rawQ) + "" + esc(Live.apiKeys ? k.key : "") + "" + esc(k.created) + "" + (k.last ? timeCell(k.last) : esc(T("settings.ak.last.never"))) + "" + (k.status === "active" ? '' + T("settings.ak.status.active") + "" : '' + esc(k.status || "—") + "") + " " + + "
" + hl(k.name, rawQ) + "" + esc(Live.apiKeys ? k.key : "") + "" + esc(k.created) + "" + (k.last ? timeCell(k.last) : esc(T("settings.ak.last.never"))) + "" + (k.status === "active" ? '' + T("settings.ak.status.active") + "" : '' + esc(k.status || "—") + "") + " " + " " + "
" + hl(u.name || u.email, rawEmpQ) + "" + + return "
" + hl(u.name || u.email, rawEmpQ) + "" + "
" + hl(u.email, rawEmpQ) + "
" + (u.role === "admin" ? 'admin' : u.role === "ops" ? 'ops' : 'user') + "" + (u.dept_name ? hl(u.dept_name, rawEmpQ) : '' + T("common.unassigned") + "") + "' + D.fmt(u.balance || 0) + "' + D.fmt(u.gift_balance || 0) + "' + D.fmt((u.balance || 0) + (u.gift_balance || 0)) + " " + + "" + (u.role === "admin" ? 'admin' : u.role === "ops" ? 'ops' : 'user') + "" + (u.dept_name ? hl(u.dept_name, rawEmpQ) : '' + T("common.unassigned") + "") + "' + D.fmt(u.balance || 0) + "' + D.fmt(u.gift_balance || 0) + "' + D.fmt((u.balance || 0) + (u.gift_balance || 0)) + " " + "
" + esc(m.provider) + "" + esc(m.model) + "' + D.fmt(m.input_per_m || 0) + "' + D.fmt(m.output_per_m || 0) + "' + fmtCtx(m.context_length || m.context_window || 0) + "' + fmtCtx(m.max_output || 0) + "" + (m.vision ? '' + T("admin.models.vision.yes") + "" : '' + T("admin.models.vision.no") + "") + " " + + "
" + esc(m.provider) + "" + esc(m.model) + "' + D.fmt(m.input_per_m || 0) + "' + D.fmt(m.output_per_m || 0) + "' + fmtCtx(m.context_length || m.context_window || 0) + "' + fmtCtx(m.max_output || 0) + "" + (m.vision ? '' + T("admin.models.vision.yes") + "" : '' + T("admin.models.vision.no") + "") + " " + "
" + hl(d.name, rawQ) + "' + T("cnt.members", { n: members }) + "' + D.fmt(d.quota) + " " + T("common.points") + "' + D.fmt(used) + " " + T("common.points") + "' + D.fmt(d.quota - used) + " " + T("common.points") + "" + st + " " + + return "
" + hl(d.name, rawQ) + "' + T("cnt.members", { n: members }) + "' + D.fmt(d.quota) + " " + T("common.points") + "' + D.fmt(used) + " " + T("common.points") + "' + D.fmt(d.quota - used) + " " + T("common.points") + "" + st + " " + "
" + hl(u.name, rawQ) + "" + hl(u.email, rawQ) + "' + D.fmt(u.balance || 0) + " " + T("common.points") + "
" + hl(u.name, rawQ) + "" + hl(u.email, rawQ) + "' + D.fmt(u.balance || 0) + " " + T("common.points") + "