Skip to content

Commit 5188404

Browse files
feat(i18n): simplify I18nLabelSchema to string-only, migrate plugins and update tests
BREAKING CHANGE: I18nLabelSchema no longer accepts i18n objects ({ key, defaultValue, params }). All label/description/title fields in UI schemas now accept only plain strings. i18n translation keys will be auto-generated by the framework at registration time. Closes #1054 Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/e1cf9252-11e8-44d6-a2d2-1f4d43c59d12 Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
1 parent 6bd459f commit 5188404

8 files changed

Lines changed: 72 additions & 94 deletions

File tree

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/i18n.test.ts

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ describe('I18nObjectSchema', () => {
4747
});
4848

4949
describe('I18nLabelSchema', () => {
50-
it('should accept plain string (backward compatible)', () => {
50+
it('should accept plain string', () => {
5151
const result = I18nLabelSchema.parse('All Active');
5252
expect(result).toBe('All Active');
5353
});
@@ -57,29 +57,22 @@ describe('I18nLabelSchema', () => {
5757
expect(result).toBe('');
5858
});
5959

60-
it('should accept i18n object', () => {
61-
const label: I18nLabel = {
60+
it('should reject i18n object (no longer accepted)', () => {
61+
expect(() => I18nLabelSchema.parse({
6262
key: 'views.task_list.label',
6363
defaultValue: 'Task List',
64-
};
65-
66-
const result = I18nLabelSchema.parse(label);
67-
expect(typeof result).toBe('object');
68-
expect((result as I18nObject).key).toBe('views.task_list.label');
64+
})).toThrow();
6965
});
7066

71-
it('should accept i18n object with params', () => {
72-
const label = {
67+
it('should reject i18n object with params', () => {
68+
expect(() => I18nLabelSchema.parse({
7369
key: 'common.item_count',
7470
defaultValue: '{count} items',
7571
params: { count: 42 },
76-
};
77-
78-
const result = I18nLabelSchema.parse(label);
79-
expect((result as I18nObject).params).toEqual({ count: 42 });
72+
})).toThrow();
8073
});
8174

82-
it('should reject non-string, non-object values', () => {
75+
it('should reject non-string values', () => {
8376
expect(() => I18nLabelSchema.parse(123)).toThrow();
8477
expect(() => I18nLabelSchema.parse(true)).toThrow();
8578
expect(() => I18nLabelSchema.parse(null)).toThrow();
@@ -104,17 +97,22 @@ describe('AriaPropsSchema', () => {
10497
expect(result.ariaLabel).toBe('Close dialog');
10598
});
10699

107-
it('should accept ariaLabel as i18n object', () => {
100+
it('should accept ariaLabel as string only', () => {
108101
const props = {
102+
ariaLabel: 'Close dialog',
103+
};
104+
105+
const result = AriaPropsSchema.parse(props);
106+
expect(result.ariaLabel).toBe('Close dialog');
107+
});
108+
109+
it('should reject ariaLabel as i18n object (no longer accepted)', () => {
110+
expect(() => AriaPropsSchema.parse({
109111
ariaLabel: {
110112
key: 'common.close_dialog',
111113
defaultValue: 'Close dialog',
112114
},
113-
};
114-
115-
const result = AriaPropsSchema.parse(props);
116-
expect(typeof result.ariaLabel).toBe('object');
117-
expect((result.ariaLabel as I18nObject).key).toBe('common.close_dialog');
115+
})).toThrow();
118116
});
119117

120118
it('should accept all ARIA properties', () => {
@@ -139,20 +137,23 @@ describe('AriaPropsSchema', () => {
139137
});
140138
});
141139

142-
describe('I18n Integration (backward compatibility)', () => {
143-
it('should seamlessly support both string and object in same context', () => {
144-
// Simulates a record with mixed label types (migration scenario)
140+
describe('I18n Integration', () => {
141+
it('should only accept string labels', () => {
145142
const labels: I18nLabel[] = [
146143
'Plain String Label',
147-
{ key: 'labels.translated', defaultValue: 'Translated Label' },
148144
'Another Plain String',
149-
{ key: 'labels.with_params', params: { count: 10 } },
145+
'Setup',
150146
];
151147

152148
labels.forEach(label => {
153149
expect(() => I18nLabelSchema.parse(label)).not.toThrow();
154150
});
155151
});
152+
153+
it('should reject i18n objects in label context', () => {
154+
expect(() => I18nLabelSchema.parse({ key: 'labels.translated', defaultValue: 'Translated Label' })).toThrow();
155+
expect(() => I18nLabelSchema.parse({ key: 'labels.with_params', params: { count: 10 } })).toThrow();
156+
});
156157
});
157158

158159
describe('PluralRuleSchema', () => {

packages/spec/src/ui/i18n.zod.ts

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,32 +29,20 @@ export const I18nObjectSchema = z.object({
2929
export type I18nObject = z.infer<typeof I18nObjectSchema>;
3030

3131
/**
32-
* I18n Label Schema (Union)
32+
* I18n Label Schema
3333
*
34-
* Supports two modes for backward compatibility:
35-
* 1. **Plain string** — Direct label text (legacy/simple usage)
36-
* 2. **I18n object** — Structured translation key with parameters
34+
* A plain string label for display purposes.
35+
* i18n translation keys are auto-generated by the framework at registration time
36+
* based on a standardized naming convention (e.g., `apps.<packageId>.<name>.label`).
37+
* Developers only need to provide the default-language string; translations are
38+
* managed through translation files, not inline i18n objects.
3739
*
38-
* This union type allows gradual migration from hardcoded strings
39-
* to fully internationalized labels without breaking existing configurations.
40-
*
41-
* @example Plain string (backward compatible)
40+
* @example
4241
* ```typescript
4342
* const label: I18nLabel = "All Active";
4443
* ```
45-
*
46-
* @example I18n object
47-
* ```typescript
48-
* const label: I18nLabel = {
49-
* key: "views.task_list.label",
50-
* defaultValue: "Task List",
51-
* };
52-
* ```
5344
*/
54-
export const I18nLabelSchema = z.union([
55-
z.string(),
56-
I18nObjectSchema,
57-
]).describe('Display label: plain string or i18n translation object');
45+
export const I18nLabelSchema = z.string().describe('Display label (plain string; i18n keys are auto-generated by the framework)');
5846

5947
export type I18nLabel = z.infer<typeof I18nLabelSchema>;
6048

0 commit comments

Comments
 (0)