You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
[finding] Two spellings for "a hole i18next must NOT fill": auth.forgotPassword.successDescription uses {{email}}, its sibling three lines away uses {seconds} #4135
Measured while building objectui#3845's interpolation-parity gate — not in that PR's scope, and observation-class: the current behaviour is correct in all ten languages and nothing a user hits today is broken.
The pattern
Some translated strings are labels handed to a component as a prop, and the component substitutes into them itself, after t() has already returned. The call site cannot do the substitution because it renders before the value exists. ForgotPasswordForm is the case: it takes labels.successDescription and fills in the address only once the user has submitted the form.
That works because i18next leaves an unmatched{{name}} in its output verbatim, so the string travels through t() with its hole intact and arrives at the component still fillable. All ten packs carry the hole (en: We've sent a password reset link to {{email}}. Please check your inbox.), so this is the shape in every language, not an en quirk.
The finding: the repo already has a different spelling for exactly this
Three lines away in the same labels object, apps/console/src/pages/auth/ForgotPasswordPage.tsx, the same pattern is spelled with single braces, with a comment saying why:
// `{seconds}` (single braces) is the component's own hole — kept out
// of i18next's `{{…}}` interpolation on purpose.
resendOtpCountdownText: t('auth.forgotPassword.resendOtpCountdownText', {
defaultValue: 'Resend in {seconds}s',
So the same concept — a hole whose filler is downstream of i18next — has two spellings inside one form's label list, and only one of them is documented as deliberate. The {{email}} spelling is indistinguishable, at a glance and to every static tool, from an ordinary i18next hole that somebody simply forgot to pass an argument for.
Why it matters, and why it is dormant rather than broken
It cost the objectui#3845 census two false positives (both auth.forgotPassword.successDescription call sites), which is now handled by an explicit EXTERNALLY_INTERPOLATED_HOLES registry in scripts/check-i18n-call-site-keys.mjs — an entry with a reason, whose premise is re-verified against both the pack and ForgotPasswordForm.tsx by the self-test. That closes the tooling half.
What stays open is the authoring half, and it is a live trap in one specific direction: passing email at the call site would break it. i18next would consume the hole, includes('{{email}}') would then be false, and the fallback branch would append the address a second time — ...link to a@b.com. Please check your inbox. a@b.com. That is precisely the "fix" a reader who does not know about the second stage would reach for on seeing a {{email}} with no argument. The gate now reports that argument, so the trap is guarded; the ambiguity that invites it is not removed.
Options
A. Converge on single braces. Move {{email}} to {email} in all ten packs and update ForgotPasswordForm's marker. Makes the two spellings one, and makes "this is not i18next's hole" true by construction rather than by convention. Cost: a ten-pack value change, so it moves through scripts/check-i18n-en-drift.mjs, and it is a value edit on a string users read.
B. Leave the spelling, document it in the pack. Cheapest; keeps the ambiguity, and a pack comment is not visible from the call site where the mistake gets made.
C. Pass the value properly. Give ForgotPasswordForm an email prop it interpolates through i18next rather than by string replacement — arguably the contract-first shape, since the two-stage substitution exists only because the label is computed too early. Largest change; touches the auth component's public props.
No recommendation attached — the axes here are a public-prop contract in @object-ui/auth and a ten-pack copy edit, both outside what a gate finding should decide.
Related: objectui#3845 (the gate and its registry), objectui#3512 (createSafeTranslation's provider-less interpolation recognises only one of i18next's four hole spellings — same family, different face), objectui#3650 (the en-drift gate option A would move through).
Findings triage: held — finding stays (labels unchanged; this comment records why, so state and label agree).
Premise current by construction: filed ~25 minutes before this round against today's main; the EXTERNALLY_INTERPOLATED_HOLES registry entry plus its self-test guard the dangerous direction (passing email at the call site now trips the gate), so the live trap has a tripwire.
Why held rather than promoted or escalated: behaviour is correct in all ten languages today, and the A/B/C fork is real — A is a ten-pack value edit on user-visible copy, C changes @object-ui/auth's public props. Neither should be picked by a queue dev in passing, and the decision box should not take a question with no demonstrated pull while the gate already guards the failure mode.
Restart conditions: ① any PR touching ForgotPasswordForm's label contract or the auth.forgotPassword.* pack values — the toucher inherits the convergence question; ② a second externally-interpolated hole joining the registry (the pattern generalising is what would justify option A's cost); ③ an i18n-convention ruling that settles hole spellings repo-wide.
Re-verified @ objectui origin/main564252c: both spellings live — packages/auth/src/ForgotPasswordForm.tsx:181/:286-287 ({{email}}, double braces, filled downstream) vs apps/console/src/pages/auth/ForgotPasswordPage.tsx:57/:60 ({seconds}, single braces, documented as deliberate). The tooling half remains closed by the EXTERNALLY_INTERPOLATED_HOLES registry. Held because what remains is an authoring-convention pick (one blessed spelling for downstream-filled holes) — cheap whenever the next i18n sweep runs, harmless until then.
Maintainer ruling — 2026-08-11. From the four-lens review (platform long-term coherence / measured business pull / AI-agent error-resistance / startup scope discipline).
Ruling: Option A — converge on single braces, and the choice is established as a repo-wide authoring convention, not just this card's fix:
{{x}} (double braces) is reserved exclusively for i18next holes — a {{x}} at a call site MUST have an argument passed. A hole filled downstream of t() (component-side substitution) is ALWAYS spelled {x} (single braces), which is outside i18next's syntax and therefore safe by construction.
The work, one PR: move {{email}} → {email} in all ten packs for auth.forgotPassword.successDescription, update ForgotPasswordForm.tsx's replacement marker in the same change (atomicity pinned by #3845's interpolation-parity gate + the en-drift gate #3650), and retire the EXTERNALLY_INTERPOLATED_HOLES registry entry in scripts/check-i18n-call-site-keys.mjs — with the ambiguity gone by construction, the gate no longer needs an exemption fence there, and any remaining argument-less {{x}} is a real defect the gate may report loudly.
Rendered output is byte-identical in all languages (only the hole marker changes; the component fills it either way). Rationale weighted on the AI-error lens: an argument-less {{email}} is indistinguishable from a forgotten argument, and the natural "fix" (passing the value) triggers the double-append regression on the forgot-password success screen in all ten languages — removing the ambiguity beats fencing it.
Family note: ui#3512 (createSafeTranslation hole-spelling recognition) aligns to this same convention when it is next graded — recorded there.
State: finding → pm:queue.
Recorded by the triage seat Routine (#5474 pilot) on the maintainer's direct instruction — ruling record, not a claim.
Scope: the maintainer's 2026-08-11 ruling verbatim — Option A as one atomic PR: {{email}} → {email} in all ten packs for auth.forgotPassword.successDescription, ForgotPasswordForm.tsx's replacement marker updated in the same change, and the EXTERNALLY_INTERPOLATED_HOLES registry entry retired from scripts/check-i18n-call-site-keys.mjs (+ its self-test). Rendered output must remain byte-identical in all languages. PR will be Fixes #4135.
Serialization note: #4206 also edits check-i18n-call-site-keys.mjs and is deliberately held until this PR merges (hot-file serial queue) — its dispatch follows this card's landing.
{
"issue": 4135,
"status": "done",
"branch": "claude/issue-4135-single-brace-convention",
"pr": "https://github.com/objectstack-ai/objectui/pull/4714",
"premise_still_valid": true,
"summary": "Implemented the 2026-08-11 maintainer ruling (Option A) verbatim as one atomic PR: all ten locale packs' auth.forgotPassword.successDescription moved from {{email}} to {email}; ForgotPasswordForm.tsx's replacement marker AND its own built-in default label (line ~181, not explicitly named in the dispatch prompt but load-bearing -- it shares the same marker check) moved to the same spelling; apps/console/src/pages/auth/ForgotPasswordPage.tsx's inline defaultValue (also not explicitly named) was updated too, required to keep the default-value-drift gate green since it must byte-match the pack value. scripts/check-i18n-call-site-keys.mjs's EXTERNALLY_INTERPOLATED_HOLES registry entry for this key is retired to an empty array (mechanism kept for a future case), and its self-test rewritten to pin the retirement plus prove the gate now reports an argument-less {{hole}} loudly on this key instead of silencing it. head_sha=bb6804e21 (full bb6804e213021a81f30d5a94bf34936feedddc21). changed_files (16): .changeset/single-brace-downstream-holes-4135.md (new), apps/console/src/pages/auth/ForgotPasswordPage.tsx, packages/auth/src/ForgotPasswordForm.tsx, packages/auth/src/__tests__/forgot-password-success-message.test.tsx (new), packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts, scripts/__tests__/check-i18n-call-site-keys.test.ts, scripts/check-i18n-call-site-keys.mjs. Changesets: single-brace-downstream-holes-4135.md declares patch for @object-ui/i18n, @object-ui/auth, @object-ui/console (all private:false, all in the changeset fixed group; verified via package.json and .changeset/config.json). No skip-changeset label applies -- this PR carries a real user-visible copy/behavior change, not a test/workflow-only one. Note on report shape: this dispatch prompt's '交报' section asked for a differently-shaped JSON (pr_number/pr_url/head_sha/changed_files/gates/changesets/notes as top-level fields) than this system's canonical final-message schema; followed the canonical schema per 'Standard clauses live HERE, not in your dispatch prompt' and folded all of the requested detail into summary/tests below rather than picking a shape silently.",
"tests": "All commands run at final HEAD bb6804e21 (bb6804e213021a81f30d5a94bf34936feedddc21) unless noted otherwise. Gates: `node scripts/check-i18n-call-site-keys.mjs` EXIT=0 ('0 key(s) whose holes are filled downstream' -- registry confirmed empty; '2932 keys' resolve, 0 findings). `node scripts/check-i18n-en-drift.mjs` EXIT=0 ('1 en value(s) changed ... 9 pack value(s) followed'). `node scripts/check-control-bytes.mjs` EXIT=0 (4240 files scanned). `node scripts/check-changeset-presence.mjs` EXIT=0 ('13 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)'). `node scripts/check-changeset-no-major.mjs` EXIT=0. `node scripts/check-changeset-fixed.mjs` EXIT=0. Dependency closures built first: `pnpm --workspace-concurrency=2 --filter '@object-ui/auth^...' build`, `'@object-ui/i18n^...' build`, `'@object-ui/console^...' build` (all clean). Typecheck: `pnpm --filter '@object-ui/auth' type-check` clean; `pnpm --filter '@object-ui/i18n' type-check` clean; `pnpm --filter '@object-ui/console' type-check` clean; `pnpm run type-check:scripts` clean. Lint: `pnpm --filter '@object-ui/auth' lint` 0 errors / 52 pre-existing warnings (none in touched files); `pnpm --filter '@object-ui/i18n' lint` 0 errors / 35 pre-existing warnings; `pnpm --filter '@object-ui/console' lint` 0 errors / 204 pre-existing warnings (none on ForgotPasswordPage.tsx). Tests: `pnpm exec vitest run packages/i18n/ packages/auth/ scripts/ apps/console/ --maxWorkers=2` -> 152 test files, 2482 tests, all passed (includes the new packages/auth/src/__tests__/forgot-password-success-message.test.tsx, 4/4 passing, and the rewritten scripts/__tests__/check-i18n-call-site-keys.test.ts, 64/64 passing). Reverse verification (mechanism assumption #2, done post-commit per AGENTS.md: commit fix first, then mutate, then restore from the branch): temporarily reintroduced {{email}} into packages/i18n/src/locales/en.ts ONLY (uncommitted), reran `node scripts/check-i18n-call-site-keys.mjs` -> EXIT=1, reporting `default-value-drift` on apps/console/src/pages/auth/ForgotPasswordPage.tsx:46 AND `interpolation-parity: unfilled=[email]` on BOTH call sites referencing the key (apps/console and packages/app-shell's ForgotPasswordPage.tsx) -- proving the registry retirement restored the 'loud report' the ruling wanted. Restored via `git checkout claude/issue-4135-single-brace-convention -- packages/i18n/src/locales/en.ts`; `git status`/`git diff` confirmed byte-clean afterward; gate re-ran green (EXIT=0). Also captured the natural pre-update failure of the OLD self-test against the retired registry (before I rewrote it): 3 tests failed, most notably `does not demand an argument for a registered hole` flipping from `expect([]).toEqual([])` to actually receiving `['auth.forgotPassword.successDescription: inert=[] unfilled=[email]']` -- same transition, independent evidence. Byte-identical rendering (all 10 locales): wrote a small node script (scratchpad) that reads each locale's pre-PR (origin/main) and post-PR successDescription value via `git show`, computes `beforeValue.includes('{{email}}') ? beforeValue.replace('{{email}}', SAMPLE_EMAIL) : beforeValue+' '+SAMPLE_EMAIL` vs the equivalent {email}-based post-PR computation, and diffs them -- all 10 locales (en/zh/ar/de/es/fr/ja/ko/pt/ru) report OK, byte-identical. Double-append positive control: forgot-password-success-message.test.tsx's 4th test ('positive control: never pastes the address twice...') asserts the append branch (label with no marker at all, simulating a hole i18next already consumed) pastes the address exactly once, never twice -- passing.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #4716: ForgotPasswordForm's {email} marker match is substring-based (.includes('{email}')) -- a caller-supplied labels.successDescription still spelling the retired {{email}} way (a public-prop input this PR does not and cannot control) partial-matches and produces stray literal braces around the address ('...to {user@example.com}...') instead of falling to the append branch, because {email} is a literal substring of {{email}}. This is a NEW failure direction introduced by this PR's marker-spelling flip (the pre-PR direction -- a stray single-brace label against the old double-brace check -- could not partial-match, since {{email}} is not a substring of {email}). Dormant: no shipped code path in this repo produces the input, only an external consumer's own labels prop could. Filed unassigned, finding-labeled, no pm:queue, three options (leave/tighten the match/document) with no recommendation attached."
]
}
(Supersedes the previous report comment above, whose marker line was written with HTML-escaped angle brackets and may not match a literal <!-- os-dev-report --> or bare os-dev-report search. This comment uses the plain fallback marker text with no angle brackets at all, per instruction, so it is guaranteed findable. Same JSON, unchanged.)
{
"issue": 4135,
"status": "done",
"branch": "claude/issue-4135-single-brace-convention",
"pr": "https://github.com/objectstack-ai/objectui/pull/4714",
"premise_still_valid": true,
"summary": "Implemented the 2026-08-11 maintainer ruling (Option A) verbatim as one atomic PR: all ten locale packs' auth.forgotPassword.successDescription moved from {{email}} to {email}; ForgotPasswordForm.tsx's replacement marker AND its own built-in default label (line ~181, not explicitly named in the dispatch prompt but load-bearing -- it shares the same marker check) moved to the same spelling; apps/console/src/pages/auth/ForgotPasswordPage.tsx's inline defaultValue (also not explicitly named) was updated too, required to keep the default-value-drift gate green since it must byte-match the pack value. scripts/check-i18n-call-site-keys.mjs's EXTERNALLY_INTERPOLATED_HOLES registry entry for this key is retired to an empty array (mechanism kept for a future case), and its self-test rewritten to pin the retirement plus prove the gate now reports an argument-less {{hole}} loudly on this key instead of silencing it. head_sha=bb6804e21 (full bb6804e213021a81f30d5a94bf34936feedddc21). changed_files (16): .changeset/single-brace-downstream-holes-4135.md (new), apps/console/src/pages/auth/ForgotPasswordPage.tsx, packages/auth/src/ForgotPasswordForm.tsx, packages/auth/src/__tests__/forgot-password-success-message.test.tsx (new), packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts, scripts/__tests__/check-i18n-call-site-keys.test.ts, scripts/check-i18n-call-site-keys.mjs. Changesets: single-brace-downstream-holes-4135.md declares patch for @object-ui/i18n, @object-ui/auth, @object-ui/console (all private:false, all in the changeset fixed group; verified via package.json and .changeset/config.json). No skip-changeset label applies -- this PR carries a real user-visible copy/behavior change, not a test/workflow-only one. Note on report shape: this dispatch prompt's '交报' section asked for a differently-shaped JSON (pr_number/pr_url/head_sha/changed_files/gates/changesets/notes as top-level fields) than this system's canonical final-message schema; followed the canonical schema per 'Standard clauses live HERE, not in your dispatch prompt' and folded all of the requested detail into summary/tests below rather than picking a shape silently.",
"tests": "All commands run at final HEAD bb6804e21 (bb6804e213021a81f30d5a94bf34936feedddc21) unless noted otherwise. Gates: `node scripts/check-i18n-call-site-keys.mjs` EXIT=0 ('0 key(s) whose holes are filled downstream' -- registry confirmed empty; '2932 keys' resolve, 0 findings). `node scripts/check-i18n-en-drift.mjs` EXIT=0 ('1 en value(s) changed ... 9 pack value(s) followed'). `node scripts/check-control-bytes.mjs` EXIT=0 (4240 files scanned). `node scripts/check-changeset-presence.mjs` EXIT=0 ('13 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)'). `node scripts/check-changeset-no-major.mjs` EXIT=0. `node scripts/check-changeset-fixed.mjs` EXIT=0. Dependency closures built first: `pnpm --workspace-concurrency=2 --filter '@object-ui/auth^...' build`, `'@object-ui/i18n^...' build`, `'@object-ui/console^...' build` (all clean). Typecheck: `pnpm --filter '@object-ui/auth' type-check` clean; `pnpm --filter '@object-ui/i18n' type-check` clean; `pnpm --filter '@object-ui/console' type-check` clean; `pnpm run type-check:scripts` clean. Lint: `pnpm --filter '@object-ui/auth' lint` 0 errors / 52 pre-existing warnings (none in touched files); `pnpm --filter '@object-ui/i18n' lint` 0 errors / 35 pre-existing warnings; `pnpm --filter '@object-ui/console' lint` 0 errors / 204 pre-existing warnings (none on ForgotPasswordPage.tsx). Tests: `pnpm exec vitest run packages/i18n/ packages/auth/ scripts/ apps/console/ --maxWorkers=2` -> 152 test files, 2482 tests, all passed (includes the new packages/auth/src/__tests__/forgot-password-success-message.test.tsx, 4/4 passing, and the rewritten scripts/__tests__/check-i18n-call-site-keys.test.ts, 64/64 passing). Reverse verification (mechanism assumption #2, done post-commit per AGENTS.md: commit fix first, then mutate, then restore from the branch): temporarily reintroduced {{email}} into packages/i18n/src/locales/en.ts ONLY (uncommitted), reran `node scripts/check-i18n-call-site-keys.mjs` -> EXIT=1, reporting `default-value-drift` on apps/console/src/pages/auth/ForgotPasswordPage.tsx:46 AND `interpolation-parity: unfilled=[email]` on BOTH call sites referencing the key (apps/console and packages/app-shell's ForgotPasswordPage.tsx) -- proving the registry retirement restored the 'loud report' the ruling wanted. Restored via `git checkout claude/issue-4135-single-brace-convention -- packages/i18n/src/locales/en.ts`; `git status`/`git diff` confirmed byte-clean afterward; gate re-ran green (EXIT=0). Also captured the natural pre-update failure of the OLD self-test against the retired registry (before I rewrote it): 3 tests failed, most notably `does not demand an argument for a registered hole` flipping from `expect([]).toEqual([])` to actually receiving `['auth.forgotPassword.successDescription: inert=[] unfilled=[email]']` -- same transition, independent evidence. Byte-identical rendering (all 10 locales): wrote a small node script (scratchpad) that reads each locale's pre-PR (origin/main) and post-PR successDescription value via `git show`, computes `beforeValue.includes('{{email}}') ? beforeValue.replace('{{email}}', SAMPLE_EMAIL) : beforeValue+' '+SAMPLE_EMAIL` vs the equivalent {email}-based post-PR computation, and diffs them -- all 10 locales (en/zh/ar/de/es/fr/ja/ko/pt/ru) report OK, byte-identical. Double-append positive control: forgot-password-success-message.test.tsx's 4th test ('positive control: never pastes the address twice...') asserts the append branch (label with no marker at all, simulating a hole i18next already consumed) pastes the address exactly once, never twice -- passing.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #4716: ForgotPasswordForm's {email} marker match is substring-based (.includes('{email}')) -- a caller-supplied labels.successDescription still spelling the retired {{email}} way (a public-prop input this PR does not and cannot control) partial-matches and produces stray literal braces around the address ('...to {user@example.com}...') instead of falling to the append branch, because {email} is a literal substring of {{email}}. This is a NEW failure direction introduced by this PR's marker-spelling flip (the pre-PR direction -- a stray single-brace label against the old double-brace check -- could not partial-match, since {{email}} is not a substring of {email}). Dormant: no shipped code path in this repo produces the input, only an external consumer's own labels prop could. Filed unassigned, finding-labeled, no pm:queue, three options (leave/tighten the match/document) with no recommendation attached."
]
}
Activity
os-zhuang commentedon Aug 10, 2026
Findings triage: held —
findingstays (labels unchanged; this comment records why, so state and label agree).EXTERNALLY_INTERPOLATED_HOLESregistry entry plus its self-test guard the dangerous direction (passingemailat the call site now trips the gate), so the live trap has a tripwire.@object-ui/auth's public props. Neither should be picked by a queue dev in passing, and the decision box should not take a question with no demonstrated pull while the gate already guards the failure mode.ForgotPasswordForm's label contract or theauth.forgotPassword.*pack values — the toucher inherits the convergence question; ② a second externally-interpolated hole joining the registry (the pattern generalising is what would justify option A's cost); ③ an i18n-convention ruling that settles hole spellings repo-wide.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
fix(i18n): fail when a call site's arguments are not the holes its en…
claude commentedon Aug 10, 2026
Findings triage: HOLD.
Re-verified @ objectui
origin/main564252c: both spellings live —packages/auth/src/ForgotPasswordForm.tsx:181/:286-287({{email}}, double braces, filled downstream) vsapps/console/src/pages/auth/ForgotPasswordPage.tsx:57/:60({seconds}, single braces, documented as deliberate). The tooling half remains closed by theEXTERNALLY_INTERPOLATED_HOLESregistry. Held because what remains is an authoring-convention pick (one blessed spelling for downstream-filled holes) — cheap whenever the next i18n sweep runs, harmless until then.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
claude commentedon Aug 11, 2026
Maintainer ruling — 2026-08-11. From the four-lens review (platform long-term coherence / measured business pull / AI-agent error-resistance / startup scope discipline).
Ruling: Option A — converge on single braces, and the choice is established as a repo-wide authoring convention, not just this card's fix:
The work, one PR: move
{{email}}→{email}in all ten packs forauth.forgotPassword.successDescription, updateForgotPasswordForm.tsx's replacement marker in the same change (atomicity pinned by #3845's interpolation-parity gate + the en-drift gate #3650), and retire theEXTERNALLY_INTERPOLATED_HOLESregistry entry inscripts/check-i18n-call-site-keys.mjs— with the ambiguity gone by construction, the gate no longer needs an exemption fence there, and any remaining argument-less{{x}}is a real defect the gate may report loudly.Rendered output is byte-identical in all languages (only the hole marker changes; the component fills it either way). Rationale weighted on the AI-error lens: an argument-less
{{email}}is indistinguishable from a forgotten argument, and the natural "fix" (passing the value) triggers the double-append regression on the forgot-password success screen in all ten languages — removing the ambiguity beats fencing it.Family note: ui#3512 (
createSafeTranslationhole-spelling recognition) aligns to this same convention when it is next graded — recorded there.State:
finding→pm:queue.Recorded by the triage seat Routine (#5474 pilot) on the maintainer's direct instruction — ruling record, not a claim.
Generated by Claude Code
yinlianghui commentedon Aug 15, 2026
Claiming — PM dispatch. Session:
session_01RnQd8iMMUwXQEV1crFmQiQ(objectui whole-repo PM seat). Branch:claude/issue-4135-single-brace-convention.Scope: the maintainer's 2026-08-11 ruling verbatim — Option A as one atomic PR:
{{email}}→{email}in all ten packs forauth.forgotPassword.successDescription,ForgotPasswordForm.tsx's replacement marker updated in the same change, and theEXTERNALLY_INTERPOLATED_HOLESregistry entry retired fromscripts/check-i18n-call-site-keys.mjs(+ its self-test). Rendered output must remain byte-identical in all languages. PR will beFixes #4135.Serialization note: #4206 also edits
check-i18n-call-site-keys.mjsand is deliberately held until this PR merges (hot-file serial queue) — its dispatch follows this card's landing.Generated by Claude Code
yinlianghui commentedon Aug 15, 2026
<!-- os-dev-report -->
{ "issue": 4135, "status": "done", "branch": "claude/issue-4135-single-brace-convention", "pr": "https://github.com/objectstack-ai/objectui/pull/4714", "premise_still_valid": true, "summary": "Implemented the 2026-08-11 maintainer ruling (Option A) verbatim as one atomic PR: all ten locale packs' auth.forgotPassword.successDescription moved from {{email}} to {email}; ForgotPasswordForm.tsx's replacement marker AND its own built-in default label (line ~181, not explicitly named in the dispatch prompt but load-bearing -- it shares the same marker check) moved to the same spelling; apps/console/src/pages/auth/ForgotPasswordPage.tsx's inline defaultValue (also not explicitly named) was updated too, required to keep the default-value-drift gate green since it must byte-match the pack value. scripts/check-i18n-call-site-keys.mjs's EXTERNALLY_INTERPOLATED_HOLES registry entry for this key is retired to an empty array (mechanism kept for a future case), and its self-test rewritten to pin the retirement plus prove the gate now reports an argument-less {{hole}} loudly on this key instead of silencing it. head_sha=bb6804e21 (full bb6804e213021a81f30d5a94bf34936feedddc21). changed_files (16): .changeset/single-brace-downstream-holes-4135.md (new), apps/console/src/pages/auth/ForgotPasswordPage.tsx, packages/auth/src/ForgotPasswordForm.tsx, packages/auth/src/__tests__/forgot-password-success-message.test.tsx (new), packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts, scripts/__tests__/check-i18n-call-site-keys.test.ts, scripts/check-i18n-call-site-keys.mjs. Changesets: single-brace-downstream-holes-4135.md declares patch for @object-ui/i18n, @object-ui/auth, @object-ui/console (all private:false, all in the changeset fixed group; verified via package.json and .changeset/config.json). No skip-changeset label applies -- this PR carries a real user-visible copy/behavior change, not a test/workflow-only one. Note on report shape: this dispatch prompt's '交报' section asked for a differently-shaped JSON (pr_number/pr_url/head_sha/changed_files/gates/changesets/notes as top-level fields) than this system's canonical final-message schema; followed the canonical schema per 'Standard clauses live HERE, not in your dispatch prompt' and folded all of the requested detail into summary/tests below rather than picking a shape silently.", "tests": "All commands run at final HEAD bb6804e21 (bb6804e213021a81f30d5a94bf34936feedddc21) unless noted otherwise. Gates: `node scripts/check-i18n-call-site-keys.mjs` EXIT=0 ('0 key(s) whose holes are filled downstream' -- registry confirmed empty; '2932 keys' resolve, 0 findings). `node scripts/check-i18n-en-drift.mjs` EXIT=0 ('1 en value(s) changed ... 9 pack value(s) followed'). `node scripts/check-control-bytes.mjs` EXIT=0 (4240 files scanned). `node scripts/check-changeset-presence.mjs` EXIT=0 ('13 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)'). `node scripts/check-changeset-no-major.mjs` EXIT=0. `node scripts/check-changeset-fixed.mjs` EXIT=0. Dependency closures built first: `pnpm --workspace-concurrency=2 --filter '@object-ui/auth^...' build`, `'@object-ui/i18n^...' build`, `'@object-ui/console^...' build` (all clean). Typecheck: `pnpm --filter '@object-ui/auth' type-check` clean; `pnpm --filter '@object-ui/i18n' type-check` clean; `pnpm --filter '@object-ui/console' type-check` clean; `pnpm run type-check:scripts` clean. Lint: `pnpm --filter '@object-ui/auth' lint` 0 errors / 52 pre-existing warnings (none in touched files); `pnpm --filter '@object-ui/i18n' lint` 0 errors / 35 pre-existing warnings; `pnpm --filter '@object-ui/console' lint` 0 errors / 204 pre-existing warnings (none on ForgotPasswordPage.tsx). Tests: `pnpm exec vitest run packages/i18n/ packages/auth/ scripts/ apps/console/ --maxWorkers=2` -> 152 test files, 2482 tests, all passed (includes the new packages/auth/src/__tests__/forgot-password-success-message.test.tsx, 4/4 passing, and the rewritten scripts/__tests__/check-i18n-call-site-keys.test.ts, 64/64 passing). Reverse verification (mechanism assumption #2, done post-commit per AGENTS.md: commit fix first, then mutate, then restore from the branch): temporarily reintroduced {{email}} into packages/i18n/src/locales/en.ts ONLY (uncommitted), reran `node scripts/check-i18n-call-site-keys.mjs` -> EXIT=1, reporting `default-value-drift` on apps/console/src/pages/auth/ForgotPasswordPage.tsx:46 AND `interpolation-parity: unfilled=[email]` on BOTH call sites referencing the key (apps/console and packages/app-shell's ForgotPasswordPage.tsx) -- proving the registry retirement restored the 'loud report' the ruling wanted. Restored via `git checkout claude/issue-4135-single-brace-convention -- packages/i18n/src/locales/en.ts`; `git status`/`git diff` confirmed byte-clean afterward; gate re-ran green (EXIT=0). Also captured the natural pre-update failure of the OLD self-test against the retired registry (before I rewrote it): 3 tests failed, most notably `does not demand an argument for a registered hole` flipping from `expect([]).toEqual([])` to actually receiving `['auth.forgotPassword.successDescription: inert=[] unfilled=[email]']` -- same transition, independent evidence. Byte-identical rendering (all 10 locales): wrote a small node script (scratchpad) that reads each locale's pre-PR (origin/main) and post-PR successDescription value via `git show`, computes `beforeValue.includes('{{email}}') ? beforeValue.replace('{{email}}', SAMPLE_EMAIL) : beforeValue+' '+SAMPLE_EMAIL` vs the equivalent {email}-based post-PR computation, and diffs them -- all 10 locales (en/zh/ar/de/es/fr/ja/ko/pt/ru) report OK, byte-identical. Double-append positive control: forgot-password-success-message.test.tsx's 4th test ('positive control: never pastes the address twice...') asserts the append branch (label with no marker at all, simulating a hole i18next already consumed) pastes the address exactly once, never twice -- passing.", "open_questions": [], "out_of_scope_findings": [ "filed as #4716: ForgotPasswordForm's {email} marker match is substring-based (.includes('{email}')) -- a caller-supplied labels.successDescription still spelling the retired {{email}} way (a public-prop input this PR does not and cannot control) partial-matches and produces stray literal braces around the address ('...to {user@example.com}...') instead of falling to the append branch, because {email} is a literal substring of {{email}}. This is a NEW failure direction introduced by this PR's marker-spelling flip (the pre-PR direction -- a stray single-brace label against the old double-brace check -- could not partial-match, since {{email}} is not a substring of {email}). Dormant: no shipped code path in this repo produces the input, only an external consumer's own labels prop could. Filed unassigned, finding-labeled, no pm:queue, three options (leave/tighten the match/document) with no recommendation attached." ] }Generated by Claude Code
yinlianghui commentedon Aug 15, 2026
os-dev-report
(Supersedes the previous report comment above, whose marker line was written with HTML-escaped angle brackets and may not match a literal
<!-- os-dev-report -->or bareos-dev-reportsearch. This comment uses the plain fallback marker text with no angle brackets at all, per instruction, so it is guaranteed findable. Same JSON, unchanged.){ "issue": 4135, "status": "done", "branch": "claude/issue-4135-single-brace-convention", "pr": "https://github.com/objectstack-ai/objectui/pull/4714", "premise_still_valid": true, "summary": "Implemented the 2026-08-11 maintainer ruling (Option A) verbatim as one atomic PR: all ten locale packs' auth.forgotPassword.successDescription moved from {{email}} to {email}; ForgotPasswordForm.tsx's replacement marker AND its own built-in default label (line ~181, not explicitly named in the dispatch prompt but load-bearing -- it shares the same marker check) moved to the same spelling; apps/console/src/pages/auth/ForgotPasswordPage.tsx's inline defaultValue (also not explicitly named) was updated too, required to keep the default-value-drift gate green since it must byte-match the pack value. scripts/check-i18n-call-site-keys.mjs's EXTERNALLY_INTERPOLATED_HOLES registry entry for this key is retired to an empty array (mechanism kept for a future case), and its self-test rewritten to pin the retirement plus prove the gate now reports an argument-less {{hole}} loudly on this key instead of silencing it. head_sha=bb6804e21 (full bb6804e213021a81f30d5a94bf34936feedddc21). changed_files (16): .changeset/single-brace-downstream-holes-4135.md (new), apps/console/src/pages/auth/ForgotPasswordPage.tsx, packages/auth/src/ForgotPasswordForm.tsx, packages/auth/src/__tests__/forgot-password-success-message.test.tsx (new), packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts, scripts/__tests__/check-i18n-call-site-keys.test.ts, scripts/check-i18n-call-site-keys.mjs. Changesets: single-brace-downstream-holes-4135.md declares patch for @object-ui/i18n, @object-ui/auth, @object-ui/console (all private:false, all in the changeset fixed group; verified via package.json and .changeset/config.json). No skip-changeset label applies -- this PR carries a real user-visible copy/behavior change, not a test/workflow-only one. Note on report shape: this dispatch prompt's '交报' section asked for a differently-shaped JSON (pr_number/pr_url/head_sha/changed_files/gates/changesets/notes as top-level fields) than this system's canonical final-message schema; followed the canonical schema per 'Standard clauses live HERE, not in your dispatch prompt' and folded all of the requested detail into summary/tests below rather than picking a shape silently.", "tests": "All commands run at final HEAD bb6804e21 (bb6804e213021a81f30d5a94bf34936feedddc21) unless noted otherwise. Gates: `node scripts/check-i18n-call-site-keys.mjs` EXIT=0 ('0 key(s) whose holes are filled downstream' -- registry confirmed empty; '2932 keys' resolve, 0 findings). `node scripts/check-i18n-en-drift.mjs` EXIT=0 ('1 en value(s) changed ... 9 pack value(s) followed'). `node scripts/check-control-bytes.mjs` EXIT=0 (4240 files scanned). `node scripts/check-changeset-presence.mjs` EXIT=0 ('13 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)'). `node scripts/check-changeset-no-major.mjs` EXIT=0. `node scripts/check-changeset-fixed.mjs` EXIT=0. Dependency closures built first: `pnpm --workspace-concurrency=2 --filter '@object-ui/auth^...' build`, `'@object-ui/i18n^...' build`, `'@object-ui/console^...' build` (all clean). Typecheck: `pnpm --filter '@object-ui/auth' type-check` clean; `pnpm --filter '@object-ui/i18n' type-check` clean; `pnpm --filter '@object-ui/console' type-check` clean; `pnpm run type-check:scripts` clean. Lint: `pnpm --filter '@object-ui/auth' lint` 0 errors / 52 pre-existing warnings (none in touched files); `pnpm --filter '@object-ui/i18n' lint` 0 errors / 35 pre-existing warnings; `pnpm --filter '@object-ui/console' lint` 0 errors / 204 pre-existing warnings (none on ForgotPasswordPage.tsx). Tests: `pnpm exec vitest run packages/i18n/ packages/auth/ scripts/ apps/console/ --maxWorkers=2` -> 152 test files, 2482 tests, all passed (includes the new packages/auth/src/__tests__/forgot-password-success-message.test.tsx, 4/4 passing, and the rewritten scripts/__tests__/check-i18n-call-site-keys.test.ts, 64/64 passing). Reverse verification (mechanism assumption #2, done post-commit per AGENTS.md: commit fix first, then mutate, then restore from the branch): temporarily reintroduced {{email}} into packages/i18n/src/locales/en.ts ONLY (uncommitted), reran `node scripts/check-i18n-call-site-keys.mjs` -> EXIT=1, reporting `default-value-drift` on apps/console/src/pages/auth/ForgotPasswordPage.tsx:46 AND `interpolation-parity: unfilled=[email]` on BOTH call sites referencing the key (apps/console and packages/app-shell's ForgotPasswordPage.tsx) -- proving the registry retirement restored the 'loud report' the ruling wanted. Restored via `git checkout claude/issue-4135-single-brace-convention -- packages/i18n/src/locales/en.ts`; `git status`/`git diff` confirmed byte-clean afterward; gate re-ran green (EXIT=0). Also captured the natural pre-update failure of the OLD self-test against the retired registry (before I rewrote it): 3 tests failed, most notably `does not demand an argument for a registered hole` flipping from `expect([]).toEqual([])` to actually receiving `['auth.forgotPassword.successDescription: inert=[] unfilled=[email]']` -- same transition, independent evidence. Byte-identical rendering (all 10 locales): wrote a small node script (scratchpad) that reads each locale's pre-PR (origin/main) and post-PR successDescription value via `git show`, computes `beforeValue.includes('{{email}}') ? beforeValue.replace('{{email}}', SAMPLE_EMAIL) : beforeValue+' '+SAMPLE_EMAIL` vs the equivalent {email}-based post-PR computation, and diffs them -- all 10 locales (en/zh/ar/de/es/fr/ja/ko/pt/ru) report OK, byte-identical. Double-append positive control: forgot-password-success-message.test.tsx's 4th test ('positive control: never pastes the address twice...') asserts the append branch (label with no marker at all, simulating a hole i18next already consumed) pastes the address exactly once, never twice -- passing.", "open_questions": [], "out_of_scope_findings": [ "filed as #4716: ForgotPasswordForm's {email} marker match is substring-based (.includes('{email}')) -- a caller-supplied labels.successDescription still spelling the retired {{email}} way (a public-prop input this PR does not and cannot control) partial-matches and produces stray literal braces around the address ('...to {user@example.com}...') instead of falling to the append branch, because {email} is a literal substring of {{email}}. This is a NEW failure direction introduced by this PR's marker-spelling flip (the pre-PR direction -- a stray single-brace label against the old double-brace check -- could not partial-match, since {{email}} is not a substring of {email}). Dormant: no shipped code path in this repo produces the input, only an external consumer's own labels prop could. Filed unassigned, finding-labeled, no pm:queue, three options (leave/tighten the match/document) with no recommendation attached." ] }Generated by Claude Code
fix(i18n,auth): converge downstream-filled holes on single braces (ob…
test(i18n): 占位符拼写门 —— 无 provider 回退只会遇到它认得的 {{name}} 拼形 (objectstack-…