Skip to content

feat(spec): resolve page metadata i18n — page:header title/subtitle (#3589)#3648

Merged
os-zhuang merged 3 commits into
mainfrom
claude/custom-system-page-i18n-6ni3f8
Jul 27, 2026
Merged

feat(spec): resolve page metadata i18n — page:header title/subtitle (#3589)#3648
os-zhuang merged 3 commits into
mainfrom
claude/custom-system-page-i18n-6ni3f8

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #3589. Implements 方案 A(framework 通用解析)from that issue.

The gap

Three plugin-carried Setup pages hard-code their page:header copy in metadata:

Page Source
Installed Apps packages/cloud-connection/src/marketplace-ui.ts
Cloud Connection packages/cloud-connection/src/cloud-connection-ui.ts
Connect an Agent packages/mcp/src/connect-ui.ts

Every other metadata type is localized at the REST boundary, but page was not — so those headers stayed English in every locale while the matching nav labels translated correctly.

Two things were missing, and shipping only one would have been another no-op:

  1. The pages namespace existed only on AppTranslationBundleSchema, whose accessors (getAppBundle / loadAppBundle) are optional on II18nService and have zero implementations. The schema the runtime actually serves — TranslationData, via getTranslations(locale) — had no pages key.
  2. There was no resolver behind it. translateMetadataDocument dispatched five types; page fell through unchanged.

Approach

Option A over a console-side point fix, because the server already has every seam this needs and the console needs none:

  • TranslationDataSchema gains pages.<name>.{label,description,title,subtitle}, alongside dashboards.
  • New translatePage in @objectstack/spec/system translates a page's own label / description and overlays title / subtitle onto every page:header in the page's regions. Immutable overlay, mirroring translateDashboard. Registered in translateMetadataDocument.
  • page added to TRANSLATABLE_META_TYPES in rest-server.ts — a one-token change. Accept-Language extraction, the locale-keyed ETag ([P2] i18n: zh-CN ↔ English switch occasionally garbled — requires page refresh #1319) and Vary: Accept-Language already applied to every metadata type on both the list and single-item routes.
  • objectstack i18n extract now emits page entries so the new namespace is not invisible to the tooling.
  • zh-CN / ja-JP / es-ES copy for the three pages.

Two design choices worth review:

  • Header copy is keyed by page name, not component id. page:header instances carry no stable id, so the page name is the only addressable identifier on the document.
  • title falls back to pages.<name>.label. For all three pages the header title and the nav label are the same string; requiring both would just invite drift. An explicit title still wins when the two genuinely differ.

Also fixed

nav_cloud_connection and nav_connect_agent existed only in zh-CN.ts — added to en / ja-JP / es-ES.

No console change needed

Pages arrive already localized from the server, so page:header renders translated copy with no @object-ui change. Two bonuses fall out for free: transformSpecTranslations forwards unknown top-level keys verbatim, so pages.* reaches the client catalog, where useObjectLabel().pageLabel already probes pages.<name>.label — page breadcrumbs pick up the translations too.

Compatibility

Authoring is unchanged and English literals stay in metadata as the fallback: a page with no pages entry renders exactly as before. The English bundle entries are byte-identical to the plugin literals, so en requests are a no-op.

Testing

translatePage (11 cases): header title/subtitle overlay, non-header components untouched, icon and other props preserved, explicit-title precedence, untranslated-page passthrough, no input mutation, no-bundle fallback, region-less pages, dispatch through translateMetadataDocument, BCP-47 chain (zhzh-CN). Plus TranslationDataSchema.pages acceptance, two REST-boundary cases (envelope + list), and a CLI extraction case asserting the exact emitted key set.

Suites run green: spec 1096, rest 173, cli 635, platform-objects 215, lint 359.

Known consideration (pre-existing, not introduced here)

Translating on the read path means a Studio round-trip could persist localized strings back into metadata. That is already true for object, app, dashboard, view and action; page now joins that set rather than diverging from it. Worth a separate fix if it bites — it needs a read-path bypass for the editor, which does not exist today for any type.

Follow-ups (not in this PR)

  • CloudConnectionPanel.tsx (the Cloud Connection page body) has no i18n at all and no cloudConnection.* namespace in any of objectui's 10 locale packs. 自定义系统页 page:header 未国际化 (Installed Apps / Cloud Connection / Connect an Agent) #3589 assumed the widget bodies were mostly translated; that one is not, so that page stays mixed-language below the header until it is done. Belongs in objectui.
  • A parity test pinning the plugin page literals against the en bundle would catch drift, but wiring it needs a package that depends on both cloud-connection and mcpplatform-objects should not.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TubWYdWquVkS9dj733sDmC


Generated by Claude Code

…3589)

Custom system pages authored as metadata (Installed Apps, Cloud Connection,
Connect an Agent) hard-code their `page:header` copy in `properties.title` /
`properties.subtitle`. Every other metadata type is localized at the REST
boundary, but `page` was not, so those headers stayed English in every locale
while the matching nav labels translated correctly.

The `pages` namespace existed only on `AppTranslationBundleSchema` — a schema
no runtime reads — with no resolver behind it. This wires the read side and
the schema together rather than adding another namespace nothing consumes.

- `TranslationDataSchema` (the shape the i18n service actually serves) gains
  `pages.<name>.{label,description,title,subtitle}`.
- `translatePage` translates a page's own label/description and overlays
  title/subtitle onto every `page:header` in its regions; registered in
  `translateMetadataDocument` so it rides the existing read path.
- `page` added to the REST boundary's TRANSLATABLE_META_TYPES. Locale
  extraction, the locale-keyed ETag and `Vary: Accept-Language` already
  covered every metadata type, so there is no new plumbing.
- `objectstack i18n extract` emits page entries, so the new namespace is not
  invisible to the tooling.
- zh-CN / ja-JP / es-ES copy for the three Setup pages, plus the missing
  `nav_cloud_connection` / `nav_connect_agent` labels (zh-CN-only until now).

Header copy is keyed by page name, not component id: `page:header` instances
carry no stable id. `title` falls back to `pages.<name>.label`, since a page's
header title and its nav label are normally the same string.

English literals stay in metadata as the fallback — a page with no `pages`
entry renders exactly as before — and the console needs no change, since
pages arrive already localized from the server.

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

vercel Bot commented Jul 27, 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 27, 2026 12:54pm

Request Review

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/cli, @objectstack/platform-objects, @objectstack/rest, @objectstack/spec.

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

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via packages/cli, @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli, @objectstack/rest, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/rest, @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/cli, 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/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 packages/spec)
  • content/docs/concepts/north-star.mdx (via 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 @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/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • 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/cli, @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/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/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • 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/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/permissions/authorization.mdx (via @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/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/cli, @objectstack/platform-objects, @objectstack/rest, @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/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • 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/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/cli, @objectstack/rest, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/releases/v9.mdx (via @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/platform-objects, @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.

`content/docs/references/system/translation.mdx` is generated from the spec
schemas, so adding `pages` to `TranslationDataSchema` left it out of date
(caught by `@objectstack/spec check:docs`). Regenerated via
`gen:schema && gen:docs`.

`content/docs/ui/translations.mdx` is hand-written and enumerated the
translatable surfaces and the per-request metadata types — both stale as of
this branch. Added the `pages` row and `page` to the resolved-types list, plus
a note on why header copy is keyed by page name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TubWYdWquVkS9dj733sDmC
`gen:api-surface` snapshot pins @objectstack/spec's public exports. Adding
`translatePage` and its `PageLike` / `PageRegionLike` / `PageComponentLike`
shapes is purely additive (0 breaking, 4 added) and mirrors the existing
`translateDashboard` / `DashboardLike` / `WidgetLike` surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TubWYdWquVkS9dj733sDmC
@os-zhuang
os-zhuang marked this pull request as ready for review July 27, 2026 13:05
@os-zhuang
os-zhuang merged commit 67452d1 into main Jul 27, 2026
19 checks passed
@os-zhuang
os-zhuang deleted the claude/custom-system-page-i18n-6ni3f8 branch July 27, 2026 13:13
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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

自定义系统页 page:header 未国际化 (Installed Apps / Cloud Connection / Connect an Agent)

2 participants