Skip to content

Commit be0712c

Browse files
os-zhuangclaude
andauthored
feat(access): localize curated capability labels client-side (#2600 B5 follow-up) (#2657)
Follow-up to #2653 (which localized the picker's group headers). The capability chip *labels* (Manage Metadata, Studio Access, …) are served in English by the sys_capability registry. Framework registry localization turned out to require a new record-data i18n pattern (no existing convention), which is disproportionate for this — the curated platform capabilities are a FIXED, known set, so localize just those on the client: - CapabilityMultiSelectField resolves labels for the 7 curated platform caps via `capability.label.<name>` (dots → underscores); package-/admin-authored capabilities keep their registry-served label. - Added the keys to useFieldTranslation defaults + en/zh bundles (others fall back to en; nested under the existing top-level `capability` key so locale parity is unaffected). Also made the toggle-off test re-query the chip at click time: the label now resolves through i18n, so an async translation-settle can re-render the chips between findBy and click — asserting on a re-queried node avoids a detached-node race. Adds a curated-vs-registry label test. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e4e8d3f commit be0712c

5 files changed

Lines changed: 81 additions & 3 deletions

File tree

packages/fields/src/widgets/CapabilityMultiSelectField.test.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,11 @@ describe('CapabilityMultiSelectField', () => {
106106
dataSource={mockDataSource()}
107107
/>,
108108
);
109-
fireEvent.click(await screen.findByRole('button', { name: 'Studio Access' }));
109+
// Chip labels now resolve through the i18n bundle (objectui#2600 B5), so an
110+
// async translation-settle can re-render the chips between findBy and the
111+
// click — re-query at click time so we act on the live node, not a detached one.
112+
await screen.findByRole('button', { name: 'Studio Access' });
113+
fireEvent.click(screen.getByRole('button', { name: 'Studio Access' }));
110114
const emitted = onChange.mock.calls[0][0];
111115
expect(typeof emitted).toBe('string');
112116
expect(JSON.parse(emitted)).toEqual(['manage_users']);
@@ -140,4 +144,25 @@ describe('CapabilityMultiSelectField', () => {
140144
expect(await screen.findByText('Studio Access')).toBeInTheDocument();
141145
expect(screen.queryByRole('button', { name: 'Manage Users' })).not.toBeInTheDocument();
142146
});
147+
148+
// objectui#2600 B5 — curated platform caps get a localized label; package/
149+
// admin-authored caps keep whatever label the registry served.
150+
it('localizes curated capability labels but preserves registry labels for others', async () => {
151+
render(
152+
<CapabilityMultiSelectField
153+
value={'[]'}
154+
onChange={vi.fn()}
155+
field={{ name: 'system_permissions' } as any}
156+
dataSource={mockDataSource([
157+
// Registry sends a shorter label; the curated client map wins.
158+
{ name: 'manage_org_users', label: 'Manage Org Users', description: 'Org members', scope: 'org', active: true },
159+
// Not a curated platform capability — its registry label is kept.
160+
{ name: 'export_data', label: 'Export Data', description: 'Export', scope: 'org', active: true },
161+
])}
162+
/>,
163+
);
164+
expect(await screen.findByRole('button', { name: 'Manage Organization Users' })).toBeInTheDocument();
165+
expect(screen.queryByRole('button', { name: 'Manage Org Users' })).not.toBeInTheDocument();
166+
expect(screen.getByRole('button', { name: 'Export Data' })).toBeInTheDocument();
167+
});
143168
});

packages/fields/src/widgets/CapabilityMultiSelectField.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,23 @@ export function parseCapabilityNames(value: unknown): string[] {
6363
*/
6464
const SCOPE_ORDER = ['platform', 'org', 'other'] as const;
6565

66+
/**
67+
* objectui#2600 B5 — the curated platform capabilities are a FIXED, known set
68+
* whose labels the sys_capability registry serves in English. Localize just
69+
* these client-side via `capability.label.<name>` (dots → underscores);
70+
* package- and admin-authored capabilities keep their authored registry label.
71+
* (Mirrors @objectstack/spec/security `PLATFORM_CAPABILITIES`.)
72+
*/
73+
const CURATED_CAPABILITY_LABELS = new Set([
74+
'manage_users',
75+
'manage_org_users',
76+
'manage_metadata',
77+
'manage_platform_settings',
78+
'setup_access',
79+
'setup_write',
80+
'studio_access',
81+
]);
82+
6683
export function CapabilityMultiSelectField({
6784
value,
6885
onChange,
@@ -121,7 +138,13 @@ export function CapabilityMultiSelectField({
121138
return map;
122139
}, [caps, selected]);
123140

124-
const labelFor = (name: string) => byName.get(name)?.label || name;
141+
// Curated platform caps get a localized label (objectui#2600 B5); everything
142+
// else keeps the registry-served label.
143+
const labelFor = (name: string) => {
144+
const norm = name.replace(/\./g, '_');
145+
if (CURATED_CAPABILITY_LABELS.has(norm)) return t(`capability.label.${norm}`);
146+
return byName.get(name)?.label || name;
147+
};
125148

126149
// Group options by scope for the editable grid. Computed BEFORE the readonly
127150
// early-return so the hook order stays stable regardless of `readonly`.
@@ -192,7 +215,7 @@ export function CapabilityMultiSelectField({
192215
: 'border-input bg-background text-foreground hover:bg-accent',
193216
)}
194217
>
195-
{cap.label || cap.name}
218+
{labelFor(cap.name)}
196219
</button>
197220
);
198221
})}

packages/fields/src/widgets/useFieldTranslation.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ const FIELD_DEFAULTS: Record<string, string> = {
4343
'capability.group.platform': 'Platform',
4444
'capability.group.org': 'Organization',
4545
'capability.group.other': 'Other',
46+
// objectui#2600 B5 — curated platform capability labels (registry serves
47+
// English; dots in the api-name become underscores in the key).
48+
'capability.label.manage_users': 'Manage Users',
49+
'capability.label.manage_org_users': 'Manage Organization Users',
50+
'capability.label.manage_metadata': 'Manage Metadata',
51+
'capability.label.manage_platform_settings': 'Manage Platform Settings',
52+
'capability.label.setup_access': 'Setup Access',
53+
'capability.label.setup_write': 'Write Settings',
54+
'capability.label.studio_access': 'Studio Access',
4655
};
4756

4857
export const useFieldTranslation = createSafeTranslation(

packages/i18n/src/locales/en.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ const en = {
1010
org: 'Organization',
1111
other: 'Other',
1212
},
13+
// Curated platform capability labels (objectui#2600 B5). Registry-served
14+
// labels are English; these mirror them and give non-en locales a fallback.
15+
label: {
16+
manage_users: 'Manage Users',
17+
manage_org_users: 'Manage Organization Users',
18+
manage_metadata: 'Manage Metadata',
19+
manage_platform_settings: 'Manage Platform Settings',
20+
setup_access: 'Setup Access',
21+
setup_write: 'Write Settings',
22+
studio_access: 'Studio Access',
23+
},
1324
},
1425
lookup: {
1526
recentlyUsed: 'Recently used',

packages/i18n/src/locales/zh.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ const zh = {
1010
org: '组织',
1111
other: '其他',
1212
},
13+
// 精选平台能力标签(objectui#2600 B5)。注册表 label 为英文,这里本地化。
14+
label: {
15+
manage_users: '管理用户',
16+
manage_org_users: '管理组织用户',
17+
manage_metadata: '管理元数据',
18+
manage_platform_settings: '管理平台设置',
19+
setup_access: '访问 Setup',
20+
setup_write: '保存设置',
21+
studio_access: '访问 Studio',
22+
},
1323
},
1424
lookup: {
1525
recentlyUsed: '最近使用',

0 commit comments

Comments
 (0)