diff --git a/CHANGELOG.md b/CHANGELOG.md index e76955950..c41339fad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/packages/plugins/plugin-audit/src/audit-plugin.ts b/packages/plugins/plugin-audit/src/audit-plugin.ts index 9ba80eac6..728a1232a 100644 --- a/packages/plugins/plugin-audit/src/audit-plugin.ts +++ b/packages/plugins/plugin-audit/src/audit-plugin.ts @@ -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'); diff --git a/packages/plugins/plugin-auth/src/auth-plugin.ts b/packages/plugins/plugin-auth/src/auth-plugin.ts index 2354f082e..17e8da5b6 100644 --- a/packages/plugins/plugin-auth/src/auth-plugin.ts +++ b/packages/plugins/plugin-auth/src/auth-plugin.ts @@ -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'); diff --git a/packages/plugins/plugin-security/src/security-plugin.ts b/packages/plugins/plugin-security/src/security-plugin.ts index 2e66bcd10..45a037947 100644 --- a/packages/plugins/plugin-security/src/security-plugin.ts +++ b/packages/plugins/plugin-security/src/security-plugin.ts @@ -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'); diff --git a/packages/plugins/plugin-setup/src/setup-app.ts b/packages/plugins/plugin-setup/src/setup-app.ts index e401b8420..169c845d3 100644 --- a/packages/plugins/plugin-setup/src/setup-app.ts +++ b/packages/plugins/plugin-setup/src/setup-app.ts @@ -12,14 +12,8 @@ import type { App, NavigationArea, NavigationItem } from '@objectstack/spec/ui'; */ export const SETUP_APP_DEFAULTS: Omit & { 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, diff --git a/packages/plugins/plugin-setup/src/setup-areas.ts b/packages/plugins/plugin-setup/src/setup-areas.ts index ac78fd89a..5aa832822 100644 --- a/packages/plugins/plugin-setup/src/setup-areas.ts +++ b/packages/plugins/plugin-setup/src/setup-areas.ts @@ -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; diff --git a/packages/spec/src/ui/action.test.ts b/packages/spec/src/ui/action.test.ts index 87e227008..a724b3655 100644 --- a/packages/spec/src/ui/action.test.ts +++ b/packages/spec/src/ui/action.test.ts @@ -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', @@ -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(); }); }); diff --git a/packages/spec/src/ui/animation.test.ts b/packages/spec/src/ui/animation.test.ts index c08ef46fb..2ebf42db2 100644 --- a/packages/spec/src/ui/animation.test.ts +++ b/packages/spec/src/ui/animation.test.ts @@ -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', () => { @@ -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', () => { diff --git a/packages/spec/src/ui/chart.test.ts b/packages/spec/src/ui/chart.test.ts index 24ece3fd9..878746616 100644 --- a/packages/spec/src/ui/chart.test.ts +++ b/packages/spec/src/ui/chart.test.ts @@ -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(); }); }); diff --git a/packages/spec/src/ui/dashboard.test.ts b/packages/spec/src/ui/dashboard.test.ts index 09d17a086..7efac76be 100644 --- a/packages/spec/src/ui/dashboard.test.ts +++ b/packages/spec/src/ui/dashboard.test.ts @@ -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', @@ -609,7 +609,7 @@ describe('Dashboard I18n Integration', () => { label: { key: 'filters.status', defaultValue: 'Status' }, type: 'select', }], - })).not.toThrow(); + })).toThrow(); }); }); @@ -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)', () => { @@ -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)', () => { @@ -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', () => { @@ -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', () => { diff --git a/packages/spec/src/ui/dnd.test.ts b/packages/spec/src/ui/dnd.test.ts index 85c1c9476..db3b96fa9 100644 --- a/packages/spec/src/ui/dnd.test.ts +++ b/packages/spec/src/ui/dnd.test.ts @@ -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', () => { @@ -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', () => { diff --git a/packages/spec/src/ui/i18n.test.ts b/packages/spec/src/ui/i18n.test.ts index deb15ad02..f40d76eab 100644 --- a/packages/spec/src/ui/i18n.test.ts +++ b/packages/spec/src/ui/i18n.test.ts @@ -47,7 +47,7 @@ describe('I18nObjectSchema', () => { }); describe('I18nLabelSchema', () => { - it('should accept plain string (backward compatible)', () => { + it('should accept plain string', () => { const result = I18nLabelSchema.parse('All Active'); expect(result).toBe('All Active'); }); @@ -57,29 +57,22 @@ describe('I18nLabelSchema', () => { expect(result).toBe(''); }); - it('should accept i18n object', () => { - const label: I18nLabel = { + it('should reject i18n object (no longer accepted)', () => { + expect(() => I18nLabelSchema.parse({ key: 'views.task_list.label', defaultValue: 'Task List', - }; - - const result = I18nLabelSchema.parse(label); - expect(typeof result).toBe('object'); - expect((result as I18nObject).key).toBe('views.task_list.label'); + })).toThrow(); }); - it('should accept i18n object with params', () => { - const label = { + it('should reject i18n object with params', () => { + expect(() => I18nLabelSchema.parse({ key: 'common.item_count', defaultValue: '{count} items', params: { count: 42 }, - }; - - const result = I18nLabelSchema.parse(label); - expect((result as I18nObject).params).toEqual({ count: 42 }); + })).toThrow(); }); - it('should reject non-string, non-object values', () => { + it('should reject non-string values', () => { expect(() => I18nLabelSchema.parse(123)).toThrow(); expect(() => I18nLabelSchema.parse(true)).toThrow(); expect(() => I18nLabelSchema.parse(null)).toThrow(); @@ -104,17 +97,22 @@ describe('AriaPropsSchema', () => { expect(result.ariaLabel).toBe('Close dialog'); }); - it('should accept ariaLabel as i18n object', () => { + it('should accept ariaLabel as string only', () => { const props = { + ariaLabel: 'Close dialog', + }; + + const result = AriaPropsSchema.parse(props); + expect(result.ariaLabel).toBe('Close dialog'); + }); + + it('should reject ariaLabel as i18n object (no longer accepted)', () => { + expect(() => AriaPropsSchema.parse({ ariaLabel: { key: 'common.close_dialog', defaultValue: 'Close dialog', }, - }; - - const result = AriaPropsSchema.parse(props); - expect(typeof result.ariaLabel).toBe('object'); - expect((result.ariaLabel as I18nObject).key).toBe('common.close_dialog'); + })).toThrow(); }); it('should accept all ARIA properties', () => { @@ -139,20 +137,23 @@ describe('AriaPropsSchema', () => { }); }); -describe('I18n Integration (backward compatibility)', () => { - it('should seamlessly support both string and object in same context', () => { - // Simulates a record with mixed label types (migration scenario) +describe('I18n Integration', () => { + it('should only accept string labels', () => { const labels: I18nLabel[] = [ 'Plain String Label', - { key: 'labels.translated', defaultValue: 'Translated Label' }, 'Another Plain String', - { key: 'labels.with_params', params: { count: 10 } }, + 'Setup', ]; labels.forEach(label => { expect(() => I18nLabelSchema.parse(label)).not.toThrow(); }); }); + + it('should reject i18n objects in label context', () => { + expect(() => I18nLabelSchema.parse({ key: 'labels.translated', defaultValue: 'Translated Label' })).toThrow(); + expect(() => I18nLabelSchema.parse({ key: 'labels.with_params', params: { count: 10 } })).toThrow(); + }); }); describe('PluralRuleSchema', () => { diff --git a/packages/spec/src/ui/i18n.zod.ts b/packages/spec/src/ui/i18n.zod.ts index fade950e1..8e6f40d38 100644 --- a/packages/spec/src/ui/i18n.zod.ts +++ b/packages/spec/src/ui/i18n.zod.ts @@ -29,32 +29,20 @@ export const I18nObjectSchema = z.object({ export type I18nObject = z.infer; /** - * I18n Label Schema (Union) + * I18n Label Schema * - * Supports two modes for backward compatibility: - * 1. **Plain string** — Direct label text (legacy/simple usage) - * 2. **I18n object** — Structured translation key with parameters + * A plain string label for display purposes. + * i18n translation keys are auto-generated by the framework at registration time + * based on a standardized naming convention (e.g., `apps...label`). + * Developers only need to provide the default-language string; translations are + * managed through translation files, not inline i18n objects. * - * This union type allows gradual migration from hardcoded strings - * to fully internationalized labels without breaking existing configurations. - * - * @example Plain string (backward compatible) + * @example * ```typescript * const label: I18nLabel = "All Active"; * ``` - * - * @example I18n object - * ```typescript - * const label: I18nLabel = { - * key: "views.task_list.label", - * defaultValue: "Task List", - * }; - * ``` */ -export const I18nLabelSchema = z.union([ - z.string(), - I18nObjectSchema, -]).describe('Display label: plain string or i18n translation object'); +export const I18nLabelSchema = z.string().describe('Display label (plain string; i18n keys are auto-generated by the framework)'); export type I18nLabel = z.infer; diff --git a/packages/spec/src/ui/keyboard.test.ts b/packages/spec/src/ui/keyboard.test.ts index db5559ac4..67cafd976 100644 --- a/packages/spec/src/ui/keyboard.test.ts +++ b/packages/spec/src/ui/keyboard.test.ts @@ -152,13 +152,12 @@ describe('Type exports', () => { }); describe('I18n and ARIA integration', () => { - it('should accept I18n description on KeyboardShortcutSchema', () => { - const result = KeyboardShortcutSchema.parse({ + it('should reject I18n description on KeyboardShortcutSchema', () => { + expect(() => KeyboardShortcutSchema.parse({ key: 'Ctrl+S', action: 'save', description: { key: 'shortcuts.save', defaultValue: 'Save the current document' }, - }); - expect(result.description).toEqual({ key: 'shortcuts.save', defaultValue: 'Save the current document' }); + })).toThrow(); }); it('should accept plain string description on KeyboardShortcutSchema', () => { diff --git a/packages/spec/src/ui/notification.test.ts b/packages/spec/src/ui/notification.test.ts index 2fd139932..d05894697 100644 --- a/packages/spec/src/ui/notification.test.ts +++ b/packages/spec/src/ui/notification.test.ts @@ -180,12 +180,11 @@ describe('ARIA integration', () => { expect(result.role).toBe('alert'); }); - it('should accept I18n ariaLabel on NotificationSchema', () => { - const result = NotificationSchema.parse({ + it('should reject I18n ariaLabel on NotificationSchema', () => { + expect(() => NotificationSchema.parse({ message: 'Error occurred', ariaLabel: { key: 'notifications.error_alert', defaultValue: 'Error alert' }, - }); - expect(result.ariaLabel).toEqual({ key: 'notifications.error_alert', defaultValue: 'Error alert' }); + })).toThrow(); }); it('should leave ARIA fields undefined when not provided', () => { diff --git a/packages/spec/src/ui/offline.test.ts b/packages/spec/src/ui/offline.test.ts index f9b7575f3..7d6ee8d36 100644 --- a/packages/spec/src/ui/offline.test.ts +++ b/packages/spec/src/ui/offline.test.ts @@ -163,11 +163,10 @@ describe('Type exports', () => { }); describe('I18n integration', () => { - it('should accept I18n offlineMessage on OfflineConfigSchema', () => { - const result = OfflineConfigSchema.parse({ + it('should reject I18n offlineMessage on OfflineConfigSchema', () => { + expect(() => OfflineConfigSchema.parse({ offlineMessage: { key: 'offline.status', defaultValue: 'You are offline' }, - }); - expect(result.offlineMessage).toEqual({ key: 'offline.status', defaultValue: 'You are offline' }); + })).toThrow(); }); it('should accept plain string offlineMessage', () => { diff --git a/packages/spec/src/ui/page.test.ts b/packages/spec/src/ui/page.test.ts index 34517b115..2ce30d188 100644 --- a/packages/spec/src/ui/page.test.ts +++ b/packages/spec/src/ui/page.test.ts @@ -421,27 +421,27 @@ describe('PageSchema', () => { }); describe('Page I18n Integration', () => { - it('should accept i18n object as page label', () => { + it('should reject i18n object as page label', () => { expect(() => PageSchema.parse({ name: 'i18n_page', label: { key: 'pages.dashboard', defaultValue: 'Dashboard' }, regions: [], - })).not.toThrow(); + })).toThrow(); }); - it('should accept i18n as page description', () => { + it('should reject i18n as page description', () => { expect(() => PageSchema.parse({ name: 'desc_page', label: 'Test', description: { key: 'pages.test.desc', defaultValue: 'A test page' }, regions: [], - })).not.toThrow(); + })).toThrow(); }); - it('should accept i18n as component label', () => { + it('should reject i18n as component label', () => { expect(() => PageComponentSchema.parse({ type: 'page:header', label: { key: 'components.header', defaultValue: 'Header' }, properties: {}, - })).not.toThrow(); + })).toThrow(); }); }); @@ -700,12 +700,12 @@ describe('PageSchema with page types', () => { expect(page.icon).toBe('bar-chart'); }); - it('should accept page with i18n label', () => { + it('should reject page with i18n label', () => { expect(() => PageSchema.parse({ name: 'i18n_page', label: { key: 'pages.overview', defaultValue: 'Overview' }, regions: [], - })).not.toThrow(); + })).toThrow(); }); it('should accept page with ARIA attributes', () => { diff --git a/packages/spec/src/ui/report.test.ts b/packages/spec/src/ui/report.test.ts index 7fa2c3ed9..16a3098e9 100644 --- a/packages/spec/src/ui/report.test.ts +++ b/packages/spec/src/ui/report.test.ts @@ -430,20 +430,19 @@ describe('ReportSchema', () => { }); describe('Report I18n Integration', () => { - it('should accept i18n object as report label', () => { + it('should reject i18n object as report label', () => { expect(() => ReportSchema.parse({ name: 'i18n_report', label: { key: 'reports.sales', defaultValue: 'Sales Report' }, objectName: 'opportunity', columns: [{ field: 'name' }], - })).not.toThrow(); + })).toThrow(); }); - it('should accept i18n object as column label', () => { - const result = ReportColumnSchema.parse({ + it('should reject i18n object as column label', () => { + expect(() => ReportColumnSchema.parse({ field: 'amount', label: { key: 'columns.amount', defaultValue: 'Amount' }, - }); - expect(typeof result.label).toBe('object'); + })).toThrow(); }); }); diff --git a/packages/spec/src/ui/touch.test.ts b/packages/spec/src/ui/touch.test.ts index aafb00d0a..cc63c0d18 100644 --- a/packages/spec/src/ui/touch.test.ts +++ b/packages/spec/src/ui/touch.test.ts @@ -167,12 +167,11 @@ describe('Type exports', () => { }); describe('I18n and ARIA integration', () => { - it('should accept I18n label on GestureConfigSchema', () => { - const result = GestureConfigSchema.parse({ + it('should reject I18n label on GestureConfigSchema', () => { + expect(() => GestureConfigSchema.parse({ type: 'swipe', label: { key: 'gestures.swipe_left', defaultValue: 'Swipe to delete' }, - }); - expect(result.label).toEqual({ key: 'gestures.swipe_left', defaultValue: 'Swipe to delete' }); + })).toThrow(); }); it('should accept plain string label on GestureConfigSchema', () => { diff --git a/packages/spec/src/ui/widget.test.ts b/packages/spec/src/ui/widget.test.ts index 0c17cd68b..a49606420 100644 --- a/packages/spec/src/ui/widget.test.ts +++ b/packages/spec/src/ui/widget.test.ts @@ -330,19 +330,18 @@ describe('FieldWidgetPropsSchema', () => { }); describe('Widget I18n Integration', () => { - it('should accept i18n object as widget manifest label', () => { - const manifest: z.input = { + it('should reject i18n object as widget manifest label', () => { + expect(() => WidgetManifestSchema.parse({ name: 'i18n_widget', label: { key: 'widgets.date_picker', defaultValue: 'Date Picker' }, - }; - expect(() => WidgetManifestSchema.parse(manifest)).not.toThrow(); + })).toThrow(); }); - it('should accept i18n as widget description', () => { + it('should reject i18n as widget description', () => { expect(() => WidgetManifestSchema.parse({ name: 'desc_widget', label: 'Test Widget', description: { key: 'widgets.test.desc', defaultValue: 'A test widget' }, - })).not.toThrow(); + })).toThrow(); }); });