Skip to content

test(ui): guard the language-pack invariants that were previously unchecked - #170

Merged
argszero merged 1 commit into
mainfrom
test/i18n-pack-invariants
Sep 11, 2026
Merged

argszero merged 1 commit into
mainfrom
test/i18n-pack-invariants

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

The ui/ layer had no automated coverage at all. Every one of the 148 existing tests is an inline #[cfg(test)] module under src/, and nothing read ui/. So three classes of defect were only detectable by eye:

  • key-set drift between the zh and en packs (a key added to one language only),
  • a data-i18n* attribute naming a key that does not exist,
  • a 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 under cfg(test) so its include_str!-embedded front-end sources never enter the release binary. No production code is touched and no dependency is added (the repo has no ui/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.md explicitly welcomes as a direct PR ("外部贡献者同样欢迎:开 Issue 或直接 PR").

Changes

  • src/i18n_pack.rs (new) — the invariant tests
  • src/main.rs — one #[cfg(test)] mod i18n_pack; declaration
  • No configuration or data-structure changes, so no example-file updates are needed

Four assertions:

  1. the zh and en packs contain exactly the same key set;
  2. neither pack contains a duplicate key (the later definition silently wins);
  3. every data-i18n / -ph / -title / -label attribute resolves in both packs;
  4. every T("literal") resolves in both packs, excluding dynamic concatenation prefixes such as "share.day." (which expands to share.day.1….7).

Notes on the extraction, since it is the part that can silently lie

  • Byte scan, not a line-anchored regex. share.day.1….6 share 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.
  • The pack region ends at the object literal's own closing brace, not at the later window.I18N export — the export sits thousands of characters further on, passed the runtime code, and scanning through it reads the ternary current === "en" ? "en" : "zh-CN" as an object key named en.
  • Expected counts are asserted as positive controls, and a failed extraction panics instead of continuing. A truncated extraction yields empty sets, and a key-set equality assertion passes on two empty sets. That is a real failure mode I hit while developing this.
  • checker_detects_injected_defects is 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 failed
  • cargo fmt --check passes
  • cargo clippy --all-targets reports nothing for the new file
  • New tests added (this PR is entirely tests)

Verified 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.points in 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

  • Branch name follows the convention (test/…; the guide lists feat//fix//docs//refactor/ and this is none of them, but adding a test is precisely the Conventional Commits test type the guide sanctions)
  • Commit message uses Conventional Commits (test(ui): …)
  • Single responsibility, minimal diff: one new file plus one module declaration

…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.
@argszero
argszero merged commit fcff194 into main Sep 11, 2026
1 check passed
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>
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant