Skip to content

docs(rules): guardrail against raw-key i18n leaks (follow-up to #2501)#2502

Draft
merchako wants to merge 3 commits into
mainfrom
add-localized-string-fallback-rule
Draft

docs(rules): guardrail against raw-key i18n leaks (follow-up to #2501)#2502
merchako wants to merge 3 commits into
mainfrom
add-localized-string-fallback-rule

Conversation

@merchako

@merchako merchako commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Why

#2501 fixed a case where raw localization keys (e.g. %webView_scope_selector_range%) leaked into the UI: several lib/platform-bible-react/ components resolve localizedStrings[key] ?? key, so a missing key falls back to the raw key. That class of bug will keep recurring unless the guardrail is written down somewhere agents and contributors actually read.

This PR is my stab at that guardrail — a starting point, not a finished spec. I'd like a dev to take it from here and shape it into whatever the team actually wants to enforce.

What's here (a first cut)

  • New agent rule .claude/rules/code-quality/localized-string-fallbacks.md: resolve a missing string to an English default (?? 'Range'), never ?? key; and type story fixtures Required<XxxLocalizedStrings> so a dropped key fails to compile.
  • Corrected .claude/rules/architecture/react-patterns.md — it listed the localization/ARIA paranext/* rules under "What's Enforced by Linting," but CI's npm run lint doesn't run them (they live in .eslintrc.ai.js / lint:ai-strict and are off for stories). Restructured as CI-enforced vs. advisory-only.
  • Fixed the same overstatement in .context/standards/Localization-Guide.md.

Docs/rules only — no code paths change.

Open questions for whoever picks this up

  • Right home / right mechanism? Is a .claude/rules agent-rule the right vehicle, or should this be a real lint rule (e.g. paranext/no-raw-key-fallback flagging strings[key] ?? key)? Honest caveat: the paranext/* plugin rules aren't wired into CI lint today, so a new rule buys nothing until .eslintrc.js adopts the plugin's recommended config — a bigger, separate decision I didn't want to make unilaterally.

  • How far to push the Required<> story-fixture convention — just these components, or library-wide?

  • The actual source fix (converting the 6 remaining raw-key components to English fallbacks, and correcting the Localization-Guide precedent list) is tracked in PT-4103 — this PR is only the guardrail/guidance, not the fix.

  • Governance / review path: .context/standards/Localization-Guide.md is a broader-review file per .claude/rules/where-to-add-guidance.md — standards changes need broader dev sign-off, not just a quick rubber-stamp. Route that file's change to the right reviewer.

  • Pairs with PT-4103: the guide's "established precedent" list still names raw-key components as English-fallback exemplars; the ⚠️ note this PR adds is a band-aid until PT-4103 converts those components and corrects the list. Treat docs(rules): guardrail against raw-key i18n leaks (follow-up to #2501) #2502 (the rule) and PT-4103 (the conformance) as a pair.

Verification

Docs only. The ESLint-enforcement claims were checked against .eslintrc.js, .eslintrc.ai.js, package.json, .github/workflows/test.yml, and lib/eslint-plugin-paranext/.

AI Involvement

AI-assisted (Claude Code): ran the retro that surfaced the guidance gap, drafted the rule, verified the config claims, and ran an adversarial review that tightened the wording. Reviewed before commit — but this is explicitly a draft for a human to own and finish.


This change is Reviewable

Resume this session: claude --resume 63aed4b7-897a-4465-80ad-b0aa34d4a0c2 (run from the fix-localisation-in-storybook-scope-selector worktree).

merchako added a commit that referenced this pull request Jul 3, 2026
Adversarial review feedback on #2502:
- Rule file: cite BookChapterControl/CommentEditor (verified direct English-fallback
  reads) instead of CommentList (which only forwards the prop; the fallback lives in its
  child CommentThread); describe the `strings[key] ?? key` resolver as a copy-pasted local
  helper, not a single shared one; collapse the triplicated lint caveat to one line + link;
  trim to house length.
- react-patterns.md: restructure "What's Enforced by Linting" into explicit
  CI-enforced vs. advisory-only groups so the heading no longer implies the advisory
  rules are enforced.
- Localization-Guide.md: fix the adjacent overstatement that
  `paranext/no-hardcoded-jsx-strings` "is enforced" — it runs only under lint:ai-strict.
@merchako merchako changed the title docs(rules): localized-string fallback rule + correct lint-enforcement claim docs(rules): guardrail against raw-key i18n leaks (follow-up to #2501) Jul 3, 2026
merchako added 2 commits July 6, 2026 20:29
…ment claim

Adds `.claude/rules/code-quality/localized-string-fallbacks.md` codifying two guardrails
for `lib/platform-bible-react/` components, so agents stop reintroducing the raw-key leak
just fixed in the ScopeSelector/Inventory stories (PR #2501):

- Resolve missing localized strings to an English default (`?? 'Range'`), never to the
  key (`?? key`), which renders the raw `%…%` token on screen.
- Type story fixtures as `Required<XxxLocalizedStrings>` so a dropped key fails to compile.

Also corrects `.claude/rules/architecture/react-patterns.md` "What's Enforced by Linting":
the localization/ARIA/color `paranext/*` rules run only under `npm run lint:ai-strict`
(`.eslintrc.ai.js`) — not the CI `npm run lint` — and are disabled for stories/tests, so
they are not a safety net. Adds a cross-reference from the Localization-Guide, flagging that
several listed components currently deviate with a raw-key fallback (cleanup tracked separately).
Adversarial review feedback on #2502:
- Rule file: cite BookChapterControl/CommentEditor (verified direct English-fallback
  reads) instead of CommentList (which only forwards the prop; the fallback lives in its
  child CommentThread); describe the `strings[key] ?? key` resolver as a copy-pasted local
  helper, not a single shared one; collapse the triplicated lint caveat to one line + link;
  trim to house length.
- react-patterns.md: restructure "What's Enforced by Linting" into explicit
  CI-enforced vs. advisory-only groups so the heading no longer implies the advisory
  rules are enforced.
- Localization-Guide.md: fix the adjacent overstatement that
  `paranext/no-hardcoded-jsx-strings` "is enforced" — it runs only under lint:ai-strict.
@merchako
merchako force-pushed the add-localized-string-fallback-rule branch from f314077 to e78be3f Compare July 7, 2026 01:30
@merchako

merchako commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Antagonistic AI-Assisted Review — PR #2502

Provenance: Adversarial review generated with Claude Code. Every finding is backed by grep/read evidence against the actual source. Human reviewers should verify independently.

Two-PR context: #2501 is the Storybook fix (merge-ready). This PR (#2502) is the guardrail rule. PT-4103 is the source fix (converting 6 remaining raw-key-fallback components to English defaults). #2502 and PT-4103 are designed to travel together — the rule tells agents/contributors what the contract is; PT-4103 makes the codebase conform to it.


ESLint Enforcement Claims

The PR's main contribution is separating "CI-enforced" from "advisory-only" lint rules. I verified each claim against .eslintrc.js, lib/platform-bible-react/.eslintrc.cjs, .eslintrc.ai.js, .github/workflows/test.yml (line 213: npm run lint), and lib/eslint-plugin-paranext/src/index.ts.

The localization/ARIA/colors split is correct. paranext/no-hardcoded-jsx-strings, paranext/require-localized-aria, and paranext/no-hardcoded-tailwind-colors are confirmed in .eslintrc.ai.js only, with stories/tests excluded at .eslintrc.ai.js lines 95-109. They are absent from .eslintrc.js. ✔

The tw: prefix claim is NOT verified. The new react-patterns.md states:

Enforced by CI (npm run lint, via .eslintrc.js) — don't duplicate:

  • Tailwind tw: prefix on utility classes

I searched .eslintrc.js, lib/platform-bible-react/.eslintrc.cjs (which extends ../../.eslintrc.js), and lib/eslint-plugin-paranext/src/index.ts — there is no ESLint rule enforcing the tw: prefix on JSX className values. The prefix is configured via prefix(tw) in the Tailwind CSS import (lib/platform-bible-react/src/index.css line 21: @import 'tailwindcss/utilities.css' layer(utilities) prefix(tw)), but missing the prefix causes silent no-op styling, not a build or lint failure. The @dreamsicle.io/stylelint-config-tailwindcss in .stylelintrc.js covers CSS/SCSS files, not JSX classNames.

This claim was in the original react-patterns.md too ("enforced by the project's Tailwind/ESLint config"), but PR #2502 strengthens it by attributing it specifically to CI npm run lint and .eslintrc.js — making a dubious carry-forward claim more assertive. A developer reading this rule will believe the prefix is machine-enforced when it isn't.

Governance: .context/standards/Localization-Guide.md Edit

where-to-add-guidance.md states .context/standards/ changes require "broader dev review" (any-team ownership). This PR modifies .context/standards/Localization-Guide.md. The PR body does not flag this requirement. The dev picking this up should be aware that the Localization-Guide.md change needs broader review sign-off before the PR merges — not just Dev-team approval.

Localization-Guide "Established Precedent" List

The guide's precedent list (BookChapterControl, BookSelector, MarkerMenu, Inventory, ScopeSelector, CommentEditor, CommentList, FootnoteEditor, UndoRedoButtons, ErrorPopover, ErrorDump) implies all those components use English-default fallbacks. But a grep confirms that at least BookSelector, UndoRedoButtons, Inventory, ScopeSelector, ErrorDump, CancelAcceptButtons, UiLanguageSelector, and ResourcePickerDialog still use strings[key] ?? key. PR #2502 adds a warning note below the list, which is better than nothing, but the list itself is still misleading — it presents raw-key components as "established precedent" for English-fallback behavior. The dev picking this up should know the precedent list will remain wrong until PT-4103 lands; the warning note is a band-aid.

Rule File Accuracy

  • BookChapterControl as exemplar — confirmed: book-chapter-control.component.tsx lines 440, 442 use ?? 'Select Chapter', ?? 'Select Verse'. ✔
  • CommentEditor as exemplar — confirmed: comment-editor.component.tsx lines 94, 97, 148, 149 use ?? 'Unassigned', ?? 'Team', etc. ✔
  • "copy-pasted, not shared" helper description — confirmed: each of the 8 components defines its own const localizeString = ... independently. ✔
  • Stories disabled in .eslintrc.ai.js — confirmed at lines 107-108: **/*.stories.ts, **/*.stories.tsx excluded from localization rules. ✔

Findings

[SHOULD-FIX before hand-off] .claude/rules/architecture/react-patterns.md — unverified tw: prefix CI-enforcement claim

File: .claude/rules/architecture/react-patterns.md, "Enforced by CI" bullet point.

The new text says the tw: prefix is "Enforced by CI (npm run lint, via .eslintrc.js)". No such ESLint rule exists in .eslintrc.js or lib/platform-bible-react/.eslintrc.cjs. Either verify the actual enforcement mechanism and name it precisely, or move this bullet to an "advisory" note, or drop it and note that the prefix is configured but not linted. Leaving the current text hands a developer a false belief that CI will catch missing tw: prefixes on JSX classNames.

[NIT] PR body doesn't mention the .context/standards/ governance flag

The PR body doesn't note that Localization-Guide.md edits need broader dev review per where-to-add-guidance.md. Low risk since the change is a correction of an overstatement, but the dev picking this up should know to get the right sign-off before merging.

[NIT] The Localization-Guide "precedent" list will still be inaccurate after this PR

The warning note added by this PR acknowledges the deviation, but the list still names raw-key components as precedent for English-default behavior. The dev should understand this will remain misleading until PT-4103 corrects the list — ideally those two land together.


Verdict: OK to hand off — directionally sound, but the tw: prefix claim needs resolution before the dev merges it

The new localized-string-fallbacks.md rule is accurate and useful. The localization/ARIA/colors CI-enforcement correction is correct and well-evidenced. The tw: prefix issue is the one factual error that would mislead a future developer if shipped as-is. The governance flag is a process note, not a blocker, but the dev should be aware of it.

Adversarial review caught that the rewritten "What's Enforced by Linting" section
listed the Tailwind `tw:` prefix under "Enforced by CI" — but no lint rule enforces it.
The prefix comes from the Tailwind v4 config (`prefix(tw)` in index.css + twMerge); a
missing prefix is a silent no-op, not a build/lint error. Reframed the section as "what
tooling catches (and doesn't)" and stated the prefix mechanism accurately, so this rule
stops making the same kind of over-claim it exists to correct.
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