@@ -488,13 +488,38 @@ const DYNAMIC_KEYS = [
488488 * Every key reached dynamically: DYNAMIC_KEYS plus the src/manifest.json fields
489489 * that MainMenu.translate(key) passes straight to t().
490490 *
491- * Only the fields CnAppNav actually resolves through its `translate` prop count:
492- * `menu[].label` (recursively through `children`) and the two nav label
493- * overrides. Anything else in the manifest is data, not UI copy — notably
494- * `observability.metrics[].name` (Prometheus metric identifiers) and
495- * `pages[].title`, which CnPageRenderer forwards to the page component as a raw
496- * prop without translating it. Harvesting those made metric names look like
497- * catalogue keys and would have put them in front of translators.
491+ * Only fields that are actually resolved through a `translate` prop count.
492+ * Anything else in the manifest is data, not UI copy — notably
493+ * `observability.metrics[].name` (Prometheus metric identifiers), which made
494+ * metric names look like catalogue keys and would have put them in front of
495+ * translators.
496+ *
497+ * WHAT COUNTS, AND HOW IT WAS ESTABLISHED
498+ * ---------------------------------------
499+ * This function used to exclude `pages[].title` on the stated grounds that
500+ * CnPageRenderer "forwards [it] to the page component as a raw prop without
501+ * translating it". That is no longer true. Measured 2026-09-06 on integriq,
502+ * which wires `<CnAppRoot :translate="translateForApp">` exactly as
503+ * `src/App.vue` does here, against a Nextcloud 34 instance with the user set
504+ * to `nl` and the app's `l10n/` deployed:
505+ *
506+ * - `menu[].label` renders translated (Sources became "Bronnen").
507+ * - `pages[].title` renders translated: the page heading was
508+ * "StUF-berichten", not "StUF messages".
509+ * - report `cards[].label` / `cards[].description` render translated;
510+ * CnReportsPage builds `resolvedCards` with `this.tr(card.label)`.
511+ *
512+ * The cost of the stale exclusion is not cosmetic: live keys get reported
513+ * UNUSED, and clean-l10n proposes deleting exactly what this function fails to
514+ * claim. Deleting a translated page title removes the localised string and
515+ * leaves the English source rendering correctly, so nothing fails and nobody
516+ * notices.
517+ *
518+ * Adding a field here can only make the cleaner more conservative, never less.
519+ * Verify a new one against the DOM rather than the source: the template renders
520+ * `{{ card.label }}` and only `resolvedCards` shows the translation, so reading
521+ * the component alone reproduces the wrong conclusion. The measurement also
522+ * needs the app's `l10n/` deployed, which the built bundle does not carry.
498523 *
499524 * @param {string } repoRoot Absolute path to the app root.
500525 * @return {Set<string> } Keys that must count as used.
@@ -509,17 +534,30 @@ function collectDynamicKeys(repoRoot) {
509534 } catch {
510535 return out
511536 }
537+ const add = ( value ) => {
538+ if ( typeof value === 'string' && value . trim ( ) !== '' ) out . add ( value )
539+ }
512540 ; ( function collectMenu ( items ) {
513541 if ( ! Array . isArray ( items ) ) return
514542 for ( const item of items ) {
515543 if ( ! item || typeof item !== 'object' ) continue
516- if ( typeof item . label === 'string' ) out . add ( item . label )
544+ add ( item . label )
517545 collectMenu ( item . children )
518546 }
519547 } ) ( manifest . menu )
520548 for ( const field of [ 'roadmapLabel' , 'documentationLabel' ] ) {
521- const v = manifest . nav ?. [ field ]
522- if ( typeof v === 'string' ) out . add ( v )
549+ add ( manifest . nav ?. [ field ] )
550+ }
551+ if ( Array . isArray ( manifest . pages ) ) {
552+ for ( const page of manifest . pages ) {
553+ if ( ! page || typeof page !== 'object' ) continue
554+ add ( page . title )
555+ for ( const card of page . config ?. cards ?? [ ] ) {
556+ if ( ! card || typeof card !== 'object' ) continue
557+ add ( card . label )
558+ add ( card . description )
559+ }
560+ }
523561 }
524562 return out
525563}
0 commit comments