Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .changeset/sso-providers-nav-label.md
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 6 additions & 3 deletions packages/cli/scripts/check-app-nav-i18n.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
13 changes: 13 additions & 0 deletions packages/platform-objects/src/apps/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },

Expand Down
5 changes: 5 additions & 0 deletions packages/platform-objects/src/apps/translations/es-ES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },

Expand Down
5 changes: 5 additions & 0 deletions packages/platform-objects/src/apps/translations/ja-JP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: 'エージェントを接続' },

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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: '<key>'` returned zero hits for
Expand Down Expand Up @@ -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<string, { label?: string }>;

// 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([]);
});
}
});
5 changes: 5 additions & 0 deletions packages/platform-objects/src/apps/translations/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '连接智能体' },

Expand Down
Loading