test(ui): guard the language-pack invariants that were previously unchecked - #170
Merged
Merged
Conversation
…hecked
The UI layer had no automated coverage at all: all 148 existing tests are
inline #[cfg(test)] modules under src/, and nothing read ui/. Key-set parity
between the zh/en packs, the resolvability of every data-i18n* attribute, and
the resolvability of every T("...") literal were therefore only verifiable by
hand. A missing or misspelled key renders the raw key name to the user (e.g.
admin.emp.col.member) and is invisible until someone looks at that screen in
that language.
Add src/i18n_pack.rs, compiled only under cfg(test) so the embedded ui/
sources never enter the release binary. It statically scans the front-end
sources (no dependency is added; the repo has no ui/package.json) and asserts:
- the zh and en packs contain exactly the same key set;
- neither pack contains a duplicate key (the later one silently wins);
- every data-i18n / -ph / -title / -label attribute resolves in both packs;
- every T("literal") resolves in both packs, excluding dynamic
concatenation prefixes such as "share.day." (which expands to .1-.7).
Key extraction walks bytes instead of a line-anchored regex: several keys
share one source line, so line anchoring silently yields 770 of 775 keys.
The pack region is bounded by the object literal's own closing brace, not by
the later window.I18N export, which would let the scan stray into runtime code
and read the ternary "en" : "zh-CN" as an object key.
The expected counts are asserted as positive controls, and check failure
panics rather than continuing, so an empty or truncated extraction can never
make the parity assertion pass on two empty sets.
cargo test: 153 passed (148 existing + 5 new); cargo fmt --check and
cargo clippy --all-targets report nothing for the new file.
8 tasks
argszero
added a commit
that referenced
this pull request
Sep 11, 2026
… require (#171) The four key-resolution tests added in #170 check that every key exists, but nothing checked that a call site actually passes the variables the text needs. ui/js/i18n.js leaves the placeholder verbatim when a variable is missing, so T("cnt.calls") renders as {n} 次 / {n} instead of 1,234 次 / 1,234 - a defect that fails no existing assertion. Adds one assertion class to src/i18n_pack.rs plus its negative control: - every T("key", { ... }) call site in ui/js/app.js supplies all placeholders that either pack uses for that key (487 sites, 0 violations today) - no value bound by a static data-i18n* attribute contains a placeholder - those are written by applyStatic() with innerHTML and have no argument channel (305 keys, 0 today) The call-site scanner reuses the module's existing T("literal") rule rather than reimplementing it (a "smarter" draft counted 517 sites instead of 520, i.e. a different set than every_t_literal_resolves). Tests only: no production code, no new dependency, no language-pack or ui/ change. Co-authored-by: argszero <argszero@argszerodeMac-mini.local>
8 tasks
argszero
added a commit
that referenced
this pull request
Sep 27, 2026
… claims (#310) The i18n section of ui/README.md described counters and guard states the code has moved past: a reader today learns numbers the gates no longer report and reads about a missing guard that in fact exists. - The pack size was frozen as "806 keys x2". That was true when 5183e41 (#249) wrote it; the positive-control constants in src/i18n_pack.rs own the real value now and fail loudly whenever the packs change. - The next line restated three more counts at once — "431 T() literals", "305 data-i18n*", "681-key union". The first two came from 818ed88 (#248) and fcff194 (#170); the 681 has no owner at all, neither a constant nor the smoke-test file it names. - The nested-data-i18n A/B note claimed orphan keys were unguarded ("today nobody guards them, 60-odd unreachable keys"), but every_pack_key_reaches_a_consumer landed two PRs later (a4cb622, #266), so the competing fix m_drop_parent is in fact rejected today: it makes the key unreachable while the sunset list stays put, and that gate compares the two exactly. The note also contradicted the section below it, which documents that very gate. - In the same section, shrinking the sunset list was still described as a follow-up round, although 77b2a82 (#270) landed it. A number a gate already owns is a second carrier of one fact, and restating it is how this drift happened; so the counts are dropped and the text points at the mechanism instead — the same treatment 45510ba (#309) gave the perf-gate header. The 681 is simply removed, since nothing carries it. No gate is added for prose: this repository has already ruled that documentation claims are corrected as data, not fenced in by a new test.
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
ui/layer had no automated coverage at all. Every one of the 148 existing tests is an inline#[cfg(test)]module undersrc/, and nothing readui/. So three classes of defect were only detectable by eye:zhandenpacks (a key added to one language only),data-i18n*attribute naming a key that does not exist,T("…")literal naming a key that does not exist.All three render the raw key name to the user (
admin.emp.col.member,nav.mian, …). They are invisible until someone opens that one screen in that one language — which is exactly the kind of thing a test should be holding, not a reviewer's attention.This adds
src/i18n_pack.rs, compiled only undercfg(test)so itsinclude_str!-embedded front-end sources never enter the release binary. No production code is touched and no dependency is added (the repo has noui/package.json, so the JS toolchain is not available on purpose — the scan is written directly).Related Issue
None — this is a small, self-contained test addition, which
CONTRIBUTING.mdexplicitly welcomes as a direct PR ("外部贡献者同样欢迎:开 Issue 或直接 PR").Changes
src/i18n_pack.rs(new) — the invariant testssrc/main.rs— one#[cfg(test)] mod i18n_pack;declarationFour assertions:
zhandenpacks contain exactly the same key set;data-i18n/-ph/-title/-labelattribute resolves in both packs;T("literal")resolves in both packs, excluding dynamic concatenation prefixes such as"share.day."(which expands toshare.day.1….7).Notes on the extraction, since it is the part that can silently lie
share.day.1….6share one source line, so line anchoring reports 770 of 775 keys. A check that under-counts is worse than no check, because it looks like it passed.window.I18Nexport — the export sits thousands of characters further on, passed the runtime code, and scanning through it reads the ternarycurrent === "en" ? "en" : "zh-CN"as an object key nameden.checker_detects_injected_defectsis a negative control: with both packs read correctly, it injects a removed key and a misspelled key and asserts the checker reports them — so the checker is shown to be capable of failing.Tests
cargo test— 153 passed (148 existing + 5 new), 0 failedcargo fmt --checkpassescargo clippy --all-targetsreports nothing for the new fileVerified beyond the suite: injecting a defect and re-running makes the corresponding assertion fail with the offending key named, and restoring it returns the suite to green — e.g. renaming
common.pointsin both packs leaves the key counts (775/775) and parity intact, yet both resolution tests fail with以下 … 不存在:["common.points"]. That isolates the resolution check from the size controls.Checklist
test/…; the guide listsfeat//fix//docs//refactor/and this is none of them, but adding a test is precisely the Conventional Commitstesttype the guide sanctions)test(ui): …)