From d37ce998a9bc0cc31b6d193d7af3ef7681d2c4d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 03:40:42 +0000 Subject: [PATCH 1/4] test(cli): put the three shipped platform record pages under an i18n gate The three record pages plugin-auth and plugin-security contribute (sys_user_detail / sys_organization_detail / sys_position_detail) were in no `os i18n extract` config and under no gate at all, so `check:i18n-coverage`'s 0 for platform-objects read as "checked, clean" over a population that never contained them. Measured with the real `collectExpectedEntries`: the three pages together offer exactly three keys, one page-level `label` each. All three author `regions: []` and everything under `slots.*`, and the shared walk roots at `regions[].components[]`, so 45 further authored copy sites -- every one an inline locale map -- have no bundle face. A region-authored control page does get its component copy offered, so the extractor is working and the absence is the pages' shape. Adds the three missing `pages.*` bundle entries in all four shipped locales, and extends the existing plugin-page drift guard with a block that reads its population from the `@objectstack/platform-objects/pages` barrel: a fourth page joins the gate by existing, and every inline locale map on those pages must carry every shipped locale, so a new section heading authored in English alone now reds instead of shipping green. Widens no walk and moves no baseline. Whether the extractor should see inline maps is a maintainer decision open on #14749. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N --- .../test/platform-page-i18n-parity.test.ts | 197 ++++++++++++++++++ .../src/apps/translations/en.ts | 18 ++ .../src/apps/translations/es-ES.ts | 18 ++ .../src/apps/translations/ja-JP.ts | 18 ++ .../src/apps/translations/zh-CN.ts | 18 ++ 5 files changed, 269 insertions(+) diff --git a/packages/cli/test/platform-page-i18n-parity.test.ts b/packages/cli/test/platform-page-i18n-parity.test.ts index d25ee2335a..c107fd2503 100644 --- a/packages/cli/test/platform-page-i18n-parity.test.ts +++ b/packages/cli/test/platform-page-i18n-parity.test.ts @@ -28,6 +28,7 @@ import { } from '@objectstack/cloud-connection'; import { CONNECT_AGENT_UI_BUNDLE } from '@objectstack/mcp'; import { SetupAppTranslations } from '@objectstack/platform-objects'; +import * as PlatformPages from '@objectstack/platform-objects/pages'; import { PAGE_COMPONENT_COPY_KEYS, translatePage } from '@objectstack/spec/system'; import { collectExpectedEntries } from '../src/utils/i18n-extract.js'; @@ -472,3 +473,199 @@ describe('i18n-extract ↔ translatePage walk parity (#13109)', () => { expect(translated.regions[0].components[0].properties.title).toEqual('L'); }); }); + + +// --- The three shipped platform RECORD pages (#14817) ---------------------- +// +// The guard at the top of this file owns the plugin-carried Setup pages whose +// copy lives in the BUNDLE. This block owns the other three pages the platform +// ships -- `sys_user_detail`, `sys_organization_detail`, `sys_position_detail`, +// contributed by plugin-auth and plugin-security -- whose copy is almost +// entirely authored INLINE, and which were in no `os i18n extract` config and +// under no gate at all. +// +// ## The measurement this block was written from +// +// Fed through the real `collectExpectedEntries`, the three pages together +// offer exactly THREE keys -- one page-level `label` each. Nothing else. All +// three author `regions: []` and put every component under `slots.*`, and the +// shared walk (`walkAddressedPageComponents`, `@objectstack/spec/system`) roots +// at `regions[].components[]` only. So 45 further authored copy sites, every +// one of them an inline `{ en, 'zh-CN', ... }` locale map, are not reachable +// from the bundle face at all. A control page authored under `regions` with a +// `component.id` DOES get its component copy offered, which is how we know the +// extractor is working and the absence is the pages' SHAPE. +// +// ## Why that is not a defect this block tries to fix +// +// The inline map is the RULED route for page copy, not a workaround: the +// maintainer ruled (2026-08-06) that it is a delivered capability, which is why +// `I18nLabelSchema` is a union of a plain string and an inline map, and why +// `translation.zod.ts` declines `content` on the bundle face for the identical +// shape. Whether the EXTRACTOR should also see those maps is an open question +// (#14749) and a maintainer decision. This block therefore does not widen +// anything -- it makes the boundary measurable and puts the surface under a +// gate for the first time. +// +// ## What each assertion buys +// +// The harm recorded on #14817 is not today's debt (there is none) -- it is that +// `check:i18n-coverage`'s `0` for `platform-objects` reads as "checked, clean" +// over a population that never contained these pages, so "a fourth plugin page, +// or one new untranslated section heading, lands green". The population below +// is read from the `@objectstack/platform-objects/pages` BARREL rather than +// listed here, so a fourth page joins this gate by existing; and the inline-map +// assertion judges the authoring site, which is the half the bundle face +// cannot see. Both directions of that sentence now red instead of shipping. + +/** Every page the platform's own `pages` barrel exports, as the plugins take them. */ +const RECORD_PAGES: Array> = Object.values( + PlatformPages as unknown as Record, +).filter( + (v): v is Record => + Boolean(v) && typeof v === 'object' && typeof (v as any).name === 'string', +); + +/** The locales the shipped bundle actually carries -- never a hard-coded list. */ +const SHIPPED_LOCALES = Object.keys(SetupAppTranslations as Record); + +/** + * A key is a locale code (`en`, `zh-CN`). Deliberately a shape test rather than + * a membership test against `SHIPPED_LOCALES`: a map carrying a locale the + * bundle does not ship is still an inline map, and must still be judged. + */ +const LOCALE_KEY = /^[a-z]{2}(-[A-Z]{2})?$/; + +/** An inline `I18nLabel` map: every key a locale code, every value a string. */ +const isInlineLocaleMap = (value: unknown): value is Record => { + if (!value || typeof value !== 'object' || Array.isArray(value)) return false; + const keys = Object.keys(value as object); + return keys.length > 0 + && keys.every((k) => LOCALE_KEY.test(k)) + && Object.values(value as Record).every((v) => typeof v === 'string'); +}; + +/** + * Every inline locale map anywhere in a page document, by authored path. A + * generic JSON walk with its own cycle guard, deliberately NOT a copy of either + * the resolver's or the extractor's traversal -- the whole point is to reach + * what those two do not. + */ +const inlineLocaleMaps = ( + node: unknown, + path: string, + out: Array<{ path: string; locales: string[] }> = [], + seen = new Set(), +): Array<{ path: string; locales: string[] }> => { + if (!node || typeof node !== 'object') return out; + if (seen.has(node)) return out; + seen.add(node); + if (Array.isArray(node)) { + node.forEach((item, i) => inlineLocaleMaps(item, `${path}[${i}]`, out, seen)); + return out; + } + for (const [key, value] of Object.entries(node as Record)) { + const here = path ? `${path}.${key}` : key; + if (isInlineLocaleMap(value)) { + out.push({ path: here, locales: Object.keys(value) }); + continue; + } + inlineLocaleMaps(value, here, out, seen); + } + return out; +}; + +describe('shipped platform record pages -- i18n ownership (#14817)', () => { + it('reads a non-empty population from the pages barrel', () => { + // A floor, not an equality: adding a page is ordinary work and must not + // red here. What this refuses is the scan that finds NOTHING -- an empty + // population would satisfy every `for` loop below and report success over + // zero pages, which is the exact shape of the `0` that reads as "clean". + expect(RECORD_PAGES.length).toBeGreaterThanOrEqual(3); + expect(RECORD_PAGES.map((p) => p.name).sort()).toEqual( + expect.arrayContaining(['sys_organization_detail', 'sys_position_detail', 'sys_user_detail']), + ); + expect(SHIPPED_LOCALES).toContain(EN); + expect(SHIPPED_LOCALES.length).toBeGreaterThan(1); + }); + + it('carries a bundle entry for every barrel page in every shipped locale', () => { + // The half that was missing entirely: these three pages had no `pages.*` + // entry in any locale, so nothing asked a translator for their titles and + // the Setup record header rendered "User" in every language. + for (const page of RECORD_PAGES) { + for (const locale of SHIPPED_LOCALES) { + const entry = pagesOf(locale)[page.name]; + expect({ page: page.name, locale, hasLabel: typeof entry?.label === 'string' && entry.label.length > 0 }) + .toEqual({ page: page.name, locale, hasLabel: true }); + } + } + }); + + it('keeps the `en` bundle byte-identical to what the extractor reads off the metadata', () => { + // Same drift the block above guards for the plugin-carried pages: an edit + // to the page literal that leaves the bundle alone renders the STALE text, + // because `translatePage` applies the bundle for `en` too. + const en = pagesOf(EN); + const expected = collectExpectedEntries({ pages: RECORD_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('records that the extractor reaches the page label and nothing under `slots`', () => { + // A BOUNDARY PIN, not an endorsement. It states the measured fact that the + // shared walk roots at `regions[].components[]` and these pages author + // `regions: []`, so the 45 inline sites under `slots.*` have no bundle + // face. If the walk is ever widened -- a maintainer decision open on + // #14749 -- this reds, and the person widening it is told, at the exact + // moment they can act on it, that these three pages gain a bundle surface + // that needs entries and a coverage home. That notice is the whole value: + // today the same change would land green over an unmeasured population. + for (const page of RECORD_PAGES) { + const offered = collectExpectedEntries({ pages: [page] } as any) + .filter((e) => e.path[0] === 'pages' && e.path[1] === page.name) + .map((e) => e.path.slice(2).join('.')) + .sort(); + expect({ page: page.name, regions: page.regions, offered }) + .toEqual({ page: page.name, regions: [], offered: ['label'] }); + } + }); + + it('holds every inline locale map on those pages complete in every shipped locale', () => { + // The recurrence guard, and the answer to "nobody would learn if it stopped + // being zero". These maps are invisible to `os i18n extract` and therefore + // to `check:i18n-coverage`; before this assertion a new section heading + // authored with `en` alone shipped green and rendered English to every + // reader. The population is measured off the documents, so it grows with + // the pages instead of needing a list here. + const maps = RECORD_PAGES.flatMap((page) => inlineLocaleMaps(page, page.name)); + + // Same refusal as the population floor: zero maps means the walk broke, not + // that the pages went monolingual. + expect(maps.length).toBeGreaterThanOrEqual(45); + + const incomplete = maps + .filter((m) => SHIPPED_LOCALES.some((locale) => !m.locales.includes(locale))) + .map((m) => ({ path: m.path, missing: SHIPPED_LOCALES.filter((l) => !m.locales.includes(l)) })); + expect(incomplete).toEqual([]); + }); + + it('localizes the page label through `translatePage` without mutating the singleton', () => { + for (const page of RECORD_PAGES) { + const before = page.label; + const translated = translatePage(page as any, SetupAppTranslations, { locale: 'zh-CN' }) as any; + expect({ page: page.name, changed: translated.label !== before }) + .toEqual({ page: page.name, changed: true }); + // These page objects are module-level singletons the kernel registers + // once; an overlay that mutated one would localize it process-wide. + expect({ page: page.name, label: page.label }).toEqual({ page: page.name, label: before }); + } + }); +}); diff --git a/packages/platform-objects/src/apps/translations/en.ts b/packages/platform-objects/src/apps/translations/en.ts index 6873d60cbb..1d1d3f6bb2 100644 --- a/packages/platform-objects/src/apps/translations/en.ts +++ b/packages/platform-objects/src/apps/translations/en.ts @@ -237,6 +237,24 @@ export const en: TranslationData = { // (@objectstack/cloud-connection, @objectstack/mcp) and exist so the other // locales have a complete key set to translate against. pages: { + // The three shipped platform RECORD pages, contributed by plugin-auth + // (sys_user / sys_organization) and plugin-security (sys_position). Their + // page-level `label` is the ONLY key `collectExpectedEntries` offers for + // them — all three author `regions: []` and put every component under + // `slots.*`, which the shared walk does not descend. Measured: 3 entries + // emitted for the three pages, against 45 inline-locale-map sites under + // `slots` that the bundle face does not reach (those are the ruled + // authoring-site route, 2026-08-06; their extractor visibility is #14749). + // `packages/cli/test/platform-page-i18n-parity.test.ts` owns these keys. + sys_user_detail: { + label: 'User', + }, + sys_organization_detail: { + label: 'Organization', + }, + sys_position_detail: { + label: 'Position', + }, marketplace_installed: { label: 'Installed Apps', subtitle: "Marketplace packages currently installed into this runtime's kernel.", diff --git a/packages/platform-objects/src/apps/translations/es-ES.ts b/packages/platform-objects/src/apps/translations/es-ES.ts index a0f1b8c64e..a679dd27dd 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.ts @@ -158,6 +158,24 @@ export const esES: TranslationData = { }, pages: { + // The three shipped platform RECORD pages, contributed by plugin-auth + // (sys_user / sys_organization) and plugin-security (sys_position). Their + // page-level `label` is the ONLY key `collectExpectedEntries` offers for + // them — all three author `regions: []` and put every component under + // `slots.*`, which the shared walk does not descend. Measured: 3 entries + // emitted for the three pages, against 45 inline-locale-map sites under + // `slots` that the bundle face does not reach (those are the ruled + // authoring-site route, 2026-08-06; their extractor visibility is #14749). + // `packages/cli/test/platform-page-i18n-parity.test.ts` owns these keys. + sys_user_detail: { + label: 'Usuario', + }, + sys_organization_detail: { + label: 'Organización', + }, + sys_position_detail: { + label: 'Puesto', + }, marketplace_installed: { label: 'Aplicaciones instaladas', subtitle: 'Paquetes del marketplace instalados actualmente en el kernel de este runtime.', diff --git a/packages/platform-objects/src/apps/translations/ja-JP.ts b/packages/platform-objects/src/apps/translations/ja-JP.ts index e2b1fdcac6..28aaf6a98a 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.ts @@ -158,6 +158,24 @@ export const jaJP: TranslationData = { }, pages: { + // The three shipped platform RECORD pages, contributed by plugin-auth + // (sys_user / sys_organization) and plugin-security (sys_position). Their + // page-level `label` is the ONLY key `collectExpectedEntries` offers for + // them — all three author `regions: []` and put every component under + // `slots.*`, which the shared walk does not descend. Measured: 3 entries + // emitted for the three pages, against 45 inline-locale-map sites under + // `slots` that the bundle face does not reach (those are the ruled + // authoring-site route, 2026-08-06; their extractor visibility is #14749). + // `packages/cli/test/platform-page-i18n-parity.test.ts` owns these keys. + sys_user_detail: { + label: 'ユーザー', + }, + sys_organization_detail: { + label: '組織', + }, + sys_position_detail: { + label: 'ポジション', + }, marketplace_installed: { label: 'インストール済みアプリ', subtitle: 'このランタイムのカーネルに現在インストールされているマーケットプレイスパッケージ。', diff --git a/packages/platform-objects/src/apps/translations/zh-CN.ts b/packages/platform-objects/src/apps/translations/zh-CN.ts index e841295b10..e1df443952 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.ts @@ -168,6 +168,24 @@ export const zhCN: TranslationData = { }, pages: { + // The three shipped platform RECORD pages, contributed by plugin-auth + // (sys_user / sys_organization) and plugin-security (sys_position). Their + // page-level `label` is the ONLY key `collectExpectedEntries` offers for + // them — all three author `regions: []` and put every component under + // `slots.*`, which the shared walk does not descend. Measured: 3 entries + // emitted for the three pages, against 45 inline-locale-map sites under + // `slots` that the bundle face does not reach (those are the ruled + // authoring-site route, 2026-08-06; their extractor visibility is #14749). + // `packages/cli/test/platform-page-i18n-parity.test.ts` owns these keys. + sys_user_detail: { + label: '用户', + }, + sys_organization_detail: { + label: '组织', + }, + sys_position_detail: { + label: '岗位', + }, marketplace_installed: { label: '已安装应用', subtitle: '当前已安装到此运行时内核的应用市场包。', From a6f1c882af40fb9c22a48a11206a14c53ce3542a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 03:46:09 +0000 Subject: [PATCH 2/4] docs(i18n): name the owner of the platform record pages from both sides `check:i18n-coverage`'s baselined 0 for platform-objects covers a population that never contained the three plugin-contributed record pages, and neither the extract config nor the gate said so. Both headers now state the boundary and name the gate that owns those pages, on the same precedent the Setup nav half already set. Adds the changeset for the user-visible half: the Setup record header for User / Organization / Position now localizes. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N --- .../platform-record-page-i18n-ownership.md | 13 ++++++++++ .../scripts/i18n-extract.config.ts | 26 +++++++++++++++++++ scripts/check-i18n-coverage.mjs | 16 ++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 .changeset/platform-record-page-i18n-ownership.md diff --git a/.changeset/platform-record-page-i18n-ownership.md b/.changeset/platform-record-page-i18n-ownership.md new file mode 100644 index 0000000000..6d80be3196 --- /dev/null +++ b/.changeset/platform-record-page-i18n-ownership.md @@ -0,0 +1,13 @@ +--- +"@objectstack/platform-objects": patch +--- + +The Setup record pages for User, Organization and Position now show a localized title, and their copy is under a gate for the first time. + +`sys_user_detail`, `sys_organization_detail` and `sys_position_detail` reach the platform through plugin-auth and plugin-security, so they were in no `os i18n extract` config. Their page-level `label` therefore had no entry in any translation bundle, and the Setup record header rendered "User" / "Organization" / "Position" in every language. Those three keys are now translated in all four shipped locales (`en`, `zh-CN`, `ja-JP`, `es-ES`). + +The ownership gap behind that is the part worth stating, because the instrument read as clean while it was open. `check:i18n-coverage` baselines this package at `0`, and that `0` meant "these pages are not in the population", not "checked, clean" — the same ambiguity, in the same baseline file, that once shipped four missing `zh-CN` Setup nav labels under a fully green build. Measured through the real extractor: the three pages offer exactly three keys between them, one page-level `label` each. All three author `regions: []` and put every component under `slots.*`, while the walk shared by the resolver and the extractor roots at `regions[].components[]` — so 45 further authored copy sites, every one an inline `{ en, 'zh-CN', … }` locale map, have no bundle face to be counted against. + +`packages/cli/test/platform-page-i18n-parity.test.ts` now owns both halves from the other side: a `pages.*` bundle entry per shipped locale for every page the `@objectstack/platform-objects/pages` barrel exports, and a completeness check over every inline locale map on those documents — the half no extractor can reach. Both populations are read from the barrel and from the page documents rather than listed in the test, so a fourth contributed page joins the gate by existing, and a new section heading authored in English alone fails instead of shipping green. + +No walk was widened and no baseline was moved. Whether the extractor should also see inline locale maps is a maintainer decision open on #14749; the inline map itself is the ruled authoring route, not a workaround. diff --git a/packages/platform-objects/scripts/i18n-extract.config.ts b/packages/platform-objects/scripts/i18n-extract.config.ts index 3308711e1e..5a4ab599cd 100644 --- a/packages/platform-objects/scripts/i18n-extract.config.ts +++ b/packages/platform-objects/scripts/i18n-extract.config.ts @@ -62,6 +62,32 @@ * still gates this package's STATIC declared surface and its 0 is real for * that; it simply is not the owner of the runtime half. * + * The SAME `0` carries the same ambiguity for a SECOND runtime-composed + * surface, and this config is why: it declares no `pages` key at all. The + * three shipped record pages -- `sys_user_detail`, + * `sys_organization_detail` (plugin-auth) and `sys_position_detail` + * (plugin-security) -- reach the platform through those plugins, so they + * are in no extract config's population and the ratchet never looked at + * them either. Declaring them here would not by itself fix that, which is + * the part worth writing down: measured through the real + * `collectExpectedEntries`, the three offer exactly THREE keys between + * them (one page-level `label` each). All three author `regions: []` and + * put every component under `slots.*`, and the shared walk + * (`walkAddressedPageComponents`, `@objectstack/spec/system`) roots at + * `regions[].components[]` -- so 45 further authored copy sites, every one + * an inline `{ en, 'zh-CN', ... }` locale map, have no bundle face to be + * counted against. A config-only change would declare pages the walk still + * cannot see: it would look like a fix and measure nothing. + * + * Their gate is `packages/cli/test/platform-page-i18n-parity.test.ts`, + * which owns both halves from the other side -- a `pages.*` bundle entry + * per shipped locale for every page the + * `@objectstack/platform-objects/pages` barrel exports, and a completeness + * check over every inline locale map on those documents, which is the half + * no extractor can reach. Whether the extractor SHOULD reach inline maps is + * a maintainer decision open on #14749; the inline map itself is the ruled + * authoring route (2026-08-06), not a workaround. + * * Omitting the hand-authored half was a measurable bug, not a style choice: * this config declares SETUP_APP / STUDIO_APP / ACCOUNT_APP and * SystemOverviewDashboard, so coverage counted all 77 `apps.*`/`dashboards.*` diff --git a/scripts/check-i18n-coverage.mjs b/scripts/check-i18n-coverage.mjs index 7b890b3703..514ef71a71 100644 --- a/scripts/check-i18n-coverage.mjs +++ b/scripts/check-i18n-coverage.mjs @@ -48,6 +48,22 @@ // different questions of different inputs, and folding a kernel boot into an // `os lint` loop would make neither readable. // +// The Setup nav is not the only surface in that class, and the second one is +// worth naming here because its `0` is the same `0`. `platform-objects` ships +// three record pages -- `sys_user_detail`, `sys_organization_detail`, +// `sys_position_detail` -- contributed at runtime by plugin-auth and +// plugin-security. Its extract config declares no `pages` key, so they are not +// in this gate's population, and its baselined `0` says nothing about them. +// Declaring them would not be enough either: all three author `regions: []` +// with every component under `slots.*`, and the walk behind `os lint` roots at +// `regions[].components[]`, so the three page-level `label`s are the only keys +// that exist for them -- 45 further authored copy sites are inline locale maps +// with no bundle face at all. Their owner is +// `packages/cli/test/platform-page-i18n-parity.test.ts`, which judges the +// bundle entries AND the inline maps directly off the page documents. Same +// rule as the nav half: do not extend this script to cover it, and do not read +// its `0` as a verdict on those pages. +// // That requirement is now CHECKED, not merely declared (#5862). It used to be the // sentence above and nothing else, and in an installed-but-unbuilt worktree the // gate answered with an uncaught exception plus a node stack: From 03d07efbfef22bb8ccb1710a564034bac8503b42 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 04:52:32 +0000 Subject: [PATCH 3/4] wip: pre-ablation checkpoint --- .../platform-record-page-i18n-ownership.md | 13 ---- .../test/platform-page-i18n-parity.test.ts | 62 +++++++------------ .../src/apps/translations/en.ts | 18 ------ .../src/apps/translations/es-ES.ts | 18 ------ .../src/apps/translations/ja-JP.ts | 18 ------ .../src/apps/translations/zh-CN.ts | 18 ------ 6 files changed, 21 insertions(+), 126 deletions(-) delete mode 100644 .changeset/platform-record-page-i18n-ownership.md diff --git a/.changeset/platform-record-page-i18n-ownership.md b/.changeset/platform-record-page-i18n-ownership.md deleted file mode 100644 index 6d80be3196..0000000000 --- a/.changeset/platform-record-page-i18n-ownership.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/platform-objects": patch ---- - -The Setup record pages for User, Organization and Position now show a localized title, and their copy is under a gate for the first time. - -`sys_user_detail`, `sys_organization_detail` and `sys_position_detail` reach the platform through plugin-auth and plugin-security, so they were in no `os i18n extract` config. Their page-level `label` therefore had no entry in any translation bundle, and the Setup record header rendered "User" / "Organization" / "Position" in every language. Those three keys are now translated in all four shipped locales (`en`, `zh-CN`, `ja-JP`, `es-ES`). - -The ownership gap behind that is the part worth stating, because the instrument read as clean while it was open. `check:i18n-coverage` baselines this package at `0`, and that `0` meant "these pages are not in the population", not "checked, clean" — the same ambiguity, in the same baseline file, that once shipped four missing `zh-CN` Setup nav labels under a fully green build. Measured through the real extractor: the three pages offer exactly three keys between them, one page-level `label` each. All three author `regions: []` and put every component under `slots.*`, while the walk shared by the resolver and the extractor roots at `regions[].components[]` — so 45 further authored copy sites, every one an inline `{ en, 'zh-CN', … }` locale map, have no bundle face to be counted against. - -`packages/cli/test/platform-page-i18n-parity.test.ts` now owns both halves from the other side: a `pages.*` bundle entry per shipped locale for every page the `@objectstack/platform-objects/pages` barrel exports, and a completeness check over every inline locale map on those documents — the half no extractor can reach. Both populations are read from the barrel and from the page documents rather than listed in the test, so a fourth contributed page joins the gate by existing, and a new section heading authored in English alone fails instead of shipping green. - -No walk was widened and no baseline was moved. Whether the extractor should also see inline locale maps is a maintainer decision open on #14749; the inline map itself is the ruled authoring route, not a workaround. diff --git a/packages/cli/test/platform-page-i18n-parity.test.ts b/packages/cli/test/platform-page-i18n-parity.test.ts index c107fd2503..4bc502a7af 100644 --- a/packages/cli/test/platform-page-i18n-parity.test.ts +++ b/packages/cli/test/platform-page-i18n-parity.test.ts @@ -517,6 +517,27 @@ describe('i18n-extract ↔ translatePage walk parity (#13109)', () => { // listed here, so a fourth page joins this gate by existing; and the inline-map // assertion judges the authoring site, which is the half the bundle face // cannot see. Both directions of that sentence now red instead of shipping. +// +// ## What this block deliberately does NOT assert, and the measurement why +// +// It does not require a `pages.*` BUNDLE entry for these three. That was tried +// and measured: the three page-level `label`s are the only keys the extractor +// offers, so translating them is the one piece of real debt here (`User` / +// `Organization` / `Position` render in English in every locale). Adding those +// entries turns `check:app-nav-i18n` RED on two of the three -- +// `pages.sys_user_detail` and `pages.sys_organization_detail` are reported as +// keys "the booted composition contains no page by that name", its phantom-key +// verdict. That gate's `CONTRIBUTORS` roster is deliberately explicit and +// deliberately a NAV roster: `@objectstack/plugin-auth`, which contributes +// those two pages, is not in it, and adding it is not a one-line edit -- +// `new AuthPlugin({})` refuses to boot ("secret is required"), and the roster +// separately requires every entry to land at least one nav id, which +// plugin-auth's conditional `nav_sso_providers` cannot promise. Only +// `sys_position_detail` (plugin-security, which IS in the roster) verifies +// clean. Splitting that roster into a nav population and a page population is a +// change to a gate's composition contract, so it is escalated rather than taken +// here. Until it is ruled, a `pages.*` entry for the plugin-auth pages would be +// exactly the unverifiable key `check:app-nav-i18n` exists to refuse. /** Every page the platform's own `pages` barrel exports, as the plugins take them. */ const RECORD_PAGES: Array> = Object.values( @@ -589,36 +610,6 @@ describe('shipped platform record pages -- i18n ownership (#14817)', () => { expect(SHIPPED_LOCALES.length).toBeGreaterThan(1); }); - it('carries a bundle entry for every barrel page in every shipped locale', () => { - // The half that was missing entirely: these three pages had no `pages.*` - // entry in any locale, so nothing asked a translator for their titles and - // the Setup record header rendered "User" in every language. - for (const page of RECORD_PAGES) { - for (const locale of SHIPPED_LOCALES) { - const entry = pagesOf(locale)[page.name]; - expect({ page: page.name, locale, hasLabel: typeof entry?.label === 'string' && entry.label.length > 0 }) - .toEqual({ page: page.name, locale, hasLabel: true }); - } - } - }); - - it('keeps the `en` bundle byte-identical to what the extractor reads off the metadata', () => { - // Same drift the block above guards for the plugin-carried pages: an edit - // to the page literal that leaves the bundle alone renders the STALE text, - // because `translatePage` applies the bundle for `en` too. - const en = pagesOf(EN); - const expected = collectExpectedEntries({ pages: RECORD_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('records that the extractor reaches the page label and nothing under `slots`', () => { // A BOUNDARY PIN, not an endorsement. It states the measured fact that the // shared walk roots at `regions[].components[]` and these pages author @@ -657,15 +648,4 @@ describe('shipped platform record pages -- i18n ownership (#14817)', () => { expect(incomplete).toEqual([]); }); - it('localizes the page label through `translatePage` without mutating the singleton', () => { - for (const page of RECORD_PAGES) { - const before = page.label; - const translated = translatePage(page as any, SetupAppTranslations, { locale: 'zh-CN' }) as any; - expect({ page: page.name, changed: translated.label !== before }) - .toEqual({ page: page.name, changed: true }); - // These page objects are module-level singletons the kernel registers - // once; an overlay that mutated one would localize it process-wide. - expect({ page: page.name, label: page.label }).toEqual({ page: page.name, label: before }); - } - }); }); diff --git a/packages/platform-objects/src/apps/translations/en.ts b/packages/platform-objects/src/apps/translations/en.ts index 1d1d3f6bb2..6873d60cbb 100644 --- a/packages/platform-objects/src/apps/translations/en.ts +++ b/packages/platform-objects/src/apps/translations/en.ts @@ -237,24 +237,6 @@ export const en: TranslationData = { // (@objectstack/cloud-connection, @objectstack/mcp) and exist so the other // locales have a complete key set to translate against. pages: { - // The three shipped platform RECORD pages, contributed by plugin-auth - // (sys_user / sys_organization) and plugin-security (sys_position). Their - // page-level `label` is the ONLY key `collectExpectedEntries` offers for - // them — all three author `regions: []` and put every component under - // `slots.*`, which the shared walk does not descend. Measured: 3 entries - // emitted for the three pages, against 45 inline-locale-map sites under - // `slots` that the bundle face does not reach (those are the ruled - // authoring-site route, 2026-08-06; their extractor visibility is #14749). - // `packages/cli/test/platform-page-i18n-parity.test.ts` owns these keys. - sys_user_detail: { - label: 'User', - }, - sys_organization_detail: { - label: 'Organization', - }, - sys_position_detail: { - label: 'Position', - }, marketplace_installed: { label: 'Installed Apps', subtitle: "Marketplace packages currently installed into this runtime's kernel.", diff --git a/packages/platform-objects/src/apps/translations/es-ES.ts b/packages/platform-objects/src/apps/translations/es-ES.ts index a679dd27dd..a0f1b8c64e 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.ts @@ -158,24 +158,6 @@ export const esES: TranslationData = { }, pages: { - // The three shipped platform RECORD pages, contributed by plugin-auth - // (sys_user / sys_organization) and plugin-security (sys_position). Their - // page-level `label` is the ONLY key `collectExpectedEntries` offers for - // them — all three author `regions: []` and put every component under - // `slots.*`, which the shared walk does not descend. Measured: 3 entries - // emitted for the three pages, against 45 inline-locale-map sites under - // `slots` that the bundle face does not reach (those are the ruled - // authoring-site route, 2026-08-06; their extractor visibility is #14749). - // `packages/cli/test/platform-page-i18n-parity.test.ts` owns these keys. - sys_user_detail: { - label: 'Usuario', - }, - sys_organization_detail: { - label: 'Organización', - }, - sys_position_detail: { - label: 'Puesto', - }, marketplace_installed: { label: 'Aplicaciones instaladas', subtitle: 'Paquetes del marketplace instalados actualmente en el kernel de este runtime.', diff --git a/packages/platform-objects/src/apps/translations/ja-JP.ts b/packages/platform-objects/src/apps/translations/ja-JP.ts index 28aaf6a98a..e2b1fdcac6 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.ts @@ -158,24 +158,6 @@ export const jaJP: TranslationData = { }, pages: { - // The three shipped platform RECORD pages, contributed by plugin-auth - // (sys_user / sys_organization) and plugin-security (sys_position). Their - // page-level `label` is the ONLY key `collectExpectedEntries` offers for - // them — all three author `regions: []` and put every component under - // `slots.*`, which the shared walk does not descend. Measured: 3 entries - // emitted for the three pages, against 45 inline-locale-map sites under - // `slots` that the bundle face does not reach (those are the ruled - // authoring-site route, 2026-08-06; their extractor visibility is #14749). - // `packages/cli/test/platform-page-i18n-parity.test.ts` owns these keys. - sys_user_detail: { - label: 'ユーザー', - }, - sys_organization_detail: { - label: '組織', - }, - sys_position_detail: { - label: 'ポジション', - }, marketplace_installed: { label: 'インストール済みアプリ', subtitle: 'このランタイムのカーネルに現在インストールされているマーケットプレイスパッケージ。', diff --git a/packages/platform-objects/src/apps/translations/zh-CN.ts b/packages/platform-objects/src/apps/translations/zh-CN.ts index e1df443952..e841295b10 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.ts @@ -168,24 +168,6 @@ export const zhCN: TranslationData = { }, pages: { - // The three shipped platform RECORD pages, contributed by plugin-auth - // (sys_user / sys_organization) and plugin-security (sys_position). Their - // page-level `label` is the ONLY key `collectExpectedEntries` offers for - // them — all three author `regions: []` and put every component under - // `slots.*`, which the shared walk does not descend. Measured: 3 entries - // emitted for the three pages, against 45 inline-locale-map sites under - // `slots` that the bundle face does not reach (those are the ruled - // authoring-site route, 2026-08-06; their extractor visibility is #14749). - // `packages/cli/test/platform-page-i18n-parity.test.ts` owns these keys. - sys_user_detail: { - label: '用户', - }, - sys_organization_detail: { - label: '组织', - }, - sys_position_detail: { - label: '岗位', - }, marketplace_installed: { label: '已安装应用', subtitle: '当前已安装到此运行时内核的应用市场包。', From 925f6ec8813561dfc0c2dddd6d52a8faa5ca63e9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 05:07:01 +0000 Subject: [PATCH 4/4] test(cli): scope the record-page gate to what needs no ruling Drops the `pages.*` bundle entries and the two assertions that depended on them. Measured reason, recorded in the block header: adding those entries turns `check:app-nav-i18n` red on the two plugin-auth pages as phantom keys -- its CONTRIBUTORS roster is a NAV roster that omits plugin-auth, and adding it is not mechanical (`new AuthPlugin({})` refuses to boot without a secret, and every entry must land at least one nav id, which plugin-auth's conditional nav_sso_providers cannot promise). Splitting that roster is a gate composition decision, so it is escalated on the card rather than taken here, and no gate is weakened to make room for it. What remains needs no ruling: the population floor, the boundary pin, and the inline-locale-map completeness guard. No changeset -- nothing here publishes from a package (skip-changeset). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N --- packages/cli/test/platform-page-i18n-parity.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cli/test/platform-page-i18n-parity.test.ts b/packages/cli/test/platform-page-i18n-parity.test.ts index 4bc502a7af..080e4dca73 100644 --- a/packages/cli/test/platform-page-i18n-parity.test.ts +++ b/packages/cli/test/platform-page-i18n-parity.test.ts @@ -647,5 +647,4 @@ describe('shipped platform record pages -- i18n ownership (#14817)', () => { .map((m) => ({ path: m.path, missing: SHIPPED_LOCALES.filter((l) => !m.locales.includes(l)) })); expect(incomplete).toEqual([]); }); - });