From 1477af3965552317799cfbef4ac287f1bb2790ec Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 17:30:39 +0000 Subject: [PATCH] fix(platform-objects): translate the Setup `nav_sso_providers` nav entry in all four locales (#6659) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `@objectstack/plugin-auth` contributes an "SSO Providers" entry into Setup's Access Control group (`sys_sso_provider`, priority 250), but no locale bundle carried a label for it: on `ea1d9165d` a grep for `nav_sso_providers` over en / zh-CN / ja-JP / es-ES returned 0 each, against a control probe (`nav_positions`) returning 1 each. A deployment with an external IdP wired rendered `SSO Providers` in English inside an otherwise translated menu. Each label matches that locale's existing `sys_sso_provider.pluralLabel`, since the entry opens exactly that object's list view. `pnpm check:app-nav-i18n` cannot reach this id: `plugin-auth` spreads its `navigationContributions` in only when `authManager.isSsoWired()` is true, so the composition that gate boots never merges the entry. Measured both ways — with all four labels missing the gate still reports `OK (10 contributor(s), 53 merged nav id(s), every id labelled in every locale)`. The id is therefore pinned by hand next to the #6660 dead-key tombstone it is the converse of: one list holds ids whose label must be gone, the other ids whose label must stay. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01W6bLax4KMrSfnE1ydFU8Dw --- .changeset/sso-providers-nav-label.md | 36 +++++++++++ packages/cli/scripts/check-app-nav-i18n.mjs | 9 ++- .../src/apps/translations/en.ts | 13 ++++ .../src/apps/translations/es-ES.ts | 5 ++ .../src/apps/translations/ja-JP.ts | 5 ++ .../setup-nav-dead-key-tombstone.test.ts | 60 ++++++++++++++++++- .../src/apps/translations/zh-CN.ts | 5 ++ 7 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 .changeset/sso-providers-nav-label.md diff --git a/.changeset/sso-providers-nav-label.md b/.changeset/sso-providers-nav-label.md new file mode 100644 index 0000000000..0a3b961d54 --- /dev/null +++ b/.changeset/sso-providers-nav-label.md @@ -0,0 +1,36 @@ +--- +'@objectstack/platform-objects': patch +--- + +Translate the Setup app's `nav_sso_providers` navigation entry in all four +locales. + +`@objectstack/plugin-auth` contributes an **SSO Providers** entry into Setup's +Access Control group (`sys_sso_provider`, priority 250), but no locale bundle +carried a label for it: measured on `origin/main` `ea1d9165d`, a grep for +`nav_sso_providers` over `en` / `zh-CN` / `ja-JP` / `es-ES` returned **0 each**, +against a control probe (`nav_positions`) that returned 1 each. A deployment +with an external IdP wired therefore rendered `SSO Providers` in English inside +an otherwise fully translated Setup menu. + +| locale | label | +| --- | --- | +| `en` | SSO Providers | +| `zh-CN` | SSO 提供方 | +| `ja-JP` | SSO プロバイダー | +| `es-ES` | Proveedores SSO | + +Each one matches that locale's existing `sys_sso_provider.pluralLabel`, since +the nav entry opens exactly that object's list view. + +**Why no gate caught it.** `pnpm check:app-nav-i18n` (#5750) boots the real +composition and asserts every *merged* Setup nav id carries a label in every +locale — and `plugin-auth` spreads its `navigationContributions` in only when +`authManager.isSsoWired()` is true. In the composition that gate boots, this +entry is never contributed, never merged, and so never judged; the gate's header +already declared that bound. This id is consequently the one Setup entry no +boot-time check can reach, so it is pinned by hand instead, next to the dead-key +tombstone (#6660) it is the converse of: one list holds ids whose label must be +**gone**, the other ids whose label must **stay**. Making the gate itself +union-aware was considered and deliberately left unbuilt — a separate +maintainer-facing call, not a prerequisite for labelling the ids it cannot see. diff --git a/packages/cli/scripts/check-app-nav-i18n.mjs b/packages/cli/scripts/check-app-nav-i18n.mjs index 2bb9ab3df0..262cdff21a 100644 --- a/packages/cli/scripts/check-app-nav-i18n.mjs +++ b/packages/cli/scripts/check-app-nav-i18n.mjs @@ -59,9 +59,12 @@ // carries no translation for a removed nav id; the same assertion here // would delete the labels of conditionally-contributed entries, because a // gated-off contribution is indistinguishable from a dead key when all you -// have is one runtime composition. The dead `apps.setup.navigation` keys -// that exist today are tracked separately rather than removed on a verdict -// this gate cannot honestly reach. +// have is one runtime composition. So Setup's reverse direction is decided +// per id by a human instead, in the two hand-kept lists of +// `setup-nav-dead-key-tombstone.test.ts`: the four keys that were dead were +// removed there under #6660, and `nav_sso_providers` — labelled in #6659 +// although no composition this gate boots ever merges it — is pinned as the +// converse case. Neither verdict is one this gate could honestly reach. import { existsSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; diff --git a/packages/platform-objects/src/apps/translations/en.ts b/packages/platform-objects/src/apps/translations/en.ts index 8fb8c9cc4b..8e2c56d6bb 100644 --- a/packages/platform-objects/src/apps/translations/en.ts +++ b/packages/platform-objects/src/apps/translations/en.ts @@ -82,6 +82,19 @@ export const en: TranslationData = { nav_permission_sets: { label: 'Permission Sets' }, nav_sharing_rules: { label: 'Sharing Rules' }, nav_record_shares: { label: 'Record Shares' }, + // `nav_sso_providers` is contributed by `@objectstack/plugin-auth` only + // when the external-IdP RP is wired (`OS_SSO_ENABLED`, or the cloud + // per-env `planAllowsSso`), so it is the one Setup entry + // `pnpm check:app-nav-i18n` structurally cannot judge: that gate boots + // ONE composition, and a contribution gated off in it is never merged + // and therefore never checked (see the gate header's bound #1). Its + // absence here was invisible for exactly that reason (#6659) — a + // deployment with SSO wired showed `SSO Providers` in English inside an + // otherwise translated menu. The pin that keeps this row honest lives + // in `setup-nav-dead-key-tombstone.test.ts`. Wording follows the + // object's own `pluralLabel` per locale (`sys_sso_provider`), since the + // entry opens that object's list view. + nav_sso_providers: { label: 'SSO Providers' }, nav_api_keys: { label: 'API Keys' }, nav_connect_agent: { label: 'Connect an Agent' }, diff --git a/packages/platform-objects/src/apps/translations/es-ES.ts b/packages/platform-objects/src/apps/translations/es-ES.ts index 12038545d3..bd4891204e 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.ts @@ -60,6 +60,11 @@ export const esES: TranslationData = { nav_permission_sets: { label: 'Conjuntos de Permisos' }, nav_sharing_rules: { label: 'Reglas de Compartición' }, nav_record_shares: { label: 'Registros Compartidos' }, + // Conditionally contributed by `@objectstack/plugin-auth` (only when an + // external IdP is wired), so `check:app-nav-i18n` cannot see it — see + // the rationale in `en.ts` (#6659). Wording matches this locale's + // `sys_sso_provider.pluralLabel`. + nav_sso_providers: { label: 'Proveedores SSO' }, nav_api_keys: { label: 'Claves API' }, nav_connect_agent: { label: 'Conectar un agente' }, diff --git a/packages/platform-objects/src/apps/translations/ja-JP.ts b/packages/platform-objects/src/apps/translations/ja-JP.ts index 09d66bee3f..a894621e62 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.ts @@ -60,6 +60,11 @@ export const jaJP: TranslationData = { nav_permission_sets: { label: '権限セット' }, nav_sharing_rules: { label: '共有ルール' }, nav_record_shares: { label: 'レコード共有' }, + // Conditionally contributed by `@objectstack/plugin-auth` (only when an + // external IdP is wired), so `check:app-nav-i18n` cannot see it — see + // the rationale in `en.ts` (#6659). Wording matches this locale's + // `sys_sso_provider.pluralLabel`. + nav_sso_providers: { label: 'SSO プロバイダー' }, nav_api_keys: { label: 'API キー' }, nav_connect_agent: { label: 'エージェントを接続' }, diff --git a/packages/platform-objects/src/apps/translations/setup-nav-dead-key-tombstone.test.ts b/packages/platform-objects/src/apps/translations/setup-nav-dead-key-tombstone.test.ts index 59be4379fc..47302af087 100644 --- a/packages/platform-objects/src/apps/translations/setup-nav-dead-key-tombstone.test.ts +++ b/packages/platform-objects/src/apps/translations/setup-nav-dead-key-tombstone.test.ts @@ -1,6 +1,8 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. // -// Tombstone for four dead `apps.setup.navigation` translation keys (#6660). +// Tombstone for four dead `apps.setup.navigation` translation keys (#6660), and +// its converse: the labels of conditionally-contributed entries, which must +// STAY (#6659). // // --------------------------------------------------------------------------- // Why a hard-coded id list instead of the general reverse direction @@ -14,8 +16,10 @@ // something", and why `pnpm check:app-nav-i18n` (which does boot) still refuses // the reverse direction: from one composition a dead key and a // conditionally-contributed key are indistinguishable (`nav_sso_providers` is -// contributed only when an external IdP is wired). Making that gate -// union-aware is tracked as #6659. +// contributed only when an external IdP is wired). Teaching that gate to +// enumerate conditional contributions — a union-aware gate — was considered and +// deliberately NOT built (#6659's triage): it is a separate maintainer-facing +// call, not a prerequisite for labelling the ids it cannot see. // // This file makes no general claim. It pins exactly four ids that were checked // ONE BY ONE against a repo-wide grep — `id: ''` returned zero hits for @@ -92,3 +96,53 @@ describe('removed Setup nav ids stay removed (#6660)', () => { expect(DEAD_SETUP_NAV_IDS.filter((id) => declared.has(id))).toEqual([]); }); }); + +// --------------------------------------------------------------------------- +// The converse case (#6659): a label that must STAY although no boot sees it +// --------------------------------------------------------------------------- +// `pnpm check:app-nav-i18n` boots the real composition and asserts every MERGED +// Setup nav id carries a label in every locale. That is the right shape for the +// eleven contributors whose entries always merge — and it is structurally blind +// to the ones that do not. `@objectstack/plugin-auth` spreads its +// `navigationContributions` in only when `authManager.isSsoWired()` is true +// (`OS_SSO_ENABLED` self-host, or the cloud per-env `planAllowsSso`), so in the +// composition that gate boots, `nav_sso_providers` is never contributed, never +// merged, and therefore never judged. It had no label in ANY of the four +// locales while that gate reported OK, and a deployment with an external IdP +// wired rendered `SSO Providers` in English inside an otherwise translated menu. +// +// So this case is a hand-kept list, exactly like `DEAD_SETUP_NAV_IDS` above and +// for the same reason: one composition cannot decide the question, so a human +// decided it per id. It is deliberately NOT a general union-aware gate — that +// was ruled a separate maintainer-facing call (#6659's triage) and is not built. +// +// Bound worth stating: this file asserts only the LABEL half. The declaring +// contribution lives in `@objectstack/plugin-auth`, which depends on this +// package and so cannot be imported from here — the same import direction that +// puts `check:app-nav-i18n` in `packages/cli`. A grep is what confirms the +// declaring side; on `ea1d9165d` it sits at `auth-plugin.ts:552`. +// +// What to do when this test goes red: it goes red when a label is dropped, or +// when a locale is added to the bundle without translating this id. Both are +// bugs. If the CONTRIBUTION is ever retired, this list loses its entry in the +// same commit that removes the nav item, and the id moves up to +// `DEAD_SETUP_NAV_IDS` — the two lists are the two halves of one ledger. +const CONDITIONAL_SETUP_NAV_IDS = ['nav_sso_providers'] as const; + +describe('conditionally-contributed Setup nav ids stay labelled (#6659)', () => { + for (const [locale, data] of Object.entries(LOCALES)) { + it(`${locale} carries a label for every conditionally-contributed Setup nav id`, () => { + const nav = (data.apps?.setup?.navigation ?? {}) as Record; + + // Control: the subtree really resolved, so a missing/renamed + // `apps.setup.navigation` cannot make the assertion below pass by vacuity + // (it would instead report every id as unlabelled — which is the point). + expect(nav.nav_api_keys?.label, 'nav_api_keys anchors this subtree').toBeTruthy(); + + expect( + CONDITIONAL_SETUP_NAV_IDS.filter((id) => !nav[id]?.label), + 'Setup nav ids with no label — no boot-time gate can see these; see this file header', + ).toEqual([]); + }); + } +}); diff --git a/packages/platform-objects/src/apps/translations/zh-CN.ts b/packages/platform-objects/src/apps/translations/zh-CN.ts index 57fb1acbb3..f44e758ad2 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.ts @@ -63,6 +63,11 @@ export const zhCN: TranslationData = { nav_permission_sets: { label: '权限集' }, nav_sharing_rules: { label: '共享规则' }, nav_record_shares: { label: '记录共享' }, + // Conditionally contributed by `@objectstack/plugin-auth` (only when an + // external IdP is wired), so `check:app-nav-i18n` cannot see it — see + // the rationale in `en.ts` (#6659). Wording matches this locale's + // `sys_sso_provider.pluralLabel`. + nav_sso_providers: { label: 'SSO 提供方' }, nav_api_keys: { label: 'API 密钥' }, nav_connect_agent: { label: '连接智能体' },