fix(ui): express the marketplace count in the unit of its rows - #276
Conversation
The toolbar count on the marketplace view was filled with
T("cnt.on", { n: list.length }), and `cnt.on` renders "{n} keys on
sale" / "{n} 个在售 key" — a key count. But `list` is the filtered
model-row list, so the number is a row count: with the probe fixture
(4 model rows, 6 keys) the label reads "4 keys on sale", contradicting
both the "No key" pill on one of its own rows and the key total.
The author's own prototype counts models ("共 N 个模型"), so this is
drift rather than a trade-off. Rename the pack key cnt.on -> cnt.models
and update its two values plus the two call sites, so the count speaks
the unit of the rows it counts.
Add src/i18n_pack.rs::the_marketplace_count_is_expressed_in_the_unit_of_its_rows,
which derives both of its rulers instead of hard-coding a wording: the
count key comes from whoever fills #mk-count, and the row-identity
noun comes from the same table's header. It also pins that the "single
key" ruler stays the same word as the marketplace row pills.
Rename only: no pack key is added or removed, so the i18n gate
constants are unchanged. Cache-bust both changed scripts.
|
Self-review (author is also the merging committer; Verification performed on this exact branch head ( Local gates
Gate A/B (11 trees, live DOM probe (jsdom on the real Scope honesty
|
Summary
The marketplace toolbar count (
#mk-count) is filled withT("cnt.on", { n: list.length }),and
cnt.onrenders "{n} keys on sale" / "{n} 个在售 key" — a key count.But
listis the filtered model-row list (marketRows()→/api/models), so the number is arow count. The same table's own header is
mk.col.providerModel("Provider / model" /"厂商 / 模型"), and each row carries its own key pill ("Available · 3 keys" / "No key").
The widget counts one thing and names another, and one screen says it twice.
With the probe fixture (4 model rows, 6 keys in total) the label reads "4 keys on sale" — a number
that is not the key count (4 ≠ 6) and a unit that contradicts the "No key" pill two lines below
it.
This change renames the pack key
cnt.on→cnt.models, updates its two values, and updates the twocall sites, so the count speaks the unit of the rows it actually counts. After the change the toolbar
reads "4 models" / "4 个模型".
Root cause and provenance — drift, not a deliberate trade-off
docs/prototype/aitokenpool-console.html)writes
$("#mk-count").textContent = "共 " + list.length + " 个模型";— the count was modelsfrom the start. The word "在售" (on sale) appears nowhere in that file.
d70e032(feat(ui): AITokenPool static HTML UI prototype (marketplace + enterprise + login) #7) is where the live page diverged;68f9f70(feat(ui): i18n — zh/en language packs with switching (v1.21) #86, the i18n extraction) carried thedivergent literal verbatim into the packs and named it
cnt.on(on= "on sale" — a name thatdescribes keys).
missed this cell.
dao::list_models_with_availability), andmarketRows()funnels every row through
modelsToView(Live.models)— so "rows" and "models" are the same sethere; only the label disagrees.
Because the name
cnt.onalso lies once the value becomes a model count, this renames the keyrather than only retyping the value. A rename keeps the pack key count identical (one name changed,
none added or removed) and the
T("…")literal count identical, so no i18n gate constant needsrecalibration.
Changes
ui/js/i18n.js(zh pack)"cnt.on": "{n} 个在售 key"→"cnt.models": "{n} 个模型"ui/js/i18n.js(en pack)"cnt.on": "{n} keys on sale"→"cnt.models": "{n} models"ui/js/app.js(empty/failure fill)T("cnt.on", { n: 0 })→T("cnt.models", { n: 0 })ui/js/app.js(list fill)T("cnt.on", { n: list.length })→T("cnt.models", { n: list.length })src/i18n_pack.rsthe_marketplace_count_is_expressed_in_the_unit_of_its_rows(+ its self-check test)ui/README.mdui/index.htmljs/i18n.jsandjs/app.js(procedure: read the live?v=value and write a strictly newer one — never copy a literal out of a plan)The gate (E5) — five rules, each with its own tooth
The gate derives both of its rulers instead of hard-coding a wording:
#mk-count'scontent in
app.jsmust take its value throughT("<literal>"), and all sites must name thesame key. If a site names more than one key, every key is judged by rules 1/3/4.
data-i18nin the<thead>of the table containing#mk-body), and its value must be of the formA / B(otherwise the noun cannot be isolated → the rule fails loudly).
模型/model).(ruler = the
share.col.keyvalue).mk.avail.family, so theruler cannot be silently swapped.
Ruler 4 is deliberately taken from the sharing view's column header, not from the market row's own
pill: if the ruler were read off the very element under suspicion, the test would be circular.
Evidence — A/B
Gate A/B (authoritative; 11 trees, every leg's expectation declared next to its verdict)
base(the defect)fix(this PR)m_zh_only(only zh fixed)m_en_only(only en fixed)m_cosmetic(append the row word, keep the key word)m_pill_weakened(fix + strip the key word from the market pills)m_ruler_renamed(fix + rename the ruler's zh value)m_header_shape(fix + row header loses itsA / Bshape)m_no_fill(fix + delete one fill site)P0b/F1/T1legs reject itm_diff_keys(fix, then the 2nd site names another key)m_hardcoded(fix, then the 2nd site writes a literal)Result: ALL LEGS AS DECLARED.
clippycontrol on the assembled tree: without the new blockrc=0, no diagnostics; with the block rc=0, no diagnostics — so no diagnostic can be
attributed to the new block.
The
basetree is built by reverse-applying the same byte-exact rename to the live pack (and theharness refuses to run unless the live tree is in the post-fix state), so the baseline is explicit
rather than implied by whatever happens to be checked out.
DOM probe (
c2172_probe.js, jsdom, 14 legs)Two independent facts are printed per leg — the declared expectation and the measured verdict — never
collapsed into one column.
14/14 as declared; the axis legs{A1,A2,A3,A4}fail exactly:"model", count"4 keys on sale""模型", count"4 个在售 key"while
F1/F2show the number equals the row count (4) and is not the key total (6).14/14 as declared; count"4 models"/"4 个模型".R1(the sharing page does count keys and its rows are keys — anti over-correction:the rule is "say what you count", not "never say key") and
T1(filtering narrows the rows and thecount follows them).
Five competing fixes were re-derived from the pre-fix baseline and each is rejected by an independent
leg (a competitor that only changes the wording is not accepted):
A1,A2A2,A4F1,T1(and the label legs)P0b,F1,T1K2Tests
cargo test— 314 passed, 0 failed on this branch (baselinemain= 312; +2 = the newgate test and its self-check).
cargo fmt --checkclean.cargo clippy --all-targets -- -D warningsclean.discriminating would fail rather than pass silently.
js/i18n.js?v=…and onejs/app.js?v=…tag remain, and eachcarries the newly written token.
What this PR deliberately does NOT do
cnt.on→cnt.modelsis a rename), so the i18npositive-control constants (
ZH/EN_KEY_COUNT,T_LITERAL_COUNT) are unchanged andUNREACHABLE_PACK_KEYSis untouched.mk.count, the value-identical orphan that used to becnt.on's twin: thatkey was already dropped by the earlier
pack-key-shrinkchange (PR chore(i18n): drop 23 keys that no consumer can reach #270), which is why the anchorfor this rename carries the key name and not only the old value.
R1/T1above pin both halves of that.
Related Issue
(No upstream issue — internal consistency fix found by the project's own maintenance pass.)
Checklist
fix/).