@umami/shiso 1.17.0 → 1.19.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.
Files changed (64) hide show
  1. package/dist/chunks/App.js +1217 -196
  2. package/dist/chunks/architectureDiagram-5GKGNRK7.js +1 -1
  3. package/dist/chunks/chunk-GMAD6QVW.js +1 -1
  4. package/dist/chunks/cose-bilkent-JH36ORCC.js +1 -1
  5. package/dist/chunks/dist.js +1 -1
  6. package/dist/chunks/docs.js +843 -22
  7. package/dist/chunks/ganttDiagram-EL5Y4UJY.js +1 -1
  8. package/dist/chunks/src.js +1 -1
  9. package/dist/components.js +1 -1
  10. package/dist/entry-client.js +1 -1
  11. package/dist/entry-server.js +1 -1
  12. package/docs.schema.json +1164 -1131
  13. package/package.json +1 -2
  14. package/scripts/check-content.mjs +44 -15
  15. package/scripts/expand-openapi-navigation.mjs +47 -21
  16. package/scripts/generate-openapi.mjs +36 -11
  17. package/scripts/generate-search-index.mjs +19 -18
  18. package/scripts/lib/openapi-project.mjs +197 -0
  19. package/scripts/lib/openapi.mjs +257 -111
  20. package/scripts/lib/request-samples.mjs +323 -0
  21. package/scripts/load-docs-config.mjs +23 -15
  22. package/scripts/load-shiso-config.mjs +30 -1
  23. package/scripts/prerender.mjs +17 -14
  24. package/scripts/vite-docs-config.mjs +1 -0
  25. package/src/App.tsx +32 -22
  26. package/src/components/ApiPlayground.tsx +522 -0
  27. package/src/components/CodeBlock.tsx +3 -1
  28. package/src/components/DocContent.tsx +15 -4
  29. package/src/components/Docs.tsx +19 -2
  30. package/src/components/Footer.tsx +3 -1
  31. package/src/components/Header.tsx +4 -3
  32. package/src/components/LanguageSwitcher.tsx +38 -29
  33. package/src/components/OpenApiOperation.tsx +73 -20
  34. package/src/components/OpenApiSchema.tsx +97 -0
  35. package/src/components/PageActions.tsx +8 -7
  36. package/src/components/SideNav.tsx +2 -2
  37. package/src/components/docs/Changelog.tsx +5 -5
  38. package/src/components/docs/CodeGroup.tsx +3 -1
  39. package/src/components/docs/Mermaid.tsx +8 -6
  40. package/src/components/docs/PropertiesTable.tsx +11 -6
  41. package/src/components/docs/Tabs.tsx +3 -1
  42. package/src/components/docs/Tree.tsx +3 -1
  43. package/src/components/docs/ZoomableImage.tsx +4 -2
  44. package/src/components/ui/dialog.tsx +7 -2
  45. package/src/components/ui/sheet.tsx +5 -2
  46. package/src/lib/label-context.tsx +7 -0
  47. package/src/lib/labels.ts +37 -0
  48. package/src/lib/openapi.generated.ts +2 -1
  49. package/src/lib/openapi.ts +123 -16
  50. package/src/lib/site-config.ts +66 -2
  51. package/src/lib/site-model.ts +14 -32
  52. package/src/lib/standalone-pages.ts +15 -1
  53. package/src/lib/translations/de.json +99 -0
  54. package/src/lib/translations/en.json +99 -0
  55. package/src/lib/translations/es.json +99 -0
  56. package/src/lib/translations/fr.json +99 -0
  57. package/src/lib/translations/ja.json +99 -0
  58. package/src/lib/translations/zh-Hans.json +99 -0
  59. package/src/lib/translations/zh-Hant.json +99 -0
  60. package/src/lib/types.ts +109 -37
  61. package/types/config.d.ts +7 -1
  62. package/types/labels.d.ts +101 -0
  63. package/vite.config.ts +3 -4
  64. package/CHANGELOG.md +0 -8
@@ -1,20 +1,28 @@
1
1
  import { useLocation, useNavigate } from 'react-router';
2
- import { Check, ChevronRight } from '@/components/icons';
2
+ import { ChevronRight } from '@/components/icons';
3
3
  import { Button } from '@/components/ui/button';
4
4
  import {
5
5
  DropdownMenu,
6
6
  DropdownMenuContent,
7
- DropdownMenuItem,
7
+ DropdownMenuRadioGroup,
8
+ DropdownMenuRadioItem,
8
9
  DropdownMenuTrigger,
9
10
  } from '@/components/ui/dropdown-menu';
10
11
  import { getLanguageScopes } from '@/lib/docs-config';
11
12
  import { getLanguageName, isValidLocale } from '@/lib/locale';
12
- import { docsSite, getScopeByPathname } from '@/lib/site-config';
13
+ import {
14
+ docsSite,
15
+ getScopeByPathname,
16
+ getStandaloneCounterpart,
17
+ getStandalonePage,
18
+ } from '@/lib/site-config';
13
19
 
14
20
  /**
15
21
  * Language selector for multi-language sites. Each option is a language's
16
- * landing scope — its default version — so switching languages always lands
17
- * on that language's default-version first page. Hidden languages never
22
+ * landing scope — its default version — so switching languages lands on that
23
+ * language's default-version first page. On a standalone page (e.g. a home
24
+ * page) it lands on the same page in the chosen language when one exists.
25
+ * Hidden languages never
18
26
  * appear as options. Each language is shown by its native name, e.g. "ja"
19
27
  * as "日本語".
20
28
  */
@@ -23,6 +31,7 @@ export function LanguageSwitcher() {
23
31
  const navigate = useNavigate();
24
32
  const current = getScopeByPathname(pathname);
25
33
  const options = getLanguageScopes(docsSite);
34
+ const standalone = getStandalonePage(pathname);
26
35
 
27
36
  if (!current.language || (options.length < 2 && !current.hidden)) {
28
37
  return null;
@@ -31,35 +40,35 @@ export function LanguageSwitcher() {
31
40
  return (
32
41
  <DropdownMenu>
33
42
  <DropdownMenuTrigger
34
- render={
35
- <Button
36
- variant="outline"
37
- className="h-auto gap-1.5 rounded-md bg-card px-2.5 py-1.5 text-sm font-medium text-foreground"
38
- />
39
- }
43
+ render={<Button variant="outline" />}
44
+ aria-label={`Language: ${getLanguageName(current.language)}`}
40
45
  >
41
46
  {getLanguageName(current.language)}
42
47
  <ChevronRight className="size-3.5 rotate-90 text-muted-foreground" />
43
48
  </DropdownMenuTrigger>
44
49
  <DropdownMenuContent align="start" className="min-w-32">
45
- {options.map(scope => (
46
- <DropdownMenuItem
47
- key={scope.id}
48
- onClick={() => {
49
- if (scope.language !== current.language) {
50
- navigate(scope.firstPageUrl);
51
- }
52
- }}
53
- >
54
- <span
55
- className="grow"
56
- lang={isValidLocale(scope.language) ? scope.language : undefined}
57
- >
58
- {scope.language ? getLanguageName(scope.language) : null}
59
- </span>
60
- {scope.language === current.language ? <Check className="size-3.5" /> : null}
61
- </DropdownMenuItem>
62
- ))}
50
+ <DropdownMenuRadioGroup
51
+ value={current.language}
52
+ onValueChange={language => {
53
+ const scope = options.find(option => option.language === language);
54
+ if (scope && scope.language !== current.language) {
55
+ const counterpart =
56
+ standalone && scope.language
57
+ ? getStandaloneCounterpart(standalone, scope.language)
58
+ : null;
59
+
60
+ navigate(counterpart?.path || scope.firstPageUrl);
61
+ }
62
+ }}
63
+ >
64
+ {options.map(scope => (
65
+ <DropdownMenuRadioItem key={scope.id} value={scope.language}>
66
+ <span lang={isValidLocale(scope.language) ? scope.language : undefined}>
67
+ {scope.language ? getLanguageName(scope.language) : null}
68
+ </span>
69
+ </DropdownMenuRadioItem>
70
+ ))}
71
+ </DropdownMenuRadioGroup>
63
72
  </DropdownMenuContent>
64
73
  </DropdownMenu>
65
74
  );
@@ -1,18 +1,28 @@
1
+ import { ApiPlayground } from '@/components/ApiPlayground';
1
2
  import { CodeBlock } from '@/components/CodeBlock';
2
3
  import { Badge } from '@/components/docs/Badge';
3
4
  import { CodeGroup } from '@/components/docs/CodeGroup';
4
5
  import { Expandable } from '@/components/docs/Expandable';
5
6
  import { PropertiesTable } from '@/components/docs/PropertiesTable';
6
- import { operationParameterSections, operationSections, statusColor } from '@/lib/openapi';
7
- import type { NormalizedOperation, SchemaNode } from '@/lib/types';
7
+ import { useLabels } from '@/lib/label-context';
8
+ import {
9
+ operationParameterSections,
10
+ operationSections,
11
+ securityFieldName,
12
+ securityForLocation,
13
+ statusColor,
14
+ } from '@/lib/openapi';
15
+ import type { NormalizedOperation, SchemaNode, SecurityScheme, ThemeLabels } from '@/lib/types';
16
+ import { securityPlaceholder } from '../../scripts/lib/request-samples.mjs';
8
17
 
9
18
  function FieldChildren({ node }: { node: SchemaNode }) {
19
+ const labels = useLabels();
10
20
  return (
11
21
  <>
12
22
  {node.description}
13
23
  {node.enum && node.enum.length > 0 && (
14
24
  <div className="mt-1">
15
- Options:{' '}
25
+ {labels.fieldOptions}{' '}
16
26
  {node.enum.map((value, index) => (
17
27
  <span key={value}>
18
28
  {index > 0 && ', '}
@@ -25,12 +35,14 @@ function FieldChildren({ node }: { node: SchemaNode }) {
25
35
  );
26
36
  }
27
37
 
28
- function schemaDetails(node: SchemaNode) {
38
+ function schemaDetails(node: SchemaNode, labels: ThemeLabels) {
29
39
  if (!node.children?.length) return undefined;
30
40
 
31
41
  return (
32
42
  <Expandable
33
- title={node.type === 'oneOf' || node.type === 'anyOf' ? 'Allowed types' : 'properties'}
43
+ title={
44
+ node.type === 'oneOf' || node.type === 'anyOf' ? labels.allowedTypes : labels.properties
45
+ }
34
46
  >
35
47
  <SchemaTable nodes={node.children} />
36
48
  </Expandable>
@@ -38,6 +50,7 @@ function schemaDetails(node: SchemaNode) {
38
50
  }
39
51
 
40
52
  function SchemaTable({ nodes }: { nodes: SchemaNode[] }) {
53
+ const labels = useLabels();
41
54
  return (
42
55
  <PropertiesTable>
43
56
  {nodes.map(node => (
@@ -48,7 +61,7 @@ function SchemaTable({ nodes }: { nodes: SchemaNode[] }) {
48
61
  required={node.required}
49
62
  deprecated={node.deprecated}
50
63
  default={node.default}
51
- details={schemaDetails(node)}
64
+ details={schemaDetails(node, labels)}
52
65
  >
53
66
  <FieldChildren node={node} />
54
67
  </PropertiesTable.Row>
@@ -99,25 +112,63 @@ function HighlightedCode({
99
112
  );
100
113
  }
101
114
 
115
+ /** How a credential is sent, e.g. "Bearer <token>" or "<api-key>". */
116
+ function credentialExample(scheme: SecurityScheme): string {
117
+ const placeholder = securityPlaceholder(scheme);
118
+ if (scheme.type === 'apiKey') return placeholder;
119
+ const prefix =
120
+ scheme.type === 'http' && scheme.scheme && scheme.scheme !== 'bearer'
121
+ ? scheme.scheme.charAt(0).toUpperCase() + scheme.scheme.slice(1)
122
+ : 'Bearer';
123
+ return `${prefix} ${placeholder}`;
124
+ }
125
+
102
126
  export interface OpenApiOperationProps {
103
127
  operation: NormalizedOperation;
128
+ /** Renders the "Try it" panel ahead of the reference; carries the proxy setting. */
129
+ playground?: { proxy?: string } | false;
104
130
  }
105
131
 
106
132
  /** The generated reference for one API operation, rendered under the page body. */
107
- export function OpenApiOperation({ operation }: OpenApiOperationProps) {
108
- const sections = new Map(operationSections(operation).map(entry => [entry.name, entry.id]));
133
+ export function OpenApiOperation({
134
+ operation,
135
+ playground: requested = false,
136
+ }: OpenApiOperationProps) {
137
+ const labels = useLabels();
138
+ // Webhooks describe requests the API sends, so there is nothing to try.
139
+ const playground = operation.webhook ? false : requested;
140
+ const options = { playground: !!playground };
141
+ const sectionEntries = operationSections(operation, labels, options);
142
+ // Ids come from the untranslated names so anchors are stable across locales.
143
+ const sections = new Map(
144
+ operationSections(operation, undefined, options).map(entry => [entry.name, entry.id]),
145
+ );
146
+ const parameterSections = operationParameterSections(operation, labels);
147
+ const parameterOffset = playground ? 1 : 0;
109
148
 
110
149
  return (
111
150
  <div className="docs-markdown">
112
- {operationParameterSections(operation).map(({ location, name }) => (
151
+ {playground && (
152
+ <section>
153
+ <h2 id={sections.get('Try it')}>{labels.apiPlayground}</h2>
154
+ <ApiPlayground operation={operation} proxy={playground.proxy} />
155
+ </section>
156
+ )}
157
+ {parameterSections.map(({ location, name }, index) => (
113
158
  <section key={location}>
114
- <h2 id={sections.get(name)}>{name}</h2>
159
+ <h2 id={sectionEntries[index + parameterOffset].id}>{name}</h2>
115
160
  <PropertiesTable>
116
- {location === 'header' && operation.security.length > 0 && (
117
- <PropertiesTable.Row name="Authorization" type="string" required>
118
- Authentication credentials, e.g. <code>Bearer &lt;token&gt;</code>.
161
+ {securityForLocation(operation, location).map(scheme => (
162
+ <PropertiesTable.Row
163
+ key={`security:${scheme.name}`}
164
+ name={securityFieldName(scheme)}
165
+ type="string"
166
+ required
167
+ >
168
+ {scheme.description || labels.apiCredentials}{' '}
169
+ <code>{credentialExample(scheme)}</code>.
119
170
  </PropertiesTable.Row>
120
- )}
171
+ ))}
121
172
  {operation.parameters[location].map(parameter => (
122
173
  <PropertiesTable.Row
123
174
  key={parameter.name}
@@ -126,7 +177,7 @@ export function OpenApiOperation({ operation }: OpenApiOperationProps) {
126
177
  required={parameter.required}
127
178
  deprecated={parameter.deprecated}
128
179
  default={parameter.default}
129
- details={schemaDetails(parameter)}
180
+ details={schemaDetails(parameter, labels)}
130
181
  >
131
182
  <FieldChildren node={parameter} />
132
183
  </PropertiesTable.Row>
@@ -136,12 +187,14 @@ export function OpenApiOperation({ operation }: OpenApiOperationProps) {
136
187
  ))}
137
188
  {operation.requestBody && (
138
189
  <section>
139
- <h2 id={sections.get('Request body')}>Request body</h2>
190
+ <h2 id={sections.get(operation.webhook ? 'Payload' : 'Request body')}>
191
+ {operation.webhook ? labels.apiPayload : labels.apiRequestBody}
192
+ </h2>
140
193
  <SchemaFields node={operation.requestBody.schema} />
141
194
  {operation.requestBody.example && (
142
195
  <HighlightedCode
143
196
  language="json"
144
- title="Example request"
197
+ title={labels.apiExampleRequest}
145
198
  html={operation.requestBody.exampleHtml}
146
199
  source={operation.requestBody.example}
147
200
  />
@@ -150,7 +203,7 @@ export function OpenApiOperation({ operation }: OpenApiOperationProps) {
150
203
  )}
151
204
  {operation.responses.length > 0 && (
152
205
  <section>
153
- <h2 id={sections.get('Responses')}>Responses</h2>
206
+ <h2 id={sections.get('Responses')}>{labels.apiResponses}</h2>
154
207
  {operation.responses.map(response => (
155
208
  <div key={response.status} className="mt-6 first:mt-0">
156
209
  <div className="flex items-center gap-2">
@@ -165,7 +218,7 @@ export function OpenApiOperation({ operation }: OpenApiOperationProps) {
165
218
  {response.example && (
166
219
  <HighlightedCode
167
220
  language="json"
168
- title={`${response.status} example`}
221
+ title={labels.apiExampleResponse.replace('{status}', () => response.status)}
169
222
  html={response.exampleHtml}
170
223
  source={response.example}
171
224
  />
@@ -176,7 +229,7 @@ export function OpenApiOperation({ operation }: OpenApiOperationProps) {
176
229
  )}
177
230
  {operation.samples.length > 0 && (
178
231
  <section>
179
- <h2 id={sections.get('Code samples')}>Code samples</h2>
232
+ <h2 id={sections.get('Code samples')}>{labels.apiCodeSamples}</h2>
180
233
  <CodeGroup>
181
234
  {operation.samples.map(sample => (
182
235
  <HighlightedCode
@@ -0,0 +1,97 @@
1
+ import { CodeBlock } from '@/components/CodeBlock';
2
+ import { Expandable } from '@/components/docs/Expandable';
3
+ import { PropertiesTable } from '@/components/docs/PropertiesTable';
4
+ import { useLabels } from '@/lib/label-context';
5
+ import { schemaSections } from '@/lib/openapi';
6
+ import type { SchemaNode, SchemaPage, ThemeLabels } from '@/lib/types';
7
+
8
+ function FieldChildren({ node }: { node: SchemaNode }) {
9
+ const labels = useLabels();
10
+ return (
11
+ <>
12
+ {node.description}
13
+ {node.enum && node.enum.length > 0 && (
14
+ <div className="mt-1">
15
+ {labels.fieldOptions}{' '}
16
+ {node.enum.map((value, index) => (
17
+ <span key={value}>
18
+ {index > 0 && ', '}
19
+ <code>{value}</code>
20
+ </span>
21
+ ))}
22
+ </div>
23
+ )}
24
+ </>
25
+ );
26
+ }
27
+
28
+ function SchemaTable({ nodes, labels }: { nodes: SchemaNode[]; labels: ThemeLabels }) {
29
+ return (
30
+ <PropertiesTable>
31
+ {nodes.map(node => (
32
+ <PropertiesTable.Row
33
+ key={node.name || node.type}
34
+ name={node.name || node.type}
35
+ type={node.type}
36
+ required={node.required}
37
+ deprecated={node.deprecated}
38
+ default={node.default}
39
+ details={
40
+ node.children?.length ? (
41
+ <Expandable
42
+ title={
43
+ node.type === 'oneOf' || node.type === 'anyOf'
44
+ ? labels.allowedTypes
45
+ : labels.properties
46
+ }
47
+ >
48
+ <SchemaTable nodes={node.children} labels={labels} />
49
+ </Expandable>
50
+ ) : undefined
51
+ }
52
+ >
53
+ <FieldChildren node={node} />
54
+ </PropertiesTable.Row>
55
+ ))}
56
+ </PropertiesTable>
57
+ );
58
+ }
59
+
60
+ export interface OpenApiSchemaProps {
61
+ page: SchemaPage;
62
+ }
63
+
64
+ /** The generated reference for one named component schema. */
65
+ export function OpenApiSchema({ page }: OpenApiSchemaProps) {
66
+ const labels = useLabels();
67
+ const sections = new Map(schemaSections(page, labels).map(entry => [entry.id, entry.name]));
68
+ const properties = sections.get('properties');
69
+ const example = sections.get('example');
70
+
71
+ return (
72
+ <div className="docs-markdown">
73
+ {properties && (
74
+ <section>
75
+ <h2 id="properties">{properties}</h2>
76
+ <SchemaTable nodes={page.schema.children ?? []} labels={labels} />
77
+ </section>
78
+ )}
79
+ {example && page.example && (
80
+ <section>
81
+ <h2 id="example">{example}</h2>
82
+ <CodeBlock data-language="json" data-title={page.name}>
83
+ {page.exampleHtml ? (
84
+ <code
85
+ className="language-json"
86
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: build-time generated markup from the project's own OpenAPI spec.
87
+ dangerouslySetInnerHTML={{ __html: page.exampleHtml }}
88
+ />
89
+ ) : (
90
+ <code className="language-json">{page.example}</code>
91
+ )}
92
+ </CodeBlock>
93
+ </section>
94
+ )}
95
+ </div>
96
+ );
97
+ }
@@ -90,7 +90,7 @@ export function PageActions({
90
90
  rel="noopener noreferrer"
91
91
  className="ml-auto inline-flex shrink-0 items-center gap-1.5 text-right no-underline hover:text-primary"
92
92
  >
93
- {site.editLink?.label || 'Edit this page'}
93
+ {site.editLink?.label || site.labels.editPage}
94
94
  <ExternalLink size={14} aria-hidden="true" />
95
95
  </a>
96
96
  )}
@@ -103,11 +103,11 @@ export function PageActions({
103
103
  data-pagefind-ignore
104
104
  >
105
105
  <fieldset
106
- aria-label={feedback.prompt || 'Was this page helpful?'}
106
+ aria-label={feedback.prompt || site.labels.feedbackPrompt}
107
107
  className="flex flex-wrap items-center gap-2"
108
108
  >
109
109
  <span className="mr-2 text-muted-foreground">
110
- {feedback.prompt || 'Was this page helpful?'}
110
+ {feedback.prompt || site.labels.feedbackPrompt}
111
111
  </span>
112
112
  {[true, false].map(helpful => (
113
113
  <button
@@ -118,14 +118,15 @@ export function PageActions({
118
118
  onClick={() => void submit(helpful)}
119
119
  className="rounded-md border border-border px-3 py-1.5 text-foreground hover:bg-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:cursor-default disabled:opacity-60 aria-pressed:border-primary aria-pressed:text-primary"
120
120
  >
121
- {helpful ? feedback.helpfulLabel || 'Yes' : feedback.unhelpfulLabel || 'No'}
121
+ {helpful
122
+ ? feedback.helpfulLabel || site.labels.feedbackYes
123
+ : feedback.unhelpfulLabel || site.labels.feedbackNo}
122
124
  </button>
123
125
  ))}
124
126
  </fieldset>
125
127
  <div role="status" aria-live="polite" className="text-muted-foreground">
126
- {status === 'success' && (feedback.successMessage || 'Thanks for your feedback!')}
127
- {status === 'error' &&
128
- (feedback.errorMessage || 'Could not send feedback. Please try again.')}
128
+ {status === 'success' && (feedback.successMessage || site.labels.feedbackSuccess)}
129
+ {status === 'error' && (feedback.errorMessage || site.labels.feedbackError)}
129
130
  </div>
130
131
  </div>
131
132
  )}
@@ -8,7 +8,7 @@ import { Button } from '@/components/ui/button';
8
8
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
9
9
  import { ScrollArea } from '@/components/ui/scroll-area';
10
10
  import { flattenNav, isNodeHidden } from '@/lib/docs-config';
11
- import { methodColor } from '@/lib/openapi';
11
+ import { methodBadgeText, methodColor } from '@/lib/openapi';
12
12
  import type { DocsTab, NavGroupNode, NavNode } from '@/lib/types';
13
13
  import { cn } from '@/lib/utils';
14
14
 
@@ -281,7 +281,7 @@ function NavNodes({
281
281
  size="xs"
282
282
  className="w-10 shrink-0 justify-center whitespace-nowrap font-mono"
283
283
  >
284
- {method === 'DELETE' ? 'DEL' : method}
284
+ {methodBadgeText(method)}
285
285
  </MethodBadge>
286
286
  ) : null}
287
287
  {label}
@@ -7,6 +7,7 @@ import {
7
7
  useMemo,
8
8
  } from 'react';
9
9
  import { useSearchParams } from 'react-router';
10
+ import { useLabels } from '@/lib/label-context';
10
11
  import { Badge } from './Badge';
11
12
  import { styles } from './styles';
12
13
  import { Update } from './Update';
@@ -73,6 +74,7 @@ function updateInfo(children: React.ReactNode): Map<string, UpdateChildInfo> {
73
74
  * Pages without any tagged updates render as a plain list.
74
75
  */
75
76
  export function Changelog({ children }: ChangelogProps) {
77
+ const labels = useLabels();
76
78
  const [searchParams, setSearchParams] = useSearchParams();
77
79
 
78
80
  const allTags = useMemo(() => {
@@ -144,7 +146,7 @@ export function Changelog({ children }: ChangelogProps) {
144
146
  return (
145
147
  <div className={styles.changelog}>
146
148
  {/* biome-ignore lint/a11y/useSemanticElements: toggle-button group labelled for the filter */}
147
- <div className={styles.changelogFilters} role="group" aria-label="Filter updates by tag">
149
+ <div className={styles.changelogFilters} role="group" aria-label={labels.filterUpdates}>
148
150
  {allTags.map(tag => {
149
151
  const active = selectedSet.has(tag.toLowerCase());
150
152
  return (
@@ -164,13 +166,11 @@ export function Changelog({ children }: ChangelogProps) {
164
166
  })}
165
167
  {selected.length > 0 ? (
166
168
  <button type="button" onClick={clear} className={styles.changelogClear}>
167
- Clear
169
+ {labels.clear}
168
170
  </button>
169
171
  ) : null}
170
172
  </div>
171
- {visibleUpdates === 0 ? (
172
- <p className={styles.changelogEmpty}>No updates match the selected tags.</p>
173
- ) : null}
173
+ {visibleUpdates === 0 ? <p className={styles.changelogEmpty}>{labels.noUpdates}</p> : null}
174
174
  {filtered}
175
175
  </div>
176
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="Code snippets"
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) : 'Diagram'}
134
+ aria-label={title ? String(title) : labels.diagram}
133
135
  />
134
136
  ) : (
135
- <pre className={styles.mermaidFallback} title="Diagram source">
137
+ <pre className={styles.mermaidFallback} title={labels.diagramSource}>
136
138
  {source}
137
139
  </pre>
138
140
  )}
139
- {error ? <p className={styles.mermaidError}>Could not render this diagram.</p> : null}
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="Zoom out"
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="Reset view" onClick={() => setZoom(1)}>
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="Zoom in"
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%]">Name</th>
44
- <th className="w-[15%]">Type</th>
45
- <th>Description</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
- required
76
+ {labels.fieldRequired}
74
77
  </Badge>
75
78
  ) : null}
76
- {deprecated ? <Badge size="sm">deprecated</Badge> : null}
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">Default: {defaultValue}</span>
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="Content tabs"
99
+ aria-label={labels.contentTabs}
98
100
  >
99
101
  {tabs.map(tab => (
100
102
  <TabsTrigger