Repository navigation
i18n: a corrected source description never reaches an already-translated locale, and no gate can tell the stale translation from a current one #9672
Description
Activity
os-support-ai commented
on Aug 18, 2026 CollaboratorMore actionsFirst-touch grading + re-routing (triage seat, session
session_014tGY3fzu4uwCoe7HrfUtWg):domain:services→domain:cli; promoted topm:queue, type Task, narrow scope.Re-route rationale (anchoring rule): the tractable deliverable lands in the i18n extract toolchain —
packages/cli/src/commands/i18n/extract.ts's config commentary and the generated bundle header template — which ispackages/clisurface, not a service.Scope graded IN: state the consequence where the next reader will look — the merge behaviour's documented effect on changed sources (a corrected source label/description never propagates into an already-translated locale; translators maintain leaves in place). Both the extract config comment and the bundle header describe the merge but not this consequence; one honest paragraph in each closes the "intended but invisible" half.
Scope graded OUT (deliberately, not silently): the source-hash staleness detector. It is a real design with a sound shape, but it is new gate surface with exactly one measured incident (#9046) behind it — under startup-scope discipline that is an appetite question, not a queue item. If a second incident lands, escalate with the four-prong block and this card as evidence; the filer's hash-marker sketch is the shape to bring.
Size/model suggestion: S, sonnet.
Generated by Claude Code
- addeddocumentationImprovements or additions to documentationImprovements or additions to documentationand removed
on Aug 18, 2026 Claim: PM loop round 22
Session:session_012WKSnqAaoqtW3QX7SSf1Vk
Branch:claude/issue-9672-i18n-merge-consequence-documented
Worktree:objectstack-issue-9672
Domain:domain:cli
File surface (region-level):packages/cli/src/commands/i18n/extract.ts— the merge-behaviour config commentary only — and the generated bundle header template (wherever that string lives; the dev locates it). Plus.changeset/**if anything publishable changes.
Container & model:S,mode:subagent,model: sonnet— tier derived this round fromnode scripts/pm/dispatch-gates.mjs --tier packages/cli/src/commands/i18n/extract.ts: no path-derived mandate (floor sonnet · default opus · ceiling fable), ⛔ not recalled. Held at the floor, matching triage's ownS, sonnetsuggestion: the scope is two paragraphs of prose stating a measured fact.
Clause-②: no — comments and a generated header. No behaviour changes at all.⚠️ The scope is narrow by an explicit triage ruling, and the excluded half is the tempting oneTriage graded this card in two halves. Both halves are carried into the dispatch verbatim, because the OUT half is the one a capable dev will naturally reach for:
IN:
"state the consequence where the next reader will look — the merge behaviour's documented effect on changed sources (a corrected source label/description never propagates into an already-translated locale; translators maintain leaves in place). Both the extract config comment and the bundle header describe the merge but not this consequence; one honest paragraph in each closes the 'intended but invisible' half."
⛔ OUT, deliberately and not silently:
"the source-hash staleness detector. It is a real design with a sound shape, but it is new gate surface with exactly one measured incident (#9046) behind it — under startup-scope discipline that is an appetite question, not a queue item. If a second incident lands, escalate with the four-prong block and this card as evidence; the filer's hash-marker sketch is the shape to bring."
⇒ ⛔ Do not build the staleness detector, do not add a hash marker, do not extend
check:i18n. The card's own body sketches that design attractively; it is ruled out on appetite, not on merit, and building it would be widening a queue item into an unappetised gate. If the implementation seems to require it, that is a STOP and report, not a licence.What "intended but invisible" means here, so the paragraphs say something
The card establishes the mechanism and it is worth the dev reading before writing: extraction merges against committed bundles (
mergeExisting: !flags['no-merge']) and--fill=defaultfills gaps — a stale-but-present string is not a gap, so it is skipped. That is correct on its own terms; the alternative (--no-merge) wipes every hand translation. Andcheck:i18nproves keys are in sync, never that a translated value still matches the meaning of its source.⇒ The honest paragraph is not "translations can be stale" in the abstract. It is: a corrected source string does not reach an already-translated locale, by design, and the maintenance path is editing the leaf in place — which the bundle header already half-says.
Serial constraints cleared
- Same file — nothing else touches
packages/cli/src/commands/i18n/extract.ts. - Same package — dispatched alongside
os inittemplate descriptions advertise views/actions/extensions that no template emits #9737, which lands inpackages/cli/src/commands/init.tsandcontent/docs/deployment/cli.mdx. Different files, permitted in parallel under the maintainer's 2026-08-18 exemption 「放开同包,同文件仍硬串行」; four replacement rules carried into both. - Lane in flight at claim time: 0.
⚠️ Premise-first: the card measuredorigin/mainatd693ba158, which is many landings ago. Re-verify both halves before writing — ⛔ a paragraph documenting behaviour that has since changed is worse than no paragraph.
Generated by Claude Code
- Same file — nothing else touches
- added a commit that references this issue
on Aug 18, 2026 <!-- os-dev-report -->
{ "issue": 9672, "status": "done", "branch": "claude/issue-9672-i18n-merge-consequence-documented", "pr": "https://github.com/objectstack-ai/objectstack/pull/9775", "premise_still_valid": true, "summary": "Added one paragraph to packages/cli/src/commands/i18n/extract.ts's mergeExisting comment and six lines to the generated-bundle header template in packages/cli/src/utils/i18n-extract.ts (renderTranslationModule), stating the merge-behaviour consequence for a corrected source string: a present-but-stale translation is not a gap, so --merge preserves it, and nothing distinguishes a deliberate translator edit from an unnoticed one. Regenerated all 9 owning packages' bundles (42 files, 256 insertions, 0 deletions -- header-only, translated content byte-identical) via node scripts/check-i18n-bundles.mjs --write. Did not build the source-hash staleness detector, per the ruling's explicit OUT scope.", "tests": "Re-verified mechanism on current main first: packages/cli/src/commands/i18n/{extract,check}.ts, packages/cli/src/utils/{i18n-extract,i18n-coverage}.ts and scripts/check-i18n-bundles.mjs all show ZERO commits between the card's measured d693ba158 and this branch's merge base (bcf275568) -- mechanism unchanged, premise holds. After merging main and rebuilding the @objectstack/cli dependency closure (pnpm --filter '@objectstack/cli...' build), ran at final commit cff1dd33c: `pnpm check:i18n` -> 9/9 packages in sync, self-test green ('check-i18n-bundles: OK (9 package(s) -- all bundles in sync, no undeclared authoring keys)'); `pnpm --filter @objectstack/cli typecheck` -> clean; `pnpm --filter @objectstack/cli test` -> 134 test files / 1458 tests passed (accidentally the full suite, not just the 8 i18n files targeted -- the vitest positional-arg filters after `--` did not narrow the run; all green regardless, so left as-is rather than re-running); targeted `eslint --no-inline-config` on both edited files -> 0 findings; `check:cross-package-test-inputs`, `check:test-source-alias` (72 pkgs scanned), `check:type-source-resolution` (76 pkgs scanned), `check:changeset-gate-self-tests` (empty-changeset/adr-0087/no-major, all self-test+real-run green), `check:objectui-changeset` (digest+range self-tests green), `check-affected-docs.mjs` self-test (242 cases) -- all green. `check:nul-bytes` self-test + real scan clean on all changed files. `check:i18n-coverage` explicitly NOT measured: examples/app-showcase's config needs @objectstack/connector-mcp built, which this worktree never built (out of scope for our diff, and dispatch-gates.mjs's derivation against our actual changed paths does not name check:i18n-coverage) -- declared not-measured rather than guessed. Gate union derived twice via node scripts/pm/dispatch-gates.mjs against the real changed paths, both before and after merging main (the first derivation was contaminated by main having advanced 4 commits past the branch point before I merged).", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
{ "issue": 9672, "status": "done", "branch": "claude/issue-9672-i18n-merge-consequence-documented", "pr": "https://github.com/objectstack-ai/objectstack/pull/9775", "premise_still_valid": true, "summary": "Added one paragraph to packages/cli/src/commands/i18n/extract.ts's mergeExisting comment and six lines to the generated-bundle header template in packages/cli/src/utils/i18n-extract.ts (renderTranslationModule), stating the merge-behaviour consequence for a corrected source string: a present-but-stale translation is not a gap, so --merge preserves it, and nothing distinguishes a deliberate translator edit from an unnoticed one. Regenerated all 9 owning packages' bundles (42 files, 256 insertions, 0 deletions -- header-only, translated content byte-identical) via node scripts/check-i18n-bundles.mjs --write. Did not build the source-hash staleness detector, per the ruling's explicit OUT scope.", "tests": "Re-verified mechanism on current main first: packages/cli/src/commands/i18n/{extract,check}.ts, packages/cli/src/utils/{i18n-extract,i18n-coverage}.ts and scripts/check-i18n-bundles.mjs all show ZERO commits between the card's measured d693ba158 and this branch's merge base (bcf275568) -- mechanism unchanged, premise holds. After merging main and rebuilding the @objectstack/cli dependency closure (pnpm --filter '@objectstack/cli...' build), ran at final commit cff1dd33c: `pnpm check:i18n` -> 9/9 packages in sync, self-test green ('check-i18n-bundles: OK (9 package(s) -- all bundles in sync, no undeclared authoring keys)'); `pnpm --filter @objectstack/cli typecheck` -> clean; `pnpm --filter @objectstack/cli test` -> 134 test files / 1458 tests passed (accidentally the full suite, not just the 8 i18n files targeted -- the vitest positional-arg filters after `--` did not narrow the run; all green regardless, so left as-is rather than re-running); targeted `eslint --no-inline-config` on both edited files -> 0 findings; `check:cross-package-test-inputs`, `check:test-source-alias` (72 pkgs scanned), `check:type-source-resolution` (76 pkgs scanned), `check:changeset-gate-self-tests` (empty-changeset/adr-0087/no-major, all self-test+real-run green), `check:objectui-changeset` (digest+range self-tests green), `check-affected-docs.mjs` self-test (242 cases) -- all green. `check:nul-bytes` self-test + real scan clean on all changed files. `check:i18n-coverage` explicitly NOT measured: examples/app-showcase's config needs @objectstack/connector-mcp built, which this worktree never built (out of scope for our diff, and dispatch-gates.mjs's derivation against our actual changed paths does not name check:i18n-coverage) -- declared not-measured rather than guessed. Gate union derived twice via node scripts/pm/dispatch-gates.mjs against the real changed paths, both before and after merging main (the first derivation was contaminated by main having advanced 4 commits past the branch point before I merged).", "open_questions": [], "out_of_scope_findings": [] }(supersedes the mis-escaped report above -- this one carries the correct literal marker)
Generated by Claude Code
- added a commit that references this issue
on Aug 23, 2026
Recording a gap surfaced while landing #9046 (PR #9669). Unassigned finding — not claiming, and not asserting a disposition.
Observed
When a field/object
descriptionorlabelis corrected in a.object.tsdeclaration, there is no mechanism that propagates the correction into a locale bundle that already holds a hand translation of the old text. The generated bundles keep the stale translation indefinitely, and every gate stays green.The two halves, both measured on
origin/main(d693ba158):Extraction deliberately will not overwrite it.
packages/cli/src/commands/i18n/extract.tsmerges against the committed bundles (mergeExisting: !flags['no-merge']), and only the default locale is filled from schema labels.--fill=defaultfills gaps — a stale-but-present string is not a gap, so it is skipped. This is correct behaviour on its own terms: the alternative (--no-merge) wipes every hand translation in the bundle, which is the incident that config's own comment warns about.check:i18ncannot see the drift. It proves the keys are in sync — that no authoring key is undeclared and no bundle carries an orphan. It never compares a translated value against the meaning of its source. So a locale asserting the opposite of what the English now says reports "all bundles in sync."Why it is worth a card
This is the same shape as the class #9046 belongs to: a declared surface with no enforcement keeping it true. The locale bundle asserts a description to every non-English reader; nothing verifies that assertion still matches the source it was translated from. On compliance-adjacent text this is not cosmetic — #9046 existed precisely because two column descriptions promised a recertification review that does not happen, and three of the four locales would have kept promising it after the English was fixed, with
check:i18ngreen throughout.The correction in #9669 reached the three localized bundles only because a human edited the
helpleaves in place — the maintenance path the generated bundle header itself names ("Edit translations in place; re-run extract (with --merge) to fill new gaps. Do not hand-edit the structure — only the leaf string values."). That path works, but it is unenforced and invisible: nothing distinguishes "translator corrected it deliberately" from "nobody noticed."What is genuinely open
⛔ I am not asserting this is a defect. It may be intended — translation staleness is normally a human/translator workflow concern, and a repo that regenerated translations automatically would be making a worse trade. The open question is whether it is intended and accepted, or an unnoticed hole:
check:i18nreport the leaves whose source has moved since. That names which strings need a translator without touching any of them, and it does not degrade a locale to English to fix a promise.⛔ Scoped honestly: such a check would be a translation-staleness detector for schema-derived strings, not coverage of "documentation that lies." It would not read prose in
content/docs/**, JSDoc, or READMEs.Related
last_certified_at/certified_by) are declared on both grant tables and nothing writes or reads them #9046 / PR fix(security): withdraw the ADR-0091 D5 recertification claim from the attestation columns (#9046) #9669 — the card that surfaced it; the PM ruling on that PR carries the full reasoning for why no generator-only option was available.authorization.mdxlists the inert D5 pair alongside the enforcedreason/delegated_fromas one set of "lifecycle-audit columns" #9671 — a separate observation from the same PR (content/docs/permissions/authorization.mdxgrouping enforced and inert columns as one set).