Skip to content

lint: validate-translation-references builds its _validations universe from top-level rules only, so a nested conditional branch's legitimate bundle entry is reported as an orphan #14700

Description

@os-musk

What

validate-translation-references collects the _validations universe with a flat walk of objects[].validations[]:

// packages/lint/src/validate-translation-references.ts:629
for (const rule of asArray(obj.validations)) {
  const ruleName = strName(rule.name);
  if (ruleName) facts.validations.add(ruleName);
}

A conditional rule's then / otherwise branch is a full rule carrying its own name, and that branch name is the address the resolver uses — checkConditional delegates to the branch and authoredRuleMessage keys on the branch's rule.name. So a bundle entry for a branch is correct and resolves at runtime, but this walk never sees the name and the rule reports it as an orphan.

Measured

Against the built rule (packages/lint/dist/index.js), a stack with one conditional carrying two named branches and bundle entries for all three names:

findings on _validations keys: 2
 - translation-target-unknown | translations[0]["zh-CN"].objects.demo_account._validations.churn_reason_present
   Translations are keyed to validation rule "churn_reason_present", which object "demo_account" does not
   declare in `validations[]`. The rule's authored `message` keeps its source locale in every refusal.
 - translation-target-unknown | translations[0]["zh-CN"].objects.demo_account._validations.churn_reason_absent
   (same)

The wrapper's own name (churn_reason_consistency) is accepted; the two branches — the only two whose messages a caller ever reads — are flagged.

Two things are wrong at once:

  1. False positive. The entry is legitimate. packages/objectql/src/validation/rule-validator.test.ts pins the behaviour directly: it('addresses a nested conditional branch by the BRANCH's own name', ...), with the wrapper's entry present and deliberately NOT rendered.
  2. The advice is inverted. The finding says the authored message "keeps its source locale in every refusal" — the opposite of the truth for these keys. Acting on it (deleting the entry) reintroduces the exact defect the entry fixes, and the message reads as authoritative.

Blast radius today

Severity is warning, and the rule id is translation-target-unknown — not the i18n/ prefix scripts/check-i18n-coverage.mjs counts (countI18nRuleIssues filters rule.startsWith('i18n/')), so no gate goes red. It is noise plus wrong advice, not a build break.

It is live in the tree as of #14518: showcase_account now carries churn_reason_present / churn_reason_absent entries, which is what a nested conditional requires.

Suggested shape

Descend into then / otherwise when building ObjectFacts.validations, the same recursion evaluateRule performs. Same shape as the missing-leg family (#13835 _tabs, #11608 flows), except the leg exists here and its universe is short.

Worth checking in the same pass whether a conditional wrapper's own name should stay in the universe. Its message is structurally unreachable (checkConditional either returns nothing, returns unevaluableRuleError — which builds its own sentence — or delegates), so an entry for it is inert. #14518 keeps one deliberately, so the bundle mirrors the declared rule set 1:1 and its pin can ask for every declared name rather than re-deriving objectql's dispatch; if the rule ever grows a "redundant, never rendered" verdict, that is the shape to weigh it against.

Where

  • packages/lint/src/validate-translation-references.ts:629 — the flat walk.
  • packages/objectql/src/validation/rule-validator.tscheckConditional / authoredRuleMessage, the addressing this must mirror.

Found while implementing #14518; outside that card's declared file surface, so recorded here rather than widened into its PR.

Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions