diff --git a/apps/console/src/__tests__/SpecCompliance.test.tsx b/apps/console/src/__tests__/SpecCompliance.test.tsx
index c335849f..feed99dc 100644
--- a/apps/console/src/__tests__/SpecCompliance.test.tsx
+++ b/apps/console/src/__tests__/SpecCompliance.test.tsx
@@ -25,7 +25,10 @@ describe('ObjectStack Spec v0.9.0 Compliance', () => {
expect(app.name).toBeDefined();
expect(typeof app.name).toBe('string');
expect(app.label).toBeDefined();
- expect(typeof app.label).toBe('string');
+ expect(['string', 'object']).toContain(typeof app.label);
+ if (typeof app.label === 'object') {
+ expect(app.label).toHaveProperty('key');
+ }
// Name convention: lowercase snake_case
expect(app.name).toMatch(/^[a-z][a-z0-9_]*$/);
@@ -36,7 +39,10 @@ describe('ObjectStack Spec v0.9.0 Compliance', () => {
// Optional fields that should be defined if present
if (app.description) {
- expect(typeof app.description).toBe('string');
+ expect(['string', 'object']).toContain(typeof app.description);
+ if (typeof app.description === 'object') {
+ expect(app.description).toHaveProperty('key');
+ }
}
if (app.version) {
expect(typeof app.version).toBe('string');
diff --git a/apps/console/src/components/AppSidebar.tsx b/apps/console/src/components/AppSidebar.tsx
index 8a533e71..f25a5183 100644
--- a/apps/console/src/components/AppSidebar.tsx
+++ b/apps/console/src/components/AppSidebar.tsx
@@ -301,7 +301,7 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
{app.icon ? React.createElement(getIcon(app.icon), { className: "size-3" }) : }
- {app.label}
+ {resolveI18nLabel(app.label, t)}
{activeApp.name === app.name && ✓}
))}
@@ -530,7 +530,7 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
return (
- {item.label}
+ {resolveI18nLabel(item.label, t)}
);
})}
diff --git a/apps/console/src/components/CommandPalette.tsx b/apps/console/src/components/CommandPalette.tsx
index f2f41f35..a28d5ec1 100644
--- a/apps/console/src/components/CommandPalette.tsx
+++ b/apps/console/src/components/CommandPalette.tsx
@@ -33,6 +33,7 @@ import {
import { useTheme } from './theme-provider';
import { useExpressionContext, evaluateVisibility } from '../context/ExpressionProvider';
import { useObjectTranslation } from '@object-ui/i18n';
+import { resolveI18nLabel } from '../utils';
/** Resolve a Lucide icon by name (kebab-case or PascalCase) */
function getIcon(name?: string): React.ElementType {
@@ -101,11 +102,11 @@ export function CommandPalette({ apps, activeApp, objects: _objects, onAppChange
return (
runCommand(() => navigate(`${baseUrl}/${item.objectName}`))}
>
- {item.label}
+ {resolveI18nLabel(item.label, t)}
);
})}
@@ -120,11 +121,11 @@ export function CommandPalette({ apps, activeApp, objects: _objects, onAppChange
.map(item => (
runCommand(() => navigate(`${baseUrl}/dashboard/${item.dashboardName}`))}
>
- {item.label}
+ {resolveI18nLabel(item.label, t)}
))}
@@ -138,11 +139,11 @@ export function CommandPalette({ apps, activeApp, objects: _objects, onAppChange
.map(item => (
runCommand(() => navigate(`${baseUrl}/page/${item.pageName}`))}
>
- {item.label}
+ {resolveI18nLabel(item.label, t)}
))}
@@ -156,11 +157,11 @@ export function CommandPalette({ apps, activeApp, objects: _objects, onAppChange
.map(item => (
runCommand(() => navigate(`${baseUrl}/report/${item.reportName}`))}
>
- {item.label}
+ {resolveI18nLabel(item.label, t)}
))}
@@ -178,11 +179,11 @@ export function CommandPalette({ apps, activeApp, objects: _objects, onAppChange
return (
runCommand(() => onAppChange(app.name))}
>
- {app.label}
+ {resolveI18nLabel(app.label, t)}
{app.name === activeApp?.name && (
{t('console.commandPalette.current')}
)}
diff --git a/apps/console/src/components/ConsoleLayout.tsx b/apps/console/src/components/ConsoleLayout.tsx
index 75a9c4d4..4f739cc7 100644
--- a/apps/console/src/components/ConsoleLayout.tsx
+++ b/apps/console/src/components/ConsoleLayout.tsx
@@ -11,6 +11,7 @@ import { AppShell } from '@object-ui/layout';
import { AppSidebar } from './AppSidebar';
import { AppHeader } from './AppHeader';
import { useResponsiveSidebar } from '../hooks/useResponsiveSidebar';
+import { resolveI18nLabel } from '../utils';
import type { ConnectionState } from '../dataSource';
interface ConsoleLayoutProps {
@@ -46,7 +47,7 @@ export function ConsoleLayout({
}
navbar={
@@ -60,7 +61,7 @@ export function ConsoleLayout({
favicon: activeApp.branding.favicon,
logo: activeApp.branding.logo,
title: activeApp.label
- ? `${activeApp.label} — ObjectStack Console`
+ ? `${resolveI18nLabel(activeApp.label)} — ObjectStack Console`
: undefined,
}
: undefined
diff --git a/apps/console/src/pages/system/AppManagementPage.tsx b/apps/console/src/pages/system/AppManagementPage.tsx
index 4efa787a..466bf4b1 100644
--- a/apps/console/src/pages/system/AppManagementPage.tsx
+++ b/apps/console/src/pages/system/AppManagementPage.tsx
@@ -27,6 +27,7 @@ import {
} from 'lucide-react';
import { toast } from 'sonner';
import { useMetadata } from '../../context/MetadataProvider';
+import { resolveI18nLabel } from '../../utils';
export function AppManagementPage() {
const navigate = useNavigate();
@@ -208,14 +209,14 @@ export function AppManagementPage() {
- {app.label || app.name}
+ {resolveI18nLabel(app.label) || app.name}
{isDefault && Default}
{isActive ? 'Active' : 'Inactive'}
{app.description && (
-
{app.description}
+
{resolveI18nLabel(app.description)}
)}
diff --git a/packages/plugin-dashboard/src/DashboardRenderer.tsx b/packages/plugin-dashboard/src/DashboardRenderer.tsx
index 94f9b5f3..23c2cafd 100644
--- a/packages/plugin-dashboard/src/DashboardRenderer.tsx
+++ b/packages/plugin-dashboard/src/DashboardRenderer.tsx
@@ -13,6 +13,13 @@ import { forwardRef, useState, useEffect, useCallback, useRef } from 'react';
import { RefreshCw } from 'lucide-react';
import { isObjectProvider } from './utils';
+/** Resolve an I18nLabel (string or {key, defaultValue}) to a plain string. */
+function resolveLabel(label: string | { key?: string; defaultValue?: string } | undefined): string | undefined {
+ if (label === undefined || label === null) return undefined;
+ if (typeof label === 'string') return label;
+ return label.defaultValue || label.key;
+}
+
// Color palette for charts
const CHART_COLORS = [
'hsl(var(--chart-1))',
@@ -248,7 +255,8 @@ export const DashboardRenderer = forwardRef
handleWidgetClick(e, widget.id),
onKeyDown: (e: React.KeyboardEvent) => handleWidgetKeyDown(e, widget.id, index),
} : {};
@@ -305,10 +313,10 @@ export const DashboardRenderer = forwardRef
- {widget.title && (
+ {resolvedTitle && (
-
- {widget.title}
+
+ {resolvedTitle}
)}
@@ -325,10 +333,10 @@ export const DashboardRenderer = forwardRef
{schema.header.showTitle !== false && schema.title && (
- {schema.title}
+ {resolveLabel(schema.title)}
)}
{schema.header.showDescription !== false && schema.description && (
- {schema.description}
+ {resolveLabel(schema.description)}
)}
{schema.header.actions && schema.header.actions.length > 0 && (