diff --git a/.changeset/setup-nav-dead-translation-keys.md b/.changeset/setup-nav-dead-translation-keys.md new file mode 100644 index 0000000000..5350a797c7 --- /dev/null +++ b/.changeset/setup-nav-dead-translation-keys.md @@ -0,0 +1,37 @@ +--- +"@objectstack/platform-objects": patch +--- + +chore(platform-objects): drop four dead `apps.setup.navigation` translation keys (#6660) + +Four ids kept a Setup nav label in the hand-written locale bundles long after +the nav item that declared them was removed. No composition renders them, so +nothing was broken — but a translated key with no declaring nav item is the +shape `app-nav-translation-parity.test.ts` already refuses for Studio: it reads +as coverage. `nav_workflows` outlived its Studio menu entry in all four locales +the same way, and nothing said so until that reverse assertion was written. + +Removed, with the reason each one is gone: + +| id | why it has no nav item | +| --- | --- | +| `nav_approval_processes` | the approval process engine was retired in favour of the approval flow node (#1408, ADR-0019 P4/P5) | +| `nav_verifications` | `sys_verification` omits `list` from `apiMethods` | +| `nav_device_codes` | `sys_device_code` likewise — both hold sensitive, ephemeral secrets, so a browse entry could only ever render "failed to load" (#2266) | +| `nav_metadata` | moved to Studio as `nav_metadata_directory` when the Studio app was split out | + +14 key/label pairs in total, not 16: `zh-CN` never carried `nav_verifications` +or `nav_device_codes`. + +Each id was checked **individually** against a repo-wide grep for a declaring +`id: ''` — zero hits each, against a control probe (`nav_webhooks`) that +returns five. That is deliberately not the same claim as a runtime diff: from a +single booted composition a dead key and a conditionally-contributed one are +indistinguishable (`plugin-auth` contributes `nav_sso_providers` only when an +external IdP is wired), which is why `pnpm check:app-nav-i18n` still refuses the +reverse direction and why this change removes exactly four named ids rather than +"everything the merged app did not declare". + +A tombstone test pins the four so they cannot drift back in without their nav +item. Re-adding `nav_verifications` / `nav_device_codes` remains a security +decision — it means enabling `list` on the object first. diff --git a/packages/platform-objects/src/apps/translations/en.ts b/packages/platform-objects/src/apps/translations/en.ts index 5daf354259..8fb8c9cc4b 100644 --- a/packages/platform-objects/src/apps/translations/en.ts +++ b/packages/platform-objects/src/apps/translations/en.ts @@ -86,7 +86,6 @@ export const en: TranslationData = { nav_connect_agent: { label: 'Connect an Agent' }, // Approvals - nav_approval_processes: { label: 'Processes' }, nav_approval_requests: { label: 'Requests' }, nav_approval_actions: { label: 'Action History' }, nav_approval_delegations: { label: 'Delegations (OOO)' }, @@ -121,13 +120,18 @@ export const en: TranslationData = { nav_datasources: { label: 'Datasources' }, // Advanced + // + // No `nav_verifications` / `nav_device_codes` here on purpose: + // `sys_verification` and `sys_device_code` omit `list` from their + // `apiMethods`, so `setup-nav.contributions.ts` deliberately declares + // no browse entry for them (#2266). Nor `nav_metadata` — that entry + // moved to Studio as `nav_metadata_directory`. Re-adding a label here + // means re-adding the nav item first; the tombstone in + // `setup-nav-dead-key-tombstone.test.ts` states the whole rule (#6660). nav_oauth_apps: { label: 'OAuth Applications' }, nav_jwks: { label: 'Signing Keys (JWKS)' }, - nav_verifications: { label: 'Verifications' }, - nav_device_codes: { label: 'Device Codes' }, nav_accounts: { label: 'Identity Links' }, nav_user_preferences: { label: 'User Preferences' }, - nav_metadata: { label: 'All Metadata' }, }, }, studio: { diff --git a/packages/platform-objects/src/apps/translations/es-ES.ts b/packages/platform-objects/src/apps/translations/es-ES.ts index 102f0c75a5..12038545d3 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.ts @@ -63,7 +63,6 @@ export const esES: TranslationData = { nav_api_keys: { label: 'Claves API' }, nav_connect_agent: { label: 'Conectar un agente' }, - nav_approval_processes: { label: 'Procesos' }, nav_approval_requests: { label: 'Solicitudes' }, nav_approval_actions: { label: 'Historial de Acciones' }, nav_approval_delegations: { label: 'Delegaciones (ausencia)' }, @@ -94,11 +93,8 @@ export const esES: TranslationData = { nav_oauth_apps: { label: 'Aplicaciones OAuth' }, nav_jwks: { label: 'Claves de Firma (JWKS)' }, - nav_verifications: { label: 'Verificaciones' }, - nav_device_codes: { label: 'Códigos de Dispositivo' }, nav_accounts: { label: 'Enlaces de Identidad' }, nav_user_preferences: { label: 'Preferencias de Usuario' }, - nav_metadata: { label: 'Todos los Metadatos' }, }, }, studio: { diff --git a/packages/platform-objects/src/apps/translations/ja-JP.ts b/packages/platform-objects/src/apps/translations/ja-JP.ts index 33a414f069..09d66bee3f 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.ts @@ -63,7 +63,6 @@ export const jaJP: TranslationData = { nav_api_keys: { label: 'API キー' }, nav_connect_agent: { label: 'エージェントを接続' }, - nav_approval_processes: { label: 'プロセス' }, nav_approval_requests: { label: 'リクエスト' }, nav_approval_actions: { label: 'アクション履歴' }, nav_approval_delegations: { label: '委任 (不在時)' }, @@ -94,11 +93,8 @@ export const jaJP: TranslationData = { nav_oauth_apps: { label: 'OAuth アプリケーション' }, nav_jwks: { label: '署名キー (JWKS)' }, - nav_verifications: { label: '検証' }, - nav_device_codes: { label: 'デバイスコード' }, nav_accounts: { label: 'ID 連携' }, nav_user_preferences: { label: 'ユーザー設定' }, - nav_metadata: { label: 'すべてのメタデータ' }, }, }, studio: { 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 new file mode 100644 index 0000000000..59be4379fc --- /dev/null +++ b/packages/platform-objects/src/apps/translations/setup-nav-dead-key-tombstone.test.ts @@ -0,0 +1,94 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// Tombstone for four dead `apps.setup.navigation` translation keys (#6660). +// +// --------------------------------------------------------------------------- +// Why a hard-coded id list instead of the general reverse direction +// --------------------------------------------------------------------------- +// `app-nav-translation-parity.test.ts` asserts the reverse direction for Studio +// ("a translation for an id the app no longer declares is dead weight that +// reads as coverage") by walking `STUDIO_APP.navigation`. Setup cannot be +// walked that way: it is a shell of empty group anchors (ADR-0029 D7) and every +// entry arrives at RUNTIME, so this file has nothing to diff against — which is +// exactly why that file's header says a Setup case there "has to boot +// 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. +// +// 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 +// each of them on `61282f906`, against a control probe (`nav_webhooks`) that +// returned five — and each of which has a recorded reason to be gone: +// +// nav_approval_processes the process engine was retired in favour of the +// approval flow node (#1408, ADR-0019 P4/P5) +// nav_verifications `sys_verification` omits `list` from `apiMethods` +// nav_device_codes `sys_device_code` likewise — both are sensitive, +// ephemeral secrets, so a browse entry could only +// ever render "failed to load" (#2266, and the +// comment that records it in +// `setup-nav.contributions.ts`) +// nav_metadata moved to Studio as `nav_metadata_directory` when +// the Studio app was split out (482eb67cc) +// +// --------------------------------------------------------------------------- +// What to do when this test goes red +// --------------------------------------------------------------------------- +// It goes red on exactly one event: one of the four ids comes back. That is not +// automatically wrong — re-adding `nav_verifications` or `nav_device_codes` is a +// deliberate security decision (it requires enabling `list` on the object +// first), and `nav_approval_processes` could return with a new owner. The rule +// is the ORDER: the declaring nav item comes back first, the label second, and +// the id's line is deleted from `DEAD_SETUP_NAV_IDS` in that same commit. A +// label with no declaring nav item is what this tombstone exists to refuse. + +import { describe, it, expect } from 'vitest'; +import { SETUP_NAV_CONTRIBUTIONS } from '../setup-nav.contributions.js'; +import { en } from './en.js'; +import { zhCN } from './zh-CN.js'; +import { jaJP } from './ja-JP.js'; +import { esES } from './es-ES.js'; + +const LOCALES = { en, 'zh-CN': zhCN, 'ja-JP': jaJP, 'es-ES': esES } as const; + +/** Removed Setup nav ids. Delete a line here only together with its nav item. */ +const DEAD_SETUP_NAV_IDS = [ + 'nav_approval_processes', + 'nav_device_codes', + 'nav_metadata', + 'nav_verifications', +] as const; + +describe('removed Setup nav ids stay removed (#6660)', () => { + for (const [locale, data] of Object.entries(LOCALES)) { + it(`${locale} carries no label for a removed Setup nav id`, () => { + const nav = (data.apps?.setup?.navigation ?? {}) as Record; + expect( + DEAD_SETUP_NAV_IDS.filter((id) => id in nav), + 'apps.setup.navigation keys with no declaring nav item — see this file header', + ).toEqual([]); + }); + } + + // The other half of the same fact, on the one Setup contributor this package + // owns. Keeping it here means a re-added nav item cannot quietly restore a + // label without this ledger being read: both assertions go red together. + it('SETUP_NAV_CONTRIBUTIONS declares none of them', () => { + const declared = new Set(); + const walk = (items: unknown[]): void => { + for (const raw of items) { + const item = raw as { id?: string; children?: unknown[] }; + if (item?.id) declared.add(item.id); + if (Array.isArray(item?.children)) walk(item.children); + } + }; + for (const contribution of SETUP_NAV_CONTRIBUTIONS) walk(contribution.items); + + // Control: the walk really reads this array, so an empty `declared` cannot + // pass the assertion below by vacuity. + expect(declared.has('nav_users'), 'nav_users is contributed here').toBe(true); + expect(DEAD_SETUP_NAV_IDS.filter((id) => declared.has(id))).toEqual([]); + }); +}); diff --git a/packages/platform-objects/src/apps/translations/zh-CN.ts b/packages/platform-objects/src/apps/translations/zh-CN.ts index 022a5b029d..57fb1acbb3 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.ts @@ -66,7 +66,6 @@ export const zhCN: TranslationData = { nav_api_keys: { label: 'API 密钥' }, nav_connect_agent: { label: '连接智能体' }, - nav_approval_processes: { label: '审批流程' }, nav_approval_requests: { label: '审批申请' }, nav_approval_actions: { label: '审批历史' }, // `审批委派` matches sys_approval_delegation's object label; `(外出)` @@ -104,7 +103,6 @@ export const zhCN: TranslationData = { nav_jwks: { label: '签名密钥 (JWKS)' }, nav_accounts: { label: '身份链接' }, nav_user_preferences: { label: '用户偏好' }, - nav_metadata: { label: '全部元数据' }, }, }, studio: {