Skip to content

fix(ui): resolve backend error messages by longest match instead of first match - #164

Merged
argszero merged 1 commit into
mainfrom
fix/err-map-shadowing
Sep 11, 2026
Merged

argszero merged 1 commit into
mainfrom
fix/err-map-shadowing

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

ERR_MAP in ui/js/i18n.js maps the backend's Chinese error strings to English translation keys, and mapErr() returned on the first entry whose literal was a substring of the message. The table mixes generic and specific literals, so two specific entries were permanently unreachable:

literal key shadowed by
验证码不存在或已过期 err.codeExpired 不存在 → err.notFound
验证码错误次数过多 err.codeLocked 验证码错误 → err.badCode

In English mode this made the verification-code flows show the wrong text, even though both correct translations already existed in the pack:

  • 验证码不存在或已过期,请重新获取 → "Resource not found" (should be "Code invalid or expired, please request a new one")
  • 验证码错误次数过多,请重新获取 → "Incorrect verification code" (should be "Too many wrong attempts, please request a new code")

The backend messages live at src/routes/mod.rs:284, :291 (reset-password) and :394, :401 (email verification); mapErr is applied to every user-visible error message in app.js, so the wrong text is what users actually see.

An all-pairs sweep of all 22 entries found exactly two shadowing pairs, both with different keys — only these two mappings were affected. No other message changes.

Related Issue

Changes

  • ui/js/i18n.js — mapErr() now selects the longest matching literal instead of the first one, which makes resolution independent of table order. The table is additionally reordered longest-first so the most specific entries read first and newly inserted entries cannot reintroduce the same problem. The mapping set is unchanged (22 entries in, 22 out; none lost, added, or duplicated).
  • ui/index.html — cache-bust ?v=20260911-8 → -9 (5 references).

No new keys and no new vocabulary: both err.codeExpired and err.codeLocked already existed.

Tests

  • cargo test — 148 passed; 0 failed
  • cargo fmt --check — clean
  • cargo clippy --all-targets -- -D warnings — clean
  • node --check on all four ui/js/*.js — clean
  • New unit tests added (n/a — see note below)

Verification of the actual behaviour (the repo has no JS test harness — its convention is Rust #[cfg(test)], and CI runs fmt/clippy/test only, so I did not add a new JS test runner as that would exceed the scope of this fix):

  • Replayed the real backend messages through the real mapErr in EN mode — both now resolve to the specific entry, and previously-correct mappings (点数余额不足, 该邮箱已注册, the generic 不存在 fallback) are unchanged.
  • Order-independence proof: 200 random shuffles of the table × 19 real messages → 0 mismatches. Running the same shuffling against the previous first-match algorithm yields 288 mismatches, confirming the test is sensitive to order and that resolution no longer depends on it.
  • Controls: zh-mode passthrough ✅, unmatched-message passthrough ✅, null input passthrough ✅.
  • Set-equality check: 22 entries before → 22 after, no loss/duplication.

Checklist

  • Branch naming follows the convention (fix/<description>)
  • Commit message uses Conventional Commits (fix(ui): …)
  • Single responsibility, minimal diff (2 files, +31/−21)

Follow-up (not in this PR)

A separate, larger gap is measurable but deliberately left out to keep this change reviewable: of the 40 distinct Chinese error strings the backend can return, 18 are covered by ERR_MAP and 22 are not, and the uncovered ones render raw Chinese in English mode (e.g. 该邮箱未注册 on the login path, src/routes/mod.rs:388). Happy to follow up with that coverage expansion if wanted.

…irst match

ERR_MAP in ui/js/i18n.js maps the backend's Chinese error strings to English
translation keys, and mapErr() returned on the FIRST entry whose literal was a
substring of the message. Because the table mixes generic and specific literals,
two specific entries were permanently unreachable:

  "不存在"          -> err.notFound    shadowed  "验证码不存在或已过期" -> err.codeExpired
  "验证码错误"      -> err.badCode     shadowed  "验证码错误次数过多"   -> err.codeLocked

so in English mode the login/verification flows showed the wrong text
("Resource not found" / "Incorrect verification code") even though the two
correct translations already existed. Only those two entries were affected;
an all-pairs sweep of the 22 entries found exactly two shadowing pairs.

Fix: select the LONGEST matching literal, which makes resolution independent of
table order, and reorder the table longest-first so the most specific entries
read first and newly inserted entries cannot reintroduce the problem.

Verified by replaying real backend messages through the real mapErr in EN mode:
  "验证码不存在或已过期,请重新获取" -> "Code invalid or expired, please request a new one"
  "验证码错误次数过多,请重新获取"   -> "Too many wrong attempts, please request a new code"
200 random shuffles of the table x 19 messages produce identical results
(0 mismatches), while the previous first-match algorithm mismatches under the
same shuffling (288) - confirming order no longer determines the outcome.
Mapping set unchanged: 22 entries in, 22 out, none lost or duplicated.
@argszero
argszero merged commit d8724c2 into main Sep 11, 2026
1 check passed
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