Skip to content

fix(spec): reject the retired key/defaultValue inline-map spellings by name, and state the measured resolver behaviour in the rejection message - #10644

Merged
os-elon merged 1 commit into
mainfrom
claude/issue-10492-i18n-key-reference-message
Aug 21, 2026
Merged

fix(spec): reject the retired key/defaultValue inline-map spellings by name, and state the measured resolver behaviour in the rejection message#10644
os-elon merged 1 commit into
mainfrom
claude/issue-10492-i18n-key-reference-message

Conversation

@os-elon

@os-elon os-elon commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Fixes #10492

Clause-②: yes — leg 2 narrows the parse-acceptance face of InlineLocaleMapSchema (contract-review tier per the triage routing comment of 2026-08-21T02:23Z).

What this does

Two legs, both in packages/spec/src/ui/i18n.zod.ts, per the triage-ruled scope:

  1. Message accuracy (text-only). The INLINE_LOCALE_KEY rejection message claimed the retired key-reference form (ADR-0049 enforce-or-remove: ui/widget.zod.ts 全文件 + ui/i18n.zod.ts 五个形状实测无门(#4001 批 16 改判) #5055) "resolves to nothing". Measured false — both resolvers (resolveI18nLabel limb 6, and objectui's pickLocalized, parity-pinned by objectui's i18nLabel-resolver-parity.test.ts) fall through to the first string value and return the raw dotted key, which renders as the visible label. The message now states the measured behaviour: "nothing looks the key up, so both resolvers fall through to the first string value and the raw key is rendered on screen". The doc comment carrying the same reasoning (the paragraph that argued a lone key was indistinguishable from a language subtag) is rewritten to record the closed boundary instead.

  2. Enforcement hole closed — the retired spellings are rejected BY NAME, in any combination. key is three letters, syntactically a valid BCP-47 primary subtag, so { key: 'common.save' } alone parsed as a "language key inline locale map" and would paint common.save on screen; the pair form was rejected only because defaultValue fails the tag grammar. INLINE_LOCALE_KEY now opens with a negative lookahead for exactly the two retired spellings — the same two names the emitted type already refuses ({ key?: never; defaultValue?: never }, [decision] InlineLocaleMap's BCP-47 key regex is erased from the emitted type, so { key, defaultValue } type-checks on every I18nLabel surface in both repos — narrow the key type, or accept publish-time-only refusal? #9925 maintainer ruling 2026-08-19 option B). One mechanism (the key schema itself), no second refinement, no separate grep gate (the [decision] InlineLocaleMap's BCP-47 key regex is erased from the emitted type, so { key, defaultValue } type-checks on every I18nLabel surface in both repos — narrow the key type, or accept publish-time-only refusal? #9925 ruling explicitly rejected the Option-C shape). This inherits the ADR-0049 enforce-or-remove: ui/widget.zod.ts 全文件 + ui/i18n.zod.ts 五个形状实测无门(#4001 批 16 改判) #5055 ruling; it is not a new contract question. Not a deny-list of English-looking words: deu / fra / yue still parse (pinned).

No resolution path changed: i18n-label-resolver.ts is untouched, and the resolver probe below is byte-identical before/after — the PM's parity assumption holds (leg 2 changes parse acceptance only).

Acceptance-probe table (issue's table, reproduced against BUILT src dist/ui/index.mjs)

probe (I18nLabelSchema.safeParse({[k]:'v'})) before (origin/main @ 5b0af2b) after (1c85818)
en / zh-CN / zh-Hans-CN / default / EN ACCEPTED ACCEPTED
key ACCEPTED (the hole) REJECTED
defaultValue REJECTED REJECTED
notALocale / x-private / e REJECTED REJECTED
{ key, defaultValue } pair REJECTED REJECTED
resolveI18nLabel({key:'common.save',defaultValue:'Save'},'en') 'common.save' 'common.save' (resolution semantics unchanged, by design)

The before-leg reproduces the issue's measurements (taken on the published 17.0.0 dist) exactly, against built src — premise verified.

Tests

Gates (all at 1c85818, clean tree; per-gate verdict lines captured, exit codes via redirect-then-read)

check:generated — "All 14 generated artifacts are up to date" (TSDoc edits moved no artifact; tree clean after full workspace build, 70/70 tasks). Dispatch-named + re-derived (node scripts/pm/dispatch-gates.mjs, no path args — 19 path-matched + 5 convention): cross-package-test-inputs · doc-formula-expressions · merge-driver · objectui-changeset · slot-lookup · spec-parsed-alias · test-source-alias · type-source-resolution · adr-0087-registration · changeset-no-major · dev-prereqs · empty-changeset · affected-docs · nul-bytes · spec empty-state / liveness / strictness-ledger / variant-docs · query-options-erasure · type-check-coverage · engine-double-contract · where-matcher — all green. check:type-check-debt: "33 ledger entr(ies) re-measured in 272.0s, 1924 raw tsc error(s) total, none above its recorded number; surplus: none".

Changeset

@objectstack/spec minor — FROM→TO prescription in the body; ADR-0087 disposition: not-required (already-registered ui-widget-i18n-family-retired) (the #5055 retirement's D3 record already carries the prescription; this closes its runtime enforcement gap).


Generated by Claude Code

…me; state the measured resolver behaviour in the message (#10492)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B4h3medzvhB9rpfoja9jcw
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 2 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/protocol/kernel/i18n-standard.mdx (via common.save (literal))
What this run could not see

Coarse fallback — 126 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6b0be02209751420425e9194db87dfedfe6e3e38packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5547533ed98cf9e64df735a22118e8e87dd54859 — the merge of head 1c85818ebc3e3af46edb6193cbda97ff9b31c3f1 into base 6b0be02209751420425e9194db87dfedfe6e3e38, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5547533ed98cf9e64df735a22118e8e87dd54859 && git checkout 5547533ed98cf9e64df735a22118e8e87dd54859
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6b0be02209751420425e9194db87dfedfe6e3e38 1c85818ebc3e3af46edb6193cbda97ff9b31c3f1 && git checkout -B drift-repro 6b0be02209751420425e9194db87dfedfe6e3e38 && git merge --no-ff 1c85818ebc3e3af46edb6193cbda97ff9b31c3f1

node scripts/docs-audit/affected-docs.mjs --json 6b0be02209751420425e9194db87dfedfe6e3e38

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 6b0be02209751420425e9194db87dfedfe6e3e38 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/m tests tooling

Projects

None yet

2 participants