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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed
- **i18n: `I18nLabelSchema` now accepts `string` only** — `label`, `description`, `title`,
and other display-text fields across all UI schemas (`AppSchema`, `NavigationArea`,
`PageSchema`, `DashboardWidgetSchema`, `ReportSchema`, `ChartSchema`, `NotificationSchema`,
`AriaPropsSchema`, etc.) now accept only plain strings. The previous `string | I18nObject`
union type has been replaced with `z.string()`. i18n translation keys will be auto-generated
by the framework at registration time; developers only need to provide the default-language
string value. Translations are managed through translation files, not inline i18n objects.
([#1054](https://github.com/objectstack-ai/framework/issues/1054))

**Migration:** Replace any `label: { key: '...', defaultValue: 'X' }` with `label: 'X'`.
Existing plain-string labels require no changes.

**Affected plugins updated:**
- `@objectstack/plugin-setup` — `setup-app.ts`, `setup-areas.ts`
- `@objectstack/plugin-auth` — navigation item labels
- `@objectstack/plugin-security` — navigation item labels
- `@objectstack/plugin-audit` — navigation item labels

### Documentation
- **README rewrite** — Rewrote `README.md` to accurately reflect the `objectstack-ai/framework`
repository. Updates include: corrected title ("ObjectStack Framework"), updated badges
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/plugin-audit/src/audit-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class AuditPlugin implements Plugin {
setupNav.contribute({
areaId: 'area_system',
items: [
{ id: 'nav_audit_logs', type: 'object', label: { key: 'setup.nav.audit_logs', defaultValue: 'Audit Logs' }, objectName: 'audit_log', icon: 'scroll-text', order: 10 },
{ id: 'nav_audit_logs', type: 'object', label: 'Audit Logs', objectName: 'audit_log', icon: 'scroll-text', order: 10 },
],
});
ctx.logger.info('Audit navigation items contributed to Setup App');
Expand Down
10 changes: 5 additions & 5 deletions packages/plugins/plugin-auth/src/auth-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,11 @@ export class AuthPlugin implements Plugin {
setupNav.contribute({
areaId: 'area_administration',
items: [
{ id: 'nav_users', type: 'object', label: { key: 'setup.nav.users', defaultValue: 'Users' }, objectName: 'user', icon: 'users', order: 10 },
{ id: 'nav_organizations', type: 'object', label: { key: 'setup.nav.organizations', defaultValue: 'Organizations' }, objectName: 'organization', icon: 'building-2', order: 20 },
{ id: 'nav_teams', type: 'object', label: { key: 'setup.nav.teams', defaultValue: 'Teams' }, objectName: 'team', icon: 'users-round', order: 30 },
{ id: 'nav_api_keys', type: 'object', label: { key: 'setup.nav.api_keys', defaultValue: 'API Keys' }, objectName: 'api_key', icon: 'key', order: 40 },
{ id: 'nav_sessions', type: 'object', label: { key: 'setup.nav.sessions', defaultValue: 'Sessions' }, objectName: 'session', icon: 'monitor', order: 50 },
{ id: 'nav_users', type: 'object', label: 'Users', objectName: 'user', icon: 'users', order: 10 },
{ id: 'nav_organizations', type: 'object', label: 'Organizations', objectName: 'organization', icon: 'building-2', order: 20 },
{ id: 'nav_teams', type: 'object', label: 'Teams', objectName: 'team', icon: 'users-round', order: 30 },
{ id: 'nav_api_keys', type: 'object', label: 'API Keys', objectName: 'api_key', icon: 'key', order: 40 },
{ id: 'nav_sessions', type: 'object', label: 'Sessions', objectName: 'session', icon: 'monitor', order: 50 },
],
});
ctx.logger.info('Auth navigation items contributed to Setup App');
Expand Down
4 changes: 2 additions & 2 deletions packages/plugins/plugin-security/src/security-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ export class SecurityPlugin implements Plugin {
setupNav.contribute({
areaId: 'area_administration',
items: [
{ id: 'nav_roles', type: 'object', label: { key: 'setup.nav.roles', defaultValue: 'Roles' }, objectName: 'role', icon: 'shield-check', order: 60 },
{ id: 'nav_permission_sets', type: 'object', label: { key: 'setup.nav.permission_sets', defaultValue: 'Permission Sets' }, objectName: 'permission_set', icon: 'lock', order: 70 },
{ id: 'nav_roles', type: 'object', label: 'Roles', objectName: 'role', icon: 'shield-check', order: 60 },
{ id: 'nav_permission_sets', type: 'object', label: 'Permission Sets', objectName: 'permission_set', icon: 'lock', order: 70 },
],
});
ctx.logger.info('Security navigation items contributed to Setup App');
Expand Down
10 changes: 2 additions & 8 deletions packages/plugins/plugin-setup/src/setup-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,8 @@ import type { App, NavigationArea, NavigationItem } from '@objectstack/spec/ui';
*/
export const SETUP_APP_DEFAULTS: Omit<App, 'areas'> & { areas: NavigationArea[] } = {
name: 'setup',
label: {
key: 'setup.app.label',
defaultValue: 'Setup',
},
description: {
key: 'setup.app.description',
defaultValue: 'Platform settings and administration',
},
label: 'Setup',
description: 'Platform settings and administration',
icon: 'settings',
active: true,
isDefault: false,
Expand Down
40 changes: 8 additions & 32 deletions packages/plugins/plugin-setup/src/setup-areas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,58 +31,34 @@ export type SetupAreaId = (typeof SETUP_AREA_IDS)[keyof typeof SETUP_AREA_IDS];
export const SETUP_AREAS: readonly NavigationArea[] = [
{
id: SETUP_AREA_IDS.administration,
label: {
key: 'setup.areas.administration',
defaultValue: 'Administration',
},
label: 'Administration',
icon: 'shield',
order: 10,
description: {
key: 'setup.areas.administration.description',
defaultValue: 'User management, roles, permissions, and security settings',
},
description: 'User management, roles, permissions, and security settings',
navigation: [],
},
{
id: SETUP_AREA_IDS.platform,
label: {
key: 'setup.areas.platform',
defaultValue: 'Platform',
},
label: 'Platform',
icon: 'layers',
order: 20,
description: {
key: 'setup.areas.platform.description',
defaultValue: 'Objects, fields, layouts, automation, and extensibility settings',
},
description: 'Objects, fields, layouts, automation, and extensibility settings',
navigation: [],
},
{
id: SETUP_AREA_IDS.system,
label: {
key: 'setup.areas.system',
defaultValue: 'System',
},
label: 'System',
icon: 'settings',
order: 30,
description: {
key: 'setup.areas.system.description',
defaultValue: 'Datasources, integrations, jobs, logs, and environment configuration',
},
description: 'Datasources, integrations, jobs, logs, and environment configuration',
navigation: [],
},
{
id: SETUP_AREA_IDS.ai,
label: {
key: 'setup.areas.ai',
defaultValue: 'AI',
},
label: 'AI',
icon: 'brain',
order: 40,
description: {
key: 'setup.areas.ai.description',
defaultValue: 'AI agents, model registry, RAG pipelines, and intelligence settings',
},
description: 'AI agents, model registry, RAG pipelines, and intelligence settings',
navigation: [],
},
] as const;
16 changes: 8 additions & 8 deletions packages/spec/src/ui/action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,28 +530,28 @@ describe('Action Factory', () => {
});

describe('Action I18n Integration', () => {
it('should accept i18n object as action label', () => {
it('should reject i18n object as action label', () => {
expect(() => ActionSchema.parse({
name: 'i18n_action',
label: { key: 'actions.approve', defaultValue: 'Approve' },
})).not.toThrow();
})).toThrow();
});
it('should accept i18n as confirmText and successMessage', () => {
it('should reject i18n as confirmText and successMessage', () => {
expect(() => ActionSchema.parse({
name: 'i18n_confirm',
label: 'Delete',
confirmText: { key: 'actions.confirm_delete', defaultValue: 'Are you sure?' },
successMessage: { key: 'actions.delete_success', defaultValue: 'Deleted!' },
})).not.toThrow();
})).toThrow();
});
it('should accept i18n in param labels', () => {
it('should reject i18n in param labels', () => {
expect(() => ActionParamSchema.parse({
name: 'reason',
label: { key: 'params.reason', defaultValue: 'Reason' },
type: 'textarea',
})).not.toThrow();
})).toThrow();
});
it('should accept i18n in param option labels', () => {
it('should reject i18n in param option labels', () => {
expect(() => ActionParamSchema.parse({
name: 'priority',
label: 'Priority',
Expand All @@ -560,7 +560,7 @@ describe('Action I18n Integration', () => {
{ label: { key: 'options.high', defaultValue: 'High' }, value: 'high' },
{ label: { key: 'options.low', defaultValue: 'Low' }, value: 'low' },
],
})).not.toThrow();
})).toThrow();
});
});

Expand Down
14 changes: 6 additions & 8 deletions packages/spec/src/ui/animation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,10 @@ describe('Type exports', () => {
});

describe('I18n and ARIA integration', () => {
it('should accept I18n label on ComponentAnimationSchema', () => {
const result = ComponentAnimationSchema.parse({
it('should reject I18n label on ComponentAnimationSchema', () => {
expect(() => ComponentAnimationSchema.parse({
label: { key: 'animations.card_enter', defaultValue: 'Card Enter' },
});
expect(result.label).toEqual({ key: 'animations.card_enter', defaultValue: 'Card Enter' });
})).toThrow();
});

it('should accept plain string label on ComponentAnimationSchema', () => {
Expand All @@ -220,11 +219,10 @@ describe('I18n and ARIA integration', () => {
expect(result.role).toBe('presentation');
});

it('should accept I18n label on MotionConfigSchema', () => {
const result = MotionConfigSchema.parse({
it('should reject I18n label on MotionConfigSchema', () => {
expect(() => MotionConfigSchema.parse({
label: { key: 'motion.global', defaultValue: 'Global Motion Config' },
});
expect(result.label).toEqual({ key: 'motion.global', defaultValue: 'Global Motion Config' });
})).toThrow();
});

it('should leave I18n/ARIA fields undefined when not provided', () => {
Expand Down
11 changes: 5 additions & 6 deletions packages/spec/src/ui/chart.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,20 +210,19 @@ describe('Real-World Chart Configuration Examples', () => {
});

describe('Chart I18n Integration', () => {
it('should accept i18n object as chart title', () => {
const result = ChartConfigSchema.parse({
it('should reject i18n object as chart title', () => {
expect(() => ChartConfigSchema.parse({
type: 'bar',
title: { key: 'charts.sales', defaultValue: 'Sales Chart' },
});
expect(typeof result.title).toBe('object');
})).toThrow();
});
it('should accept i18n as chart subtitle and description', () => {
it('should reject i18n as chart subtitle and description', () => {
expect(() => ChartConfigSchema.parse({
type: 'line',
title: 'Revenue',
subtitle: { key: 'charts.subtitle', defaultValue: 'Monthly breakdown' },
description: { key: 'charts.desc', defaultValue: 'Revenue over time' },
})).not.toThrow();
})).toThrow();
});
});

Expand Down
44 changes: 20 additions & 24 deletions packages/spec/src/ui/dashboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,30 +576,30 @@ describe('Dashboard Factory', () => {
});

describe('Dashboard I18n Integration', () => {
it('should accept i18n object as dashboard label', () => {
it('should reject i18n object as dashboard label', () => {
expect(() => DashboardSchema.parse({
name: 'i18n_dashboard',
label: { key: 'dashboards.sales', defaultValue: 'Sales Dashboard' },
widgets: [],
})).not.toThrow();
})).toThrow();
});
it('should accept i18n object as dashboard description', () => {
it('should reject i18n object as dashboard description', () => {
expect(() => DashboardSchema.parse({
name: 'test_dashboard',
label: 'Test',
description: { key: 'dashboards.test.desc', defaultValue: 'Test dashboard' },
widgets: [],
})).not.toThrow();
})).toThrow();
});
it('should accept i18n object as widget title', () => {
it('should reject i18n object as widget title', () => {
expect(() => DashboardWidgetSchema.parse({
id: 'total_revenue',
title: { key: 'widgets.revenue', defaultValue: 'Total Revenue' },
type: 'metric',
layout: { x: 0, y: 0, w: 3, h: 2 },
})).not.toThrow();
})).toThrow();
});
it('should accept i18n object in global filter label', () => {
it('should reject i18n object in global filter label', () => {
expect(() => DashboardSchema.parse({
name: 'filter_dash',
label: 'Filtered',
Expand All @@ -609,7 +609,7 @@ describe('Dashboard I18n Integration', () => {
label: { key: 'filters.status', defaultValue: 'Status' },
type: 'select',
}],
})).not.toThrow();
})).toThrow();
});
});

Expand Down Expand Up @@ -783,15 +783,14 @@ describe('DashboardWidgetSchema - description', () => {
expect(result.description).toBe('Year-to-date total revenue');
});

it('should accept widget with i18n description', () => {
const result = DashboardWidgetSchema.parse({
it('should reject widget with i18n description', () => {
expect(() => DashboardWidgetSchema.parse({
id: 'revenue_i18n',
title: 'Revenue',
description: { key: 'widgets.revenue.desc', defaultValue: 'Total revenue' },
type: 'metric',
layout: { x: 0, y: 0, w: 3, h: 2 },
});
expect(result.description).toEqual({ key: 'widgets.revenue.desc', defaultValue: 'Total revenue' });
})).toThrow();
});

it('should accept widget without description (optional)', () => {
Expand Down Expand Up @@ -1043,15 +1042,14 @@ describe('GlobalFilterSchema', () => {
expect(result.options![0].label).toBe('High');
});

it('should accept filter with i18n option labels', () => {
const result = GlobalFilterSchema.parse({
it('should reject filter with i18n option labels', () => {
expect(() => GlobalFilterSchema.parse({
field: 'priority',
type: 'select',
options: [
{ value: 'high', label: { key: 'filter.priority.high', defaultValue: 'High' } },
],
});
expect(result.options![0].label).toEqual({ key: 'filter.priority.high', defaultValue: 'High' });
})).toThrow();
});

it('should accept filter with optionsFrom (dynamic binding)', () => {
Expand Down Expand Up @@ -1295,12 +1293,11 @@ describe('DashboardHeaderActionSchema', () => {
expect(result.icon).toBe('play');
});

it('should accept i18n label', () => {
const result = DashboardHeaderActionSchema.parse({
it('should reject i18n label', () => {
expect(() => DashboardHeaderActionSchema.parse({
label: { key: 'actions.export', defaultValue: 'Export' },
actionUrl: '/export',
});
expect(result.label).toEqual({ key: 'actions.export', defaultValue: 'Export' });
})).toThrow();
});

it('should reject action without required fields', () => {
Expand Down Expand Up @@ -1429,13 +1426,12 @@ describe('WidgetMeasureSchema', () => {
expect(result.format).toBe('$0,0.00');
});

it('should accept measure with i18n label', () => {
const result = WidgetMeasureSchema.parse({
it('should reject measure with i18n label', () => {
expect(() => WidgetMeasureSchema.parse({
valueField: 'quantity',
aggregate: 'avg',
label: { key: 'measures.avg_qty', defaultValue: 'Average Quantity' },
});
expect(result.label).toEqual({ key: 'measures.avg_qty', defaultValue: 'Average Quantity' });
})).toThrow();
});

it('should accept all aggregate functions', () => {
Expand Down
15 changes: 6 additions & 9 deletions packages/spec/src/ui/dnd.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,11 @@ describe('Type exports', () => {
});

describe('I18n and ARIA integration', () => {
it('should accept I18n label on DropZoneSchema', () => {
const result = DropZoneSchema.parse({
it('should reject I18n label on DropZoneSchema', () => {
expect(() => DropZoneSchema.parse({
accept: ['card'],
label: { key: 'dnd.drop_zone', defaultValue: 'Drop items here' },
});
expect(result.label).toEqual({ key: 'dnd.drop_zone', defaultValue: 'Drop items here' });
})).toThrow();
});

it('should accept ARIA props on DropZoneSchema', () => {
Expand All @@ -216,14 +215,12 @@ describe('I18n and ARIA integration', () => {
expect(result.label).toBe('Drag this card');
});

it('should accept ARIA props on DragItemSchema', () => {
const result = DragItemSchema.parse({
it('should reject ARIA props on DragItemSchema', () => {
expect(() => DragItemSchema.parse({
type: 'row',
ariaLabel: { key: 'dnd.drag_row', defaultValue: 'Draggable row' },
ariaDescribedBy: 'row-desc',
});
expect(result.ariaLabel).toEqual({ key: 'dnd.drag_row', defaultValue: 'Draggable row' });
expect(result.ariaDescribedBy).toBe('row-desc');
})).toThrow();
});

it('should leave I18n/ARIA fields undefined when not provided', () => {
Expand Down
Loading