fix(i18n): stop filling the marketplace success-rate placeholder with a dash - #308
Merged
Merged
Conversation
… a dash
The marketplace expanding row rendered the availability line as
"可用 · 成功率 —%": mkDetailHtml() turned a missing m.success into the
display sentinel "—" and passed it into the {p} placeholder of
mk.detail.availOn/availOff, whose messages read "… 成功率 {p}%". The dash
was therefore printed as if it were the number, together with the unit.
The same condition (m.success == null) already exists elsewhere in the row
(renderMarketplace guards its cell with m.success != null), and the
repository already has the right shape next to it: render admin.org.stats.used
only calls the {p} template when there is a value.
The fix splits the message into a placeholder-free form (mk.detail.availOn /
availOff) and a form that carries the rate (mk.detail.availOnRate /
availOffRate), and picks between them on m.success == null — the sentinel
never enters a placeholder.
A new compile-time gate, a_placeholder_is_never_filled_by_a_display_sentinel
(gate module ui/js/app.js is already inside), rejects any T(key, {…}) call
site whose placeholder is fed by a string literal, or by an identifier the
enclosing function declares from a conditional whose top-level operand is
itself a string literal. The predicate is deliberately narrow — the literal
must be a whole operand — so the look-alike but legitimate forms
(raw || T("common.unnamed"), typeof x === "number",
v === "" ? T("common.unassigned") : …) keep passing; measured on the real
corpus they would otherwise produce 4-5 false positives, and whole-file name
resolution produces 5 more because n/name/model are reused all over app.js.
Scope: the gate is lexical — it proves the value fed to a placeholder is not
a display sentinel, not that the rendered number is correct (that half belongs
to a DOM probe).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The marketplace row's expanding detail printed its availability line as
mkDetailHtml(m)computedconst succ = m.success == null ? "—" : m.success;and passedsuccinto the{p}placeholder ofmk.detail.availOn/mk.detail.availOff, whose messagesare
当前可用 · 成功率 {p}%/Available · success {p}%. The display sentinel—("no suchvalue") was therefore rendered as if it were the number, together with the
%that followsthe placeholder.
Both rendering paths reach that branch:
D.MARKET(guest rows) has nosuccessfield at all,and
modelsToView()hard-codessuccess: nullfor live rows — so every row takes it.The same condition already exists elsewhere in the same row (
renderMarketplaceguards its cellwith
m.success != null), and the repository already contains the correct shape right next to thedefect:
admin.org.stats.usedonly calls its{p}template when there is a value, and putsthe dash outside the template.
Related Issue
(none)
Changes
ui/js/app.js—mkDetailHtmlpicks between a placeholder-free message and arate-carrying one on
m.success == null; the sentinel never enters a placeholder.ui/js/i18n.js— splits the two messages into a placeholder-free form(
mk.detail.availOn/availOff) and a form that carries the rate(
mk.detail.availOnRate/availOffRate), zh and en. Two new keys per pack.src/i18n_pack.rs— new compile-time gatea_placeholder_is_never_filled_by_a_display_sentinel, plus its teeth test. The sharedTCallSitescanner now also returns each interpolation's value source (obj_var_entriesis the single pass;
varsis derived from it, so "who was fed what" cannot drift from "whowas fed"), and the positive-control key/T-literal counts move to the values the gates report
(794 → 796 keys, 544 → 546
T(...)call sites).ui/README.md— records the convention and the gate's scope.ui/index.html— cache-bust fori18n.jsandapp.js.The gate's predicate, and why it is narrow
The rule fires when a value bound to a placeholder is a string literal, or a bare identifier
that the enclosing function declares from a conditional/logical expression one of whose
top-level operands is itself a whole string literal:
A wider reading ("the initializer mentions a string literal somewhere") was measured against the
real corpus and reports 4–5 false positives; resolving identifiers file-wide instead of
per-function adds 5 more, because
n/name/model/themeare reused acrossui/js/app.js. The narrow reading reports exactly the two defective call sites on the pre-fixtree and zero on the fixed tree, with 95 interpolation slots scanned.
Tests
cargo test— 409 passed / 0 failed (baseline 407; the two new tests are the gate andits teeth test)
cargo fmt --check— cleancargo clippy --all-targets -- -D warnings— cleana_placeholder_is_never_filled_by_a_display_sentinel— with a positive control on the numberof interpolation slots actually scanned (95 measured, floor 90), so the gate cannot pass by
silently reading nothing.
the_sentinel_rule_has_teeth— four shapes that must fire, four look-alikes that must pass,plus an end-to-end synthetic corpus (defect reports, clean version does not) and a
cross-function tooth (a sentinel declared in another function must not leak in).
A/B evidence
Compiling the new gate against the pre-fix tree (
4314851'sui/js/app.js+ui/js/i18n.js, with the positive-control counts set back to their pre-fix values) failsexactly one test — the new gate — with 408 passing. Restoring the fixed tree gives
409 / 0 (the two cache-bust strings in
ui/index.htmlare bumped in the committed tree).Checklist
fix/…)Scope, stated honestly
The gate is lexical: it proves the value fed into a placeholder is not a display sentinel —
it does not prove the number rendered on screen is correct (that half belongs to a DOM
probe), and it does not see the competing "fix" of deleting the rate clause entirely. This is
recorded in
ui/README.mdnext to the convention.One pre-existing staleness is not touched here: the same README still says the packs hold
"806 键 ×2", which does not match the gate's own key count (now 796). Fixing a declared count is
its own change — it needs the authority (which collection the number counts) settled first — and
is left out of this PR deliberately.