Skip to content

Commit 9772f9f

Browse files
committed
fix(cli): expect a contributed navigation item's label under its target app
collectExpectedEntries walked apps[].navigation only, so a key for an item another package contributes through manifest.navigationContributions was never expected and deleting its translation left os lint and os i18n check green, though translation-target-unknown accepts that key. Claude-Session: https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6b44efa commit 9772f9f

1 file changed

Lines changed: 58 additions & 0 deletions

File tree

‎packages/cli/src/utils/i18n-extract.ts‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@
9393
* globalActions.<action>.params.<param>.* / .resultDialog.* (same shape as object actions)
9494
* apps.<app>.label / .description
9595
* apps.<app>.navigation.<id>.label
96+
* ^ an item another package CONTRIBUTES into the app through
97+
* `manifest.navigationContributions` too, keyed under the TARGET app
98+
* (#24993; see `navigationContributionsOf`)
9699
* dashboards.<dash>.label / .description
97100
* dashboards.<dash>.widgets.<w>.title / .description
98101
* datasets.<dataset>.label / .description
@@ -1451,6 +1454,22 @@ export function collectExpectedEntries(
14511454
walkNavigation(nav, appName, out);
14521455
}
14531456

1457+
// ── Contributed navigation (ADR-0029 D7, #24993) ─────────────────
1458+
// An item a package injects into another package's app never enters that
1459+
// app's authored `navigation`: the runtime folds it in on every read
1460+
// (`SchemaRegistry.applyNavContributions`) and `translateApp` then resolves
1461+
// its label from `apps.<target>.navigation.<id>.label` — the key
1462+
// `translation-target-unknown` already accepts for a contributed item
1463+
// (#18203). Walked AFTER the apps' own trees and by the same
1464+
// `walkNavigation`, so a contributed group's children follow the rule an own
1465+
// group's do, and an id an app also declares keeps its own entry
1466+
// (`dedupeByPath`, first emission wins).
1467+
for (const contribution of navigationContributionsOf(config)) {
1468+
const targetApp = contribution?.app;
1469+
if (typeof targetApp !== 'string' || targetApp.length === 0) continue;
1470+
if (Array.isArray(contribution.items)) walkNavigation(contribution.items, targetApp, out);
1471+
}
1472+
14541473
// ── Dashboards + widgets ─────────────────────────────────────────
14551474
const dashboards: any[] = Array.isArray(config?.dashboards) ? config.dashboards : [];
14561475
for (const dash of dashboards) {
@@ -2146,6 +2165,45 @@ function walkNavigation(nav: any[], appName: string, out: ExpectedEntry[]): void
21462165
}
21472166
}
21482167

2168+
/**
2169+
* Every `navigationContributions[]` entry the stack carries, from the two
2170+
* carriers `translation-target-unknown` reads (`contributedNavItemsByApp` in
2171+
* `@objectstack/lint`'s `validate-translation-references.ts`), so the keys
2172+
* coverage expects are the keys that rule accepts:
2173+
*
2174+
* 1. `manifest.navigationContributions` — where a single-`defineStack`
2175+
* project's contributions live;
2176+
* 2. `packages[].manifest.navigationContributions` — the ADR-0130 D4 artifact
2177+
* entry, whose `manifest` is the assembled package body. On a
2178+
* `composeStacks(…, { manifest: 'preserve' })` stack this is the only
2179+
* place a module's contributions sit: the singular top-level `manifest`
2180+
* is one package's copy (the `'last'` pick), usually the app's.
2181+
*
2182+
* Both are read unconditionally, as the rule reads them. Where the top-level
2183+
* manifest IS a copy of a contributing package's, the two carriers yield the
2184+
* same items and `dedupeByPath` keeps one entry per key.
2185+
*
2186+
* ⛔ The rule's reader is not imported: it is module-private to
2187+
* `@objectstack/lint`. This copy is held to it by
2188+
* `test/i18n-contributed-nav-coverage.test.ts`, which runs the rule over a
2189+
* bundle carrying every contributed key this walk expects.
2190+
*
2191+
* Not seen, here as there: a contribution registered imperatively from a
2192+
* plugin's `init` (`engine.registerAppNavContribution`) is not metadata;
2193+
* `packages/cli/scripts/check-app-nav-i18n.mjs` boots a composition to judge
2194+
* that population.
2195+
*/
2196+
function navigationContributionsOf(config: any): any[] {
2197+
const out: any[] = [];
2198+
const add = (list: unknown): void => {
2199+
if (Array.isArray(list)) out.push(...list);
2200+
};
2201+
add(config?.manifest?.navigationContributions);
2202+
const packages: unknown[] = Array.isArray(config?.packages) ? config.packages : [];
2203+
for (const entry of packages) add((entry as any)?.manifest?.navigationContributions);
2204+
return out;
2205+
}
2206+
21492207
// ─── Filter + bundle assembly ──────────────────────────────────────────
21502208

21512209
function passesFilter(entry: ExpectedEntry, filter?: RegExp): boolean {

0 commit comments

Comments
 (0)