Skip to content

Commit dc26ed8

Browse files
authored
Merge pull request #1055 from objectstack-ai/copilot/fix-i18n-label-string
2 parents 8a2e725 + 5188404 commit dc26ed8

20 files changed

Lines changed: 147 additions & 185 deletions

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
- **i18n: `I18nLabelSchema` now accepts `string` only**`label`, `description`, `title`,
12+
and other display-text fields across all UI schemas (`AppSchema`, `NavigationArea`,
13+
`PageSchema`, `DashboardWidgetSchema`, `ReportSchema`, `ChartSchema`, `NotificationSchema`,
14+
`AriaPropsSchema`, etc.) now accept only plain strings. The previous `string | I18nObject`
15+
union type has been replaced with `z.string()`. i18n translation keys will be auto-generated
16+
by the framework at registration time; developers only need to provide the default-language
17+
string value. Translations are managed through translation files, not inline i18n objects.
18+
([#1054](https://github.com/objectstack-ai/framework/issues/1054))
19+
20+
**Migration:** Replace any `label: { key: '...', defaultValue: 'X' }` with `label: 'X'`.
21+
Existing plain-string labels require no changes.
22+
23+
**Affected plugins updated:**
24+
- `@objectstack/plugin-setup``setup-app.ts`, `setup-areas.ts`
25+
- `@objectstack/plugin-auth` — navigation item labels
26+
- `@objectstack/plugin-security` — navigation item labels
27+
- `@objectstack/plugin-audit` — navigation item labels
28+
1029
### Documentation
1130
- **README rewrite** — Rewrote `README.md` to accurately reflect the `objectstack-ai/framework`
1231
repository. Updates include: corrected title ("ObjectStack Framework"), updated badges

packages/plugins/plugin-audit/src/audit-plugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export class AuditPlugin implements Plugin {
3333
setupNav.contribute({
3434
areaId: 'area_system',
3535
items: [
36-
{ id: 'nav_audit_logs', type: 'object', label: { key: 'setup.nav.audit_logs', defaultValue: 'Audit Logs' }, objectName: 'audit_log', icon: 'scroll-text', order: 10 },
36+
{ id: 'nav_audit_logs', type: 'object', label: 'Audit Logs', objectName: 'audit_log', icon: 'scroll-text', order: 10 },
3737
],
3838
});
3939
ctx.logger.info('Audit navigation items contributed to Setup App');

packages/plugins/plugin-auth/src/auth-plugin.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,11 @@ export class AuthPlugin implements Plugin {
119119
setupNav.contribute({
120120
areaId: 'area_administration',
121121
items: [
122-
{ id: 'nav_users', type: 'object', label: { key: 'setup.nav.users', defaultValue: 'Users' }, objectName: 'user', icon: 'users', order: 10 },
123-
{ id: 'nav_organizations', type: 'object', label: { key: 'setup.nav.organizations', defaultValue: 'Organizations' }, objectName: 'organization', icon: 'building-2', order: 20 },
124-
{ id: 'nav_teams', type: 'object', label: { key: 'setup.nav.teams', defaultValue: 'Teams' }, objectName: 'team', icon: 'users-round', order: 30 },
125-
{ id: 'nav_api_keys', type: 'object', label: { key: 'setup.nav.api_keys', defaultValue: 'API Keys' }, objectName: 'api_key', icon: 'key', order: 40 },
126-
{ id: 'nav_sessions', type: 'object', label: { key: 'setup.nav.sessions', defaultValue: 'Sessions' }, objectName: 'session', icon: 'monitor', order: 50 },
122+
{ id: 'nav_users', type: 'object', label: 'Users', objectName: 'user', icon: 'users', order: 10 },
123+
{ id: 'nav_organizations', type: 'object', label: 'Organizations', objectName: 'organization', icon: 'building-2', order: 20 },
124+
{ id: 'nav_teams', type: 'object', label: 'Teams', objectName: 'team', icon: 'users-round', order: 30 },
125+
{ id: 'nav_api_keys', type: 'object', label: 'API Keys', objectName: 'api_key', icon: 'key', order: 40 },
126+
{ id: 'nav_sessions', type: 'object', label: 'Sessions', objectName: 'session', icon: 'monitor', order: 50 },
127127
],
128128
});
129129
ctx.logger.info('Auth navigation items contributed to Setup App');

packages/plugins/plugin-security/src/security-plugin.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ export class SecurityPlugin implements Plugin {
5555
setupNav.contribute({
5656
areaId: 'area_administration',
5757
items: [
58-
{ id: 'nav_roles', type: 'object', label: { key: 'setup.nav.roles', defaultValue: 'Roles' }, objectName: 'role', icon: 'shield-check', order: 60 },
59-
{ id: 'nav_permission_sets', type: 'object', label: { key: 'setup.nav.permission_sets', defaultValue: 'Permission Sets' }, objectName: 'permission_set', icon: 'lock', order: 70 },
58+
{ id: 'nav_roles', type: 'object', label: 'Roles', objectName: 'role', icon: 'shield-check', order: 60 },
59+
{ id: 'nav_permission_sets', type: 'object', label: 'Permission Sets', objectName: 'permission_set', icon: 'lock', order: 70 },
6060
],
6161
});
6262
ctx.logger.info('Security navigation items contributed to Setup App');

packages/plugins/plugin-setup/src/setup-app.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,8 @@ import type { App, NavigationArea, NavigationItem } from '@objectstack/spec/ui';
1212
*/
1313
export const SETUP_APP_DEFAULTS: Omit<App, 'areas'> & { areas: NavigationArea[] } = {
1414
name: 'setup',
15-
label: {
16-
key: 'setup.app.label',
17-
defaultValue: 'Setup',
18-
},
19-
description: {
20-
key: 'setup.app.description',
21-
defaultValue: 'Platform settings and administration',
22-
},
15+
label: 'Setup',
16+
description: 'Platform settings and administration',
2317
icon: 'settings',
2418
active: true,
2519
isDefault: false,

packages/plugins/plugin-setup/src/setup-areas.ts

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -31,58 +31,34 @@ export type SetupAreaId = (typeof SETUP_AREA_IDS)[keyof typeof SETUP_AREA_IDS];
3131
export const SETUP_AREAS: readonly NavigationArea[] = [
3232
{
3333
id: SETUP_AREA_IDS.administration,
34-
label: {
35-
key: 'setup.areas.administration',
36-
defaultValue: 'Administration',
37-
},
34+
label: 'Administration',
3835
icon: 'shield',
3936
order: 10,
40-
description: {
41-
key: 'setup.areas.administration.description',
42-
defaultValue: 'User management, roles, permissions, and security settings',
43-
},
37+
description: 'User management, roles, permissions, and security settings',
4438
navigation: [],
4539
},
4640
{
4741
id: SETUP_AREA_IDS.platform,
48-
label: {
49-
key: 'setup.areas.platform',
50-
defaultValue: 'Platform',
51-
},
42+
label: 'Platform',
5243
icon: 'layers',
5344
order: 20,
54-
description: {
55-
key: 'setup.areas.platform.description',
56-
defaultValue: 'Objects, fields, layouts, automation, and extensibility settings',
57-
},
45+
description: 'Objects, fields, layouts, automation, and extensibility settings',
5846
navigation: [],
5947
},
6048
{
6149
id: SETUP_AREA_IDS.system,
62-
label: {
63-
key: 'setup.areas.system',
64-
defaultValue: 'System',
65-
},
50+
label: 'System',
6651
icon: 'settings',
6752
order: 30,
68-
description: {
69-
key: 'setup.areas.system.description',
70-
defaultValue: 'Datasources, integrations, jobs, logs, and environment configuration',
71-
},
53+
description: 'Datasources, integrations, jobs, logs, and environment configuration',
7254
navigation: [],
7355
},
7456
{
7557
id: SETUP_AREA_IDS.ai,
76-
label: {
77-
key: 'setup.areas.ai',
78-
defaultValue: 'AI',
79-
},
58+
label: 'AI',
8059
icon: 'brain',
8160
order: 40,
82-
description: {
83-
key: 'setup.areas.ai.description',
84-
defaultValue: 'AI agents, model registry, RAG pipelines, and intelligence settings',
85-
},
61+
description: 'AI agents, model registry, RAG pipelines, and intelligence settings',
8662
navigation: [],
8763
},
8864
] as const;

packages/spec/src/ui/action.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -530,28 +530,28 @@ describe('Action Factory', () => {
530530
});
531531

532532
describe('Action I18n Integration', () => {
533-
it('should accept i18n object as action label', () => {
533+
it('should reject i18n object as action label', () => {
534534
expect(() => ActionSchema.parse({
535535
name: 'i18n_action',
536536
label: { key: 'actions.approve', defaultValue: 'Approve' },
537-
})).not.toThrow();
537+
})).toThrow();
538538
});
539-
it('should accept i18n as confirmText and successMessage', () => {
539+
it('should reject i18n as confirmText and successMessage', () => {
540540
expect(() => ActionSchema.parse({
541541
name: 'i18n_confirm',
542542
label: 'Delete',
543543
confirmText: { key: 'actions.confirm_delete', defaultValue: 'Are you sure?' },
544544
successMessage: { key: 'actions.delete_success', defaultValue: 'Deleted!' },
545-
})).not.toThrow();
545+
})).toThrow();
546546
});
547-
it('should accept i18n in param labels', () => {
547+
it('should reject i18n in param labels', () => {
548548
expect(() => ActionParamSchema.parse({
549549
name: 'reason',
550550
label: { key: 'params.reason', defaultValue: 'Reason' },
551551
type: 'textarea',
552-
})).not.toThrow();
552+
})).toThrow();
553553
});
554-
it('should accept i18n in param option labels', () => {
554+
it('should reject i18n in param option labels', () => {
555555
expect(() => ActionParamSchema.parse({
556556
name: 'priority',
557557
label: 'Priority',
@@ -560,7 +560,7 @@ describe('Action I18n Integration', () => {
560560
{ label: { key: 'options.high', defaultValue: 'High' }, value: 'high' },
561561
{ label: { key: 'options.low', defaultValue: 'Low' }, value: 'low' },
562562
],
563-
})).not.toThrow();
563+
})).toThrow();
564564
});
565565
});
566566

packages/spec/src/ui/animation.test.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -197,11 +197,10 @@ describe('Type exports', () => {
197197
});
198198

199199
describe('I18n and ARIA integration', () => {
200-
it('should accept I18n label on ComponentAnimationSchema', () => {
201-
const result = ComponentAnimationSchema.parse({
200+
it('should reject I18n label on ComponentAnimationSchema', () => {
201+
expect(() => ComponentAnimationSchema.parse({
202202
label: { key: 'animations.card_enter', defaultValue: 'Card Enter' },
203-
});
204-
expect(result.label).toEqual({ key: 'animations.card_enter', defaultValue: 'Card Enter' });
203+
})).toThrow();
205204
});
206205

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

223-
it('should accept I18n label on MotionConfigSchema', () => {
224-
const result = MotionConfigSchema.parse({
222+
it('should reject I18n label on MotionConfigSchema', () => {
223+
expect(() => MotionConfigSchema.parse({
225224
label: { key: 'motion.global', defaultValue: 'Global Motion Config' },
226-
});
227-
expect(result.label).toEqual({ key: 'motion.global', defaultValue: 'Global Motion Config' });
225+
})).toThrow();
228226
});
229227

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

packages/spec/src/ui/chart.test.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,20 +210,19 @@ describe('Real-World Chart Configuration Examples', () => {
210210
});
211211

212212
describe('Chart I18n Integration', () => {
213-
it('should accept i18n object as chart title', () => {
214-
const result = ChartConfigSchema.parse({
213+
it('should reject i18n object as chart title', () => {
214+
expect(() => ChartConfigSchema.parse({
215215
type: 'bar',
216216
title: { key: 'charts.sales', defaultValue: 'Sales Chart' },
217-
});
218-
expect(typeof result.title).toBe('object');
217+
})).toThrow();
219218
});
220-
it('should accept i18n as chart subtitle and description', () => {
219+
it('should reject i18n as chart subtitle and description', () => {
221220
expect(() => ChartConfigSchema.parse({
222221
type: 'line',
223222
title: 'Revenue',
224223
subtitle: { key: 'charts.subtitle', defaultValue: 'Monthly breakdown' },
225224
description: { key: 'charts.desc', defaultValue: 'Revenue over time' },
226-
})).not.toThrow();
225+
})).toThrow();
227226
});
228227
});
229228

packages/spec/src/ui/dashboard.test.ts

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -576,30 +576,30 @@ describe('Dashboard Factory', () => {
576576
});
577577

578578
describe('Dashboard I18n Integration', () => {
579-
it('should accept i18n object as dashboard label', () => {
579+
it('should reject i18n object as dashboard label', () => {
580580
expect(() => DashboardSchema.parse({
581581
name: 'i18n_dashboard',
582582
label: { key: 'dashboards.sales', defaultValue: 'Sales Dashboard' },
583583
widgets: [],
584-
})).not.toThrow();
584+
})).toThrow();
585585
});
586-
it('should accept i18n object as dashboard description', () => {
586+
it('should reject i18n object as dashboard description', () => {
587587
expect(() => DashboardSchema.parse({
588588
name: 'test_dashboard',
589589
label: 'Test',
590590
description: { key: 'dashboards.test.desc', defaultValue: 'Test dashboard' },
591591
widgets: [],
592-
})).not.toThrow();
592+
})).toThrow();
593593
});
594-
it('should accept i18n object as widget title', () => {
594+
it('should reject i18n object as widget title', () => {
595595
expect(() => DashboardWidgetSchema.parse({
596596
id: 'total_revenue',
597597
title: { key: 'widgets.revenue', defaultValue: 'Total Revenue' },
598598
type: 'metric',
599599
layout: { x: 0, y: 0, w: 3, h: 2 },
600-
})).not.toThrow();
600+
})).toThrow();
601601
});
602-
it('should accept i18n object in global filter label', () => {
602+
it('should reject i18n object in global filter label', () => {
603603
expect(() => DashboardSchema.parse({
604604
name: 'filter_dash',
605605
label: 'Filtered',
@@ -609,7 +609,7 @@ describe('Dashboard I18n Integration', () => {
609609
label: { key: 'filters.status', defaultValue: 'Status' },
610610
type: 'select',
611611
}],
612-
})).not.toThrow();
612+
})).toThrow();
613613
});
614614
});
615615

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

786-
it('should accept widget with i18n description', () => {
787-
const result = DashboardWidgetSchema.parse({
786+
it('should reject widget with i18n description', () => {
787+
expect(() => DashboardWidgetSchema.parse({
788788
id: 'revenue_i18n',
789789
title: 'Revenue',
790790
description: { key: 'widgets.revenue.desc', defaultValue: 'Total revenue' },
791791
type: 'metric',
792792
layout: { x: 0, y: 0, w: 3, h: 2 },
793-
});
794-
expect(result.description).toEqual({ key: 'widgets.revenue.desc', defaultValue: 'Total revenue' });
793+
})).toThrow();
795794
});
796795

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

1046-
it('should accept filter with i18n option labels', () => {
1047-
const result = GlobalFilterSchema.parse({
1045+
it('should reject filter with i18n option labels', () => {
1046+
expect(() => GlobalFilterSchema.parse({
10481047
field: 'priority',
10491048
type: 'select',
10501049
options: [
10511050
{ value: 'high', label: { key: 'filter.priority.high', defaultValue: 'High' } },
10521051
],
1053-
});
1054-
expect(result.options![0].label).toEqual({ key: 'filter.priority.high', defaultValue: 'High' });
1052+
})).toThrow();
10551053
});
10561054

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

1298-
it('should accept i18n label', () => {
1299-
const result = DashboardHeaderActionSchema.parse({
1296+
it('should reject i18n label', () => {
1297+
expect(() => DashboardHeaderActionSchema.parse({
13001298
label: { key: 'actions.export', defaultValue: 'Export' },
13011299
actionUrl: '/export',
1302-
});
1303-
expect(result.label).toEqual({ key: 'actions.export', defaultValue: 'Export' });
1300+
})).toThrow();
13041301
});
13051302

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

1432-
it('should accept measure with i18n label', () => {
1433-
const result = WidgetMeasureSchema.parse({
1429+
it('should reject measure with i18n label', () => {
1430+
expect(() => WidgetMeasureSchema.parse({
14341431
valueField: 'quantity',
14351432
aggregate: 'avg',
14361433
label: { key: 'measures.avg_qty', defaultValue: 'Average Quantity' },
1437-
});
1438-
expect(result.label).toEqual({ key: 'measures.avg_qty', defaultValue: 'Average Quantity' });
1434+
})).toThrow();
14391435
});
14401436

14411437
it('should accept all aggregate functions', () => {

0 commit comments

Comments
 (0)