Skip to content

fix(i18n): translate the platform packages' declared surface; gate all nine bundles, not one (#3762) - #3780

Merged
os-zhuang merged 2 commits into
mainfrom
claude/unify-object-ui-types-spec-53x3pt
Jul 28, 2026
Merged

fix(i18n): translate the platform packages' declared surface; gate all nine bundles, not one (#3762)#3780
os-zhuang merged 2 commits into
mainfrom
claude/unify-object-ui-types-spec-53x3pt

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes the fillable half of #3762. Follows #3370#3753#3768.

The finding that reframed this

Nine packages own a translation bundle. One was wired into a drift check. The other eight shipped a scripts/i18n-extract.config.ts that nothing ever ran — and four of them had already drifted out of sync with the schema:

plugin-approvals   in sync ✓
plugin-security    4 locales out of date
plugin-audit       4 locales out of date
plugin-sharing     4 locales out of date
plugin-webhooks    4 locales out of date

That is precisely the silent staleness pnpm check:i18n exists to prevent, sitting one directory outside the only path it guarded.

Translated

plugin-security (45 strings/locale), plugin-webhooks (15), plugin-audit (8), plugin-sharing (7), service-storage (7) — all now at zero untranslated declared strings across zh-CN / ja-JP / es-ES. 246 translations.

Most of these were newly visible rather than newly missing: #3753 taught the coverage detector to walk action params, resultDialog, listViews, object description, field help/placeholder. This is what it found.

Wording was harvested from the repo's own bundles first — 1382 unambiguous en→locale mappings scraped from everything already translated — so Created At is 创建时间 here because that is what it is everywhere else. Only 59 strings had no existing precedent and were written by hand. Protocol tokens are deliberately left identical across locales: GET / POST / PUT / PATCH / DELETE, ETag, ACL, URL.

Gated

scripts/check-i18n-bundles.mjs replaces the single-package pnpm check:i18n and covers all nine (40 bundles).

It does not restate each package's command. It parses the regenerate command out of that config's own docstring and runs exactly that, plus --check — so the documented command and the gate cannot diverge. Same anti-drift principle as making the coverage detector share the extractor's walker in #3753: one source, not two that agree today.

The coverage ratchet from #3768 grows the same way, examples/* → twelve configs:

config baselined debt
app-showcase / app-todo / app-crm 456 / 212 / 97
platform-objects 231
the other eight packages 0

Eight at zero means the ratchet is the strict gate for them — any regression is immediately red, not merely visible.

Verified both gates bite, not just run:

A real truncation bug this exposed

Wiring the ratchet to platform-objects failed on unparseable JSON. The output was exactly 65536 bytes — one pipe buffer.

console.log(big) followed by process.exit(1) tears the process down before an async pipe write drains. Interactively it looks perfect, because stdout to a TTY is written synchronously. Piped — i.e. every scripted consumer — it silently yields invalid JSON.

emitJson (packages/cli/src/utils/format.ts) waits for the write callback and sets process.exitCode instead of calling process.exit. lint, i18n check and i18n extract use it; os lint --json on platform-objects goes from 65536 truncated bytes to 234603 that parse. ~30 other CLI commands share the pattern and are not touched here — a mechanical sweep I could not verify command-by-command in this change.

The ratchet also captures to a file rather than a pipe regardless, since a gate that can read a truncated payload and report a smaller number is worse than no gate.

Also

The nine documented regenerate commands gain --no-metadata-forms (added in #3768) — the Studio metadata-form baseline belongs to platform-objects alone, not a copy in every plugin.

Not in scope

platform-objects' own 77-per-locale gap is apps.* / dashboards.* navigation and widget labels. Those live outside the objects subtree, so the package's --objects-only extract cannot scaffold them — which is why its --check reports in sync while coverage reports 77 missing. That needs an emit decision (drop --objects-only, or a companion <locale>.apps.generated.ts) before any translating; it stays in #3762, and the 231 is frozen in the baseline meanwhile.

Also filed: #3778 — the file-authored objects.<name> bundles and the runtime translation metadata type's o.<object> shape are unbridged, so translations cannot be authored in the product at all. Wants an ADR, not a patch.

Tests

CLI suite 667 green (66 files). The five translated packages: plugin-security 673, service-storage 199, plugin-sharing 101, plugin-audit 46, plugin-webhooks 25 — all green, including their bundle-ownership guards. ESLint clean. Both gates green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BQwnjqgCyAhSQjhNriW3BW


Generated by Claude Code

…l nine bundles, not one (#3762)

Only `platform-objects` was wired into a translation-drift check. The other
eight packages shipped a `scripts/i18n-extract.config.ts` that nothing ever ran,
and four of them had already drifted out of sync with the schema — the same rot
`pnpm check:i18n` exists to catch, one directory over.

Translated: plugin-security (45 strings per locale), plugin-webhooks (15),
plugin-audit (8), plugin-sharing (7) and service-storage (7) are now at zero
untranslated declared strings in zh-CN / ja-JP / es-ES — 246 translations. Most
were newly visible rather than newly missing; #3753 taught the coverage detector
to walk action params, resultDialog, listViews and the rest, and this is what it
found. Wording was harvested from the repo's own bundles wherever a string was
already translated somewhere (1382 unambiguous sources), so terminology matches
what ships elsewhere instead of being reinvented. Protocol tokens stay identical
across locales on purpose: GET/POST/PUT/PATCH/DELETE, ETag, ACL, URL.

Gated: `scripts/check-i18n-bundles.mjs` replaces the single-package check and
covers all nine. It does not restate each command — it parses the one already
documented in that config's own docstring and runs it, so the documented
regenerate command and the gate cannot diverge. The coverage ratchet grows from
`examples/*` to twelve configs; eight sit at zero, making it the strict gate
there.

Fixed a truncation bug this exposed: `os lint --json` on a large config came out
of a pipe cut off at exactly 65536 bytes. `console.log(big)` then
`process.exit(1)` tears the process down before an async pipe write drains,
while an interactive run (TTY, written synchronously) looks perfect — so every
scripted consumer silently got invalid JSON. `emitJson` waits for the drain and
sets `process.exitCode`; lint, i18n check and i18n extract use it. ~30 other CLI
commands share the pattern and are untouched.

Not fixed here: platform-objects' own 77-per-locale gap is apps/dashboards
navigation and widget labels, outside the `objects` subtree and unscaffoldable
while the package extracts with `--objects-only`. Needs an emit decision — #3762.

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

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 28, 2026 4:23am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation ci/cd dependencies Pull requests that update a dependency file tooling size/xl labels Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 6 package(s): @objectstack/cli, @objectstack/plugin-audit, @objectstack/plugin-security, @objectstack/plugin-sharing, @objectstack/plugin-webhooks, packages/services.

32 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx (via packages/cli)
  • content/docs/automation/webhooks.mdx (via packages/plugins/plugin-webhooks, packages/services)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli, @objectstack/plugin-audit, @objectstack/plugin-security)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/plugin-audit)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli, packages/services)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/permissions/authorization.mdx (via @objectstack/plugin-security, packages/plugins/plugin-sharing)
  • content/docs/permissions/explain.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-security, packages/plugins/plugin-sharing)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/plugin-security)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-security)
  • content/docs/plugins/packages.mdx (via @objectstack/cli, @objectstack/plugin-audit, @objectstack/plugin-security, @objectstack/plugin-sharing, @objectstack/plugin-webhooks, packages/services)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/services)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/protocol/objectql/security.mdx (via packages/plugins/plugin-sharing)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli, @objectstack/plugin-audit, @objectstack/plugin-security, @objectstack/plugin-webhooks)
  • content/docs/releases/v16.mdx (via @objectstack/cli)
  • content/docs/ui/audience-based-interfaces.mdx (via packages/plugins/plugin-security)
  • content/docs/ui/dashboards.mdx (via @objectstack/plugin-security)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…kages

The guardrail table named only platform-objects and pointed at
`pnpm i18n:extract`. Eight more packages now have gated bundles, and the
command that regenerates all of them is `check-i18n-bundles.mjs --write`.

Also states what the gate actually checks, which the old wording left
ambiguous: translation VALUES are hand-written and editing one is fine — the
check runs in merge mode and preserves it. Adding or dropping KEYS is the drift
it fails on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BQwnjqgCyAhSQjhNriW3BW
@os-zhuang
os-zhuang marked this pull request as ready for review July 28, 2026 05:20
@os-zhuang
os-zhuang merged commit aff9e56 into main Jul 28, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/unify-object-ui-types-spec-53x3pt branch July 28, 2026 05:20
os-zhuang added a commit that referenced this pull request Jul 28, 2026
…e the debt was 1 string, not 231 (#3803)

Two follow-ups to #3780. Both were smaller in the work and larger in the
finding than the issue text implied.

## 1 — `--json` truncation: every command, and the second document it hid

#3780 routed THREE commands through `emitJson`. The other ~100 emission sites
still wrote machine output with `console.log`, which on a PIPE is cut off at one
64 KiB buffer when the command exits right after. Invisible to whoever writes
it: stdout to a TTY is synchronous, so every interactive run looks perfect while
every scripted consumer — the only audience `--json` has — gets invalid JSON.

The exit need not be explicit. oclif ends failing commands with `handle()` →
`Exit.exit()` → `process.exit()` and flushes nothing on that path (`flush()`
runs only on `execute()`'s success path), so a plain `this.exit(1)` or any
thrown error truncates identically. Measured on a 300 009-byte payload:
`process.exit(1)` 65 536, `throw` 65 536, natural return 300 009,
`process.exitCode = 1` 300 009. 73 of the 104 sites had that shape, and `lint`
was itself only half fixed by #3780 — `--eval --json` writes a whole corpus
report and was still on `console.log`.

- All 104 sites go through `emitJson`; `formatOutput`'s json/yaml branches
  through the same drain-aware `emitText` (`--format yaml` truncated too),
  making it async — 32 call sites now await it.
- Control flow untouched: a following `this.exit(1)` stays, simply safe once
  the buffer has drained.
- Output bytes unchanged. Roughly half the sites emitted compact and half
  indented; `{ compact: true }` preserves whichever each had, keeping this a
  truncation fix rather than an output-format change.
- An ESLint rule (root script runs `--no-inline-config`, so no per-site opt-out)
  rejects `console.log(JSON.stringify(…))` under packages/cli/src and
  un-awaited `formatOutput`. It earned its place immediately: #3789 landed
  `os validate`'s authoring lints with the old pattern and the rule caught it
  on the merge.

Draining the write exposed a second defect underneath. Because `this.exit(1)`
THROWS, a command whose body sits in one `try` unwinds its inner "report and
stop" into the outer `catch`, which reports again — `os validate --json` printed
TWO JSON documents, neither valid JSON nor valid JSONL. Truncation had been
hiding the second one. Nine commands had this shape; their catches now re-throw
the exit signal (`isExitSignal`).

`os validate --json`, 900 schema errors, piped: 131 072 bytes unparseable
before; 1 514 711 in two documents with the truncation fix alone; 1 514 648 as
one parseable document with both.

Deliberately NOT fixed by forcing stdout into blocking mode process-wide, which
would be one line and cover everything: the same binary runs `os serve` /
`os dev`, and a blocking write to a pipe with a slow reader blocks the event
loop — trading truncated JSON for a server that stalls on its own logs.

## 2 — #3762's remainder: the debt was 1 string, not 231

The open item read "77 strings short per locale in apps.*/dashboards.*, needs an
emit decision (drop --objects-only, or a companion .apps.generated.ts)".

Measured, the premise did not hold. Of the 77 declared keys per locale, 76 were
already translated in the hand-authored `<locale>.ts` files and had been for
months. Exactly one was genuinely missing:
`apps.studio.navigation.nav_app_builder.label`, absent in all four locales
including `en`. The 231 was a measurement artifact — the config declares
SETUP_APP / STUDIO_APP / ACCOUNT_APP and SystemOverviewDashboard, but its
`translations` merge baseline listed only the two GENERATED subtrees.

Neither proposed emit is right, and the second would have caused damage. The
Setup app is a shell of empty group anchors; its ~25 entries are contributed at
RUNTIME by SETUP_NAV_CONTRIBUTIONS and by capability plugins (ADR-0029 D7), so
a bundle generated from a static walk is structurally incomplete —
regenerating over the hand-authored files would have DELETED 40 live nav
translations per locale. Dropping `--objects-only` fails differently:
`kind:'full'` folds all 803 metadata-form keys into the objects bundle and
renames the export the baseline imports.

The split is correct and is now written down: objects/metadataForms are
generated and gated by the bundle-drift check; apps/dashboards/pages are
hand-authored and gated by the coverage ratchet. Only the baseline was wrong.

- Extract config's `translations` carries the per-locale assemblers, with
  objects/metadataForms still pinned to the committed generated files.
- nav_app_builder translated in all four locales, wording harvested from the
  repo's own precedent for "builder" (构建器 / ビルダー / generador).
- nav_workflows removed from all four — its entry is gone from STUDIO_APP and
  nothing contributes to that app, so the translation was dead.
- Ratchet baselined 231 -> 0 (repo total 996 -> 765), making platform-objects
  the ninth package where the ratchet is a strict gate.
- A CLI-independent parity test walks the statically declared Studio and
  Account navigation plus the dashboard's widgets and asserts a translation in
  every locale, and the reverse — no translation outlives its nav item.

An untranslated nav id is invisible in the UI: it falls back to the app's own
English label, so a Chinese Studio menu shows one English entry among thirty.
That is why this needs a gate and not a one-time sweep.

## On the gates themselves

Every new gate was checked in BOTH directions — red before the fix, green after.
That discipline caught a defect in my own test: the truncation control case
initially raced the reader's drain, passing locally at 65 536 bytes and failing
on a runner whose reader kept up. A gate that goes red by machine speed is no
better than one that cannot go red, so it was rewritten to remove the race —
never read the pipe until the child exits, 4 MB payload, assert only the
shortfall. The technique is valid only for the broken pattern: pointed at
`emitJson` it hangs forever, because awaiting the write callback is what the fix
does. Verified rather than assumed.

Closes #3762.

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants