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
37 changes: 37 additions & 0 deletions .changeset/setup-nav-dead-translation-keys.md
Original file line number Diff line number Diff line change
@@ -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: '<key>'` — 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.
12 changes: 8 additions & 4 deletions packages/platform-objects/src/apps/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)' },
Expand Down Expand Up @@ -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: {
Expand Down
4 changes: 0 additions & 4 deletions packages/platform-objects/src/apps/translations/es-ES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)' },
Expand Down Expand Up @@ -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: {
Expand Down
4 changes: 0 additions & 4 deletions packages/platform-objects/src/apps/translations/ja-JP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '委任 (不在時)' },
Expand Down Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
@@ -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: '<key>'` 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<string, { label?: string }>;
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<string>();
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([]);
});
});
2 changes: 0 additions & 2 deletions packages/platform-objects/src/apps/translations/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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; `(外出)`
Expand Down Expand Up @@ -104,7 +103,6 @@ export const zhCN: TranslationData = {
nav_jwks: { label: '签名密钥 (JWKS)' },
nav_accounts: { label: '身份链接' },
nav_user_preferences: { label: '用户偏好' },
nav_metadata: { label: '全部元数据' },
},
},
studio: {
Expand Down
Loading