Skip to content

Commit efcc1a7

Browse files
fix(app-shell): draw a served object or list view label as served in the Console sidebar, recent list and object list header (#30388)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgRs2m6UxsYUoAdT4puvameb)_ domain:ui#3 · os-bill · session_01Utp6CbPN5UYMnBaNJPJWfG Refs #30311 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:Console 侧栏里未写标签的对象/列表视图入口、侧栏与首页的「最近访问」里的对象行和列表视图行、对象列表页标题、顶栏对象标题、对象 `/data` 页标题,改为直接显示服务端返回的名字;只有服务端没给名字(客户端自己派生的视图、还没读到的对象、待发布草稿)时才查客户端词典。 - 为什么改:客户在 `.custom` 里给已安装包的对象或列表视图改了名,服务端返回新名字,但 Console 拿服务端的名字再去查词典,基础包的旧译名又把它盖回去;实测 zh-CN 下对象页标题和最近访问仍显示「任务」「进行中」。 - 风险与回滚:未改名的条目显示不变(服务端本就用同一份词典翻译);记录页、表单、抽屉等其余约 43 处同类调用本 PR 未改,交席位立收口卡。回滚即还原本 PR。应用名一半等 #30062 服务端,所以写 Refs 不关卡。 - 先看:① `ui/packages/app-shell/src/hooks/useNavTargetLabel.ts`(object/view 分支与 `servedObjectPluralLabel`);② `ui/packages/app-shell/src/views/ObjectView.tsx` 的 `listTitleOf`(标题、收藏、无标签视图同一来源);③ `ui/packages/app-shell/src/layout/AppHeader.tsx` 对象分支。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/app-shell/src/hooks/__tests__/useRecentItemLabel.test.tsx packages/app-shell/src/layout/__tests__/AppHeader.servedLabels-11295.test.tsx packages/app-shell/src/views/ObjectView.listTitlePlural-11696.test.tsx packages/app-shell/src/views/ObjectDataPage.listTitlePlural-11696.test.tsx`,预期 4 个文件全部通过。 - 需要你做的:无,席位落地。 ## What changes - **The rule (the one PR #30308 applied to report, page and dashboard).** A label the `/meta` read or the app bundle served is already translated by the server, by its label order for an overridden item (#30193), so it is drawn as given. The catalog key (`useObjectLabel`'s `objectLabel` / `objectPluralLabel` / `viewLabel`) is asked only for text no server translated. - `useNavTargetLabel.ts` - `object` target (an unlabelled sidebar entry): the served object's label as given; a pending draft's label still goes through the catalog. - `view` target (an unlabelled sidebar entry, and a recent view row): a served view's label as given, decided by `isServedView` — the identity question the object page's view tabs already ask (objectui#11295, objectui#11336). A view the client derived (a container expansion) still goes through `viewLabel`. By name, the served reads are the names-only scope's own `served` and `views`, from the same `readObjectScope` call `readObjectsByName` makes, so no new request. - New private helper `servedObjectPluralLabel(def, language, objectPluralLabel)`: what a served object's list is called — served `pluralLabel`, else served `label`, else the catalog under its name. Internal to app-shell (not in the package index). - `useRecentItemLabel.ts`: the recent object row uses `servedObjectPluralLabel`. - The object list header (Expected names the header; the seat widened the claim's surface, comment 6652505627, narrowed in 6652679548): - `ObjectView.tsx`: one `listTitleOf` feeds the page header's object crumb (`ObjectViewPageHeader`), the list favorite's label and an unlabelled view's tab text, so the three still agree (the header compares the tab text with the plural). - `AppHeader.tsx`: the object route's title, and the unlabelled-view fallback on a view route. - `ObjectDataPage.tsx`: the `/data` page title. - **Not changed:** `ui/packages/i18n` (`useObjectLabel` is published; changing its order is a `Clause-②` question). The rest of the family — record pages, record forms, drawers, preview cards, and call sites outside app-shell — is listed in Acceptance notes for the seat's closing card. ## Tests - New, one top-level describe per existing harness: - `useRecentItemLabel.test.tsx` (objectstack#30311 describe, 9 cases): renamed object and view in the recent list and in `useNavTargetLabel` (whole-list and names-only-scope reads), served plural first, a view embedded in the served object document; controls: a client-derived view, an object the cache does not hold, a pending draft object, en. - `AppHeader.servedLabels-11295.test.tsx` (4 cases), `ObjectView.listTitlePlural-11696.test.tsx` (2), `ObjectDataPage.listTitlePlural-11696.test.tsx` (2). - **Fixtures that served English in a zh-CN session** (the world PR #30308 named as not occurring: the Console's catalog is the server's own `/api/v1/i18n/translations/:locale` answer, and the server translates a served label with it): `useRecentItemLabel.objectPlural-11733`, `ObjectView.listTitlePlural-11696`, `ObjectView.listLinksPlural-11733`, `ObjectView.labelLessViewTab-12774`, `ObjectDataPage.listTitlePlural-11696`, `AppHeader.objectCrumbPlural-11696`, `AppHeader.labelLessViewTitle-12805`, `StudioDesignSurface.standardNavEntries-11201` (8 files) now serve the zh-CN answer in zh-CN. Every existing assertion is unchanged. - **Ablation** (the five source files restored to `ff3f83bb` with `git restore --source`, marker count 0 on disk, restored with `git checkout HEAD --`, `git status` clean and blob hashes equal to HEAD after): `Tests 9 failed | 63 passed (72)`. The 9 are exactly the renamed cells (object page title, app header en/zh, `/data` title, recent object, recent plural, recent/nav view, names-only scope, embedded view); every control stayed green. <details><summary>Test runs</summary> - Pins, fix in place: `Test Files 6 passed (6)`, `Tests 72 passed (72)`, `VERDICT command-exit 0`. - Consumer sweep 1 (before the header half; 563 test files in app-shell and the Console that reference a module importing either hook): `Test Files 1 failed | 562 passed (563)`, `Tests 1 failed | 4324 passed (4325)`. The red was `StudioDesignSurface.standardNavEntries-11201` (English served in zh-CN), fixed as above. - Consumer sweep 2 (after merging `origin/main` and rebuilding, head `23114841`): the 6 pin files plus 136 more test files that reference `ObjectView`, `ObjectDataPage` or `AppHeader`: `Test Files 1 failed | 140 passed (141)`, `Tests 2 failed | 1395 passed (1397)`. The 2 reds were `ObjectView.listLinksPlural-11733`'s zh-CN cells (English served in zh-CN), fixed as above; that file then `Tests 7 passed (7)` at `ec3b0e69`. This sweep ran 136 of the 298 test files its own reference search found, not all of them (see Acceptance notes). - Consumer sweep 3 (CI at `ec3b0e69` red on 7 zh-CN cells in `AppHeader.objectCrumbPlural-11696`, `AppHeader.labelLessViewTitle-12805` and `ObjectView.labelLessViewTab-12774`, reproduced locally, fixtures fixed at `1553cdd6`): every `AppHeader.*`, `ObjectView.*` and `ObjectDataPage.*` test in app-shell, `Test Files 114 passed (114)`, `Tests 929 passed (929)`; then all 298 files of sweep 2's reference search at `6583026f` (`origin/main` merged in, no `ui/` file among the 37 it brought), in three locked runs: `Test Files 97 passed (97)` / `101 passed (101)` / `100 passed (100)`, `Tests 905` / `790` / `950 passed`, each `VERDICT command-exit 0`. - `eslint --no-inline-config` on the changed TypeScript files (at `23114841`): 0 errors (104 warnings, all on lines this PR does not touch). - `pnpm exec turbo run type-check --filter=@object-ui/app-shell --concurrency=2` at `ec3b0e69`: `Tasks 68 successful`, `VERDICT command-exit 0`. </details> ## Live (showcase `--fresh`, zh-CN, Console from `ui/` src through Vite) Overrides written the way the platform takes them: `POST /api/v1/packages/com.example.showcase/ensure-customization` (pair `com.example.showcase.custom`); the object through an `object_extension` (`PUT /api/v1/meta/object_extension/showcase_task?package=com.example.showcase.custom&mode=draft` with `extend`, `label: Work Item (renamed-30311)`, `pluralLabel: Work Items (renamed-30311)`, then publish — an object `overrides` save is refused `NOT_OVERRIDABLE`, ADR-0146 D10); the view through a view override (`showcase_task.in_progress`, `label: In flight (renamed-30311)`, `overrides: { package: 'com.example.showcase', basedOn: '0.1.0' }`, then publish). Served under `Accept-Language: zh-CN` after the overrides: `/meta/object/showcase_task` → `Work Item (renamed-30311)` / `Work Items (renamed-30311)`; `/meta/view/showcase_task.in_progress` → `In flight (renamed-30311)`; the app bundle `/meta/app/showcase_app/bundle` carries the renamed object too. The zh-CN catalog keeps `objects.showcase_task.label` = 任务 and `_views.in_progress.label` = 进行中. | Console, zh-CN, same overrides | `origin/main` ui (`ff3f83bb` sources) | this branch | |:--|:--|:--| | object list page title (crumb) | 任务 / 全部任务 | Work Items (renamed-30311) / 全部任务 | | view page title | 任务 / In flight (renamed-30311) | Work Items (renamed-30311) / In flight (renamed-30311) | | sidebar 「最近使用」 | 客户 · 任务 · 进行中 | 客户 · Work Items (renamed-30311) · In flight (renamed-30311) | | Home 「最近访问」 | 客户 · 任务 · 进行中 | 客户 · Work Items (renamed-30311) · In flight (renamed-30311) | Each leg restarted the Console's Vite (`--force`) after switching the five sources and checked that the served modules carried (or lacked) the change before the run: the first after-run read a stale transform from Vite's watcher. Screenshots (1440×900 at 2×, before/after of the object page, the view page, the expanded sidebar recent group and Home) are in the project's files at `ui-seat3/30311-shots/`; this channel cannot upload images to the PR body, so the table above is their text. The live leg did not reach an unlabelled sidebar entry: every showcase navigation entry carries its own label, so the sidebar's object/view rows draw those (unchanged). That path is pinned by the `useNavTargetLabel` cases. ## Acceptance notes - **The card's premise "the object's own page shows the new name" did not hold.** Measured above: on `origin/main` the object page title also showed 任务. The seat folded the header into this PR and named the rest of the family for a closing card. - **The rest of the family, not touched here** (`objectLabel(` / `objectPluralLabel(` over a served definition, at `23114841`): in app-shell, `AppContent` 4, `ActivityObjectLabel` 1, `ConsoleLayout` 1, `ObjectView` 3 more (import wizard, delete confirmations, `objectLabelOf`), `ObjectDataPage` 1 (record drawer title), `RecordDetailView` 11, `RecordFormPage` 2, `appReportIndex` 2, `RecordPreviewCard` 1, `recordDrawerStack` 2, `PackagedActionsSection` 1, `WidgetRefCellFaces` 1; outside app-shell: `fields` `RecordHoverCard` 1, `plugin-detail` 3, `plugin-form` 2, `plugin-gantt` 1, `plugin-grid` 1. Some pass text no server translated (`{ name, label: name }` fallbacks, a widget ref's own `label`), so each needs a per-site reading. The other way to close the class is to change `useObjectLabel`'s own order, a `Clause-②` card. - **A rename of `label` alone keeps the served plural.** The server translates `label` and `pluralLabel` separately: an extension that renames only `label` still serves the base's translated `pluralLabel`, so a list title (which reads the plural first) keeps it. That is the server's answer and Studio's; the live leg renamed both. - **Deviation: an incomplete consumer sweep.** Sweep 2 was described as covering every test file that references `ObjectView`, `ObjectDataPage` or `AppHeader`; it ran 136 of the 298 its search found. Three of the 162 it skipped carried the same English-in-zh-CN fixture and went red in CI at `ec3b0e69`. They are fixed at `1553cdd6` and the full 298 now pass (sweep 3). - **App labels** are unchanged (the app half waits for #30062's server half), hence `Refs`. ## Verification - Since `ec3b0e69` the branch gained three test-fixture files (`1553cdd6`) and a merge of `origin/main` (`6583026f`, no `ui/` file). `dispatch-gates --commands` at `1553cdd6` derives the same 35 commands; they were not re-run there. `pnpm --filter @objectstack/spec check:liveness` at `6583026f`: exit 0 (#30367 fixed on `main` by #30368). - Derived gates on `ec3b0e69`: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` lists 35 commands (the same list as at `23114841`), each run with its exit code captured before any pipe, all 35 exit 0; `--ran` reconciliation: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. `pnpm --dir ui exec vitest run scripts/` ran under `os-verify-lock` with `--maxWorkers=2` (`Test Files 170 passed | 2 skipped (172)`): its first run at `23114841`, with the showcase stack up, was SIGKILLed for memory. - `ui/scripts/check-new-cross-file-line-citations.mjs` at `ec3b0e69`: 0 new citations, exit 0. - Changeset: `.changeset/30311-served-object-view-labels.md`, `@object-ui/app-shell` patch, both declaration lines. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `eslint` on the changed files reports 0 errors (the repo-wide `pnpm lint` is CI's). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (not touched) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Utp6CZywtebM5fE7KZuMEM <!-- signed: domain:ui#3 · os-bill · session_01Utp6CbPN5UYMnBaNJPJWfG --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 10ecafd commit efcc1a7

16 files changed

Lines changed: 482 additions & 79 deletions
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): the Console sidebar, recent list and object list header draw a served object or list view label as served
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Now.** An unlabelled sidebar entry that opens an object or a list view, a recent object or list view row (in the sidebar and on Home), the object list page's title, the app header's title on an object route and an object's `/data` page title draw the label the `/meta` read or the app bundle served as given. The server has already translated it, by its label order for an overridden item: a `.custom` object extension's or view override's own text is kept over the base package's translation. Before, the served label reached the client's catalog lookup only as its fallback, so the base package's translation of the old name won. A list is titled by the served `pluralLabel`, else the served `label`.
11+
- **Unchanged.** A list view the client derived (no served read names it), an object the cache does not hold yet, and a pending draft's label are still named by the catalog key, then by the machine name as before. An unedited item shows the same text as before, because the server serves the catalog's translation for it. Record pages, record forms and drawers still look the object's name up in the catalog; they are a follow-up.

‎ui/packages/app-shell/src/hooks/__tests__/useRecentItemLabel.objectPlural-11733.test.tsx‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,21 @@ import type { RecentItem } from '../../context/RecentItemsProvider';
3838

3939
type Lang = 'en' | 'zh-CN';
4040

41-
function metadata() {
41+
/**
42+
* The served objects, per language: the server translates what it serves
43+
* under the request's language from the same catalog the console loads, so a
44+
* zh-CN session holds the zh-CN answer (objectstack#30311).
45+
*/
46+
const SERVED_PROJECT = {
47+
en: { name: 'showcase_project', label: 'Project', pluralLabel: 'Projects' },
48+
'zh-CN': { name: 'showcase_project', label: '项目', pluralLabel: '项目清单' },
49+
} as const;
50+
51+
function metadata(lang: Lang) {
4252
return {
4353
apps: [],
4454
objects: [
45-
{ name: 'showcase_project', label: 'Project', pluralLabel: 'Projects' },
55+
SERVED_PROJECT[lang],
4656
{ name: 'showcase_note', label: 'Note' },
4757
],
4858
dashboards: [],
@@ -69,7 +79,7 @@ function wrapper(lang: Lang) {
6979
expect(i18n.language).toBe(lang);
7080
return ({ children }: { children: ReactNode }) => (
7181
<I18nProvider instance={i18n} persistLanguage={false}>
72-
<MetadataCtx.Provider value={metadata() as never}>{children}</MetadataCtx.Provider>
82+
<MetadataCtx.Provider value={metadata(lang) as never}>{children}</MetadataCtx.Provider>
7383
</I18nProvider>
7484
);
7585
}

‎ui/packages/app-shell/src/hooks/__tests__/useRecentItemLabel.test.tsx‎

Lines changed: 141 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { renderHook } from '@testing-library/react';
2222
import { MetadataCtx } from '@object-ui/react';
2323
import { createI18n, I18nProvider } from '@object-ui/i18n';
2424
import { useRecentItemLabel } from '../useRecentItemLabel';
25+
import { DraftTargetLabels, useNavTargetLabel } from '../useNavTargetLabel';
2526
import type { RecentItem } from '../../context/RecentItemsProvider';
2627
import { recentViewId } from '../useTrackRouteAsRecent';
2728

@@ -32,10 +33,22 @@ import { recentViewId } from '../useTrackRouteAsRecent';
3233
*/
3334
const SERVED_REPORT = { en: 'Q3 Results', 'zh-CN': '第三季度结果' } as const;
3435

35-
function metadata(language = 'en', served: { reports?: unknown[]; pages?: unknown[]; dashboards?: unknown[] } = {}) {
36+
type Served = {
37+
reports?: unknown[];
38+
pages?: unknown[];
39+
dashboards?: unknown[];
40+
/** The object list as the console composes it: served objects, served views merged in. */
41+
objects?: unknown[];
42+
/** `getItemsByType`: the `/meta/view` and `/meta/object` reads as served, before the merge. */
43+
byType?: { view?: unknown[]; object?: unknown[] };
44+
/** The names-only scope read (`readObjectScope`), answered while the object list is idle. */
45+
scope?: { objects: unknown[]; served: unknown[]; views: unknown[] };
46+
};
47+
48+
function metadata(language = 'en', served: Served = {}) {
3649
return {
3750
apps: [],
38-
objects: [
51+
objects: served.objects ?? [
3952
{
4053
name: 'showcase_task',
4154
label: 'Tasks',
@@ -53,8 +66,11 @@ function metadata(language = 'en', served: { reports?: unknown[]; pages?: unknow
5366
invalidate: () => {},
5467
ensureType: async () => [],
5568
getItem: async () => null,
56-
getItemsByType: () => [],
57-
getTypeStatus: () => 'ready' as const,
69+
getItemsByType: (type: string) => (type === 'view' || type === 'object' ? served.byType?.[type] : undefined) ?? [],
70+
getTypeStatus: (type?: string) => (served.scope && type === 'object' ? ('idle' as const) : ('ready' as const)),
71+
...(served.scope
72+
? { readObjectScope: () => ({ ...served.scope, missing: [], settled: true }) }
73+
: {}),
5874
};
5975
}
6076

@@ -65,15 +81,21 @@ const CATALOG = {
6581
reports: { q3_results: { label: '第三季度结果' } },
6682
pages: { welcome_tour: { label: '欢迎' } },
6783
dashboards: { ops_board: { label: '运营看板' } },
84+
objects: {
85+
showcase_task: { label: '任务', pluralLabel: '任务列表', _views: { open_tasks: { label: '未完成任务' } } },
86+
showcase_draft: { label: '草稿对象' },
87+
},
6888
},
6989
},
7090
};
7191

72-
function wrapper(language: string, served?: Parameters<typeof metadata>[1]) {
92+
function wrapper(language: string, served?: Served, drafts: Array<{ type: string; name: string; label?: unknown }> = []) {
7393
const i18n = createI18n({ defaultLanguage: language, detectBrowserLanguage: false, resources: CATALOG });
7494
return ({ children }: { children: ReactNode }) => (
7595
<I18nProvider instance={i18n} persistLanguage={false}>
76-
<MetadataCtx.Provider value={metadata(language, served) as never}>{children}</MetadataCtx.Provider>
96+
<MetadataCtx.Provider value={metadata(language, served) as never}>
97+
<DraftTargetLabels.Provider value={drafts}>{children}</DraftTargetLabels.Provider>
98+
</MetadataCtx.Provider>
7799
</I18nProvider>
78100
);
79101
}
@@ -212,3 +234,116 @@ describe('useRecentItemLabel — a served report, page or dashboard is named as
212234
expect(result.current(item('dashboard', 'ops_board'))).toBe('ops_board');
213235
});
214236
});
237+
238+
/**
239+
* objectstack#30311 — a served object or list view is named as served, in the
240+
* sidebar's unlabelled entries (`useNavTargetLabel`) and the recent list.
241+
*
242+
* A `.custom` override renamed the object and its view, and the `/meta` read
243+
* serves the new names by the server's label order for an overridden item.
244+
* The object and view rungs re-applied the packaged catalog's translation of
245+
* the old names over them, because the served label reached the catalog
246+
* lookup only as its fallback.
247+
*
248+
* Directions, written before the run: the renamed cells RED before the change
249+
* (the packaged zh-CN string), GREEN after; the controls (a view the client
250+
* derived, an object the cache does not hold, a pending draft, en) GREEN on
251+
* both sides.
252+
*/
253+
describe('useRecentItemLabel / useNavTargetLabel — a served object or list view is named as served (objectstack#30311)', () => {
254+
const VIEW_ID = 'showcase_task.open_tasks';
255+
const RENAMED_VIEW = { name: VIEW_ID, object: 'showcase_task', label: 'Still to do (renamed-30311)' };
256+
// The object as served (the `/meta/object` document), and as the console
257+
// composes it (the served view merged into its `listViews`).
258+
const servedObject = (extra: Record<string, unknown> = {}) => ({ name: 'showcase_task', label: 'Work items (renamed-30311)', ...extra });
259+
const RENAMED: Served = {
260+
objects: [{ ...servedObject(), listViews: { [VIEW_ID]: RENAMED_VIEW } }],
261+
byType: { view: [RENAMED_VIEW], object: [servedObject()] },
262+
};
263+
const objectEntry: RecentItem = { id: 'object:showcase_task', type: 'object', name: 'showcase_task', href: '/o', visitedAt: at };
264+
const viewEntry: RecentItem = {
265+
id: recentViewId('showcase_task', VIEW_ID),
266+
type: 'view',
267+
object: 'showcase_task',
268+
name: VIEW_ID,
269+
href: `/apps/showcase/showcase_task/view/${VIEW_ID}`,
270+
visitedAt: at,
271+
};
272+
273+
it('zh-CN: a renamed object shows the served label in the recent list and an unlabelled sidebar entry', () => {
274+
const recent = renderHook(() => useRecentItemLabel(), { wrapper: wrapper('zh-CN', RENAMED) });
275+
expect(recent.result.current(objectEntry)).toBe('Work items (renamed-30311)');
276+
const nav = renderHook(() => useNavTargetLabel({ byName: true }), { wrapper: wrapper('zh-CN', RENAMED) });
277+
expect(nav.result.current({ kind: 'object', objectName: 'showcase_task' })).toBe('Work items (renamed-30311)');
278+
});
279+
280+
it('zh-CN: a recent object row reads the served plural first', () => {
281+
const served: Served = { objects: [servedObject({ pluralLabel: 'All work items (renamed-30311)' })] };
282+
const { result } = renderHook(() => useRecentItemLabel(), { wrapper: wrapper('zh-CN', served) });
283+
expect(result.current(objectEntry)).toBe('All work items (renamed-30311)');
284+
});
285+
286+
it('zh-CN: a renamed served view shows the served label in the recent list and an unlabelled sidebar entry', () => {
287+
const recent = renderHook(() => useRecentItemLabel(), { wrapper: wrapper('zh-CN', RENAMED) });
288+
expect(recent.result.current(viewEntry)).toBe('Still to do (renamed-30311)');
289+
const nav = renderHook(() => useNavTargetLabel({ byName: true }), { wrapper: wrapper('zh-CN', RENAMED) });
290+
expect(nav.result.current({ kind: 'view', objectName: 'showcase_task', viewName: 'open_tasks' })).toBe('Still to do (renamed-30311)');
291+
});
292+
293+
it('zh-CN: by name, the served reads are the names-only scope’s own', () => {
294+
const scoped: Served = {
295+
objects: [],
296+
byType: { view: [], object: [] },
297+
scope: { objects: RENAMED.objects!, served: [servedObject()], views: [RENAMED_VIEW] },
298+
};
299+
const nav = renderHook(() => useNavTargetLabel({ byName: true }), { wrapper: wrapper('zh-CN', scoped) });
300+
expect(nav.result.current({ kind: 'view', objectName: 'showcase_task', viewName: 'open_tasks' })).toBe('Still to do (renamed-30311)');
301+
expect(nav.result.current({ kind: 'object', objectName: 'showcase_task' })).toBe('Work items (renamed-30311)');
302+
});
303+
304+
it('zh-CN: a view the served object document embeds in its own listViews is served too', () => {
305+
const embedded = { [VIEW_ID]: { label: 'Still to do (renamed-30311)' } };
306+
const served: Served = {
307+
objects: [servedObject({ listViews: embedded })],
308+
byType: { view: [], object: [servedObject({ listViews: embedded })] },
309+
};
310+
const nav = renderHook(() => useNavTargetLabel(), { wrapper: wrapper('zh-CN', served) });
311+
expect(nav.result.current({ kind: 'view', objectName: 'showcase_task', viewName: VIEW_ID })).toBe('Still to do (renamed-30311)');
312+
});
313+
314+
it('zh-CN: control — a view the client derived (no served read names it) is named by the bundle', () => {
315+
const derived: Served = {
316+
objects: [{ ...servedObject(), listViews: { [VIEW_ID]: { name: VIEW_ID, label: 'Open tasks' } } }],
317+
byType: { view: [], object: [servedObject()] },
318+
};
319+
const { result } = renderHook(() => useRecentItemLabel(), { wrapper: wrapper('zh-CN', derived) });
320+
expect(result.current(viewEntry)).toBe('未完成任务');
321+
});
322+
323+
it('zh-CN: control — an object the cache does not hold is named by the bundle', () => {
324+
const empty: Served = { objects: [], byType: { view: [], object: [] } };
325+
const recent = renderHook(() => useRecentItemLabel(), { wrapper: wrapper('zh-CN', empty) });
326+
expect(recent.result.current(objectEntry)).toBe('任务列表');
327+
const nav = renderHook(() => useNavTargetLabel(), { wrapper: wrapper('zh-CN', empty) });
328+
expect(nav.result.current({ kind: 'object', objectName: 'showcase_task' })).toBe('任务');
329+
});
330+
331+
it('zh-CN: control — a pending draft object’s label, which no server translated, still goes through the bundle', () => {
332+
const drafts = [
333+
{ type: 'object', name: 'showcase_draft', label: 'Draft object' },
334+
{ type: 'object', name: 'showcase_other_draft', label: 'Other draft' },
335+
];
336+
const { result } = renderHook(() => useNavTargetLabel(), { wrapper: wrapper('zh-CN', { objects: [] }, drafts) });
337+
expect(result.current({ kind: 'object', objectName: 'showcase_draft' })).toBe('草稿对象');
338+
expect(result.current({ kind: 'object', objectName: 'showcase_other_draft' })).toBe('Other draft');
339+
});
340+
341+
it('en: unchanged — the served object and view labels', () => {
342+
const recent = renderHook(() => useRecentItemLabel(), { wrapper: wrapper('en', RENAMED) });
343+
expect(recent.result.current(objectEntry)).toBe('Work items (renamed-30311)');
344+
expect(recent.result.current(viewEntry)).toBe('Still to do (renamed-30311)');
345+
const plain = renderHook(() => useRecentItemLabel(), { wrapper: wrapper('en') });
346+
expect(plain.result.current(objectEntry)).toBe('Tasks');
347+
expect(plain.result.current(viewEntry)).toBe('Open tasks');
348+
});
349+
});

0 commit comments

Comments
 (0)