@umami/shiso 1.16.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/shiso.mjs +0 -0
- package/dist/chunks/App.js +310 -205
- package/dist/chunks/architectureDiagram-5GKGNRK7.js +1 -1
- package/dist/chunks/chunk-GMAD6QVW.js +1 -1
- package/dist/chunks/cose-bilkent-JH36ORCC.js +1 -1
- package/dist/chunks/dist.js +1 -1
- package/dist/chunks/docs.js +848 -202
- package/dist/chunks/ganttDiagram-EL5Y4UJY.js +1 -1
- package/dist/chunks/src.js +1 -1
- package/dist/entry-client.js +1 -1
- package/dist/entry-server.js +2 -2
- package/docs.schema.json +1135 -1003
- package/package.json +1 -1
- package/scripts/check-content.mjs +2 -0
- package/scripts/load-shiso-config.mjs +30 -1
- package/src/App.tsx +41 -23
- package/src/components/CodeBlock.tsx +3 -1
- package/src/components/DocContent.tsx +15 -6
- package/src/components/Docs.tsx +3 -1
- package/src/components/Footer.tsx +3 -1
- package/src/components/Header.tsx +4 -3
- package/src/components/LanguageSwitcher.tsx +27 -7
- package/src/components/OpenApiOperation.tsx +22 -15
- package/src/components/PageActions.tsx +135 -0
- package/src/components/docs/Changelog.tsx +27 -13
- package/src/components/docs/CodeGroup.tsx +3 -1
- package/src/components/docs/Mermaid.tsx +8 -6
- package/src/components/docs/PropertiesTable.tsx +11 -6
- package/src/components/docs/Tabs.tsx +3 -1
- package/src/components/docs/Tree.tsx +3 -1
- package/src/components/docs/ZoomableImage.tsx +4 -2
- package/src/components/ui/dialog.tsx +7 -2
- package/src/components/ui/sheet.tsx +5 -2
- package/src/lib/label-context.tsx +7 -0
- package/src/lib/labels.ts +37 -0
- package/src/lib/locale.ts +25 -0
- package/src/lib/openapi.ts +21 -8
- package/src/lib/site-config.ts +66 -2
- package/src/lib/site-model.ts +4 -32
- package/src/lib/standalone-pages.ts +15 -1
- package/src/lib/translations/de.json +74 -0
- package/src/lib/translations/en.json +74 -0
- package/src/lib/translations/es.json +74 -0
- package/src/lib/translations/fr.json +74 -0
- package/src/lib/translations/ja.json +74 -0
- package/src/lib/translations/zh-Hans.json +74 -0
- package/src/lib/translations/zh-Hant.json +74 -0
- package/src/lib/types.ts +44 -32
- package/src/styles/global.css +1 -1
- package/types/config.d.ts +7 -1
- package/types/labels.d.ts +76 -0
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
Fragment,
|
|
4
|
+
isValidElement,
|
|
5
|
+
type ReactElement,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
useMemo,
|
|
8
|
+
} from 'react';
|
|
2
9
|
import { useSearchParams } from 'react-router';
|
|
10
|
+
import { useLabels } from '@/lib/label-context';
|
|
3
11
|
import { Badge } from './Badge';
|
|
4
12
|
import { styles } from './styles';
|
|
5
13
|
import { Update } from './Update';
|
|
@@ -13,12 +21,17 @@ interface UpdateChildInfo {
|
|
|
13
21
|
tags: string[];
|
|
14
22
|
}
|
|
15
23
|
|
|
24
|
+
interface UpdateChildProps {
|
|
25
|
+
tags?: unknown;
|
|
26
|
+
children?: ReactNode;
|
|
27
|
+
}
|
|
28
|
+
|
|
16
29
|
/** Flattens fragments so <Update> entries are found however MDX nests them. */
|
|
17
|
-
function flatElements(children: React.ReactNode): ReactElement<
|
|
18
|
-
const flat: ReactElement<
|
|
30
|
+
function flatElements(children: React.ReactNode): ReactElement<UpdateChildProps>[] {
|
|
31
|
+
const flat: ReactElement<UpdateChildProps>[] = [];
|
|
19
32
|
|
|
20
33
|
Children.forEach(children, child => {
|
|
21
|
-
if (!isValidElement<
|
|
34
|
+
if (!isValidElement<UpdateChildProps>(child)) {
|
|
22
35
|
return;
|
|
23
36
|
}
|
|
24
37
|
if (child.type === Fragment) {
|
|
@@ -43,9 +56,11 @@ function updateInfo(children: React.ReactNode): Map<string, UpdateChildInfo> {
|
|
|
43
56
|
const key = String(child.key ?? `update-${index}`);
|
|
44
57
|
const raw = child.props.tags;
|
|
45
58
|
const tags = Array.isArray(raw)
|
|
46
|
-
? [
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
? [
|
|
60
|
+
...new Set(
|
|
61
|
+
raw.filter((tag): tag is string => typeof tag === 'string' && tag.trim().length > 0),
|
|
62
|
+
),
|
|
63
|
+
].map(tag => tag.trim())
|
|
49
64
|
: [];
|
|
50
65
|
info.set(key, { key, tags });
|
|
51
66
|
});
|
|
@@ -59,6 +74,7 @@ function updateInfo(children: React.ReactNode): Map<string, UpdateChildInfo> {
|
|
|
59
74
|
* Pages without any tagged updates render as a plain list.
|
|
60
75
|
*/
|
|
61
76
|
export function Changelog({ children }: ChangelogProps) {
|
|
77
|
+
const labels = useLabels();
|
|
62
78
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
63
79
|
|
|
64
80
|
const allTags = useMemo(() => {
|
|
@@ -130,7 +146,7 @@ export function Changelog({ children }: ChangelogProps) {
|
|
|
130
146
|
return (
|
|
131
147
|
<div className={styles.changelog}>
|
|
132
148
|
{/* biome-ignore lint/a11y/useSemanticElements: toggle-button group labelled for the filter */}
|
|
133
|
-
<div className={styles.changelogFilters} role="group" aria-label=
|
|
149
|
+
<div className={styles.changelogFilters} role="group" aria-label={labels.filterUpdates}>
|
|
134
150
|
{allTags.map(tag => {
|
|
135
151
|
const active = selectedSet.has(tag.toLowerCase());
|
|
136
152
|
return (
|
|
@@ -142,7 +158,7 @@ export function Changelog({ children }: ChangelogProps) {
|
|
|
142
158
|
className={styles.changelogFilterButton}
|
|
143
159
|
data-active={active ? '' : undefined}
|
|
144
160
|
>
|
|
145
|
-
<Badge size="sm"
|
|
161
|
+
<Badge size="sm" tone={active ? 'primary' : undefined}>
|
|
146
162
|
{tag}
|
|
147
163
|
</Badge>
|
|
148
164
|
</button>
|
|
@@ -150,13 +166,11 @@ export function Changelog({ children }: ChangelogProps) {
|
|
|
150
166
|
})}
|
|
151
167
|
{selected.length > 0 ? (
|
|
152
168
|
<button type="button" onClick={clear} className={styles.changelogClear}>
|
|
153
|
-
|
|
169
|
+
{labels.clear}
|
|
154
170
|
</button>
|
|
155
171
|
) : null}
|
|
156
172
|
</div>
|
|
157
|
-
{visibleUpdates === 0 ?
|
|
158
|
-
<p className={styles.changelogEmpty}>No updates match the selected tags.</p>
|
|
159
|
-
) : null}
|
|
173
|
+
{visibleUpdates === 0 ? <p className={styles.changelogEmpty}>{labels.noUpdates}</p> : null}
|
|
160
174
|
{filtered}
|
|
161
175
|
</div>
|
|
162
176
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import { useMemo, useState } from 'react';
|
|
3
3
|
import { TabsContent, TabsList, Tabs as TabsPrimitive, TabsTrigger } from '@/components/ui/tabs';
|
|
4
|
+
import { useLabels } from '@/lib/label-context';
|
|
4
5
|
import { styles } from './styles';
|
|
5
6
|
import { findCodeLanguage, findCodeTitle, slugify, toElementArray } from './utils';
|
|
6
7
|
|
|
@@ -9,6 +10,7 @@ export interface CodeGroupProps {
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export function CodeGroup({ children }: CodeGroupProps) {
|
|
13
|
+
const labels = useLabels();
|
|
12
14
|
const blocks = useMemo(() => {
|
|
13
15
|
const counts = new Map<string, number>();
|
|
14
16
|
|
|
@@ -55,7 +57,7 @@ export function CodeGroup({ children }: CodeGroupProps) {
|
|
|
55
57
|
<TabsList
|
|
56
58
|
variant="line"
|
|
57
59
|
className="h-9 w-full justify-start gap-5 overflow-x-auto overflow-y-hidden rounded-none px-3 py-0 shadow-[inset_0_-1px_0_var(--border)] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
|
58
|
-
aria-label=
|
|
60
|
+
aria-label={labels.codeSnippets}
|
|
59
61
|
>
|
|
60
62
|
{blocks.map(block => (
|
|
61
63
|
<TabsTrigger
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ReactNode, useEffect, useId, useRef, useState } from 'react';
|
|
2
|
+
import { useLabels } from '@/lib/label-context';
|
|
2
3
|
import { styles } from './styles';
|
|
3
4
|
|
|
4
5
|
export type MermaidPlacement = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
@@ -63,6 +64,7 @@ export function Mermaid({
|
|
|
63
64
|
placement = 'bottom-right',
|
|
64
65
|
children,
|
|
65
66
|
}: MermaidProps) {
|
|
67
|
+
const labels = useLabels();
|
|
66
68
|
const source = mermaidSource(chart, children);
|
|
67
69
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
68
70
|
const svgHostRef = useRef<HTMLDivElement>(null);
|
|
@@ -129,29 +131,29 @@ export function Mermaid({
|
|
|
129
131
|
// biome-ignore lint/security/noDangerouslySetInnerHtml: mermaid generates this SVG from the page's own diagram source.
|
|
130
132
|
dangerouslySetInnerHTML={{ __html: svg }}
|
|
131
133
|
role="img"
|
|
132
|
-
aria-label={title ? String(title) :
|
|
134
|
+
aria-label={title ? String(title) : labels.diagram}
|
|
133
135
|
/>
|
|
134
136
|
) : (
|
|
135
|
-
<pre className={styles.mermaidFallback} title=
|
|
137
|
+
<pre className={styles.mermaidFallback} title={labels.diagramSource}>
|
|
136
138
|
{source}
|
|
137
139
|
</pre>
|
|
138
140
|
)}
|
|
139
|
-
{error ? <p className={styles.mermaidError}>
|
|
141
|
+
{error ? <p className={styles.mermaidError}>{labels.diagramError}</p> : null}
|
|
140
142
|
{showControls && svg && !error ? (
|
|
141
143
|
<div className={`${styles.mermaidControls} ${PLACEMENT_CLASS[placement]}`}>
|
|
142
144
|
<button
|
|
143
145
|
type="button"
|
|
144
|
-
aria-label=
|
|
146
|
+
aria-label={labels.zoomOut}
|
|
145
147
|
onClick={() => setZoom(z => Math.max(0.5, +(z - 0.25).toFixed(2)))}
|
|
146
148
|
>
|
|
147
149
|
−
|
|
148
150
|
</button>
|
|
149
|
-
<button type="button" aria-label=
|
|
151
|
+
<button type="button" aria-label={labels.resetView} onClick={() => setZoom(1)}>
|
|
150
152
|
⟳
|
|
151
153
|
</button>
|
|
152
154
|
<button
|
|
153
155
|
type="button"
|
|
154
|
-
aria-label=
|
|
156
|
+
aria-label={labels.zoomIn}
|
|
155
157
|
onClick={() => setZoom(z => Math.min(2.5, +(z + 0.25).toFixed(2)))}
|
|
156
158
|
>
|
|
157
159
|
+
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
+
import { useLabels } from '@/lib/label-context';
|
|
2
3
|
import { Badge } from './Badge';
|
|
3
4
|
import { decodeHtmlEntities, toElementArray } from './utils';
|
|
4
5
|
|
|
@@ -27,6 +28,7 @@ function displayValue(value: PropertiesTableRowProps['type']): string | undefine
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export function PropertiesTable({ children }: PropertiesTableProps) {
|
|
31
|
+
const labels = useLabels();
|
|
30
32
|
const rows = toElementArray<PropertiesTableRowProps>(children);
|
|
31
33
|
|
|
32
34
|
if (!rows.length) {
|
|
@@ -40,9 +42,9 @@ export function PropertiesTable({ children }: PropertiesTableProps) {
|
|
|
40
42
|
<table className="m-0 table-fixed [overflow-wrap:anywhere]">
|
|
41
43
|
<thead>
|
|
42
44
|
<tr>
|
|
43
|
-
<th className="w-[30%]">
|
|
44
|
-
<th className="w-[15%]">
|
|
45
|
-
<th>
|
|
45
|
+
<th className="w-[30%]">{labels.fieldName}</th>
|
|
46
|
+
<th className="w-[15%]">{labels.fieldType}</th>
|
|
47
|
+
<th>{labels.fieldDescription}</th>
|
|
46
48
|
</tr>
|
|
47
49
|
</thead>
|
|
48
50
|
<tbody>{rows}</tbody>
|
|
@@ -60,6 +62,7 @@ function PropertiesTableRow({
|
|
|
60
62
|
children,
|
|
61
63
|
details,
|
|
62
64
|
}: PropertiesTableRowProps) {
|
|
65
|
+
const labels = useLabels();
|
|
63
66
|
const defaultValue = displayValue(value);
|
|
64
67
|
|
|
65
68
|
return (
|
|
@@ -70,17 +73,19 @@ function PropertiesTableRow({
|
|
|
70
73
|
{name}
|
|
71
74
|
{required ? (
|
|
72
75
|
<Badge size="sm" tone="primary">
|
|
73
|
-
|
|
76
|
+
{labels.fieldRequired}
|
|
74
77
|
</Badge>
|
|
75
78
|
) : null}
|
|
76
|
-
{deprecated ? <Badge size="sm">
|
|
79
|
+
{deprecated ? <Badge size="sm">{labels.fieldDeprecated}</Badge> : null}
|
|
77
80
|
</span>
|
|
78
81
|
</td>
|
|
79
82
|
<td>{displayValue(type)}</td>
|
|
80
83
|
<td className="[&_p]:m-0">
|
|
81
84
|
{children}
|
|
82
85
|
{defaultValue ? (
|
|
83
|
-
<span className="mt-1 block text-muted-foreground">
|
|
86
|
+
<span className="mt-1 block text-muted-foreground">
|
|
87
|
+
{labels.fieldDefault} {defaultValue}
|
|
88
|
+
</span>
|
|
84
89
|
) : null}
|
|
85
90
|
</td>
|
|
86
91
|
</tr>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import { useEffect, useMemo, useState } from 'react';
|
|
3
3
|
import { TabsContent, TabsList, Tabs as TabsPrimitive, TabsTrigger } from '@/components/ui/tabs';
|
|
4
|
+
import { useLabels } from '@/lib/label-context';
|
|
4
5
|
import { slugify, toElementArray } from './utils';
|
|
5
6
|
|
|
6
7
|
interface TabChildProps {
|
|
@@ -18,6 +19,7 @@ export interface TabsProps {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export function Tabs({ children, group }: TabsProps) {
|
|
22
|
+
const labels = useLabels();
|
|
21
23
|
const tabs = useMemo(() => {
|
|
22
24
|
const counts = new Map<string, number>();
|
|
23
25
|
|
|
@@ -94,7 +96,7 @@ export function Tabs({ children, group }: TabsProps) {
|
|
|
94
96
|
<TabsList
|
|
95
97
|
variant="line"
|
|
96
98
|
className="w-full justify-start gap-5 overflow-x-auto overflow-y-hidden p-0 shadow-[inset_0_-1px_0_var(--border)] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
|
97
|
-
aria-label=
|
|
99
|
+
aria-label={labels.contentTabs}
|
|
98
100
|
>
|
|
99
101
|
{tabs.map(tab => (
|
|
100
102
|
<TabsTrigger
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
Folder as FolderIcon,
|
|
15
15
|
FolderOpen,
|
|
16
16
|
} from '@/components/icons';
|
|
17
|
+
import { useLabels } from '@/lib/label-context';
|
|
17
18
|
import { styles } from './styles';
|
|
18
19
|
|
|
19
20
|
export interface TreeFolderProps {
|
|
@@ -208,6 +209,7 @@ function visibleFocusables(root: HTMLElement): HTMLElement[] {
|
|
|
208
209
|
}
|
|
209
210
|
|
|
210
211
|
function TreeRoot({ children, style, className }: TreeProps) {
|
|
212
|
+
const labels = useLabels();
|
|
211
213
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
212
214
|
const searchRef = useRef({ text: '', timer: 0 });
|
|
213
215
|
|
|
@@ -334,7 +336,7 @@ function TreeRoot({ children, style, className }: TreeProps) {
|
|
|
334
336
|
onKeyDown={onKeyDown}
|
|
335
337
|
>
|
|
336
338
|
{/* biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: tree is the required ARIA role for this widget */}
|
|
337
|
-
<ul role="tree" aria-label=
|
|
339
|
+
<ul role="tree" aria-label={labels.fileTree} className={styles.treeList}>
|
|
338
340
|
{content}
|
|
339
341
|
</ul>
|
|
340
342
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ImgHTMLAttributes } from 'react';
|
|
2
2
|
import { Dialog, DialogContent, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
|
3
|
+
import { useLabels } from '@/lib/label-context';
|
|
3
4
|
import { cn } from '@/lib/utils';
|
|
4
5
|
|
|
5
6
|
export interface ZoomableImageProps extends ImgHTMLAttributes<HTMLImageElement> {
|
|
@@ -13,6 +14,7 @@ export function ZoomableImage({
|
|
|
13
14
|
src,
|
|
14
15
|
...props
|
|
15
16
|
}: ZoomableImageProps) {
|
|
17
|
+
const labels = useLabels();
|
|
16
18
|
const image = (
|
|
17
19
|
<img
|
|
18
20
|
{...props}
|
|
@@ -26,7 +28,7 @@ export function ZoomableImage({
|
|
|
26
28
|
return image;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
const label = alt ?
|
|
31
|
+
const label = alt ? labels.viewNamedImage.replace('{alt}', () => alt) : labels.viewImage;
|
|
30
32
|
|
|
31
33
|
return (
|
|
32
34
|
<Dialog>
|
|
@@ -47,7 +49,7 @@ export function ZoomableImage({
|
|
|
47
49
|
overlayClassName="bg-black/80 supports-backdrop-filter:backdrop-blur-sm"
|
|
48
50
|
className="flex max-h-[calc(100vh-2rem)] w-auto max-w-[calc(100vw-2rem)] items-center justify-center bg-transparent p-0 ring-0 shadow-none sm:max-w-[calc(100vw-2rem)]"
|
|
49
51
|
>
|
|
50
|
-
<DialogTitle className="sr-only">{alt ||
|
|
52
|
+
<DialogTitle className="sr-only">{alt || labels.fullSizeImage}</DialogTitle>
|
|
51
53
|
<img
|
|
52
54
|
src={src}
|
|
53
55
|
srcSet={props.srcSet}
|
|
@@ -2,6 +2,7 @@ import { Dialog as DialogPrimitive } from '@base-ui/react/dialog';
|
|
|
2
2
|
import { XIcon } from 'lucide-react';
|
|
3
3
|
import type * as React from 'react';
|
|
4
4
|
import { Button } from '@/components/ui/button';
|
|
5
|
+
import { useLabels } from '@/lib/label-context';
|
|
5
6
|
import { cn } from '@/lib/utils';
|
|
6
7
|
|
|
7
8
|
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
|
|
@@ -43,6 +44,7 @@ function DialogContent({
|
|
|
43
44
|
showCloseButton?: boolean;
|
|
44
45
|
overlayClassName?: string;
|
|
45
46
|
}) {
|
|
47
|
+
const labels = useLabels();
|
|
46
48
|
return (
|
|
47
49
|
<DialogPortal>
|
|
48
50
|
<DialogOverlay className={overlayClassName} />
|
|
@@ -61,7 +63,7 @@ function DialogContent({
|
|
|
61
63
|
render={<Button variant="ghost" className="absolute top-2 right-2" size="icon-sm" />}
|
|
62
64
|
>
|
|
63
65
|
<XIcon />
|
|
64
|
-
<span className="sr-only">
|
|
66
|
+
<span className="sr-only">{labels.close}</span>
|
|
65
67
|
</DialogPrimitive.Close>
|
|
66
68
|
)}
|
|
67
69
|
</DialogPrimitive.Popup>
|
|
@@ -83,6 +85,7 @@ function DialogFooter({
|
|
|
83
85
|
}: React.ComponentProps<'div'> & {
|
|
84
86
|
showCloseButton?: boolean;
|
|
85
87
|
}) {
|
|
88
|
+
const labels = useLabels();
|
|
86
89
|
return (
|
|
87
90
|
<div
|
|
88
91
|
data-slot="dialog-footer"
|
|
@@ -94,7 +97,9 @@ function DialogFooter({
|
|
|
94
97
|
>
|
|
95
98
|
{children}
|
|
96
99
|
{showCloseButton && (
|
|
97
|
-
<DialogPrimitive.Close render={<Button variant="outline" />}>
|
|
100
|
+
<DialogPrimitive.Close render={<Button variant="outline" />}>
|
|
101
|
+
{labels.close}
|
|
102
|
+
</DialogPrimitive.Close>
|
|
98
103
|
)}
|
|
99
104
|
</div>
|
|
100
105
|
);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { useLabels } from '@/lib/label-context';
|
|
2
|
+
|
|
3
|
+
('use client');
|
|
2
4
|
|
|
3
5
|
import { Dialog as SheetPrimitive } from '@base-ui/react/dialog';
|
|
4
6
|
import { XIcon } from 'lucide-react';
|
|
@@ -45,6 +47,7 @@ function SheetContent({
|
|
|
45
47
|
side?: 'top' | 'right' | 'bottom' | 'left';
|
|
46
48
|
showCloseButton?: boolean;
|
|
47
49
|
}) {
|
|
50
|
+
const labels = useLabels();
|
|
48
51
|
return (
|
|
49
52
|
<SheetPortal>
|
|
50
53
|
<SheetOverlay />
|
|
@@ -64,7 +67,7 @@ function SheetContent({
|
|
|
64
67
|
render={<Button variant="ghost" className="absolute top-3 right-3" size="icon-sm" />}
|
|
65
68
|
>
|
|
66
69
|
<XIcon />
|
|
67
|
-
<span className="sr-only">
|
|
70
|
+
<span className="sr-only">{labels.close}</span>
|
|
68
71
|
</SheetPrimitive.Close>
|
|
69
72
|
)}
|
|
70
73
|
</SheetPrimitive.Popup>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { resolveLocale } from '@/lib/locale';
|
|
2
|
+
import de from '@/lib/translations/de.json';
|
|
3
|
+
import en from '@/lib/translations/en.json';
|
|
4
|
+
import es from '@/lib/translations/es.json';
|
|
5
|
+
import fr from '@/lib/translations/fr.json';
|
|
6
|
+
import ja from '@/lib/translations/ja.json';
|
|
7
|
+
import zhHans from '@/lib/translations/zh-Hans.json';
|
|
8
|
+
import zhHant from '@/lib/translations/zh-Hant.json';
|
|
9
|
+
import type { ThemeLabels, Translations } from '@/lib/types';
|
|
10
|
+
|
|
11
|
+
export const englishLabels: ThemeLabels = en;
|
|
12
|
+
const dictionaries: Record<string, ThemeLabels> = {
|
|
13
|
+
en,
|
|
14
|
+
de,
|
|
15
|
+
es,
|
|
16
|
+
fr,
|
|
17
|
+
ja,
|
|
18
|
+
'zh-Hans': zhHans,
|
|
19
|
+
'zh-Hant': zhHant,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Merge from least to most specific; overrides win at each locale level. */
|
|
23
|
+
export function resolveLabels(locale: string, overrides: Translations = {}): ThemeLabels {
|
|
24
|
+
const tag = new Intl.Locale(resolveLocale(locale, 'en'));
|
|
25
|
+
const script = tag.script || (tag.language === 'zh' ? tag.maximize().script : undefined);
|
|
26
|
+
const candidates = new Set([
|
|
27
|
+
'en',
|
|
28
|
+
tag.language,
|
|
29
|
+
...(script ? [`${tag.language}-${script}`] : []),
|
|
30
|
+
tag.baseName,
|
|
31
|
+
]);
|
|
32
|
+
const labels = { ...englishLabels };
|
|
33
|
+
for (const candidate of candidates) {
|
|
34
|
+
Object.assign(labels, dictionaries[candidate], overrides[candidate]);
|
|
35
|
+
}
|
|
36
|
+
return labels;
|
|
37
|
+
}
|
package/src/lib/locale.ts
CHANGED
|
@@ -37,3 +37,28 @@ export function getTextDirection(locale: string): 'ltr' | 'rtl' {
|
|
|
37
37
|
const primary = locale.split('-')[0]?.toLowerCase() || '';
|
|
38
38
|
return RTL_LANGUAGES.has(primary) ? 'rtl' : 'ltr';
|
|
39
39
|
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A language's name written in that language, for language selectors:
|
|
43
|
+
* "ja" -> "日本語", "zh-Hant" -> "繁體中文", "es" -> "Español". Labels that
|
|
44
|
+
* are not valid locale codes (e.g. "English") are returned unchanged.
|
|
45
|
+
*/
|
|
46
|
+
export function getLanguageName(language: string): string {
|
|
47
|
+
if (!isValidLocale(language)) {
|
|
48
|
+
return language;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const locale = Intl.getCanonicalLocales(language)[0];
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const name = new Intl.DisplayNames([locale], { type: 'language', fallback: 'none' }).of(locale);
|
|
55
|
+
|
|
56
|
+
if (!name) {
|
|
57
|
+
return language;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return name.charAt(0).toLocaleUpperCase(locale) + name.slice(1);
|
|
61
|
+
} catch {
|
|
62
|
+
return language;
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/lib/openapi.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { englishLabels } from '@/lib/labels';
|
|
1
2
|
/**
|
|
2
3
|
* Runtime helpers for OpenAPI reference pages. Operation data is produced at
|
|
3
4
|
* build time by scripts/generate-openapi.mjs and reaches the client through
|
|
@@ -7,7 +8,7 @@
|
|
|
7
8
|
import type { BadgeColor } from '@/components/docs/Badge';
|
|
8
9
|
import { OPENAPI_OPERATIONS } from '@/lib/openapi.generated';
|
|
9
10
|
import { createSlugger } from '@/lib/slug';
|
|
10
|
-
import type { NormalizedOperation, TocEntry } from '@/lib/types';
|
|
11
|
+
import type { NormalizedOperation, ThemeLabels, TocEntry } from '@/lib/types';
|
|
11
12
|
|
|
12
13
|
export const METHOD_COLORS: Record<string, BadgeColor> = {
|
|
13
14
|
GET: 'green',
|
|
@@ -46,12 +47,15 @@ export function hasParameters(operation: NormalizedOperation): boolean {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
/** Parameter groups shared by the renderer and table of contents. */
|
|
49
|
-
export function operationParameterSections(
|
|
50
|
+
export function operationParameterSections(
|
|
51
|
+
operation: NormalizedOperation,
|
|
52
|
+
labels: ThemeLabels = englishLabels,
|
|
53
|
+
) {
|
|
50
54
|
const groups = [
|
|
51
|
-
{ location: 'header', name:
|
|
52
|
-
{ location: 'path', name:
|
|
53
|
-
{ location: 'query', name:
|
|
54
|
-
{ location: 'cookie', name:
|
|
55
|
+
{ location: 'header', name: labels.apiHeaders },
|
|
56
|
+
{ location: 'path', name: labels.apiPathParameters },
|
|
57
|
+
{ location: 'query', name: labels.apiQueryParameters },
|
|
58
|
+
{ location: 'cookie', name: labels.apiCookieParameters },
|
|
55
59
|
] as const;
|
|
56
60
|
|
|
57
61
|
return groups.filter(
|
|
@@ -66,7 +70,10 @@ export function operationParameterSections(operation: NormalizedOperation) {
|
|
|
66
70
|
* truth for section ids: the component, the table of contents, the content
|
|
67
71
|
* checker, and the search indexer all derive their anchors from these labels.
|
|
68
72
|
*/
|
|
69
|
-
export function operationSections(
|
|
73
|
+
export function operationSections(
|
|
74
|
+
operation: NormalizedOperation,
|
|
75
|
+
labels: ThemeLabels = englishLabels,
|
|
76
|
+
): TocEntry[] {
|
|
70
77
|
const slugger = createSlugger();
|
|
71
78
|
const names = [
|
|
72
79
|
...operationParameterSections(operation).map(section => section.name),
|
|
@@ -75,5 +82,11 @@ export function operationSections(operation: NormalizedOperation): TocEntry[] {
|
|
|
75
82
|
operation.samples.length ? 'Code samples' : undefined,
|
|
76
83
|
].filter((name): name is string => Boolean(name));
|
|
77
84
|
|
|
78
|
-
|
|
85
|
+
const translated = [
|
|
86
|
+
...operationParameterSections(operation, labels).map(section => section.name),
|
|
87
|
+
...(operation.requestBody ? [labels.apiRequestBody] : []),
|
|
88
|
+
...(operation.responses.length ? [labels.apiResponses] : []),
|
|
89
|
+
...(operation.samples.length ? [labels.apiCodeSamples] : []),
|
|
90
|
+
];
|
|
91
|
+
return names.map((name, index) => ({ name: translated[index], id: slugger.slug(name), size: 2 }));
|
|
79
92
|
}
|
package/src/lib/site-config.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
getPageByPathname as getSitePageByPathname,
|
|
9
9
|
normalizeDocsSite,
|
|
10
10
|
} from '@/lib/docs-config';
|
|
11
|
+
import { resolveLabels } from '@/lib/labels';
|
|
11
12
|
import { getTextDirection, resolveLocale } from '@/lib/locale';
|
|
12
13
|
import { DOCS_PREFIX, stripBase } from '@/lib/paths';
|
|
13
14
|
import { resolveSiteModel } from '@/lib/site-model';
|
|
@@ -53,10 +54,60 @@ export function getStandalonePage(pathname: string): StandalonePage | null {
|
|
|
53
54
|
return getStandalonePageByPathname(standalonePages, stripBase(pathname));
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
/**
|
|
57
|
+
/** Landing scope of a language: its default version. */
|
|
58
|
+
function getLanguageScope(language: string): DocsScope | undefined {
|
|
59
|
+
return docsSite.scopes.find(scope => scope.language === language && scope.isLanguageDefault);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Scope that owns the current pathname. A standalone page tagged with a
|
|
64
|
+
* `language` belongs to that language's landing scope; unknown paths and
|
|
65
|
+
* untagged standalone pages belong to the default scope.
|
|
66
|
+
*/
|
|
57
67
|
export function getScopeByPathname(pathname: string): DocsScope {
|
|
58
68
|
const page = getPageByPathname(pathname);
|
|
59
|
-
|
|
69
|
+
|
|
70
|
+
if (page) {
|
|
71
|
+
return getScopeForPage(docsSite, page);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const standalone = getStandalonePage(pathname);
|
|
75
|
+
const languageScope = standalone?.language ? getLanguageScope(standalone.language) : undefined;
|
|
76
|
+
|
|
77
|
+
return languageScope || getDefaultScope(docsSite);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The same standalone page in another language, matched by `key`. Untagged
|
|
82
|
+
* pages count as the default scope's language.
|
|
83
|
+
*/
|
|
84
|
+
export function getStandaloneCounterpart(
|
|
85
|
+
page: StandalonePage,
|
|
86
|
+
language: string,
|
|
87
|
+
): StandalonePage | null {
|
|
88
|
+
const defaultLanguage = getDefaultScope(docsSite).language;
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
standalonePages.find(
|
|
92
|
+
candidate =>
|
|
93
|
+
candidate.key === page.key && (candidate.language || defaultLanguage) === language,
|
|
94
|
+
) || null
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Home link for the current pathname: the "/" standalone page in the current
|
|
100
|
+
* language when one exists, else "/", else the docs home when nothing owns "/".
|
|
101
|
+
*/
|
|
102
|
+
export function getHomeHref(pathname: string): string {
|
|
103
|
+
const root = standalonePages.find(page => page.path === '/');
|
|
104
|
+
|
|
105
|
+
if (!root) {
|
|
106
|
+
return docsHomeUrl;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const language = getScopeByPathname(pathname).language;
|
|
110
|
+
return (language && getStandaloneCounterpart(root, language)?.path) || '/';
|
|
60
111
|
}
|
|
61
112
|
|
|
62
113
|
/** Document language and direction for a pathname, from its scope's language. */
|
|
@@ -65,6 +116,19 @@ export function getLocaleByPathname(pathname: string): { lang: string; dir: 'ltr
|
|
|
65
116
|
return { lang, dir: getTextDirection(lang) };
|
|
66
117
|
}
|
|
67
118
|
|
|
119
|
+
/** Resolve UI text on every route, including standalone pages and language switches. */
|
|
120
|
+
export function getSiteModelByPathname(pathname: string) {
|
|
121
|
+
const { lang } = getLocaleByPathname(pathname);
|
|
122
|
+
const labels = resolveLabels(lang, shisoConfig.translations);
|
|
123
|
+
const customPrompt =
|
|
124
|
+
typeof siteConfig.search === 'object' ? siteConfig.search.prompt?.trim() : undefined;
|
|
125
|
+
return {
|
|
126
|
+
...siteModel,
|
|
127
|
+
labels,
|
|
128
|
+
search: { ...siteModel.search, prompt: customPrompt || labels.searchPlaceholder },
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
68
132
|
export const siteName = siteModel.name;
|
|
69
133
|
|
|
70
134
|
/** Trailing-slash-insensitive route key for redirect matching. */
|
package/src/lib/site-model.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveCodeBlockConfig } from '@/lib/code-blocks';
|
|
2
|
+
import { resolveLabels, englishLabels as SHISO_THEME_LABELS } from '@/lib/labels';
|
|
2
3
|
import { toAbsoluteUrl, toHref } from '@/lib/paths';
|
|
3
4
|
import { resolveSearchConfig } from '@/lib/search/config';
|
|
4
5
|
import type {
|
|
@@ -18,37 +19,6 @@ import type {
|
|
|
18
19
|
ThemeLabels,
|
|
19
20
|
} from '@/lib/types';
|
|
20
21
|
|
|
21
|
-
const SHISO_THEME_LABELS: ThemeLabels = {
|
|
22
|
-
menu: 'Menu',
|
|
23
|
-
documentationNavigation: 'Documentation navigation',
|
|
24
|
-
sections: 'Sections',
|
|
25
|
-
tableOfContents: 'On this page',
|
|
26
|
-
tableOfContentsNavigation: 'Table of contents',
|
|
27
|
-
searchTitle: 'Search',
|
|
28
|
-
searching: 'Searching...',
|
|
29
|
-
searchUnavailable: 'Search unavailable',
|
|
30
|
-
noResults: 'No results',
|
|
31
|
-
lastUpdated: 'Last updated on',
|
|
32
|
-
relatedTopics: 'Related topics',
|
|
33
|
-
previousPage: 'Previous',
|
|
34
|
-
nextPage: 'Next',
|
|
35
|
-
notFound: 'Page not found',
|
|
36
|
-
dismissBanner: 'Dismiss banner',
|
|
37
|
-
toggleTheme: 'Toggle theme',
|
|
38
|
-
moreOptions: 'More options',
|
|
39
|
-
copied: 'Copied',
|
|
40
|
-
expand: 'Expand',
|
|
41
|
-
collapse: 'Collapse',
|
|
42
|
-
copyPage: 'Copy page',
|
|
43
|
-
copyPageDescription: 'Copy this page as Markdown',
|
|
44
|
-
viewMarkdown: 'View as Markdown',
|
|
45
|
-
viewMarkdownDescription: 'Open this page as plain Markdown',
|
|
46
|
-
openInChatGPT: 'Open in ChatGPT',
|
|
47
|
-
openInClaude: 'Open in Claude',
|
|
48
|
-
openInPerplexity: 'Open in Perplexity',
|
|
49
|
-
askQuestionsAboutPage: 'Ask questions about this page',
|
|
50
|
-
};
|
|
51
|
-
|
|
52
22
|
function isInternalHref(href: string): boolean {
|
|
53
23
|
return /^(?:#|\/|\.\.?\/)/.test(href);
|
|
54
24
|
}
|
|
@@ -136,10 +106,12 @@ export function resolveSiteModel(
|
|
|
136
106
|
search: resolveSearchConfig(config.search),
|
|
137
107
|
contextualOptions: config.contextual?.options || [],
|
|
138
108
|
error404: { ...config.errors?.['404'], redirect: config.errors?.['404']?.redirect !== false },
|
|
109
|
+
editLink: config.editLink || null,
|
|
110
|
+
feedback: config.feedback || null,
|
|
139
111
|
showTimestamp: config.metadata?.timestamp === true,
|
|
140
112
|
drilldown: config.interaction?.drilldown,
|
|
141
113
|
locale: shiso?.locale || 'en-US',
|
|
142
|
-
labels:
|
|
114
|
+
labels: resolveLabels(shiso?.locale || 'en-US', shiso?.translations),
|
|
143
115
|
docs,
|
|
144
116
|
};
|
|
145
117
|
}
|