Skip to content

Commit e787608

Browse files
os-zhuangclaude
andauthored
fix(platform-objects): translate the Setup nav_sso_providers nav entry in all four locales (#6659) (#6787)
`@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. Claude-Session: https://claude.ai/code/session_01W6bLax4KMrSfnE1ydFU8Dw Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4ed5f7f commit e787608

7 files changed

Lines changed: 127 additions & 6 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
Translate the Setup app's `nav_sso_providers` navigation entry in all four
6+
locales.
7+
8+
`@objectstack/plugin-auth` contributes an **SSO Providers** entry into Setup's
9+
Access Control group (`sys_sso_provider`, priority 250), but no locale bundle
10+
carried a label for it: measured on `origin/main` `ea1d9165d`, a grep for
11+
`nav_sso_providers` over `en` / `zh-CN` / `ja-JP` / `es-ES` returned **0 each**,
12+
against a control probe (`nav_positions`) that returned 1 each. A deployment
13+
with an external IdP wired therefore rendered `SSO Providers` in English inside
14+
an otherwise fully translated Setup menu.
15+
16+
| locale | label |
17+
| --- | --- |
18+
| `en` | SSO Providers |
19+
| `zh-CN` | SSO 提供方 |
20+
| `ja-JP` | SSO プロバイダー |
21+
| `es-ES` | Proveedores SSO |
22+
23+
Each one matches that locale's existing `sys_sso_provider.pluralLabel`, since
24+
the nav entry opens exactly that object's list view.
25+
26+
**Why no gate caught it.** `pnpm check:app-nav-i18n` (#5750) boots the real
27+
composition and asserts every *merged* Setup nav id carries a label in every
28+
locale — and `plugin-auth` spreads its `navigationContributions` in only when
29+
`authManager.isSsoWired()` is true. In the composition that gate boots, this
30+
entry is never contributed, never merged, and so never judged; the gate's header
31+
already declared that bound. This id is consequently the one Setup entry no
32+
boot-time check can reach, so it is pinned by hand instead, next to the dead-key
33+
tombstone (#6660) it is the converse of: one list holds ids whose label must be
34+
**gone**, the other ids whose label must **stay**. Making the gate itself
35+
union-aware was considered and deliberately left unbuilt — a separate
36+
maintainer-facing call, not a prerequisite for labelling the ids it cannot see.

packages/cli/scripts/check-app-nav-i18n.mjs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,12 @@
5959
// carries no translation for a removed nav id; the same assertion here
6060
// would delete the labels of conditionally-contributed entries, because a
6161
// gated-off contribution is indistinguishable from a dead key when all you
62-
// have is one runtime composition. The dead `apps.setup.navigation` keys
63-
// that exist today are tracked separately rather than removed on a verdict
64-
// this gate cannot honestly reach.
62+
// have is one runtime composition. So Setup's reverse direction is decided
63+
// per id by a human instead, in the two hand-kept lists of
64+
// `setup-nav-dead-key-tombstone.test.ts`: the four keys that were dead were
65+
// removed there under #6660, and `nav_sso_providers` — labelled in #6659
66+
// although no composition this gate boots ever merges it — is pinned as the
67+
// converse case. Neither verdict is one this gate could honestly reach.
6568
import { existsSync } from 'node:fs';
6669
import { join, dirname } from 'node:path';
6770
import { fileURLToPath } from 'node:url';

packages/platform-objects/src/apps/translations/en.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,19 @@ export const en: TranslationData = {
8282
nav_permission_sets: { label: 'Permission Sets' },
8383
nav_sharing_rules: { label: 'Sharing Rules' },
8484
nav_record_shares: { label: 'Record Shares' },
85+
// `nav_sso_providers` is contributed by `@objectstack/plugin-auth` only
86+
// when the external-IdP RP is wired (`OS_SSO_ENABLED`, or the cloud
87+
// per-env `planAllowsSso`), so it is the one Setup entry
88+
// `pnpm check:app-nav-i18n` structurally cannot judge: that gate boots
89+
// ONE composition, and a contribution gated off in it is never merged
90+
// and therefore never checked (see the gate header's bound #1). Its
91+
// absence here was invisible for exactly that reason (#6659) — a
92+
// deployment with SSO wired showed `SSO Providers` in English inside an
93+
// otherwise translated menu. The pin that keeps this row honest lives
94+
// in `setup-nav-dead-key-tombstone.test.ts`. Wording follows the
95+
// object's own `pluralLabel` per locale (`sys_sso_provider`), since the
96+
// entry opens that object's list view.
97+
nav_sso_providers: { label: 'SSO Providers' },
8598
nav_api_keys: { label: 'API Keys' },
8699
nav_connect_agent: { label: 'Connect an Agent' },
87100

packages/platform-objects/src/apps/translations/es-ES.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ export const esES: TranslationData = {
6060
nav_permission_sets: { label: 'Conjuntos de Permisos' },
6161
nav_sharing_rules: { label: 'Reglas de Compartición' },
6262
nav_record_shares: { label: 'Registros Compartidos' },
63+
// Conditionally contributed by `@objectstack/plugin-auth` (only when an
64+
// external IdP is wired), so `check:app-nav-i18n` cannot see it — see
65+
// the rationale in `en.ts` (#6659). Wording matches this locale's
66+
// `sys_sso_provider.pluralLabel`.
67+
nav_sso_providers: { label: 'Proveedores SSO' },
6368
nav_api_keys: { label: 'Claves API' },
6469
nav_connect_agent: { label: 'Conectar un agente' },
6570

packages/platform-objects/src/apps/translations/ja-JP.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ export const jaJP: TranslationData = {
6060
nav_permission_sets: { label: '権限セット' },
6161
nav_sharing_rules: { label: '共有ルール' },
6262
nav_record_shares: { label: 'レコード共有' },
63+
// Conditionally contributed by `@objectstack/plugin-auth` (only when an
64+
// external IdP is wired), so `check:app-nav-i18n` cannot see it — see
65+
// the rationale in `en.ts` (#6659). Wording matches this locale's
66+
// `sys_sso_provider.pluralLabel`.
67+
nav_sso_providers: { label: 'SSO プロバイダー' },
6368
nav_api_keys: { label: 'API キー' },
6469
nav_connect_agent: { label: 'エージェントを接続' },
6570

packages/platform-objects/src/apps/translations/setup-nav-dead-key-tombstone.test.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22
//
3-
// Tombstone for four dead `apps.setup.navigation` translation keys (#6660).
3+
// Tombstone for four dead `apps.setup.navigation` translation keys (#6660), and
4+
// its converse: the labels of conditionally-contributed entries, which must
5+
// STAY (#6659).
46
//
57
// ---------------------------------------------------------------------------
68
// Why a hard-coded id list instead of the general reverse direction
@@ -14,8 +16,10 @@
1416
// something", and why `pnpm check:app-nav-i18n` (which does boot) still refuses
1517
// the reverse direction: from one composition a dead key and a
1618
// conditionally-contributed key are indistinguishable (`nav_sso_providers` is
17-
// contributed only when an external IdP is wired). Making that gate
18-
// union-aware is tracked as #6659.
19+
// contributed only when an external IdP is wired). Teaching that gate to
20+
// enumerate conditional contributions — a union-aware gate — was considered and
21+
// deliberately NOT built (#6659's triage): it is a separate maintainer-facing
22+
// call, not a prerequisite for labelling the ids it cannot see.
1923
//
2024
// This file makes no general claim. It pins exactly four ids that were checked
2125
// ONE BY ONE against a repo-wide grep — `id: '<key>'` returned zero hits for
@@ -92,3 +96,53 @@ describe('removed Setup nav ids stay removed (#6660)', () => {
9296
expect(DEAD_SETUP_NAV_IDS.filter((id) => declared.has(id))).toEqual([]);
9397
});
9498
});
99+
100+
// ---------------------------------------------------------------------------
101+
// The converse case (#6659): a label that must STAY although no boot sees it
102+
// ---------------------------------------------------------------------------
103+
// `pnpm check:app-nav-i18n` boots the real composition and asserts every MERGED
104+
// Setup nav id carries a label in every locale. That is the right shape for the
105+
// eleven contributors whose entries always merge — and it is structurally blind
106+
// to the ones that do not. `@objectstack/plugin-auth` spreads its
107+
// `navigationContributions` in only when `authManager.isSsoWired()` is true
108+
// (`OS_SSO_ENABLED` self-host, or the cloud per-env `planAllowsSso`), so in the
109+
// composition that gate boots, `nav_sso_providers` is never contributed, never
110+
// merged, and therefore never judged. It had no label in ANY of the four
111+
// locales while that gate reported OK, and a deployment with an external IdP
112+
// wired rendered `SSO Providers` in English inside an otherwise translated menu.
113+
//
114+
// So this case is a hand-kept list, exactly like `DEAD_SETUP_NAV_IDS` above and
115+
// for the same reason: one composition cannot decide the question, so a human
116+
// decided it per id. It is deliberately NOT a general union-aware gate — that
117+
// was ruled a separate maintainer-facing call (#6659's triage) and is not built.
118+
//
119+
// Bound worth stating: this file asserts only the LABEL half. The declaring
120+
// contribution lives in `@objectstack/plugin-auth`, which depends on this
121+
// package and so cannot be imported from here — the same import direction that
122+
// puts `check:app-nav-i18n` in `packages/cli`. A grep is what confirms the
123+
// declaring side; on `ea1d9165d` it sits at `auth-plugin.ts:552`.
124+
//
125+
// What to do when this test goes red: it goes red when a label is dropped, or
126+
// when a locale is added to the bundle without translating this id. Both are
127+
// bugs. If the CONTRIBUTION is ever retired, this list loses its entry in the
128+
// same commit that removes the nav item, and the id moves up to
129+
// `DEAD_SETUP_NAV_IDS` — the two lists are the two halves of one ledger.
130+
const CONDITIONAL_SETUP_NAV_IDS = ['nav_sso_providers'] as const;
131+
132+
describe('conditionally-contributed Setup nav ids stay labelled (#6659)', () => {
133+
for (const [locale, data] of Object.entries(LOCALES)) {
134+
it(`${locale} carries a label for every conditionally-contributed Setup nav id`, () => {
135+
const nav = (data.apps?.setup?.navigation ?? {}) as Record<string, { label?: string }>;
136+
137+
// Control: the subtree really resolved, so a missing/renamed
138+
// `apps.setup.navigation` cannot make the assertion below pass by vacuity
139+
// (it would instead report every id as unlabelled — which is the point).
140+
expect(nav.nav_api_keys?.label, 'nav_api_keys anchors this subtree').toBeTruthy();
141+
142+
expect(
143+
CONDITIONAL_SETUP_NAV_IDS.filter((id) => !nav[id]?.label),
144+
'Setup nav ids with no label — no boot-time gate can see these; see this file header',
145+
).toEqual([]);
146+
});
147+
}
148+
});

packages/platform-objects/src/apps/translations/zh-CN.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ export const zhCN: TranslationData = {
6363
nav_permission_sets: { label: '权限集' },
6464
nav_sharing_rules: { label: '共享规则' },
6565
nav_record_shares: { label: '记录共享' },
66+
// Conditionally contributed by `@objectstack/plugin-auth` (only when an
67+
// external IdP is wired), so `check:app-nav-i18n` cannot see it — see
68+
// the rationale in `en.ts` (#6659). Wording matches this locale's
69+
// `sys_sso_provider.pluralLabel`.
70+
nav_sso_providers: { label: 'SSO 提供方' },
6671
nav_api_keys: { label: 'API 密钥' },
6772
nav_connect_agent: { label: '连接智能体' },
6873

0 commit comments

Comments
 (0)