Skip to content

docs(cli): document the i18n merge consequence for a corrected source string - #9775

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-9672-i18n-merge-consequence-documented
Aug 19, 2026
Merged

docs(cli): document the i18n merge consequence for a corrected source string#9775
os-zhuang merged 2 commits into
mainfrom
claude/issue-9672-i18n-merge-consequence-documented

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#9672

What this documents

os i18n extract's merge behaviour was already correct and already intentional: it
never overwrites an existing non-default-locale translation, so --fill=default only
fills gaps. The consequence — that a source label/description correction does not
propagate into a locale that already holds a translation of the old text — was true and
unchanged, but stated nowhere the next reader (a translator, or the next author touching
this tool) would look. This PR closes that gap with two paragraphs, per the triage
ruling on #9672 — no behaviour change.

1. packages/cli/src/commands/i18n/extract.ts — a comment above the
mergeExisting option passed to extractTranslations, explaining why merge is the
default and what it means for a corrected source string.

2. packages/cli/src/utils/i18n-extract.ts — six new lines in
renderTranslationModule's default header, the single template every generated
<locale>.objects.generated.ts / <locale>.metadata-forms.generated.ts bundle is
built from (confirmed: not duplicated anywhere else in the repo — 41 generated files,
one call site, no override).

Because the header is baked into every generated bundle, all 9 packages that own an
i18n-extract.config.ts were regenerated (node scripts/check-i18n-bundles.mjs --write) so the committed output matches the new template. Translated content is
byte-identical; only the header comment gained 6 lines per file (42 files touched, 256
insertions, 0 deletions — confirmed via diff).

What this deliberately does NOT do

Per the triage ruling, the source-hash staleness detector sketched in #9672's body is
explicitly out of scope — it's a real design, but new gate surface behind one measured
incident (#9046), an appetite question rather than a queue item. No staleness marker,
no check:i18n value comparison, no threshold change.

Mechanism re-verified on current main

The issue measured origin/main at d693ba158. Re-checked before writing:
packages/cli/src/commands/i18n/extract.ts, packages/cli/src/utils/i18n-extract.ts,
packages/cli/src/commands/i18n/check.ts, packages/cli/src/utils/i18n-coverage.ts,
and scripts/check-i18n-bundles.mjs all show zero commits between d693ba158 and
this branch's merge base — the mechanism the card describes is unchanged. Both halves
hold:

  • mergeExisting (merge, the default) only skips a gap; a present translated value
    for a non-default locale is preserved verbatim, corrected source or not.
  • check:i18n's bundle-drift verdict compares a fresh extract's content against the
    committed file — and a fresh extract for a non-default locale reuses the same stale
    merged value, so drift never fires on staleness. computeI18nCoverage (which backs
    os i18n check) only asks whether a key's value is undefined, never whether it
    matches its source's meaning.

Verification (commit cff1dd33c, after merging main)

  • pnpm check:i18n — 9/9 packages in sync, no undeclared authoring keys, self-test
    green.
  • pnpm --filter @objectstack/cli typecheck — clean.
  • pnpm --filter @objectstack/cli test — 134 test files / 1458 tests passed (ran wider
    than intended — the -- file-path filters were absorbed as vitest positional args
    that matched everything rather than narrowing the run; all green regardless).
  • Targeted eslint --no-inline-config on both edited source files — 0 findings.
  • check:cross-package-test-inputs, check:test-source-alias,
    check:type-source-resolution, check:changeset-gate-self-tests
    (empty-changeset / adr-0087 / no-major), check:objectui-changeset,
    check-affected-docs.mjs self-test — all green.
  • check:nul-bytes — clean (self-test + real scan).
  • check:i18n-coveragenot measured: this fresh worktree has no build output for
    @objectstack/connector-mcp, which examples/app-showcase's config needs to lint,
    and that gate is not implicated by this diff's own path derivation
    (node scripts/pm/dispatch-gates.mjs). Declaring not-measured rather than guessing.

Changeset

@objectstack/cli is publishable and this diff touches its src/** with a real,
if purely textual, change to what the CLI writes to disk — not a docs/tests-only PR, so
skip-changeset does not apply. Added .changeset/i18n-merge-consequence-documented.md
(patch).


Generated by Claude Code

… string
`os i18n extract` merges against committed bundles by default so a re-run
never wipes a hand translation, and `--fill=default` only fills gaps. That
means a source label/description that is later corrected does not propagate
into a locale that already holds a translation of the old text -- a
present-but-stale string is not a gap, so it is left as-is. This is intended
behaviour and is unchanged; it was simply undocumented where the next reader
looks.
Two places now say so:
- the merge-options comment in `os i18n extract`'s command implementation
(packages/cli/src/commands/i18n/extract.ts)
- the header comment written into every generated bundle
(packages/cli/src/utils/i18n-extract.ts's renderTranslationModule)
Committed bundles across the 9 packages that own i18n-extract.config.ts are
regenerated (`node scripts/check-i18n-bundles.mjs --write`) so their header
matches the new template -- translated content is byte-identical apart from
the added header lines.
Fixes#9672
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 10 package(s): @objectstack/cli, @objectstack/platform-objects, @objectstack/plugin-approvals, @objectstack/plugin-audit, @objectstack/plugin-security, @objectstack/plugin-sharing, @objectstack/plugin-webhooks, @objectstack/service-messaging, @objectstack/service-realtime, @objectstack/service-storage, touching 3 documentable anchor(s).

2 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 os i18n extract (command))
  • content/docs/ui/translations.mdx(via os i18n extract (command))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx(via os i18n extract (command))
  • content/docs/releases/v16.mdx(via os i18n extract (command))
  • content/docs/releases/v17.mdx(via os i18n extract (command))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 40 changed file(s) yielded no anchor (packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts, packages/platform-objects/src/apps/translations/en.objects.generated.ts, packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts, …) — pages documenting those are invisible to this run

Coarse fallback — 42 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 e9534a4acab63aa91a67dfb76110b6695be68df0packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6d397439f7e6cf1a868d1282e65c7aac0b1d0e40 — the merge of head cff1dd33cefd873247deb26e8693463ad87d06ed into base e9534a4acab63aa91a67dfb76110b6695be68df0, 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 6d397439f7e6cf1a868d1282e65c7aac0b1d0e40 && git checkout 6d397439f7e6cf1a868d1282e65c7aac0b1d0e40
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e9534a4acab63aa91a67dfb76110b6695be68df0 cff1dd33cefd873247deb26e8693463ad87d06ed && git checkout -B drift-repro e9534a4acab63aa91a67dfb76110b6695be68df0 && git merge --no-ff cff1dd33cefd873247deb26e8693463ad87d06ed
node scripts/docs-audit/affected-docs.mjs --json e9534a4acab63aa91a67dfb76110b6695be68df0

⚠️ 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 e9534a4acab63aa91a67dfb76110b6695be68df0 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

PM review — domain:cli seat, session session_012WKSnqAaoqtW3QX7SSf1Vk. Accepted; flipped
out of draft at 23:07:02Z and auto-merge armed at 23:07:05Z.

The one thing worth verifying independently

A PR that regenerates 42 committed i18n bundles to change a header comment has exactly one way to
do real damage: quietly overwriting a hand-edited translated value somewhere in those 42 files.
The PR body asserts "translated content is byte-identical"; that assertion is the whole risk, so
I did not take it on trust. Measured on the diff itself: 256 insertions, 0 deletions, and
every inserted line is inside the generated header comment block. Zero non-comment lines
changed
across all 42 regenerated files — no translated value moved, in any locale, in any
package. The assertion holds.

The rest

  • The mechanism re-verification is the right shape: zero commits between the card's d693ba158
    reading and this branch's merge base across all five implicated files, so the card's premise was
    re-established rather than assumed. That is the stale-premise check done properly.
  • Both halves of the documented consequence are stated where the next reader will actually be:
    the mergeExisting call site, and the header baked into every generated bundle — one template,
    one call site, no override, confirmed.
  • The staleness detector from the card's body is correctly left out. It is new gate surface behind
    one measured incident; that is an appetite question, not a queue item.
  • check:i18n-coveragedeclared not-measured, with the reason (missing build output for
    @objectstack/connector-mcp in a fresh worktree) and the evidence that this diff's own path
    derivation does not implicate it. Declaring not-measured beats reporting a gate you did not run —
    ⛔ the failure mode to avoid is the opposite one, and this report avoids it.
  • The -- filter absorption that widened the vitest run is reported rather than papered over.
    All green either way, and the wider run is strictly more evidence.
  • Changeset reasoning is right: @objectstack/cli is publishable and its src/** changed what the
    CLI writes to disk, so skip-changeset does not apply. Patch is the correct bump.

Temporal Conformance (live PG + MySQL) is green here (23:06:15Z) — which also served as the
control that cleared PR #9774's red on the same base. Remaining checks were still running at
review time; auto-merge holds the gate.


Generated by Claude Code

@os-zhuang
os-zhuang added this pull request to the merge queueAug 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32196793380 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (3/3) — 失败步骤: Run this shard's tests(日志不可读,点进 job 看)

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 7 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks Aug 18, 2026
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

Dequeued by a known flake — diagnosed, and deliberately not re-queued

Queue build 32196793380
failed Test Core (3/3). Worked the triage bot's checklist in order; the answer is item 2.

It is not a regression in this PR — measured, not asserted

The failing turbo task is @objectstack/example-showcase#test, a package this PR does touch
(regenerated i18n bundles), so item 1 had to be taken seriously. It fails:

Test Files 21 passed (21)
Tests 337 passed (337)
Errors 3 errors

Zero tests failed. The non-zero exit comes from three unhandled rejections, all identical:

EnvironmentTeardownError: [vitest-worker]: Closing rpc while "onUserConsoleLog" was pending
This error originated in "test/approval-resume-relation-expand.test.ts"

That is a vitest worker-teardown race — the RPC channel closes while a console-log delivery is still
in flight. It asserts nothing about product code, and this PR's contribution to app-showcase is
comment-only (verified earlier: 256 insertions, 0 deletions, 0 non-comment lines changed across
all 42 regenerated bundles).

The control, on the identical diff

examples/app-showcase ran PR-side too — in Test Core (2/3), which passed. Same diff, same suite,
~20 minutes apart:

PR-side cff1dd33cqueue f798a332
files / tests21 / 337 passed21 / 337 passed
errorsnone3 → exit 1
duration · transform · import25.99s · 23.52s · 64.38s72.74s · 65.69s · 188.22s

Identical verdict; the queue runner was ~3× slower on transform and import. The race tracks load, not
this diff.

Known signature — second sighting

#9371 (tests, pm:on-hold, type Bug) filed exactly this signature on 2026-08-17, same package,
same file, same message, 334/334 passing. It stated its own recurrence condition — "if it recurs,
that is the second data point that makes it a confirmed flaky"
— and that condition is now met.
Full paired evidence posted there rather than here, so one mechanism keeps one home.

Why I am not re-queuing

The triage bot's own guidance: 「修好前重排只会再烧一轮全队列」. With the flake confirmed rather than
suspected, re-queuing spends a full-queue rebuild for every PR behind this one to dodge a recorded,
unowned defect — and it would be me retrying my own PR to green. ⛔ Not doing that. ⛔ And nothing here
gets skipped, quarantined, retry-wrapped or silenced.

State: open, non-draft, mergeable_state: clean, all six required checks green on cff1dd33c,
out of the queue, auto-merge off. It is ready to land the moment someone decides to re-queue it —
that decision now belongs to the next domain:cli PM or the maintainer, with #9371 as the reason to
weigh. This seat is off shift; recorded on the seat post #6024.


Generated by Claude Code

@os-zhuang
os-zhuang added this pull request to the merge queueAug 18, 2026
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

Disposition reversed — re-queued at 23:42Z. Correcting my previous comment, which said this seat
would not re-queue.

The maintainer's instruction (verbatim): 「你收工了也要跟踪合并」 — going off shift does not release
this seat from driving these PRs to landing. Re-queued, and tracked to merge rather than handed over.

The reason I gave for holding no longer applies either. I wrote that a re-queue would "spend a
full-queue rebuild to dodge a defect that is recorded and unowned" — but the recording is now complete:
#9371 carries the paired PR-side/queue-side measurement, the full signature, and the load-dependence
evidence. Re-queuing destroys no reproduction context, because the context is already banked. What is
left is a green, verified PR being held by a harness race, and holding it costs the lane more than it
buys anyone.

⛔ Unchanged: nothing weakened, skipped, quarantined or retry-wrapped; all six required checks still
have to pass on their own. If it is evicted again by the same signature, that is a third data point
for #9371 and it gets posted there.


Generated by Claude Code

Merged via the queue into main with commit f21fe32Aug 19, 2026
26 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-9672-i18n-merge-consequence-documented branch August 19, 2026 00:03
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mtooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

i18n: a corrected source description never reaches an already-translated locale, and no gate can tell the stale translation from a current one

2 participants

@os-zhuang@claude