From 3e9ac3c49cfe8a6bf4dcd2982f3e8c785c8a9e22 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 14:12:56 +0000 Subject: [PATCH 1/2] test(cli): pin the plugin-carried Setup pages against their translations (#3589) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three Setup pages ship as metadata inside capability plugins (`@objectstack/cloud-connection`, `@objectstack/mcp`) while their translations live in `@objectstack/platform-objects` — two packages, edited independently. `translatePage` applies the bundle for EVERY locale including `en`, so an `en` entry that has drifted from the metadata literal silently *overrides* the newer authored copy instead of falling back to it: the edit appears to do nothing and nothing in the build notices. The English side of #3648 was aligned by hand, which is exactly the kind of alignment that rots. The pages are fed through the CLI's own `collectExpectedEntries` — the extractor behind `os i18n extract` / `coverage` — so this also dogfoods that tooling against the platform's own shipped metadata. Covers: all three pages reachable through the plugins' UI bundles; an `en` entry for each; the `en` bundle byte-identical to the metadata literals; the `pages` key set identical across every shipped locale; and an end-to-end `translatePage` pass asserting zh-CN copy reaches `page:header` while `icon` survives and the plugin's module-level page object is not mutated. Placement: `packages/cli` is the only package that already depends on all three, and it owns the i18n coverage tooling. `platform-objects` must not depend on plugins (wrong direction) and the qa contract packages carry none of them. Verified by mutation rather than a green run: drifting one `en` subtitle fails the guard, restoring it passes. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TubWYdWquVkS9dj733sDmC --- .../test/platform-page-i18n-parity.test.ts | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 packages/cli/test/platform-page-i18n-parity.test.ts diff --git a/packages/cli/test/platform-page-i18n-parity.test.ts b/packages/cli/test/platform-page-i18n-parity.test.ts new file mode 100644 index 0000000000..ee3906108a --- /dev/null +++ b/packages/cli/test/platform-page-i18n-parity.test.ts @@ -0,0 +1,127 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Drift guard for the plugin-carried Setup pages' i18n (#3589). + * + * Three Setup pages ship as metadata inside capability plugins + * (`@objectstack/cloud-connection`, `@objectstack/mcp`) while their + * translations live in `@objectstack/platform-objects` — the two are edited + * in different packages by different people. `translatePage` applies the + * bundle for EVERY locale including `en`, so an `en` entry that has drifted + * from the metadata literal silently *overrides* the newer authored copy + * rather than falling back to it. Nothing else in the build notices. + * + * The pages are fed through the CLI's own `collectExpectedEntries` — the same + * extractor behind `os i18n extract` / `coverage` — so this doubles as a + * dogfood of that tooling against the platform's own shipped metadata. + * + * Placement: `packages/cli` is the only package that already depends on all + * three, and it owns the i18n coverage tooling. `platform-objects` must not + * depend on plugins (wrong direction), and the qa contract packages carry + * none of them. + */ + +import { describe, it, expect } from 'vitest'; +import { + MARKETPLACE_INSTALLED_UI_BUNDLE, + CLOUD_CONNECTION_UI_BUNDLE, +} from '@objectstack/cloud-connection'; +import { CONNECT_AGENT_UI_BUNDLE } from '@objectstack/mcp'; +import { SetupAppTranslations } from '@objectstack/platform-objects'; +import { translatePage } from '@objectstack/spec/system'; +import { collectExpectedEntries } from '../src/utils/i18n-extract'; + +/** The pages exactly as the plugins register them with the kernel. */ +const PAGES = [ + MARKETPLACE_INSTALLED_UI_BUNDLE.pages?.[0], + CLOUD_CONNECTION_UI_BUNDLE.pages?.[0], + CONNECT_AGENT_UI_BUNDLE.pages?.[0], +].filter(Boolean) as Array>; + +const EN = 'en'; +const pagesOf = (locale: string): Record => + ((SetupAppTranslations as Record)[locale]?.pages ?? {}); + +const read = (node: unknown, path: string): unknown => + path.split('.').reduce( + (acc, seg) => (acc && typeof acc === 'object' ? (acc as Record)[seg] : undefined), + node, + ); + +describe('plugin-carried Setup pages — i18n drift guard (#3589)', () => { + it('finds all three pages through the plugins’ UI bundles', () => { + expect(PAGES.map((p) => p.name).sort()).toEqual([ + 'cloud_connection_settings', + 'connect_agent', + 'marketplace_installed', + ]); + }); + + it('has an `en` translation entry for every page', () => { + const en = pagesOf(EN); + for (const page of PAGES) { + expect({ page: page.name, hasEntry: Boolean(en[page.name]) }) + .toEqual({ page: page.name, hasEntry: true }); + } + }); + + it('keeps the `en` bundle byte-identical to the metadata literals', () => { + // The real drift: someone edits `properties.subtitle` in the plugin and + // leaves the bundle alone. `en` requests then render the STALE bundle + // text, so the edit appears to do nothing. + const en = pagesOf(EN); + const expected = collectExpectedEntries({ pages: PAGES } as any) + .filter((e) => e.path[0] === 'pages'); + + expect(expected.length).toBeGreaterThan(0); + + for (const entry of expected) { + const [, pageName, ...rest] = entry.path; + const key = rest.join('.'); + expect({ page: pageName, key, value: read(en[pageName], key) }) + .toEqual({ page: pageName, key, value: entry.sourceValue }); + } + }); + + it('translates every page in every shipped locale (no silent English fallback)', () => { + const locales = Object.keys(SetupAppTranslations as Record); + expect(locales).toContain(EN); + expect(locales.length).toBeGreaterThan(1); + + const enKeys = Object.keys(pagesOf(EN)).sort(); + for (const locale of locales) { + expect({ locale, pages: Object.keys(pagesOf(locale)).sort() }) + .toEqual({ locale, pages: enKeys }); + } + }); + + it('rewrites the page:header copy end-to-end for a non-English locale', () => { + // The whole point of the chain: metadata in, localized header out. + const headerOf = (page: Record) => { + for (const region of page.regions ?? []) { + for (const component of region.components ?? []) { + if (component.type === 'page:header') return component.properties ?? {}; + } + } + return {}; + }; + + for (const page of PAGES) { + // Snapshot by value up front — comparing two live reads of the same + // object afterwards could not detect a mutation. + const before = { ...headerOf(page) }; + const translated = translatePage(page as any, SetupAppTranslations, { locale: 'zh-CN' }); + const after = headerOf(translated); + + expect({ page: page.name, titleChanged: after.title !== before.title }) + .toEqual({ page: page.name, titleChanged: true }); + expect({ page: page.name, subtitleChanged: after.subtitle !== before.subtitle }) + .toEqual({ page: page.name, subtitleChanged: true }); + // Non-translatable props survive the overlay. + expect({ page: page.name, icon: after.icon }).toEqual({ page: page.name, icon: before.icon }); + // The shared plugin-owned page object must not be mutated — it is a + // module-level singleton the kernel registers once. + expect({ page: page.name, header: headerOf(page) }).toEqual({ page: page.name, header: before }); + } + }); +}); From d2e6568920fe7206dcae2ec94c42a9584ca40926 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 14:24:08 +0000 Subject: [PATCH 2/2] chore: add an empty changeset for the test-only drift guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Check Changeset gate requires a changeset on every PR and sanctions an empty one for changes that release nothing. This PR adds a test and ships no package change, so it takes that path — matching the existing docs/assessment changesets that carry empty frontmatter. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TubWYdWquVkS9dj733sDmC --- .changeset/plugin-page-i18n-drift-guard.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .changeset/plugin-page-i18n-drift-guard.md diff --git a/.changeset/plugin-page-i18n-drift-guard.md b/.changeset/plugin-page-i18n-drift-guard.md new file mode 100644 index 0000000000..1fab894cc7 --- /dev/null +++ b/.changeset/plugin-page-i18n-drift-guard.md @@ -0,0 +1,14 @@ +--- +--- + +test(cli): pin the plugin-carried Setup pages against their translations (#3589) + +Test-only; releases nothing. Adds a drift guard asserting the `en` entries in +`@objectstack/platform-objects` stay byte-identical to the `page:header` +literals authored in `@objectstack/cloud-connection` and `@objectstack/mcp`, +that the `pages` key set matches across every shipped locale, and that +`translatePage` localizes the three Setup pages end-to-end. + +`translatePage` applies the bundle for every locale including `en`, so a +drifted `en` entry silently overrides newer authored copy rather than falling +back to it — previously nothing in the build caught that.