Skip to content

Commit 062236c

Browse files
fix(console): in-app doc links keep the app shell; docs chrome reads the session language (#30579)
<!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4Uu4MbnkfHMbgaMJXYX2yp8UL)_ Fixes #30554 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:在应用里看文档时,正文里的文档链接改为指向当前应用的文档路由,点开后顶栏和应用菜单都还在;文档页头部的「Documentation」、文档门户标题和侧栏的「Uncategorized」分组改为按会话语言显示(10 种语言都补了词)。 - 为什么改:之前正文链接一律指向全局文档门户 `/docs/NAME`,用户在 HotCRM 手册里点「线索」会跳出应用、只能靠浏览器后退;中文界面里还夹着两个英文词。 - 风险与回滚:只改控制台文档页的链接目标和三处文案,路由形状不变、不碰 `packages/**`;门户(应用外)的链接保持原样;回滚即 revert 本 PR。 - 先看:`ui/apps/console/src/pages/DocPage.tsx`(链接基点 `base` 与点击跳转)、`ui/apps/console/src/pages/DocShell.tsx`(头部首页链接)、`ui/apps/console/src/pages/book-nav.ts`(`labelUncategorized` 按 key 取名)。 - 两分钟验证:`pnpm --dir ui exec vitest run apps/console/src/pages/DocPage.docLinkBasename-12686.test.tsx apps/console/src/pages/docs-portal.bookLocale-26690.test.tsx`,预期 2 个文件 16 条全过。 - 需要你做的:无,席位落地。 ## What changes - **Body cross-references inside an app stay in the app.** `DocPage` rewrote `[x](./other_doc.md)` with `useHref('/docs')`, the portal, even when the page was read under `/apps/APP/docs/BOOK/DOC` (ADR-0048). It now resolves them under the route it is read under (`/apps/APP/docs` inside an app, `/docs` outside), basename included as before (objectui#12686); a plain left click navigates to the same base. The flat `APP/docs/NAME` segment already redirects to the doc's in-book URL under that base (`DocsSlug`), so the reader lands on `/apps/APP/docs/BOOK/NAME` with the top bar and the app navigation. - **The header's home link is the app's docs index inside an app** (`DocShell`), the same defect class as the body links, in a claimed file: before, it also pointed at the portal and left the app. Outside an app it is still `/docs`. - **The docs chrome reads the session language.** New keys `help.documentation` and `help.uncategorized` in all ten locale packs. `DocShell`'s home label and the `DocsIndex` heading read the first; the book sidebar's synthetic catch-all group is titled with the second by its key (`labelUncategorized` in the console's resolver copy `book-nav.ts`), so the resolver's English label stays as authored and authored groups keep the server's translation (`translateBook`, objectstack#26690). No server change: the body links were rewritten in the console, not server-side, so the card stays in the ui lane whole. ## Tests - `DocPage.docLinkBasename-12686.test.tsx` (the DocPage link harness), new top-level describe: under `/_console` an app doc's body link is the href react-router's own `Link` renders for `/apps/APP/docs/NAME`; a left click lands in the app route, never the portal; root mount; the header home link is the app's docs index; CONTROL: outside an app the home link is still the portal. - `docs-portal.bookLocale-26690.test.tsx` (the docs locale harness, real route table, layout, reader and sidebar), new top-level describe: under a real `I18nProvider` for zh, ja and es the sidebar's Uncategorized group and the header home link read the pack's words (expectations read from the packs, guarded as non-English); CONTROL with no provider reads English. This harness's `useObjectTranslation` stub (defaultValue only) was dropped so the provider can answer; its two existing cases are unchanged and still pass. One unplaced doc was added to its fixture to put an Uncategorized group on screen. ## Acceptance notes - Still English, outside this card's two rows, not filed (polish of the same pages): the `DocsIndex` subtitle "Books and guides from your installed packages.", the "Documentation not found" / "Failed to load documentation" notices in `DocPage` / `DocsSlug`, and the narrow-screen disclosure's fallback "Contents" in `DocPage`. - Studio's doc-draft preview (`ui/packages/app-shell/src/views/metadata-admin/previews/doc-draft.ts`) resolves through the spec's `resolveBookTree` and would show its synthetic group as "Uncategorized"; not in this claim's file surface, noted only. - Branch is the card thread's platform-assigned `claude/project-thread-y9p3c7`, not `claude/issue-30554-*`; the claim amendment names it. ## Verification <details><summary>Live rig on origin/main vs this branch (showcase + console dev server, zh-CN)</summary> Showcase `objectstack dev --seed-admin --fresh` with a temporary unplaced doc (never committed) that links `./showcase_index.md`; console Vite dev server proxied to it; Playwright signed in as the seeded admin with the console language set to zh-CN, opened `/apps/com.example.showcase/docs/showcase_rig_note`, read the header, the sidebar group titles and the body links, then clicked the body link. Before (base c23eafa): - body link href `/docs/showcase_index`; after the click the path is `/docs/manual/showcase_index`, app help button absent, app sidebar absent (left the app shell) - header home link text `Documentation`, href `/docs` - sidebar groups `入门, 导览, 指南, Uncategorized` After (this branch): - body link href `/apps/com.example.showcase/docs/showcase_index`; after the click the path is `/apps/com.example.showcase/docs/manual/showcase_index`, app help button present, app sidebar present - header home link text `文档`, href `/apps/com.example.showcase/docs` - sidebar groups `入门, 导览, 指南, 未分类` - en-US control: `Documentation`, `Getting Started, Guided Tour, Guides, Uncategorized`, links in the app route </details> <details><summary>Ablation (fix reverted, tests kept)</summary> `DocPage.tsx`, `DocShell.tsx`, `book-nav.ts` and `DocsIndex.tsx` restored to base c23eafa (disk hash = base blob for all four), the two test files run: 8 failed / 8 passed — every new case red except the outside-app CONTROL, which holds on both trees. Restored with `git checkout HEAD --`, each file's hash re-checked equal to its HEAD blob. </details> <details><summary>Local runs at e2e2012</summary> - `pnpm --dir ui exec vitest run packages/i18n apps/console/src/pages/Doc apps/console/src/pages/docs apps/console/src/pages/book-nav apps/console/src/pages/BookSidebar apps/console/src/pages/doc-`: Test Files 110 passed, Tests 1557 passed / 13 skipped - `pnpm turbo run type-check --filter=@object-ui/console --filter=@object-ui/i18n`: 75/75 tasks successful - eslint `--no-inline-config` on every changed file: 0 errors (2 pre-existing `no-explicit-any` warnings in `DocPage.tsx`, untouched lines) - `node scripts/pm/dispatch-gates.mjs --commands`: 35 derived, each run with exit 0; `--ran` reconciliation: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN - merge-tree against origin/main 48b3ae6: clean </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (No spec change.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. (Changed files linted locally; the repo-wide run 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. (The 维护者速读 block is the dispatch contract's Chinese summary.) - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01L6bhgTtGFiNwm2iGyWMAsv --- _Generated by [Claude Code](https://claude.ai/code/session_01L6bhgTtGFiNwm2iGyWMAsv)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6c30357 commit 062236c

17 files changed

Lines changed: 193 additions & 19 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@object-ui/console': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
fix(console): a link inside an app's doc keeps the app shell, and the docs chrome reads the session language
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
A doc read inside an app (`/apps/APP/docs/BOOK/DOC`) rewrote its body cross-references to the portal's `/docs/NAME`, so following one left the app: no top bar, no app navigation. They now resolve under the app's own docs route, as the book sidebar already did, and the header's home link goes to the app's docs index. The header's "Documentation" label, the portal heading and the book sidebar's synthetic "Uncategorized" group title now read the new `help.documentation` and `help.uncategorized` keys, present in all ten locale packs; the group is titled by its key, and authored group labels keep the server's translation.

‎ui/apps/console/src/pages/DocPage.docLinkBasename-12686.test.tsx‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,66 @@ describe('a doc-to-doc link carries the router basename (objectui#12686)', () =>
133133
expect(screen.getByTestId('where').textContent).toBe('/docs/crm_manual/crm_lead_guide');
134134
});
135135
});
136+
137+
/**
138+
* objectstack#30554 — a doc read inside an app (`/apps/APP/docs/BOOK/DOC`,
139+
* ADR-0048) resolves its body cross-references under that app's docs tree, so
140+
* following one keeps the app shell. Before, the href was the portal's
141+
* `/docs/NAME`, and a click left the app (no top bar, no app nav).
142+
*/
143+
const APP = 'app.example.crm';
144+
145+
function renderAppDoc(basename: string) {
146+
const prefix = basename === '/' ? '' : basename;
147+
return render(
148+
<MemoryRouter basename={basename} initialEntries={[`${prefix}/apps/${APP}/docs/crm_manual/crm_lead_guide`]}>
149+
<Routes>
150+
<Route path="/apps/:appName/docs/:slug/:name" element={<DocPage />} />
151+
<Route path="/apps/:appName/docs/:slug" element={<div>landed in app</div>} />
152+
<Route path="/docs/:name" element={<div>left the app</div>} />
153+
</Routes>
154+
<Link to={`/apps/${APP}/docs/crm_index`} data-testid="oracle">oracle</Link>
155+
<Link to={`/docs/crm_index`} data-testid="portal">portal</Link>
156+
<Where />
157+
</MemoryRouter>,
158+
);
159+
}
160+
161+
describe("a doc-to-doc link in an app's doc stays in that app's docs route (objectstack#30554)", () => {
162+
it("under `/_console`, the href is the app's docs route, basename included", async () => {
163+
renderAppDoc('/_console');
164+
const link = await screen.findByRole('link', { name: 'index' });
165+
const oracle = screen.getByTestId('oracle').getAttribute('href');
166+
expect(oracle).toBe(`/_console/apps/${APP}/docs/crm_index`);
167+
expect(link.getAttribute('href')).toBe(oracle);
168+
expect(link.getAttribute('href')).not.toBe(screen.getByTestId('portal').getAttribute('href'));
169+
expect(screen.getByRole('link', { name: 'setup' }).getAttribute('href')).toBe(`${oracle}#setup`);
170+
});
171+
172+
it('a plain left click navigates inside the app, never to the portal', async () => {
173+
renderAppDoc('/_console');
174+
const link = await screen.findByRole('link', { name: 'index' });
175+
expect(fireEvent.click(link, { button: 0 })).toBe(false);
176+
expect(await screen.findByText('landed in app')).toBeTruthy();
177+
expect(screen.queryByText('left the app')).toBeNull();
178+
expect(screen.getByTestId('where').textContent).toBe(`/apps/${APP}/docs/crm_index`);
179+
});
180+
181+
it('on a root mount the href is `/apps/APP/docs/NAME`', async () => {
182+
renderAppDoc('/');
183+
const link = await screen.findByRole('link', { name: 'index' });
184+
expect(link.getAttribute('href')).toBe(`/apps/${APP}/docs/crm_index`);
185+
});
186+
187+
it("the header's home link is the app's docs index", async () => {
188+
renderAppDoc('/_console');
189+
await screen.findByRole('link', { name: 'index' });
190+
expect(screen.getByRole('link', { name: 'Documentation' }).getAttribute('href')).toBe(`/_console/apps/${APP}/docs`);
191+
});
192+
193+
it('CONTROL: outside an app the header home link is still the portal', async () => {
194+
renderDoc('/_console');
195+
await screen.findByRole('link', { name: 'index' });
196+
expect(screen.getByRole('link', { name: 'Documentation' }).getAttribute('href')).toBe('/_console/docs');
197+
});
198+
});

‎ui/apps/console/src/pages/DocPage.tsx‎

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { DocShell } from './DocShell';
1717
import { BookSidebar } from './BookSidebar';
1818
import { DocRefusal } from './BookPage';
1919
import { useBookData } from './use-book-data';
20-
import { resolveBookTree, bookSlug, bookNamedBy, type ResolvedBook } from './book-nav';
20+
import { resolveBookTree, bookSlug, bookNamedBy, labelUncategorized, type ResolvedBook } from './book-nav';
2121

2222
interface DocItem {
2323
name: string;
@@ -47,10 +47,16 @@ export default function DocPage() {
4747
// the parent route's package-id segment under `/apps/:appName/docs/:slug/:name`.
4848
const { slug, name, appName } = useParams<{ slug?: string; name: string; appName?: string }>();
4949
const navigate = useNavigate();
50-
// The docs route's href, router basename included (objectui#12686): the
50+
// The docs route this page is read under: the app's own docs tree inside an
51+
// app (ADR-0048), else the portal. Body cross-references resolve under it, so
52+
// a link followed from an app's doc keeps the app shell (top bar, app nav),
53+
// the same answer the book sidebar gives; the flat `<base>/<name>` segment
54+
// redirects to the doc's in-book URL under that same base (DocsSlug).
55+
const base = appName ? `/apps/${appName}/docs` : '/docs';
56+
// That route's href, router basename included (objectui#12686): the
5157
// rewritten cross-references carry it so a new tab or a copied link reaches
5258
// the doc. `navigate` takes the bare router path and applies the basename.
53-
const docsHref = useHref('/docs');
59+
const docsHref = useHref(base);
5460
const adapter = useAdapter();
5561
const { t } = useObjectTranslation();
5662
const [doc, setDoc] = useState<DocItem | null>(null);
@@ -128,13 +134,13 @@ export default function DocPage() {
128134
if (!href) return;
129135
if (href.startsWith(`${docsHref}/`)) {
130136
e.preventDefault();
131-
navigate(`/docs${href.slice(docsHref.length)}`);
137+
navigate(`${base}${href.slice(docsHref.length)}`);
132138
} else if (href.startsWith('#')) {
133139
e.preventDefault();
134140
scrollToHeading(decodeURIComponent(href.slice(1)));
135141
}
136142
},
137-
[docsHref, navigate, scrollToHeading],
143+
[base, docsHref, navigate, scrollToHeading],
138144
);
139145

140146
// ToC entries are bare `#id` anchors; scroll in JS for the same <base> reason.
@@ -159,13 +165,16 @@ export default function DocPage() {
159165
// the portal book set; degrades to no sidebar if the slug is unknown (e.g.
160166
// the backend serves no books yet).
161167
const { books, authoredBooks, docs: allDocs } = useBookData();
162-
const base = appName ? `/apps/${appName}/docs` : '/docs';
168+
// The synthetic catch-all group is titled in the session language, by its key.
169+
const uncategorizedLabel = t('help.uncategorized', { defaultValue: 'Uncategorized' });
163170
const resolvedBook = useMemo<ResolvedBook | null>(() => {
164171
const book = books.find((b) => bookSlug(b) === slug);
165172
// Every authored book judges the Uncategorized group, so a doc a sibling
166173
// book places is not listed there (objectstack#27091).
167-
return book ? resolveBookTree(book, allDocs, undefined, authoredBooks) : null;
168-
}, [books, authoredBooks, allDocs, slug]);
174+
return book
175+
? labelUncategorized(resolveBookTree(book, allDocs, undefined, authoredBooks), uncategorizedLabel)
176+
: null;
177+
}, [books, authoredBooks, allDocs, slug, uncategorizedLabel]);
169178
const docHref = useCallback((docName: string) => `${base}/${slug}/${docName}`, [base, slug]);
170179
// A `:slug` segment that is a book's NAME rather than its slug — what a
171180
// `{ type: 'doc', book, doc }` navigation entry links to (objectui#11197) — is

‎ui/apps/console/src/pages/DocShell.tsx‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
*/
88

99
import type { ReactNode } from 'react';
10-
import { Link } from 'react-router-dom';
10+
import { Link, useParams } from 'react-router-dom';
1111
import { BookOpen } from 'lucide-react';
12+
import { useObjectTranslation } from '@object-ui/i18n';
1213

1314
/**
1415
* Lightweight chrome for the package-documentation routes (ADR-0046).
@@ -18,18 +19,26 @@ import { BookOpen } from 'lucide-react';
1819
* "Documentation" home link (→ `/docs`) plus an optional breadcrumb — so a
1920
* reader always knows where they are and has a way back, without pulling in
2021
* the full app shell (which ADR-0046 deliberately keeps out of v1).
22+
*
23+
* Under an app (`/apps/:appName/docs/...`, ADR-0048) the home link is that
24+
* app's docs index, so it keeps the reader inside the app shell, the same
25+
* answer the sidebar and the body cross-references give. The label reads the
26+
* session language (`help.documentation`).
2127
*/
2228
export function DocShell({ breadcrumb, children }: { breadcrumb?: string; children: ReactNode }) {
29+
const { appName } = useParams<{ appName?: string }>();
30+
const { t } = useObjectTranslation();
31+
const home = appName ? `/apps/${appName}/docs` : '/docs';
2332
return (
2433
<div className="min-h-full">
2534
<header className="sticky top-0 z-10 border-b border-border bg-background/80 backdrop-blur supports-[backdrop-filter]:bg-background/60">
2635
<div className="mx-auto flex max-w-3xl items-center gap-2 px-4 py-3 sm:px-6">
2736
<Link
28-
to="/docs"
37+
to={home}
2938
className="flex items-center gap-2 text-sm font-semibold hover:opacity-80"
3039
>
3140
<BookOpen className="h-4 w-4 text-muted-foreground" />
32-
<span>Documentation</span>
41+
<span>{t('help.documentation', { defaultValue: 'Documentation' })}</span>
3342
</Link>
3443
{breadcrumb ? (
3544
<>

‎ui/apps/console/src/pages/DocsIndex.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export default function DocsIndex() {
5252
<DocShell>
5353
<div className="mx-auto max-w-4xl p-4 sm:p-6">
5454
<header className="mb-6">
55-
<h1 className="text-2xl font-bold tracking-tight">Documentation</h1>
55+
<h1 className="text-2xl font-bold tracking-tight">{t('help.documentation', { defaultValue: 'Documentation' })}</h1>
5656
<p className="mt-1 text-sm text-muted-foreground">
5757
Books and guides from your installed packages.
5858
</p>

‎ui/apps/console/src/pages/book-nav.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,29 @@ export interface ResolvedBook {
109109
groups: ResolvedGroup[];
110110
}
111111

112+
/**
113+
* The key of the synthetic Uncategorized group. Its `label` is the framework's
114+
* English default (the spec's `resolveBookTree` answers the same); a reader
115+
* names the group in the session language by this key, never by the label
116+
* (see {@link labelUncategorized}).
117+
*/
112118
const UNCATEGORIZED_KEY = 'uncategorized';
113119

120+
/**
121+
* The resolved book with its synthetic Uncategorized group titled `label` —
122+
* the reader's own word for it. Authored groups keep their served labels,
123+
* which the server already answers in the request locale; an authored group
124+
* that happens to use the key `uncategorized` is not synthetic and is left
125+
* as authored.
126+
*/
127+
export function labelUncategorized(book: ResolvedBook, label: string): ResolvedBook {
128+
if (!book.groups.some((g) => g.synthetic && g.key === UNCATEGORIZED_KEY)) return book;
129+
return {
130+
...book,
131+
groups: book.groups.map((g) => (g.synthetic && g.key === UNCATEGORIZED_KEY ? { ...g, label } : g)),
132+
};
133+
}
134+
114135
/** The namespace prefix of a metadata name (everything before the first `_`). */
115136
export function namePrefix(name: string): string {
116137
const i = name.indexOf('_');

‎ui/apps/console/src/pages/docs-portal.bookLocale-26690.test.tsx‎

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,18 @@
1818
* the authored book run through the spec's own translator with a translation
1919
* bundle, for the locale the "request" carries. So the chain pinned here is
2020
* bundle → served book → sidebar, with nothing hand-translated in between.
21+
*
22+
* objectstack#30554 adds the console's own words in that chrome: the header's
23+
* "Documentation" home link and the synthetic Uncategorized group (which no
24+
* bundle carries; the console titles it by its key). Those pins mount a real
25+
* `I18nProvider` and read their expectations from the locale packs.
2126
*/
2227

2328
import { describe, it, expect, vi, afterEach } from 'vitest';
2429
import { render, screen, cleanup, within } from '@testing-library/react';
2530
import { MemoryRouter, Routes, Route } from 'react-router-dom';
31+
import { I18nProvider } from '@object-ui/i18n';
32+
import { builtInLocales } from '@object-ui/i18n/locales';
2633
// Paid at collection, not inside the adapter's clocked `getItems` call.
2734
import '@objectstack/spec/system';
2835
import type { TranslationBundle } from '@objectstack/spec/system';
@@ -32,6 +39,8 @@ const { ADAPTER, REQUEST } = vi.hoisted(() => {
3239
const DOCS = [
3340
{ name: 'crm_start_login', label: 'Signing in', _packageId: 'com.example.crm', order: 1 },
3441
{ name: 'crm_pipe_stages', label: 'Stages', _packageId: 'com.example.crm', order: 2 },
42+
// Claimed by no group: the book's Uncategorized catch-all lists it.
43+
{ name: 'crm_loose_note', label: 'Loose note', _packageId: 'com.example.crm', order: 3 },
3544
];
3645
const BOOK = {
3746
name: 'crm_manual',
@@ -79,18 +88,13 @@ vi.mock('@object-ui/plugin-markdown', async (importOriginal) => ({
7988
extractToc: () => [],
8089
}));
8190

82-
vi.mock('@object-ui/i18n', async (importOriginal) => ({
83-
...(await importOriginal<Record<string, unknown>>()),
84-
useObjectTranslation: () => ({ t: (_k: string, o?: { defaultValue?: string }) => o?.defaultValue ?? _k }),
85-
}));
86-
8791
// Imported AFTER the mocks so the pages pick up the mocked modules.
8892
import DocsLayout from './DocsLayout';
8993
import DocsSlug from './DocsSlug';
9094
import DocPage from './DocPage';
9195

92-
function Portal({ entry }: { entry: string }) {
93-
return (
96+
function Portal({ entry, language }: { entry: string; language?: string }) {
97+
const routed = (
9498
<MemoryRouter initialEntries={[entry]}>
9599
<Routes>
96100
<Route path="/apps/:appName/docs" element={<DocsLayout />}>
@@ -100,6 +104,13 @@ function Portal({ entry }: { entry: string }) {
100104
</Routes>
101105
</MemoryRouter>
102106
);
107+
return language ? (
108+
<I18nProvider config={{ defaultLanguage: language, detectBrowserLanguage: false }} persistLanguage={false}>
109+
{routed}
110+
</I18nProvider>
111+
) : (
112+
routed
113+
);
103114
}
104115

105116
/**
@@ -140,3 +151,33 @@ describe('objectstack#26690 — the docs sidebar serves the request locale', ()
140151
}
141152
});
142153
});
154+
155+
describe("objectstack#30554 — the docs chrome's own words follow the session language", () => {
156+
const ENTRY = '/apps/com.example.crm/docs/manual/crm_start_login';
157+
158+
for (const language of ['zh', 'ja', 'es'] as const) {
159+
it(`a ${language} reader sees the home link and the Uncategorized title in ${language}`, async () => {
160+
const pack = builtInLocales[language].help;
161+
// Guard: the expectations are real translations, not the English words.
162+
expect(pack.documentation).not.toBe('Documentation');
163+
expect(pack.uncategorized).not.toBe('Uncategorized');
164+
render(<Portal entry={ENTRY} language={language} />);
165+
const navs = await sidebars('User Manual');
166+
for (const nav of navs) {
167+
expect(within(nav).getByText(pack.uncategorized)).toBeInTheDocument();
168+
expect(within(nav).queryByText('Uncategorized')).toBeNull();
169+
expect(within(nav).getByText('Loose note')).toBeInTheDocument();
170+
}
171+
const home = screen.getByRole('link', { name: pack.documentation });
172+
expect(home.getAttribute('href')).toBe('/apps/com.example.crm/docs');
173+
expect(screen.queryByText('Documentation')).toBeNull();
174+
});
175+
}
176+
177+
it('CONTROL: with no provider the chrome reads its English', async () => {
178+
render(<Portal entry={ENTRY} />);
179+
const navs = await sidebars('User Manual');
180+
for (const nav of navs) expect(within(nav).getByText('Uncategorized')).toBeInTheDocument();
181+
expect(screen.getByRole('link', { name: 'Documentation' }).getAttribute('href')).toBe('/apps/com.example.crm/docs');
182+
});
183+
});

‎ui/packages/i18n/src/locales/ar.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3355,6 +3355,8 @@ const ar = {
33553355
allDocs: 'كل الوثائق',
33563356
onlineDocs: 'الوثائق عبر الإنترنت',
33573357
loadingDocumentation: 'جارٍ تحميل الوثائق',
3358+
documentation: 'الوثائق',
3359+
uncategorized: 'غير مصنف',
33583360
about: 'حول',
33593361
aboutTitle: 'حول وحدة التحكم هذه',
33603362
aboutDescription: 'الإصدار الذي بُنيت منه وحدة التحكم هذه.',

‎ui/packages/i18n/src/locales/de.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3101,6 +3101,8 @@ const de = {
31013101
allDocs: 'Gesamte Dokumentation',
31023102
onlineDocs: 'Online-Dokumentation',
31033103
loadingDocumentation: 'Dokumentation wird geladen',
3104+
documentation: 'Dokumentation',
3105+
uncategorized: 'Nicht kategorisiert',
31043106
about: 'Über',
31053107
aboutTitle: 'Über diese Konsole',
31063108
aboutDescription: 'Der Build, aus dem diese Konsole kompiliert wurde.',

‎ui/packages/i18n/src/locales/en.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3701,6 +3701,8 @@ const en = {
37013701
keyboardShortcuts: 'Keyboard shortcuts',
37023702
onlineDocs: 'Online documentation',
37033703
loadingDocumentation: 'Loading documentation',
3704+
documentation: 'Documentation',
3705+
uncategorized: 'Uncategorized',
37043706
about: 'About',
37053707
aboutTitle: 'About this console',
37063708
aboutDescription: 'The build this console was compiled from.',

0 commit comments

Comments
 (0)