Skip to content

fix(i18n): one translation shape — the translation type speaks objects., not o. (#3778) - #3808

Merged
os-zhuang merged 1 commit into
mainfrom
claude/i18n-unbridged-bundle-shapes-vilm7f
Jul 28, 2026
Merged

fix(i18n): one translation shape — the translation type speaks objects., not o. (#3778)#3808
os-zhuang merged 1 commit into
mainfrom
claude/i18n-unbridged-bundle-shapes-vilm7f

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #3778.

The bug

A translation authored in the product saved successfully and then rendered nothing.

Not a resolver gap — a contract split. The translation metadata type (allowRuntimeCreate: true, so Studio / the metadata API / an agent can author it) was registered against AppTranslationBundleSchema, an object-first shape keyed on o.<object>. Every resolver, os i18n extract, os i18n check, the objectui hooks, and all nine shipped bundles read objects.<object>. Nothing bridged them, so the save path and the read path never met.

The plumbing was never the problem: #2591's authored-translation sync is shape-agnostic, and the metadata door stores payloads verbatim. Only the registered authoring contract pointed at a shape nobody reads.

Why converge instead of bridge

A converter (normalize o.objects. at ingestion) was the obvious fix and the wrong one. It would be throwaway code, and worse, it would start producing working o.-shaped rows — closing the migration-free window that exists precisely because the feature never functioned.

The retired shape's real-world footprint was zero. All three *.translation.ts files in the tree (platform-objects setup, CRM and todo examples) were already objects.-shaped TranslationBundles, contradicting the type's own registered schema. Converging is a registration fix, not a migration.

objects.<name> is itself object-first — all of one object's content under one key — so the workbench/coverage benefits the protocol doc attributed to o. are fully retained. The difference was spelling, plus a few divergent leaf names (confirmMessage vs confirmText, no successMessage, a duplicate _options track, flat vs app-scoped nav) that disappear with the split.

Breaking changes

  • AppTranslationBundleSchema, ObjectTranslationNodeSchema and their types are deleted — no deprecation cycle. Nothing worked end-to-end through them, so there is no functioning consumer to protect, and a deprecated-but-present schema is exactly the exemplar an AI agent copies into new code. Every o. exemplar (schemas, protocol doc section, skill section, diff examples) is purged in the same PR.
  • II18nService.getAppBundle / loadAppBundle are removed: zero implementers, so they advertised a capability the runtime never delivered (Prime Directive chore: version packages #10).
  • ObjectTranslationData.label becomes optional (see below).

The two JSON Schemas were retired from json-schema.manifest.json — the ratchet gate caught the removal and required the deliberate opt-out, as designed.

The replacement

TranslationItemSchema — one locale of the same TranslationData groups a file bundle uses, plus the locale it translates, with a defineTranslation() factory. An item is one entry of a TranslationBundle; that is the whole type.

Three details are deliberate, all aimed at making the failure loud instead of silent — the property this bug lacked:

locale is required, not inferred from the item name. The sync skips an item whose locale it cannot resolve, and a skip is invisible to whoever — or whatever — authored it. (The name fallback still covers rows written before this.)

Retired keys are rejected, not stripped. Zod drops undeclared keys silently, which would reproduce this bug exactly: save succeeds, nothing renders. A pre-parse guard turns that silence into a 422 naming the group to use:

'o' belongs to the retired object-first translation shape, which no resolver reads
  — use 'objects.<object_name>'.

It runs ahead of the parse rather than as a refinement on declared keys, so the retired keys stay out of the schema itself. Verified through the real /meta/types path — the emitted JSON Schema lists only real groups with locale required, so neither it nor the Studio editor advertises a shape that cannot work. A test locks that in, because a regression there re-teaches the wrong shape to every agent reading the schema. Blanket .strict() was rejected: the protocol deliberately stores Studio auxiliary fields (isPinned, …) verbatim.

ObjectTranslationData.label becomes optional. Partial translation is the normal state (that's why coverage tooling exists) and every resolver already treats each key as independent. Requiring it forced authors — human or AI — to restate the source label just to pass validation, filling bundles with fake translations that mask real coverage gaps.

Aspirational groups with no consumer (reports, notifications, errors, _globalOptions, _meta.direction, namespace) were not carried over, per the #3494 liveness precedent: schema-only fields invite agents to author dead data users then believe is live.

Also in this change

  • The authored sync warns, naming the row and the fix, when it meets a row still in the retired shape, instead of loading it into nowhere. It also no longer merges publish bookkeeping (_lockReason, _packageVersion, …) into the translation layer.
  • GET /i18n/labels/:object/:locale's fallback reads the nested objects.<obj>.fields.<field>.label data it is actually handed. It scanned for flat dotted o.<obj>.fields.<field> keys — a third dialect no producer ever wrote — so it always returned {}. Its test fixture had been written to match the broken code, which is why it was green.
  • A translation create seed ({ locale: 'en', objects: {} }) so the Studio create flow round-trips instead of 422-ing on an empty body.
  • Docs (i18n-standard.mdx, translations.mdx, regenerated references), the i18n skill, and the metadata-validation-sweep fixture no longer teach the retired shape. The skill matters most here — it is what an agent reads before authoring bundles.

Verification

Gate Result
turbo run test --concurrency=4 (CI-equivalent) 129/129 tasks, zero failures
pnpm build 71/71
check:skill-examples / check:skill-docs 32 examples typecheck; docs in sync
check:liveness, check:spec-changes, check:upgrade-guide, check:doc-authoring pass
@objectstack/docs build pass

New tests: schema acceptance/rejection incl. per-key migration messages; the JSON-Schema-stays-clean assertion; and end-to-end — an authored objects. row syncs and comes back translated through translateMetadataDocument (object label, plural, field label, inline action label + confirm), a retired-shape row is skipped with the warning while a valid sibling still loads, and bookkeeping stays out of the layer.

metadata-validation-sweep's translation fixture failed on this branch — it was authored in the old shape. That is the guard working, not a false positive; the fixture is corrected. Two other packages failed intermittently in early parallel runs (plugin-pinyin-search build, plugin-approvals test); both were environmental — a tsup/turbo dts ordering race and a stale node_modules after merging main — and both pass in isolation and in the clean CI-concurrency run.

Out of scope (recorded, not fixed)

  • The authored sync is process-wide across organizations (noted in its own header): a translation published in one tenant resolves in every tenant on the same process. Must be resolved before a multi-tenant translation-workbench story ships. Now stated plainly in translations.mdx under "Current boundaries" rather than left buried.
  • The translation type's filePatterns (**/*.translation.ts, …) look vestigial — the three matching files are multi-locale bundles loaded via code/config, not through the metadata door.
  • docs/audits/2026-07-app-metadata-reference-integrity-assessment.md cites the two schema shapes as a complexity factor for the proposed validate-translation-references lint; that estimate is now simpler (one shape). Left as a dated record rather than edited.

🤖 Generated with Claude Code

https://claude.ai/code/session_017wPvotUdM6WFtJm1KKryLE


Generated by Claude Code

…ects.`, not `o.` (#3778)

A translation authored in the product saved successfully and rendered nothing.
Not a resolver gap — a contract split. The `translation` metadata type
(`allowRuntimeCreate: true`) was registered against `AppTranslationBundleSchema`,
an object-first shape keyed on `o.<object>`. Every resolver, `os i18n extract`,
`os i18n check`, the objectui hooks, and all nine shipped bundles read
`objects.<object>`. Nothing bridged them, so the save path and the read path
never met.

A converter was the obvious fix and the wrong one: throwaway code that would
start producing *working* `o.`-shaped rows, closing the migration-free window
that exists precisely because the feature never functioned. The retired shape's
real-world footprint was zero — all three `*.translation.ts` files in the tree
were already `objects.`-shaped, contradicting the type's own registered schema.
Converging is a registration fix, not a migration.

BREAKING: `AppTranslationBundleSchema`, `ObjectTranslationNodeSchema` and their
types are deleted with no deprecation cycle — nothing worked end-to-end through
them, so there is no consumer to protect, and a deprecated-but-present schema is
exactly the exemplar an agent copies into new code. `II18nService.getAppBundle` /
`loadAppBundle` go with them (zero implementers — a capability the runtime never
delivered).

`TranslationItemSchema` replaces them: one locale of the same `TranslationData`
groups a file bundle uses, plus the `locale` it translates, with a
`defineTranslation()` factory. Three details are deliberate, all aimed at making
the failure loud instead of silent:

- `locale` is required, not inferred from the item name. The sync skips an item
  whose locale it cannot resolve, and a skip is invisible to whoever authored it.
- Retired keys are rejected, not stripped. Zod drops undeclared keys silently,
  which would reproduce this bug exactly. A pre-parse guard turns that silence
  into a 422 naming the right group, and runs ahead of the parse so the keys stay
  out of the schema — the generated JSON Schema and the Studio editor never
  advertise a shape that cannot work.
- `ObjectTranslationData.label` becomes optional. Partial translation is the
  normal state; requiring it forced authors to restate the source label just to
  validate, filling bundles with fake translations that mask coverage gaps.

Also here: the authored sync warns (naming the row and the fix) on a row still in
the retired shape instead of loading it into nowhere, and no longer merges
publish bookkeeping into the translation layer. `GET /i18n/labels/:object/:locale`
reads the nested field data it is actually given — it scanned for flat dotted
`o.<obj>.fields.<field>` keys, a third dialect no producer ever wrote, so it
always returned `{}`. A `translation` create seed makes the Studio create flow
round-trip. Docs, the i18n skill, and the sweep-test fixture no longer teach the
retired shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wPvotUdM6WFtJm1KKryLE
@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 6:30am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:system tests tooling size/xl labels Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/core, @objectstack/objectql, packages/services, @objectstack/spec.

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

  • content/docs/ai/actions-as-tools.mdx (via @objectstack/core)
  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/knowledge-rag.mdx (via @objectstack/core)
  • content/docs/ai/natural-language-queries.mdx (via @objectstack/core)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/core, packages/services, @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/core, packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/core, @objectstack/objectql)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/core)
  • content/docs/kernel/runtime-services/index.mdx (via packages/services, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/core, @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/core, @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/core, @objectstack/objectql)
  • content/docs/permissions/authorization.mdx (via packages/core, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/anatomy.mdx (via @objectstack/core)
  • content/docs/plugins/development.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/core, @objectstack/objectql, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/core, @objectstack/objectql, packages/services, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/services, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/core, @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/core, @objectstack/objectql, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v15.mdx (via @objectstack/core)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

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.

@os-zhuang
os-zhuang marked this pull request as ready for review July 28, 2026 06:42
@os-zhuang
os-zhuang merged commit 4cca74c into main Jul 28, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/i18n-unbridged-bundle-shapes-vilm7f branch July 28, 2026 06:43
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:system size/xl tests tooling

Projects

None yet

2 participants