Skip to content

Commit 4985dde

Browse files
feat(i18n, app-shell): one word per state in Studio and Setup, and the permission matrix columns in Chinese (#29841)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbMPuCd86KE2QReyiA2Xfs9j)_ Fixes #29729 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:Studio 和 Setup 里同一个状态只用一个词。中文里“打开/关闭”统一为 启用/停用,“定制”这个动作统一为 定制(不再混用 自定义),环境里新建、非软件包带来的权限集标为 新增;权限矩阵的列头在中文下直接写 创建、读取……查看全部、修改全部,不再是 C/R/U/D 字母;“OWD”前缀改为 记录共享模型。英文里流程状态统一为 Switched on / Switched off。 - **为什么改**:同一件事在相邻两个界面叫不同的名字,用户会以为是两件不同的事;中文用户看不懂 C/R/U/D 和 OWD 这些英文缩写。另外修了一个缺陷:中文界面下 Setup 的权限矩阵如果在中文词表加载完成前就渲染,图例和列头提示会一直显示英文。 - **风险与回滚**:几乎只改文案和一处 React memo 的依赖,没有数据、接口或权限行为的变化。回滚就是还原这个 PR。英文有 7 条 Studio 自动化状态文案改了词,已按设计豁免登记在 en 漂移门禁的基线里。 - **先看**:`ui/packages/i18n/src/namespaces/studio/zh.ts` 的改动行,以及 `PermissionMatrixEditor.tsx` 里列头、图例和 OWD 标签的几处改动。 - **两分钟验证**:用中文打开 Setup 的某个权限集和 Studio 的“访问”页,看列头是中文词、没有字母图例、对象行的标签是“记录共享模型:私有(默认)”;打开 Studio 的自动化页,看软件包流程的开关写“本部署中已启用/已停用”。截图见下方“Screenshots”。 - **需要你做的**:确认 新增 / Added 这个权限集徽标用词(理由见下方 Acceptance notes),以及 #29809 合入后是否需要跟进。 ## What changes Strings, one legend gate, one chip and one memo. No data, API, permission or protocol behaviour changes. 1. **Switched on / switched off, one word.** - zh: the packaged-flow ledger family (`engine.studio.ledger.on/off/notRunning/hint/walled/switchedOn/switchedOff`) moves from 开启 / 关闭 to 启用 / 停用. Console `packagedAutomation.off` moves from 已关闭 to 已停用, and `actionsSubtitle` from 关闭后 to 停用后. - en: `engine.studio.ledger.on/off/notRunning` read "Switched on/off for this deployment[, not running here]". `engine.studio.auto.on/off/offTitle/enabled/disabled/enabledToast/disabledToast` read "Switched on" / "Switched off" (they said On / Off and Enabled / Disabled). These are the words #29705 landed for the landing. The verbs (`enableTitle` / `disableTitle`) stay verbs. 2. **The Customize act reads 定制** (zh), in 12 rows: the button, its titles, the walled and no-version lines, the draft footer, the restore confirmations, the landing lines quoting the button, and the two form-editor notes. This matches 已定制 and 可定制. Nine rows where 自定义 means user-defined (a custom value, key, renderer, page or code) keep 自定义. The census in the stateVocabulary test lists both groups. 3. **`perm.badge.custom` reads 新增 / "Added"** (it read 自定义 / "Custom"). See Acceptance notes for why it is not 已定制. 4. **Permission matrix columns.** - zh: the object-action headers spell the words through a new `perm.col.*` family: 创建 读取 编辑 删除 导出 转移 查看全部 修改全部. The two long ones break between 查看/全部 and 修改/全部 (`break-keep`). - en: keeps C R U D E Tr VA MA. - The legend line that spells the letters out is drawn only where the headers are abbreviations (`headersAbbreviate`). So zh no longer repeats the words, and en keeps the legend. 5. **Setup's per-object chip** reads through `perm.owd.chip`: en "OWD {value}", zh 记录共享模型:{value}. Before, it was a hard-coded `OWD ` prefix. 6. **Fixed: lazy-catalogue memo.** `getObjectActions(locale)` was memoised on the locale alone. A zh matrix whose first render ran before the lazily fetched `studio` catalogue was resident kept the en answer, so Setup's legend and every header tooltip read English in zh-CN. The deps now carry `isStudioCatalogueLoaded(locale)`. 7. **`check-i18n-en-drift` accepts `objectstack#N`.** `ISSUE_PATTERN` is now `^(?:objectui|objectstack)#\d+$`, because the ui tree lives in this repo now. The 7 en `auto.*` rows above are designer waivers keyed to `objectstack#29729`, and the test's designer ceiling moves 0 → 7. ## Acceptance notes - **Why the badge says 新增 / Added, not 已定制.** `PermissionMatrixEditor.tsx` L748 picks the badge with `const shippedByPackage = !!packageId || codeIsArtifact;`. The `perm.badge.custom` branch is therefore a permission set that no package ships and that has no code artifact: one this deployment added. Nothing on that branch checks whether a shipped base was changed, so 已定制 (customized) would claim something the editor does not know. 新增 / Added is the word `engine.customizationMark.added` already uses for an item added beside a base, and the census test pins the two to the same word. - **#29809 (open, seat 4)** changes which branch a set added by a `.custom` pair takes. This PR does not touch that choice: only the word on the `custom` branch changes. If #29809 lands first, the rationale above still holds for whatever reaches that branch. - **Not changed here:** - The en Console `packagedAutomation` On / Off: not ruled on this card. - From main, new since the card was written: the zh Console profile app lock (已开启 / 已关闭, a different surface), and `engine.packages.detail.*Disableable` (禁用并重新启用, a new 禁用 / 停用 split). These are left for a follow-up card. ## Screenshots On the merged tree, in `/mnt/project-files/issue-29729/after/` (project files): - `after-environment-{zh,en}-1304.png`: Setup's matrix, zh words with no legend, en letters with the legend, and the chip. - `after-studio-{zh,en}-1304.png`: the Studio Access pillar. - `rail-{en,zh}-1024.png`: the automation rail at the narrowest Studio width. "Switched on" is 69 px in a 215 px legend with no overflow (scrollWidth equals width). The en legend grows from 3 lines to 4 (72 px high). zh is unchanged at 53 px. Before shots and the legend options are in `/mnt/project-files/issue-29729/legend/`. ## Tests - New `PermissionMatrixEditor.columnHeaders-29729.test.tsx`. It does not preload the catalogue and asserts the not-resident precondition first. It checks zh headers, tooltips and no legend in both renderings; an en CONTROL with letters and the legend; and the chip in zh and en. - `DirectoryPage.stateVocabulary-29705.test.tsx`: a second-pass census. - No 开启 / 关闭 in the switched-on/off rows. - No bare On / Off / Enabled / Disabled in the en rows. - The packaged actions in zh, with an en CONTROL. - The 12 Customize rows read 定制 and agree with the button and landing. - A 7-row 自定义 CONTROL. - The badge equals the customization mark's `added` word. - `check-i18n-en-drift.test.ts`: accepts `objectstack#N`. A CONTROL still refuses `objectstack 29729`, `objectstack#`, `cloud#12`, `#29729`, `objectstack#29729x` and `objectui-3650`. The ceiling ratchet moves to 7. - These tests are updated to the new words: the badge, provenanceBadge-7611, ledgerSwitch-28148, FlowStatusDot and customizeFlow-28570. customizeFlow now checks the header by its disable title, because "Switched on" also appears in the Status pill. <details><summary>Verification</summary> - Nine ablations each turned red, with the tree restored after each: - the memo deps; - the Setup legend gate; - zh `perm.col.create` back to C; - the `OWD ` prefix back; - zh ledger.off back to 已关闭; - zh customize.action back to 自定义; - zh badge back to 自定义; - zh `packagedAutomation.off` back to 已关闭; - en `auto.off` back to Off. The gate script is pinned by its CONTROL test, not by ablation. - 184 neighbour test files are green, in three chunks. After the main merge, the 13 touched test files (220 tests) are green. - `dispatch-gates`: all 35 derived gates exit 0. `check-i18n-en-drift` (run by hand, not derived) is green with 7 waived. - Type-check passes for `@object-ui/i18n` and `@object-ui/app-shell`. ESLint: 0 errors. - These gates exit 0 after the merge: sdui-manifest, changeset-no-major, empty-changeset, adr-0087, nul-bytes, type-check-debt. - Deviation: the refusal text and the help-text examples in `check-i18n-en-drift.mjs` now spell `objectstack#1234`, beyond the one-line pattern change. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jR2PhzeUkFuXHiwpdp)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 97f8f6f commit 4985dde

15 files changed

Lines changed: 519 additions & 108 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@object-ui/i18n': patch
3+
'@object-ui/app-shell': patch
4+
---
5+
6+
feat(i18n, app-shell): Studio and Setup give a switched-on, switched-off or customized item one word, and the permission matrix reads its columns in Chinese
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
- Switched on and switched off read 启用 / 停用 in zh-CN wherever Studio and Setup name the state: the packaged-flow ledger switch said 开启 / 关闭, and Setup's packaged actions said 已关闭 in the column and 开启 / 关闭 in the switch-off confirm and its toasts. In en, Studio's flow run status says "Switched on" / "Switched off" (it said On / Off and Enabled / Disabled), the word the landing and the ledger switch use; the ledger switch reads "Switched on/off for this deployment".
12+
- The Customize act reads 定制 in zh-CN (the button, its titles, the restore confirmations, the landing lines quoting the button, the form-editor notes), matching 已定制 and 可定制. 自定义 stays where it means user-defined (a custom value, key, renderer, page or code).
13+
- A permission set no package ships reads 新增 / "Added" (it read 自定义 / "Custom"): it is an item this deployment added, not a customized one.
14+
- The permission matrix's object columns read the action words in zh-CN (创建 … 查看全部, 修改全部) through new `perm.col.*` rows; en keeps C … MA and the legend line that spells them out, which zh no longer needs. Setup's per-object chip reads through `perm.owd.chip` (zh 记录共享模型:私有), not a hard-coded `OWD ` prefix.
15+
- Fixed: in zh-CN, Setup's matrix legend and every column tooltip read English when the matrix rendered before the Chinese Studio strings had loaded. The columns are rebuilt once they land.

‎ui/packages/app-shell/src/views/metadata-admin/DirectoryPage.stateVocabulary-29705.test.tsx‎

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { describe, it, expect, vi, afterEach } from 'vitest';
2828
import { render, screen, cleanup, within } from '@testing-library/react';
2929
import { MemoryRouter } from 'react-router-dom';
3030
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
31+
import { en as CONSOLE_EN, zh as CONSOLE_ZH } from '@object-ui/i18n/locales';
3132
import { MetadataAreaHostContext, type MetadataAreaHost } from './metadataAreaHost';
3233
import type { RichMetadataTypeEntry } from './useMetadata';
3334

@@ -200,8 +201,8 @@ const VOCABULARY: ReadonlyArray<{ state: string; key: string; en: string; zh: st
200201
{ state: 'can be added', key: 'engine.studio.customize.addable', en: 'Can be added', zh: '可新增' },
201202
{ state: 'customized', key: 'engine.customizationMark.customized', en: 'Customized ({baseName})', zh: '已定制({baseName})' },
202203
{ state: 'added', key: 'engine.customizationMark.added', en: 'Added', zh: '新增' },
203-
// en still reads "Off" here; its move to "Switched off" is objectstack#29729.
204-
{ state: 'switched off', key: 'engine.studio.auto.off', en: 'Off', zh: '已停用' },
204+
{ state: 'switched off', key: 'engine.studio.auto.off', en: 'Switched off', zh: '已停用' },
205+
{ state: 'switched on', key: 'engine.studio.auto.on', en: 'Switched on', zh: '已启用' },
205206
];
206207

207208
/** Every other key that draws one of the states, and the word it must carry. */
@@ -288,3 +289,108 @@ describe('Access: sharing settings in zh carry no English doublet or bare OWD (o
288289
expect(EN['engine.studio.access.colOwd']).toBe('OWD');
289290
});
290291
});
292+
293+
/**
294+
* objectstack#29729 — the words #29705 left with a second spelling outside its
295+
* diff. A switched-on or switched-off flow or action reads 启用 / 停用 in zh and
296+
* "Switched on" / "Switched off" in en wherever Studio and Setup name that
297+
* state; the Customize act reads 定制 (its state is 已定制, its offer 可定制);
298+
* a permission set no package ships reads 新增 / "Added", not 自定义.
299+
*/
300+
describe('one word per state, the second pass (objectstack#29729)', () => {
301+
const SWITCHED_ON = ['engine.studio.auto.on', 'engine.studio.auto.enabled', 'engine.studio.auto.ledger.on', 'engine.studio.auto.ledger.notRunning', 'engine.studio.auto.ledger.switchedOn'];
302+
const SWITCHED_OFF = ['engine.studio.auto.off', 'engine.studio.auto.offTitle', 'engine.studio.auto.disabled', 'engine.studio.auto.disabledToast', 'engine.studio.auto.ledger.off', 'engine.studio.auto.ledger.switchedOff'];
303+
const LEDGER_PROSE = ['engine.studio.auto.ledger.hint', 'engine.studio.auto.ledger.walled'];
304+
305+
it.each(SWITCHED_ON)('%s: switched on is 启用 / "Switched on" (or "switched on")', (key) => {
306+
expect(ZH[key]).toContain('启用');
307+
expect(EN[key]).toMatch(/switched on/i);
308+
});
309+
310+
it.each(SWITCHED_OFF)('%s: switched off is 停用 / "Switched off" (or "switched off")', (key) => {
311+
expect(ZH[key]).toContain('停用');
312+
expect(EN[key]).toMatch(/switched off/i);
313+
});
314+
315+
it.each([...SWITCHED_ON, ...SWITCHED_OFF, ...LEDGER_PROSE])('%s: no 开启 / 关闭 in zh, no On/Off/Enabled/Disabled in en', (key) => {
316+
expect(ZH[key]).not.toMatch(/开启|关闭/u);
317+
// The old state labels: a bare word, or the word before "for this deployment" / " — …".
318+
expect(EN[key]).not.toMatch(/^(On|Off|Enabled|Disabled)( for | — |,|$)/u);
319+
});
320+
321+
it("Setup's packaged actions name the same state the same way", () => {
322+
const zh = (CONSOLE_ZH as any).packagedAutomation;
323+
expect(zh.on).toBe('已启用');
324+
expect(zh.off).toBe('已停用');
325+
expect(zh.actionsSubtitle).toContain('停用后');
326+
expect(zh.actionsSubtitle).not.toMatch(/关闭/u);
327+
// CONTROL: en keeps its own pair for this page.
328+
expect((CONSOLE_EN as any).packagedAutomation.off).toBe('Off');
329+
});
330+
331+
// The switch-off confirm, its toasts and the cell all name one state: a
332+
// user who switches an action off must not read 关闭 in the dialog, 已关闭
333+
// in the toast and 已停用 in the column.
334+
it.each(Object.keys((CONSOLE_ZH as any).packagedAutomation))(
335+
"Setup's packaged actions zh row %s: no 开启 / 关闭",
336+
(key) => {
337+
expect((CONSOLE_ZH as any).packagedAutomation[key]).not.toMatch(/开启|关闭/u);
338+
},
339+
);
340+
341+
it("Setup's packaged actions: the confirm, the toasts and the cell share 启用 / 停用", () => {
342+
const zh = (CONSOLE_ZH as any).packagedAutomation;
343+
expect(zh.switchedOn).toBe('「{{label}}」已启用。');
344+
expect(zh.switchedOff).toBe('「{{label}}」已在本部署中停用。');
345+
expect(zh.actionOffTitle).toBe('停用「{{label}}」?');
346+
expect(zh.actionOffBody).toMatch(/^在重新启用之前/u);
347+
expect(zh.actionOffConfirm).toBe('停用');
348+
});
349+
350+
const CUSTOMIZE_ACT = [
351+
'engine.studio.customize.action',
352+
'engine.studio.customize.actionTitle',
353+
'engine.studio.customize.walled',
354+
'engine.studio.customize.noVersion',
355+
'engine.studio.customize.draftFooter',
356+
'engine.studio.customize.restoreBody',
357+
'engine.studio.customize.restoreFlowBody',
358+
'engine.edit.resetConfirmBody',
359+
'engine.studio.landing.description',
360+
'engine.studio.landing.readonlyHint',
361+
'engine.studio.area.formEditor.oneLocale',
362+
'engine.studio.area.formEditor.translationWhole',
363+
];
364+
365+
it.each(CUSTOMIZE_ACT)('%s: the Customize act reads 定制, never 自定义', (key) => {
366+
expect(ZH[key]).toContain('定制');
367+
expect(ZH[key]).not.toContain('自定义');
368+
});
369+
370+
it('the Customize button and the landing lines that quote it agree', () => {
371+
expect(ZH['engine.studio.customize.action']).toBe('定制');
372+
expect(ZH['engine.studio.landing.description']).toContain('「定制」');
373+
expect(ZH['engine.studio.landing.readonlyHint']).toContain('「定制」');
374+
});
375+
376+
// CONTROL: 自定义 meaning "user-defined" (a custom value, key, renderer, page
377+
// or code) is another sense and keeps its word.
378+
it.each([
379+
'engine.inspector.action.mode.custom',
380+
'engine.inspector.flowNode.advancedHint',
381+
'engine.inspector.flowEdge.branchCustom',
382+
'engine.inspector.combo.noMatch',
383+
'engine.form.fallbackJson',
384+
'engine.studio.if.kindDesc.page',
385+
'engine.flowNode.script.hint',
386+
])('CONTROL — %s keeps 自定义 (user-defined, not the Customize act)', (key) => {
387+
expect(ZH[key]).toContain('自定义');
388+
});
389+
390+
it('a permission set no package ships reads the added word, not 自定义 / "Custom"', () => {
391+
expect(ZH['perm.badge.custom']).toBe(ZH['engine.customizationMark.added']);
392+
expect(EN['perm.badge.custom']).toBe(EN['engine.customizationMark.added']);
393+
// CONTROL: the shipped answer is untouched.
394+
expect(EN['perm.badge.package']).toBe('Package');
395+
});
396+
});

‎ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.badge.test.tsx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/**
44
* The provenance badge in the permission set editor has two answers: "Package"
55
* (a package ships the set: the package door's `packageId`, or the layered
6-
* envelope's code artifact) and "Custom" (authored in this environment).
6+
* envelope's code artifact) and "Added" (authored in this environment).
77
*
88
* objectui#12753 — spec 18 retired the authored `managedBy` on a permission set
99
* (objectstack#23891). The badge used to read it for a third answer,
@@ -101,19 +101,19 @@ describe.each<Site>(['page', 'studio'])('the provenance badge (%s site)', (site)
101101
expect(badgeText()).toBe('Package');
102102
});
103103

104-
it('a set with neither reads "Custom"', async () => {
104+
it('a set with neither reads "Added"', async () => {
105105
await renderWith(site, {});
106-
expect(badgeText()).toBe('Custom');
106+
expect(badgeText()).toBe('Added');
107107
});
108108

109109
it('a stray managedBy "platform" on an old stored row does not read "Platform"', async () => {
110110
await renderWith(site, { managedBy: 'platform' });
111-
expect(badgeText()).toBe('Custom');
111+
expect(badgeText()).toBe('Added');
112112
expect(screen.queryByText('Platform')).not.toBeInTheDocument();
113113
});
114114

115115
it('a stray managedBy "package" does not make an environment-authored set read "Package"', async () => {
116116
await renderWith(site, { managedBy: 'package' });
117-
expect(badgeText()).toBe('Custom');
117+
expect(badgeText()).toBe('Added');
118118
});
119119
});
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* objectstack#29729 — the permission matrix's object columns in zh-CN.
5+
*
6+
* Three things, over the real editor in both renderings (Setup's environment
7+
* scope and the Studio Access pillar):
8+
*
9+
* - the column headers spell the action word in zh (创建 … 修改 全部) and keep
10+
* the abbreviations in en (C … MA); the legend line that spells the
11+
* abbreviations out is drawn only where the headers are abbreviations;
12+
* - the headers, their tooltips and the legend are rebuilt once the lazily
13+
* fetched zh `studio` catalogue lands. They were memoised on the locale
14+
* alone, so a matrix whose first render ran before the catalogue was
15+
* resident kept that render's `en` answer: Setup's legend line and every
16+
* header tooltip read English in zh-CN;
17+
* - Setup's per-object sharing-model chip reads through the catalogue
18+
* (`perm.owd.chip`), not a hard-coded `OWD ` prefix.
19+
*
20+
* ⛔ No module-scope `loadStudioCatalogue('zh')` here, unlike the sibling
21+
* suites: the defect only exists while the catalogue is NOT yet resident, so
22+
* the zh case asserts that precondition first. The `dom` project runs each
23+
* file isolated, so no other file can have loaded it.
24+
*/
25+
26+
import '@testing-library/jest-dom/vitest';
27+
import * as React from 'react';
28+
import { afterEach, describe, expect, it, vi } from 'vitest';
29+
import { cleanup, render, screen, within } from '@testing-library/react';
30+
import { MemoryRouter } from 'react-router-dom';
31+
import { I18nProvider, createI18n } from '@object-ui/i18n';
32+
import { isStudioCatalogueLoaded } from '@object-ui/i18n/namespaces/studio';
33+
34+
const SET = {
35+
name: 'sales_rep',
36+
label: 'Sales rep',
37+
objects: { a_account: { allowRead: true } },
38+
fields: {},
39+
} as Record<string, unknown>;
40+
41+
function makeClient() {
42+
return {
43+
layered: async () => ({ effective: SET, code: null, overlay: null, overlayScope: null }),
44+
getDraft: async () => null,
45+
withPreviewDrafts() {
46+
return this;
47+
},
48+
list: async (type: string) => (type === 'object' ? [{ item: { name: 'a_account', label: 'Account' } }] : []),
49+
get: async (type: string) => (type === 'object' ? { fields: [{ name: 'name', label: 'Name' }] } : null),
50+
save: async (_t: string, _n: string, payload: Record<string, unknown>) => payload,
51+
} as any;
52+
}
53+
54+
let clientImpl: any;
55+
56+
vi.mock('./useMetadata', async (importOriginal) => ({
57+
...(await importOriginal<Record<string, unknown>>()),
58+
useMetadataClient: () => clientImpl,
59+
useMetadataTypes: () => ({
60+
loading: false,
61+
error: null,
62+
entries: [{ type: 'permission', label: 'Permission', allowEnvironmentOverlay: true, allowRuntimeCreate: true }],
63+
}),
64+
}));
65+
vi.mock('./AssignedUsersSection', () => ({ AssignedUsersSection: () => null }));
66+
67+
import { PermissionMatrixEditPage } from './PermissionMatrixEditor';
68+
69+
afterEach(() => {
70+
cleanup();
71+
});
72+
73+
type Rendering = 'setup' | 'studio';
74+
75+
function mount(language: 'zh' | 'en', rendering: Rendering) {
76+
clientImpl = makeClient();
77+
const i18n = createI18n({ defaultLanguage: language, detectBrowserLanguage: false, resources: { [language]: {} } });
78+
render(
79+
<I18nProvider instance={i18n}>
80+
<MemoryRouter>
81+
<PermissionMatrixEditPage
82+
type="permission"
83+
name="sales_rep"
84+
{...(rendering === 'studio'
85+
? { packageId: 'app.a', studio: { tab: 'objects', onTabChange: () => {} } }
86+
: {})}
87+
/>
88+
</MemoryRouter>
89+
</I18nProvider>,
90+
);
91+
}
92+
93+
const ZH_HEADERS = ['创建', '读取', '编辑', '删除', '导出', '转移', '查看 全部', '修改 全部'];
94+
const EN_HEADERS = ['C', 'R', 'U', 'D', 'E', 'Tr', 'VA', 'MA'];
95+
96+
/** The eight object-action headers, in column order, with their tooltips. */
97+
function actionHeaders(): Array<{ text: string; title: string | null }> {
98+
const pane = screen.getByTestId('perm-matrix-pane');
99+
return within(pane)
100+
.getAllByRole('columnheader')
101+
.filter((th) => th.querySelector('span.break-keep'))
102+
.map((th) => ({ text: (th.textContent ?? '').trim(), title: th.getAttribute('title') }));
103+
}
104+
105+
describe('the matrix column headers in zh, after the lazy catalogue lands (objectstack#29729)', () => {
106+
it.each<Rendering>(['setup', 'studio'])(
107+
'%s: a matrix mounted before the zh catalogue is resident spells the action words, with zh tooltips and no legend line',
108+
async (rendering) => {
109+
if (rendering === 'setup') {
110+
expect(isStudioCatalogueLoaded('zh'), 'precondition: the zh catalogue is not resident yet').toBe(false);
111+
}
112+
mount('zh', rendering);
113+
await screen.findByText('创建', { selector: 'th span' }, { timeout: 8000 });
114+
const headers = actionHeaders();
115+
expect(headers.map((h) => h.text)).toEqual(ZH_HEADERS);
116+
expect(headers[0].title).toBe('创建');
117+
expect(headers[7].title).toBe('修改所有记录(绕过共享规则)');
118+
// The headers ARE the words: a legend line would only repeat them.
119+
expect(screen.queryByTestId('perm-column-legend')).toBeNull();
120+
},
121+
);
122+
});
123+
124+
describe('CONTROL — en keeps the abbreviations and their legend line (objectstack#29729)', () => {
125+
it.each<Rendering>(['setup', 'studio'])('%s: C … MA, spelled out in the legend line', async (rendering) => {
126+
mount('en', rendering);
127+
await screen.findByText('Account');
128+
const headers = actionHeaders();
129+
expect(headers.map((h) => h.text)).toEqual(EN_HEADERS);
130+
expect(headers[0].title).toBe('Create');
131+
const legend = screen.getByTestId('perm-column-legend');
132+
expect(legend).toHaveTextContent('VA');
133+
expect(legend).toHaveTextContent(rendering === 'setup' ? 'View All Records' : 'View all');
134+
});
135+
});
136+
137+
describe("Setup's sharing-model chip reads through the catalogue (objectstack#29729)", () => {
138+
it('zh names the sharing model in Chinese, with no bare OWD', async () => {
139+
mount('zh', 'setup');
140+
const chip = await screen.findByText(/^记录共享模型:/u, undefined, { timeout: 8000 });
141+
expect(chip.textContent).toBe('记录共享模型:私有(默认)');
142+
expect(chip.textContent).not.toMatch(/OWD/u);
143+
});
144+
145+
it('CONTROL — en keeps "OWD {value}"', async () => {
146+
mount('en', 'setup');
147+
expect(await screen.findByText('OWD Private (default)')).toBeInTheDocument();
148+
});
149+
});

‎ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.provenanceBadge-7611.test.tsx‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
/**
44
* objectui#7611 (F2) — the identity strip's provenance badge reads the layered
5-
* envelope, so a set a code package SHIPS reads "Package", not "Custom".
5+
* envelope, so a set a code package SHIPS reads "Package", not "Added".
66
*
77
* ## The defect
88
*
99
* The badge read only `draft.managedBy` and the `packageId` prop. The registry
1010
* serves no `managedBy` on a permission set, and the Setup catalog (the
1111
* environment scope) passes no `packageId`, so every packaged set opened there
12-
* read "Custom" — beside the artifact lock that says a code package provides
12+
* read "Added" — beside the artifact lock that says a code package provides
1313
* it. The editor already holds the fact that answers the question: the layered
1414
* envelope's `code` layer and its ADR-0010 `provenance`, read by
1515
* `isArtifactBackedLayer` into `codeIsArtifact` for the artifact gate. The badge
@@ -21,7 +21,7 @@
2121
* exclusions (`PermissionMatrixEditor.artifactTier.test.tsx` pins them for the
2222
* lock): a set with no code layer, an organization-authored set read back with
2323
* a code-looking `_packageId`, and the `sys_metadata` save-path sentinel. Each
24-
* must still read "Custom", so the fix cannot be "always Package".
24+
* must still read "Added", so the fix cannot be "always Package".
2525
*/
2626

2727
import '@testing-library/jest-dom/vitest';
@@ -98,7 +98,7 @@ async function renderMatrix() {
9898
const badgeText = () => screen.getByTestId('perm-provenance-badge').textContent;
9999

100100
describe('the provenance badge reads the layered envelope (objectui#7611, F2)', () => {
101-
it('a set a code package ships reads "Package" beside its lock, not "Custom"', async () => {
101+
it('a set a code package ships reads "Package" beside its lock, not "Added"', async () => {
102102
codeLayer = { _packageId: 'com.example.showcase', name: 'showcase_contributor' };
103103
provenance = 'package';
104104
await renderMatrix();
@@ -107,21 +107,21 @@ describe('the provenance badge reads the layered envelope (objectui#7611, F2)',
107107
expect(screen.getByText(/^Read-only \(/)).toBeInTheDocument();
108108
});
109109

110-
it('a set with no code layer (authored in this environment) still reads "Custom"', async () => {
110+
it('a set with no code layer (authored in this environment) still reads "Added"', async () => {
111111
await renderMatrix();
112-
expect(badgeText()).toBe('Custom');
112+
expect(badgeText()).toBe('Added');
113113
});
114114

115-
it('an organization-authored set read back with a code-looking _packageId still reads "Custom"', async () => {
115+
it('an organization-authored set read back with a code-looking _packageId still reads "Added"', async () => {
116116
codeLayer = { _packageId: 'com.example.showcase', name: 'showcase_contributor' };
117117
provenance = 'org';
118118
await renderMatrix();
119-
expect(badgeText()).toBe('Custom');
119+
expect(badgeText()).toBe('Added');
120120
});
121121

122-
it('the sys_metadata save-path sentinel still reads "Custom"', async () => {
122+
it('the sys_metadata save-path sentinel still reads "Added"', async () => {
123123
codeLayer = { _packageId: 'sys_metadata', name: 'showcase_contributor' };
124124
await renderMatrix();
125-
expect(badgeText()).toBe('Custom');
125+
expect(badgeText()).toBe('Added');
126126
});
127127
});

0 commit comments

Comments
 (0)