Found while implementing #4307 (console i18n conversion for this page). Filed unassigned, NOT fixed there — out of that card's scope, which is the page's own chrome.
Fact
apps/console/src/pages/system/AppManagementPage.tsx, the search filter:
const filteredApps = (apps || []).filter((app) => {
if (!searchQuery) return true;
const q = searchQuery.toLowerCase();
return (
(app.name || '').toLowerCase().includes(q) ||
(app.label || '').toLowerCase().includes(q) || // ← here
(app.description || '').toLowerCase().includes(q)
);
});
app.label may be objectui's KEYED i18n label form — an object { key, defaultValue?, params? }. The same file resolves exactly that shape through resolveKeyedI18nLabel for display, which is why the resolver is imported there at all. An object is truthy, so (app.label || '') yields the object and .toLowerCase is undefined:
TypeError: (l || "").toLowerCase is not a function
app.description is the same shape and the same hazard on the line below it.
Impact
The throw happens inside filter during render, so it takes out the page rather than degrading search — but only once the operator types something (if (!searchQuery) return true short-circuits the empty case). So the page loads fine and breaks on first keystroke.
Not measured, and it decides the severity: whether any shipping app metadata actually carries a keyed label/description today. The render path is defensive about it and the search path is not, which is the inconsistency worth recording either way; the grading of "live crash" versus "latent" is the triage round's, not this filing's.
Suggested shape
Route both reads through the resolver the file already imports, so search matches what the user actually sees rather than a second, less capable reading of the same field:
(resolveKeyedI18nLabel(app.label) || '').toLowerCase().includes(q)
Worth checking whether sibling list/search surfaces filter on a raw label the same way before fixing just this one.
Related: #4163 (the I18nLabel audit — that one is the spec's INLINE per-locale map form; this is objectui's keyed form, a different shape with the same class of unguarded read).
Generated by Claude Code
Found while implementing #4307 (console i18n conversion for this page). Filed unassigned, NOT fixed there — out of that card's scope, which is the page's own chrome.
Fact
apps/console/src/pages/system/AppManagementPage.tsx, the search filter:app.labelmay be objectui's KEYED i18n label form — an object{ key, defaultValue?, params? }. The same file resolves exactly that shape throughresolveKeyedI18nLabelfor display, which is why the resolver is imported there at all. An object is truthy, so(app.label || '')yields the object and.toLowerCaseisundefined:app.descriptionis the same shape and the same hazard on the line below it.Impact
The throw happens inside
filterduring render, so it takes out the page rather than degrading search — but only once the operator types something (if (!searchQuery) return trueshort-circuits the empty case). So the page loads fine and breaks on first keystroke.Not measured, and it decides the severity: whether any shipping app metadata actually carries a keyed
label/descriptiontoday. The render path is defensive about it and the search path is not, which is the inconsistency worth recording either way; the grading of "live crash" versus "latent" is the triage round's, not this filing's.Suggested shape
Route both reads through the resolver the file already imports, so search matches what the user actually sees rather than a second, less capable reading of the same field:
Worth checking whether sibling list/search surfaces filter on a raw label the same way before fixing just this one.
Related: #4163 (the
I18nLabelaudit — that one is the spec's INLINE per-locale map form; this is objectui's keyed form, a different shape with the same class of unguarded read).Generated by Claude Code