Translate OpenAPI blocks (#3166)

This commit is contained in:
Greg Bergé
2025-04-18 13:29:55 +02:00
committed by GitHub
parent d67699a1f1
commit 8ed1bda2f2
52 changed files with 873 additions and 198 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@gitbook/react-openapi": patch
"gitbook": patch
---
Translate OpenAPI blocks
+17 -17
View File
@@ -10,18 +10,18 @@ const testCases: TestsCase[] = [
{ name: 'OpenAPI', url: '/snyk-api/reference/apps', run: waitForCookiesDialog },
],
},
{
name: 'Nexthink',
contentBaseURL: 'https://docs.nexthink.com',
tests: [
{
name: 'Home',
url: '/',
screenshot: { waitForTOCScrolling: false },
run: waitForCookiesDialog,
},
],
},
// {
// name: 'Nexthink',
// contentBaseURL: 'https://docs.nexthink.com',
// tests: [
// {
// name: 'Home',
// url: '/',
// screenshot: { waitForTOCScrolling: false },
// run: waitForCookiesDialog,
// },
// ],
// },
{
name: 'asiksupport-stg.dto.kemkes.go.id',
contentBaseURL: 'https://asiksupport-stg.dto.kemkes.go.id',
@@ -157,11 +157,11 @@ const testCases: TestsCase[] = [
contentBaseURL: 'https://wiki.redmodding.org',
tests: [{ name: 'Home', url: '/' }],
},
{
name: 'docs.cherry-ai.com',
contentBaseURL: 'https://docs.cherry-ai.com',
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
},
// {
// name: 'docs.cherry-ai.com',
// contentBaseURL: 'https://docs.cherry-ai.com',
// tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
// },
{
name: 'docs.snyk.io',
contentBaseURL: 'https://docs.snyk.io',
@@ -51,7 +51,7 @@ async function OpenAPIOperationBody(props: BlockProps<AnyOpenAPIOperationsBlock>
return (
<BaseOpenAPIOperation
data={data}
context={getOpenAPIContext({ props, specUrl })}
context={getOpenAPIContext({ props, specUrl, context: context.contentContext })}
className="openapi-block"
/>
);
@@ -51,7 +51,7 @@ async function OpenAPISchemasBody(props: BlockProps<OpenAPISchemasBlock>) {
<BaseOpenAPISchemas
schemas={data.schemas}
grouped={block.data.grouped}
context={getOpenAPIContext({ props, specUrl })}
context={getOpenAPIContext({ props, specUrl, context: context.contentContext })}
className="openapi-block"
/>
);
@@ -51,7 +51,7 @@ async function OpenAPIWebhookBody(props: BlockProps<OpenAPIWebhookBlock>) {
return (
<BaseOpenAPIWebhook
data={data}
context={getOpenAPIContext({ props, specUrl })}
context={getOpenAPIContext({ props, specUrl, context: context.contentContext })}
className="openapi-block"
/>
);
@@ -1,6 +1,6 @@
import type { JSONDocument } from '@gitbook/api';
import { Icon } from '@gitbook/icons';
import type { OpenAPIContext } from '@gitbook/react-openapi';
import { type OpenAPIContextInput, checkIsValidLocale } from '@gitbook/react-openapi';
import { tcls } from '@/lib/tailwind';
@@ -11,11 +11,13 @@ import { Heading } from '../Heading';
import './scalar.css';
import './style.css';
import { DEFAULT_LOCALE, getCustomizationLocale } from '@/intl/server';
import type {
AnyOpenAPIOperationsBlock,
OpenAPISchemasBlock,
OpenAPIWebhookBlock,
} from '@/lib/openapi/types';
import type { GitBookAnyContext } from '@v2/lib/context';
/**
* Get the OpenAPI context to render a block.
@@ -23,9 +25,17 @@ import type {
export function getOpenAPIContext(args: {
props: BlockProps<AnyOpenAPIOperationsBlock | OpenAPISchemasBlock | OpenAPIWebhookBlock>;
specUrl: string;
}): OpenAPIContext {
const { props, specUrl } = args;
context: GitBookAnyContext | undefined;
}): OpenAPIContextInput {
const { props, specUrl, context } = args;
const { block } = props;
const customization = context && 'customization' in context ? context.customization : null;
const customizationLocale = customization
? getCustomizationLocale(customization)
: DEFAULT_LOCALE;
const locale = checkIsValidLocale(customizationLocale) ? customizationLocale : DEFAULT_LOCALE;
return {
specUrl,
icons: {
@@ -73,5 +83,6 @@ export function getOpenAPIContext(args: {
defaultInteractiveOpened: props.context.mode === 'print',
id: block.meta?.id,
blockKey: block.key,
locale,
};
}
@@ -229,19 +229,19 @@
}
.openapi-schema-required {
@apply text-warning-subtle text-[0.813rem];
@apply text-warning-subtle text-[0.813rem] lowercase;
}
.openapi-schema-optional {
@apply text-info-subtle text-[0.813rem];
@apply text-info-subtle text-[0.813rem] lowercase;
}
.openapi-schema-readonly {
@apply text-primary-subtle/9 text-[0.813rem];
@apply text-primary-subtle/9 text-[0.813rem] lowercase;
}
.openapi-schema-writeonly {
@apply text-success dark:text-success-subtle/9 text-[0.813rem];
@apply text-success dark:text-success-subtle/9 text-[0.813rem] lowercase;
}
.openapi-schema-type {
+12 -3
View File
@@ -4,17 +4,26 @@ import { type TranslationLanguage, languages } from './translations';
export * from './translate';
export const DEFAULT_LOCALE = 'en';
/**
* Get the locale of the customization.
*/
export function getCustomizationLocale(customization: SiteCustomizationSettings): string {
return customization.internationalization.locale;
}
/**
* Create the translation context for a space to use in the server components.
*/
export function getSpaceLanguage(customization: SiteCustomizationSettings): TranslationLanguage {
const fallback = languages.en;
const fallback = languages[DEFAULT_LOCALE];
const { locale } = customization.internationalization;
const locale = getCustomizationLocale(customization);
let language = fallback;
// @ts-ignore
if (locale !== 'en' && languages[locale]) {
if (locale !== DEFAULT_LOCALE && languages[locale]) {
// @ts-ignore
language = languages[locale];
}
@@ -102,6 +102,7 @@ export function InteractiveSection(props: {
) : null}
{header}
</SectionHeaderContent>
{/* biome-ignore lint/a11y/useKeyWithClickEvents: we prevent default here */}
<div
className={clsx(
'openapi-section-header-controls',
@@ -6,9 +6,10 @@ import {
import { OpenAPICodeSampleBody } from './OpenAPICodeSampleSelector';
import { ScalarApiButton } from './ScalarApiButton';
import { type CodeSampleGenerator, codeSampleGenerators } from './code-samples';
import { type OpenAPIContext, getOpenAPIClientContext } from './context';
import { generateMediaTypeExamples, generateSchemaExample } from './generateSchemaExample';
import { stringifyOpenAPI } from './stringifyOpenAPI';
import type { OpenAPIContext, OpenAPIOperationData } from './types';
import type { OpenAPIOperationData } from './types';
import { getDefaultServerURL } from './util/server';
import { checkIsReference } from './utils';
@@ -44,7 +45,12 @@ export function OpenAPICodeSample(props: {
}
return (
<OpenAPICodeSampleBody data={data} items={samples} selectIcon={context.icons.chevronDown} />
<OpenAPICodeSampleBody
context={getOpenAPIClientContext(context)}
data={data}
items={samples}
selectIcon={context.icons.chevronDown}
/>
);
}
@@ -215,7 +221,14 @@ function OpenAPICodeSampleFooter(props: {
) : (
<span />
)}
{!hideTryItPanel && <ScalarApiButton method={method} path={path} specUrl={specUrl} />}
{!hideTryItPanel && (
<ScalarApiButton
context={getOpenAPIClientContext(context)}
method={method}
path={path}
specUrl={specUrl}
/>
)}
</div>
);
}
@@ -6,6 +6,7 @@ import { useStore } from 'zustand';
import { OpenAPIPath } from './OpenAPIPath';
import { OpenAPISelect, OpenAPISelectItem } from './OpenAPISelect';
import { StaticSection } from './StaticSection';
import type { OpenAPIClientContext } from './context';
import { getOrCreateStoreByKey } from './getOrCreateStoreByKey';
import type { OpenAPIOperationData } from './types';
@@ -26,12 +27,13 @@ function OpenAPICodeSampleHeader(props: {
items: CodeSampleItem[];
data: OpenAPIOperationData;
selectIcon?: React.ReactNode;
context: OpenAPIClientContext;
}) {
const { data, items, selectIcon } = props;
const { data, items, selectIcon, context } = props;
return (
<>
<OpenAPIPath canCopy={false} withServer={false} data={data} />
<OpenAPIPath context={context} canCopy={false} withServer={false} data={data} />
{items.length > 1 ? (
<OpenAPISelect
icon={selectIcon}
@@ -56,8 +58,9 @@ export function OpenAPICodeSampleBody(props: {
items: CodeSampleItem[];
data: OpenAPIOperationData;
selectIcon?: React.ReactNode;
context: OpenAPIClientContext;
}) {
const { items, data, selectIcon } = props;
const { items, data, selectIcon, context } = props;
if (!items[0]) {
throw new Error('No items provided');
}
@@ -72,7 +75,14 @@ export function OpenAPICodeSampleBody(props: {
return (
<StaticSection
header={<OpenAPICodeSampleHeader selectIcon={selectIcon} data={data} items={items} />}
header={
<OpenAPICodeSampleHeader
context={context}
selectIcon={selectIcon}
data={data}
items={items}
/>
}
className="openapi-codesample"
>
<div id={selected.key as string} className="openapi-codesample-panel">
@@ -2,11 +2,14 @@
import { useState } from 'react';
import { Button, type ButtonProps, Tooltip, TooltipTrigger } from 'react-aria-components';
import type { OpenAPIClientContext } from './context';
import { t } from './translate';
export function OpenAPICopyButton(
props: ButtonProps & {
value: string;
children: React.ReactNode;
context: OpenAPIClientContext;
label?: string;
/**
* Whether to show a tooltip.
@@ -15,7 +18,7 @@ export function OpenAPICopyButton(
withTooltip?: boolean;
}
) {
const { value, label, children, onPress, className, withTooltip = true } = props;
const { value, label, children, onPress, className, context, withTooltip = true } = props;
const [copied, setCopied] = useState(false);
const [isOpen, setIsOpen] = useState(false);
@@ -60,7 +63,9 @@ export function OpenAPICopyButton(
offset={4}
className="openapi-tooltip"
>
{copied ? 'Copied' : label || 'Copy to clipboard'}
{copied
? t(context.translation, 'copied')
: label || t(context.translation, 'copy_to_clipboard')}
</Tooltip>
</TooltipTrigger>
);
@@ -1,7 +1,8 @@
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
import type { OpenAPIContext, OpenAPIUniversalContext } from './context';
import { json2xml } from './json2xml';
import { stringifyOpenAPI } from './stringifyOpenAPI';
import type { OpenAPIContext } from './types';
import { t } from './translate';
/**
* Display an example.
@@ -15,7 +16,7 @@ export function OpenAPIExample(props: {
const code = stringifyExample({ example, xml: syntax === 'xml' });
if (code === null) {
return <OpenAPIEmptyExample />;
return <OpenAPIEmptyExample context={context} />;
}
return context.renderCodeBlock({ code, syntax });
@@ -42,10 +43,13 @@ function stringifyExample(args: { example: OpenAPIV3.ExampleObject; xml: boolean
/**
* Empty response example.
*/
export function OpenAPIEmptyExample() {
export function OpenAPIEmptyExample(props: {
context: OpenAPIUniversalContext;
}) {
const { context } = props;
return (
<pre className="openapi-example-empty">
<p>No Content</p>
<p>{t(context.translation, 'no_content')}</p>
</pre>
);
}
@@ -1,8 +1,10 @@
'use client';
import type { Key } from 'react-aria';
import { OpenAPIEmptyExample } from './OpenAPIExample';
import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect';
import { StaticSection } from './StaticSection';
import type { OpenAPIClientContext } from './context';
type OpenAPIMediaTypeItem = OpenAPISelectItem & {
body: React.ReactNode;
@@ -24,8 +26,9 @@ export function OpenAPIMediaTypeContent(props: {
items: OpenAPIMediaTypeItem[];
selectIcon?: React.ReactNode;
stateKey: string;
context: OpenAPIClientContext;
}) {
const { stateKey, items, selectIcon } = props;
const { stateKey, items, selectIcon, context } = props;
const state = useMediaTypesState(stateKey, items[0]?.key);
const examples = items.find((item) => item.key === state.key)?.examples ?? [];
@@ -48,7 +51,12 @@ export function OpenAPIMediaTypeContent(props: {
}
className="openapi-response-media-types-examples"
>
<OpenAPIMediaTypeBody stateKey={stateKey} items={items} examples={examples} />
<OpenAPIMediaTypeBody
context={context}
stateKey={stateKey}
items={items}
examples={examples}
/>
</StaticSection>
);
}
@@ -100,8 +108,9 @@ function OpenAPIMediaTypeBody(props: {
items: OpenAPIMediaTypeItem[];
examples?: OpenAPIMediaTypeItem[];
stateKey: string;
context: OpenAPIClientContext;
}) {
const { stateKey, items, examples } = props;
const { stateKey, items, examples, context } = props;
const state = useMediaTypesState(stateKey, items[0]?.key);
const selectedItem = items.find((item) => item.key === state.key) ?? items[0];
@@ -120,7 +129,7 @@ function OpenAPIMediaTypeBody(props: {
examples.find((example) => example.key === exampleState.key) ?? examples[0];
if (!selectedExample) {
return <OpenAPIEmptyExample />;
return <OpenAPIEmptyExample context={context} />;
}
return selectedExample.body;
@@ -3,7 +3,8 @@ import { OpenAPICodeSample } from './OpenAPICodeSample';
import { OpenAPIResponseExample } from './OpenAPIResponseExample';
import { OpenAPIColumnSpec } from './common/OpenAPIColumnSpec';
import { OpenAPISummary } from './common/OpenAPISummary';
import type { OpenAPIContext, OpenAPIOperationData } from './types';
import { type OpenAPIContextInput, resolveOpenAPIContext } from './context';
import type { OpenAPIOperationData } from './types';
/**
* Display an interactive OpenAPI operation.
@@ -11,9 +12,11 @@ import type { OpenAPIContext, OpenAPIOperationData } from './types';
export function OpenAPIOperation(props: {
className?: string;
data: OpenAPIOperationData;
context: OpenAPIContext;
context: OpenAPIContextInput;
}) {
const { className, data, context } = props;
const { className, data, context: contextInput } = props;
const context = resolveOpenAPIContext(contextInput);
return (
<div className={clsx('openapi-operation', className)}>
@@ -22,8 +25,8 @@ export function OpenAPIOperation(props: {
<OpenAPIColumnSpec data={data} context={context} />
<div className="openapi-column-preview">
<div className="openapi-column-preview-body">
<OpenAPICodeSample {...props} />
<OpenAPIResponseExample {...props} />
<OpenAPICodeSample data={data} context={context} />
<OpenAPIResponseExample data={data} context={context} />
</div>
</div>
</div>
@@ -0,0 +1,34 @@
import type { OpenAPICustomOperationProperties, OpenAPIV3 } from '@gitbook/openapi-parser';
import { Markdown } from './Markdown';
import type { OpenAPIContext } from './context';
import { resolveDescription } from './utils';
/**
* Display the description of an OpenAPI operation.
*/
export function OpenAPIOperationDescription(props: {
operation: OpenAPIV3.OperationObject<OpenAPICustomOperationProperties>;
context: OpenAPIContext;
}) {
const { operation } = props;
if (operation['x-gitbook-description-document']) {
return (
<div className="openapi-intro">
{props.context.renderDocument({
document: operation['x-gitbook-description-document'],
})}
</div>
);
}
const description = resolveDescription(operation);
if (!description) {
return null;
}
return (
<div className="openapi-intro">
<Markdown className="openapi-description" source={description} />
</div>
);
}
@@ -0,0 +1,39 @@
import type { OpenAPIStability } from '@gitbook/openapi-parser';
import type { OpenAPIContext } from './context';
import { t } from './translate';
/**
* Display the stability of an OpenAPI operation.
*/
export function OpenAPIOperationStability(props: {
stability: OpenAPIStability;
context: OpenAPIContext;
}) {
const { stability, context } = props;
const stabilityLabel = getStabilityLabel(stability, context);
if (!stabilityLabel) {
return null;
}
return (
<div className={`openapi-stability openapi-stability-${stability}`}>{stabilityLabel}</div>
);
}
/**
* Get the stability label for the given stability level.
*/
function getStabilityLabel(stability: OpenAPIStability, context: OpenAPIContext) {
switch (stability) {
case 'experimental':
return t(context.translation, 'stability_experimental');
case 'alpha':
return t(context.translation, 'stability_alpha');
case 'beta':
return t(context.translation, 'stability_beta');
default:
return null;
}
}
+4 -1
View File
@@ -1,4 +1,5 @@
import { OpenAPICopyButton } from './OpenAPICopyButton';
import { type OpenAPIUniversalContext, getOpenAPIClientContext } from './context';
import type { OpenAPIOperationData } from './types';
import { getDefaultServerURL } from './util/server';
@@ -7,6 +8,7 @@ import { getDefaultServerURL } from './util/server';
*/
export function OpenAPIPath(props: {
data: OpenAPIOperationData;
context: OpenAPIUniversalContext;
/** Whether to show the server URL.
* @default true
*/
@@ -17,7 +19,7 @@ export function OpenAPIPath(props: {
*/
canCopy?: boolean;
}) {
const { data, withServer = true, canCopy = true } = props;
const { data, context, withServer = true, canCopy = true } = props;
const { method, path, operation } = data;
const server = getDefaultServerURL(data.servers);
@@ -41,6 +43,7 @@ export function OpenAPIPath(props: {
className="openapi-path-title"
data-deprecated={operation.deprecated}
isDisabled={!canCopy}
context={getOpenAPIClientContext(context)}
>
{element}
</OpenAPICopyButton>
@@ -1,7 +1,9 @@
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
import { InteractiveSection } from './InteractiveSection';
import { OpenAPIRootSchema } from './OpenAPISchemaServer';
import type { OpenAPIClientContext, OpenAPIOperationData, OpenAPIWebhookData } from './types';
import type { OpenAPIClientContext } from './context';
import { t } from './translate';
import type { OpenAPIOperationData, OpenAPIWebhookData } from './types';
import { checkIsReference, createStateKey } from './utils';
/**
@@ -18,11 +20,9 @@ export function OpenAPIRequestBody(props: {
return null;
}
const header = 'name' in data ? 'Payload' : 'Body';
return (
<InteractiveSection
header={header}
header={t(context.translation, 'name' in data ? 'payload' : 'body')}
className="openapi-requestbody"
stateKey={createStateKey('request-body-media-type', context.blockKey)}
selectIcon={context.icons.chevronDown}
@@ -1,7 +1,7 @@
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
import { OpenAPISchemaProperties } from './OpenAPISchemaServer';
import type { OpenAPIClientContext } from './types';
import type { OpenAPIClientContext } from './context';
import { parameterToProperty, resolveDescription } from './utils';
/**
@@ -3,7 +3,8 @@ import { Markdown } from './Markdown';
import { OpenAPIEmptyExample, OpenAPIExample } from './OpenAPIExample';
import { OpenAPIMediaTypeContent } from './OpenAPIMediaType';
import { OpenAPIResponseExampleContent } from './OpenAPIResponseExampleContent';
import type { OpenAPIContext, OpenAPIOperationData, OpenAPIWebhookData } from './types';
import { type OpenAPIContext, getOpenAPIClientContext } from './context';
import type { OpenAPIOperationData, OpenAPIWebhookData } from './types';
import { getExampleFromReference, getExamples } from './util/example';
import { createStateKey, getStatusCodeDefaultLabel } from './utils';
import { checkIsReference, resolveDescription } from './utils';
@@ -45,7 +46,7 @@ export function OpenAPIResponseExample(props: {
const label = description ? (
<Markdown source={description} />
) : (
getStatusCodeDefaultLabel(key)
getStatusCodeDefaultLabel(key, context)
);
if (checkIsReference(responseObject)) {
@@ -55,7 +56,7 @@ export function OpenAPIResponseExample(props: {
statusCode: key,
body: (
<OpenAPIExample
example={getExampleFromReference(responseObject)}
example={getExampleFromReference(responseObject, context)}
context={context}
syntax="json"
/>
@@ -68,7 +69,7 @@ export function OpenAPIResponseExample(props: {
key: key,
label,
statusCode: key,
body: <OpenAPIEmptyExample />,
body: <OpenAPIEmptyExample context={context} />,
};
}
@@ -127,6 +128,7 @@ function OpenAPIResponse(props: {
selectIcon={context.icons.chevronDown}
stateKey={createStateKey('response-media-types', context.blockKey)}
items={tabs}
context={getOpenAPIClientContext(context)}
/>
);
}
@@ -7,7 +7,8 @@ import { OpenAPIDisclosureGroup } from './OpenAPIDisclosureGroup';
import { OpenAPIResponse } from './OpenAPIResponse';
import { useResponseExamplesState } from './OpenAPIResponseExampleContent';
import { StaticSection } from './StaticSection';
import type { OpenAPIClientContext } from './types';
import type { OpenAPIClientContext } from './context';
import { t } from './translate';
import { createStateKey, getStatusCodeClassName, getStatusCodeDefaultLabel } from './utils';
/**
@@ -76,7 +77,7 @@ export function OpenAPIResponses(props: {
className="openapi-response-description"
/>
) : (
getStatusCodeDefaultLabel(statusCode)
getStatusCodeDefaultLabel(statusCode, context)
)}
</div>
),
@@ -88,7 +89,7 @@ export function OpenAPIResponses(props: {
const state = useResponseExamplesState(context.blockKey, groups[0]?.key);
return (
<StaticSection header="Responses" className="openapi-responses">
<StaticSection header={t(context.translation, 'responses')} className="openapi-responses">
<OpenAPIDisclosureGroup
icon={context.icons.chevronRight}
expandedKeys={state.key ? new Set([state.key]) : new Set()}
+27 -12
View File
@@ -10,9 +10,10 @@ import { Markdown } from './Markdown';
import { OpenAPICopyButton } from './OpenAPICopyButton';
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
import { OpenAPISchemaName } from './OpenAPISchemaName';
import type { OpenAPIClientContext } from './context';
import { retrocycle } from './decycle';
import { stringifyOpenAPI } from './stringifyOpenAPI';
import type { OpenAPIClientContext } from './types';
import { tString } from './translate';
import { checkIsReference, resolveDescription, resolveFirstExample } from './utils';
type CircularRefsIds = Map<OpenAPIV3.SchemaObject, string>;
@@ -40,7 +41,7 @@ function OpenAPISchemaProperty(props: {
return (
<div id={id} className={clsx('openapi-schema', className)}>
<OpenAPISchemaPresentation property={property} />
<OpenAPISchemaPresentation context={context} property={property} />
{(() => {
const circularRefId = parentCircularRefs.get(schema);
// Avoid recursing infinitely, and instead render a link to the parent schema
@@ -56,7 +57,9 @@ function OpenAPISchemaProperty(props: {
return (
<OpenAPIDisclosure
icon={context.icons.plus}
label={(isExpanded) => getDisclosureLabel(schema, isExpanded)}
label={(isExpanded) =>
getDisclosureLabel({ schema, isExpanded, context })
}
>
<OpenAPISchemaProperties
properties={properties}
@@ -213,7 +216,7 @@ function OpenAPISchemaAlternative(props: {
) : null}
<OpenAPIDisclosure
icon={context.icons.plus}
label={(isExpanded) => getDisclosureLabel(schema, isExpanded)}
label={(isExpanded) => getDisclosureLabel({ schema, isExpanded, context })}
>
{properties?.length ? (
<OpenAPISchemaProperties
@@ -252,8 +255,9 @@ function OpenAPISchemaCircularRef(props: { id: string; schema: OpenAPIV3.SchemaO
*/
function OpenAPISchemaEnum(props: {
schema: OpenAPIV3.SchemaObject & OpenAPICustomOperationProperties;
context: OpenAPIClientContext;
}) {
const { schema } = props;
const { schema, context } = props;
const enumValues = (() => {
// Render x-gitbook-enum first, as it has a different format
@@ -296,6 +300,7 @@ function OpenAPISchemaEnum(props: {
value={item.value}
label={item.description}
withTooltip={!!item.description}
context={context}
>
<code>{`${item.value}`}</code>
</OpenAPICopyButton>
@@ -308,9 +313,13 @@ function OpenAPISchemaEnum(props: {
/**
* Render the top row of a schema. e.g: name, type, and required status.
*/
function OpenAPISchemaPresentation(props: { property: OpenAPISchemaPropertyEntry }) {
function OpenAPISchemaPresentation(props: {
property: OpenAPISchemaPropertyEntry;
context: OpenAPIClientContext;
}) {
const {
property: { schema, propertyName, required },
context,
} = props;
const description = resolveDescription(schema);
@@ -323,6 +332,7 @@ function OpenAPISchemaPresentation(props: { property: OpenAPISchemaPropertyEntry
type={getSchemaTitle(schema)}
propertyName={propertyName}
required={required}
context={context}
/>
{typeof schema['x-deprecated-sunset'] === 'string' ? (
<div className="openapi-deprecated-sunset openapi-schema-description openapi-markdown">
@@ -355,7 +365,7 @@ function OpenAPISchemaPresentation(props: { property: OpenAPISchemaPropertyEntry
Pattern: <code>{schema.pattern}</code>
</span>
) : null}
<OpenAPISchemaEnum schema={schema} />
<OpenAPISchemaEnum schema={schema} context={context} />
</div>
);
}
@@ -592,21 +602,26 @@ function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string {
return type;
}
function getDisclosureLabel(schema: OpenAPIV3.SchemaObject, isExpanded: boolean) {
function getDisclosureLabel(props: {
schema: OpenAPIV3.SchemaObject;
isExpanded: boolean;
context: OpenAPIClientContext;
}) {
const { schema, isExpanded, context } = props;
let label: string;
if (schema.type === 'array' && !!schema.items) {
if (schema.items.oneOf) {
label = 'available items';
label = tString(context.translation, 'available_items').toLowerCase();
}
// Fallback to "child attributes" for enums and objects
else if (schema.items.enum || schema.items.type === 'object') {
label = 'child attributes';
label = tString(context.translation, 'child_attributes').toLowerCase();
} else {
label = schema.items.title ?? schema.title ?? getSchemaTitle(schema.items);
}
} else {
label = schema.title || 'child attributes';
label = schema.title || tString(context.translation, 'child_attributes').toLowerCase();
}
return `${isExpanded ? 'Hide' : 'Show'} ${label}`;
return `${isExpanded ? tString(context.translation, 'hide') : tString(context.translation, 'show')} ${label}`;
}
@@ -1,11 +1,14 @@
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
import type React from 'react';
import type { OpenAPIClientContext } from './context';
import { t, tString } from './translate';
interface OpenAPISchemaNameProps {
schema?: OpenAPIV3.SchemaObject;
propertyName?: string | React.JSX.Element;
required?: boolean;
type?: string;
context: OpenAPIClientContext;
}
/**
@@ -13,9 +16,9 @@ interface OpenAPISchemaNameProps {
* It includes the property name, type, required and deprecated status.
*/
export function OpenAPISchemaName(props: OpenAPISchemaNameProps) {
const { schema, type, propertyName, required } = props;
const { schema, type, propertyName, required, context } = props;
const additionalItems = schema && getAdditionalItems(schema);
const additionalItems = schema && getAdditionalItems(schema, context);
return (
<span className="openapi-schema-name">
@@ -30,33 +33,45 @@ export function OpenAPISchemaName(props: OpenAPISchemaNameProps) {
<span className="openapi-schema-type">{additionalItems}</span>
) : null}
</span>
{schema?.readOnly ? <span className="openapi-schema-readonly">read-only</span> : null}
{schema?.readOnly ? (
<span className="openapi-schema-readonly">
{t(context.translation, 'read_only')}
</span>
) : null}
{schema?.writeOnly ? (
<span className="openapi-schema-writeonly">write-only</span>
<span className="openapi-schema-writeonly">
{t(context.translation, 'write_only')}
</span>
) : null}
{required ? (
<span className="openapi-schema-required">required</span>
<span className="openapi-schema-required">
{t(context.translation, 'required')}
</span>
) : (
<span className="openapi-schema-optional">optional</span>
<span className="openapi-schema-optional">
{t(context.translation, 'optional')}
</span>
)}
{schema?.deprecated ? <span className="openapi-deprecated">Deprecated</span> : null}
{schema?.deprecated ? (
<span className="openapi-deprecated">{t(context.translation, 'deprecated')}</span>
) : null}
</span>
);
}
function getAdditionalItems(schema: OpenAPIV3.SchemaObject): string {
function getAdditionalItems(schema: OpenAPIV3.SchemaObject, context: OpenAPIClientContext): string {
let additionalItems = '';
if (schema.minimum || schema.minLength || schema.minItems) {
additionalItems += ` · min: ${schema.minimum || schema.minLength || schema.minItems}`;
additionalItems += ` · ${tString(context.translation, 'min').toLowerCase()}: ${schema.minimum || schema.minLength || schema.minItems}`;
}
if (schema.maximum || schema.maxLength || schema.maxItems) {
additionalItems += ` · max: ${schema.maximum || schema.maxLength || schema.maxItems}`;
additionalItems += ` · ${tString(context.translation, 'max').toLowerCase()}: ${schema.maximum || schema.maxLength || schema.maxItems}`;
}
if (schema.nullable) {
additionalItems = ' | nullable';
additionalItems = ` | ${tString(context.translation, 'nullable').toLowerCase()}`;
}
return additionalItems;
@@ -4,8 +4,8 @@ import {
OpenAPISchemaPropertiesFromServer,
type OpenAPISchemaPropertyEntry,
} from './OpenAPISchema';
import type { OpenAPIClientContext } from './context';
import { decycle } from './decycle';
import type { OpenAPIClientContext } from './types';
export function OpenAPISchemaProperties(props: {
id?: string;
@@ -1,11 +1,9 @@
import { InteractiveSection } from './InteractiveSection';
import { Markdown } from './Markdown';
import { OpenAPISchemaName } from './OpenAPISchemaName';
import type {
OpenAPIClientContext,
OpenAPIOperationData,
OpenAPISecurityWithRequired,
} from './types';
import type { OpenAPIClientContext } from './context';
import { t } from './translate';
import type { OpenAPIOperationData, OpenAPISecurityWithRequired } from './types';
import { createStateKey, resolveDescription } from './utils';
/**
@@ -23,7 +21,7 @@ export function OpenAPISecurities(props: {
return (
<InteractiveSection
header="Authorizations"
header={t(context.translation, 'authorizations')}
stateKey={createStateKey('securities', context.blockKey)}
toggeable
defaultOpened={false}
@@ -38,7 +36,7 @@ export function OpenAPISecurities(props: {
body: (
<div className="openapi-schema">
<div className="openapi-schema-presentation">
{getLabelForType(security)}
{getLabelForType(security, context)}
{description ? (
<Markdown
@@ -55,11 +53,12 @@ export function OpenAPISecurities(props: {
);
}
function getLabelForType(security: OpenAPISecurityWithRequired) {
function getLabelForType(security: OpenAPISecurityWithRequired, context: OpenAPIClientContext) {
switch (security.type) {
case 'apiKey':
return (
<OpenAPISchemaName
context={context}
propertyName={security.name ?? 'apiKey'}
type="string"
required={security.required}
@@ -69,6 +68,7 @@ function getLabelForType(security: OpenAPISecurityWithRequired) {
if (security.scheme === 'basic') {
return (
<OpenAPISchemaName
context={context}
propertyName="Authorization"
type="string"
required={security.required}
@@ -81,6 +81,7 @@ function getLabelForType(security: OpenAPISecurityWithRequired) {
return (
<>
<OpenAPISchemaName
context={context}
propertyName="Authorization"
type="string"
required={security.required}
@@ -96,11 +97,29 @@ function getLabelForType(security: OpenAPISecurityWithRequired) {
);
}
return <OpenAPISchemaName propertyName="HTTP" required={security.required} />;
return (
<OpenAPISchemaName
context={context}
propertyName="HTTP"
required={security.required}
/>
);
case 'oauth2':
return <OpenAPISchemaName propertyName="OAuth2" required={security.required} />;
return (
<OpenAPISchemaName
context={context}
propertyName="OAuth2"
required={security.required}
/>
);
case 'openIdConnect':
return <OpenAPISchemaName propertyName="OpenID Connect" required={security.required} />;
return (
<OpenAPISchemaName
context={context}
propertyName="OpenID Connect"
required={security.required}
/>
);
default:
// @ts-ignore
return security.type;
+13 -8
View File
@@ -5,7 +5,9 @@ import { OpenAPIResponses } from './OpenAPIResponses';
import { OpenAPISchemaProperties } from './OpenAPISchemaServer';
import { OpenAPISecurities } from './OpenAPISecurities';
import { StaticSection } from './StaticSection';
import type { OpenAPIClientContext, OpenAPIOperationData, OpenAPIWebhookData } from './types';
import type { OpenAPIClientContext } from './context';
import { tString } from './translate';
import type { OpenAPIOperationData, OpenAPIWebhookData } from './types';
import { parameterToProperty } from './utils';
export function OpenAPISpec(props: {
@@ -17,7 +19,7 @@ export function OpenAPISpec(props: {
const { operation } = data;
const parameters = operation.parameters ?? [];
const parameterGroups = groupParameters(parameters);
const parameterGroups = groupParameters(parameters, context);
const securities = 'securities' in data ? data.securities : [];
@@ -61,7 +63,10 @@ export function OpenAPISpec(props: {
);
}
function groupParameters(parameters: OpenAPI.Parameters): Array<{
function groupParameters(
parameters: OpenAPI.Parameters,
context: OpenAPIClientContext
): Array<{
key: string;
label: string;
parameters: OpenAPI.Parameters;
@@ -78,7 +83,7 @@ function groupParameters(parameters: OpenAPI.Parameters): Array<{
.filter((parameter) => parameter.in)
.forEach((parameter) => {
const key = parameter.in;
const label = getParameterGroupName(parameter.in);
const label = getParameterGroupName(parameter.in, context);
const group = groups.find((group) => group.key === key);
if (group) {
group.parameters.push(parameter);
@@ -96,14 +101,14 @@ function groupParameters(parameters: OpenAPI.Parameters): Array<{
return groups;
}
function getParameterGroupName(paramIn: string): string {
function getParameterGroupName(paramIn: string, context: OpenAPIClientContext): string {
switch (paramIn) {
case 'path':
return 'Path parameters';
return tString(context.translation, 'path_parameters');
case 'query':
return 'Query parameters';
return tString(context.translation, 'query_parameters');
case 'header':
return 'Header parameters';
return tString(context.translation, 'header_parameters');
default:
return paramIn;
}
@@ -2,7 +2,8 @@ import clsx from 'clsx';
import { OpenAPIWebhookExample } from './OpenAPIWebhookExample';
import { OpenAPIColumnSpec } from './common/OpenAPIColumnSpec';
import { OpenAPISummary } from './common/OpenAPISummary';
import type { OpenAPIContext, OpenAPIWebhookData } from './types';
import { type OpenAPIContextInput, resolveOpenAPIContext } from './context';
import type { OpenAPIWebhookData } from './types';
/**
* Display an interactive OpenAPI webhook.
@@ -10,9 +11,11 @@ import type { OpenAPIContext, OpenAPIWebhookData } from './types';
export function OpenAPIWebhook(props: {
className?: string;
data: OpenAPIWebhookData;
context: OpenAPIContext;
context: OpenAPIContextInput;
}) {
const { className, data, context } = props;
const { className, data, context: contextInput } = props;
const context = resolveOpenAPIContext(contextInput);
return (
<div className={clsx('openapi-webhook', className)}>
@@ -1,7 +1,7 @@
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
import { OpenAPIEmptyExample } from './OpenAPIExample';
import { OpenAPIMediaTypeContent } from './OpenAPIMediaType';
import type { OpenAPIContext } from './types';
import { type OpenAPIContext, getOpenAPIClientContext } from './context';
import type { OpenAPIWebhookData } from './types';
import { getExamples } from './util/example';
import { createStateKey } from './utils';
@@ -27,7 +27,7 @@ export function OpenAPIWebhookExample(props: {
return {
key,
label: key,
body: <OpenAPIEmptyExample />,
body: <OpenAPIEmptyExample context={context} />,
};
}
@@ -52,6 +52,7 @@ export function OpenAPIWebhookExample(props: {
selectIcon={context.icons.chevronDown}
stateKey={createStateKey('request-body-media-type', context.blockKey)}
items={items}
context={getOpenAPIClientContext(context)}
/>
</div>
</div>
@@ -6,6 +6,8 @@ import { createPortal } from 'react-dom';
import type { OpenAPIV3_1 } from '@gitbook/openapi-parser';
import { useOpenAPIOperationContext } from './OpenAPIOperationContext';
import type { OpenAPIClientContext } from './context';
import { t } from './translate';
/**
* Button which launches the Scalar API Client
@@ -14,8 +16,9 @@ export function ScalarApiButton(props: {
method: OpenAPIV3_1.HttpMethods;
path: string;
specUrl: string;
context: OpenAPIClientContext;
}) {
const { method, path, specUrl } = props;
const { method, path, specUrl, context } = props;
const [isOpen, setIsOpen] = useState(false);
const controllerRef = useRef<ScalarModalControllerRef>(null);
return (
@@ -27,7 +30,7 @@ export function ScalarApiButton(props: {
setIsOpen(true);
}}
>
Test it
{t(context.translation, 'test_it')}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 12" fill="currentColor">
<path
stroke="currentColor"
@@ -1,6 +1,7 @@
import { OpenAPISpec } from '../OpenAPISpec';
import { getOpenAPIClientContext } from '../context';
import type { OpenAPIContext, OpenAPIOperationData, OpenAPIWebhookData } from '../types';
import { type OpenAPIContext, getOpenAPIClientContext } from '../context';
import { t } from '../translate';
import type { OpenAPIOperationData, OpenAPIWebhookData } from '../types';
import { OpenAPIOperationDescription } from './OpenAPIOperationDescription';
export function OpenAPIColumnSpec(props: {
@@ -16,11 +17,11 @@ export function OpenAPIColumnSpec(props: {
<div className="openapi-column-spec">
{operation['x-deprecated-sunset'] ? (
<div className="openapi-deprecated-sunset openapi-description openapi-markdown">
This operation is deprecated and will be sunset on{' '}
<span className="openapi-deprecated-sunset-date">
{operation['x-deprecated-sunset']}
</span>
{'.'}
{t(context.translation, 'deprecated_and_sunset_on', [
<span key="date" className="openapi-deprecated-sunset-date">
{operation['x-deprecated-sunset']}
</span>,
])}
</div>
) : null}
<OpenAPIOperationDescription operation={operation} context={context} />
@@ -1,6 +1,6 @@
import type { OpenAPICustomOperationProperties, OpenAPIV3 } from '@gitbook/openapi-parser';
import { Markdown } from '../Markdown';
import type { OpenAPIContext } from '../types';
import type { OpenAPIContext } from '../context';
import { resolveDescription } from '../utils';
export function OpenAPIOperationDescription(props: {
@@ -1,5 +1,6 @@
import { OpenAPIPath } from '../OpenAPIPath';
import type { OpenAPIContext, OpenAPIOperationData, OpenAPIWebhookData } from '../types';
import type { OpenAPIContext } from '../context';
import type { OpenAPIOperationData, OpenAPIWebhookData } from '../types';
import { OpenAPIStability } from './OpenAPIStability';
export function OpenAPISummary(props: {
@@ -38,7 +39,7 @@ export function OpenAPISummary(props: {
title,
})
: null}
{'path' in data ? <OpenAPIPath data={data} /> : null}
{'path' in data ? <OpenAPIPath data={data} context={context} /> : null}
</div>
);
}
+37 -2
View File
@@ -1,4 +1,11 @@
import { type Translation, type TranslationLocale, translations } from './translations';
export interface OpenAPIClientContext {
/**
* The translation language to use.
*/
translation: Translation;
/**
* Icons used in the block.
*/
@@ -23,9 +30,14 @@ export interface OpenAPIClientContext {
* Optional id attached to the heading and used as an anchor.
*/
id?: string;
/**
* Mark the context as a client context.
*/
$$isClientContext$$: true;
}
export interface OpenAPIContext extends OpenAPIClientContext {
export interface OpenAPIContext extends Omit<OpenAPIClientContext, '$$isClientContext$$'> {
/**
* Render a code block.
*/
@@ -51,14 +63,37 @@ export interface OpenAPIContext extends OpenAPIClientContext {
specUrl: string;
}
export type OpenAPIUniversalContext = OpenAPIClientContext | OpenAPIContext;
export interface OpenAPIContextInput extends Omit<OpenAPIContext, 'translation'> {
/**
* The translation language to use.
* @default 'en'
*/
locale?: TranslationLocale | undefined;
}
/**
* Resolve OpenAPI context from the input.
*/
export function resolveOpenAPIContext(context: OpenAPIContextInput): OpenAPIContext {
const { locale, ...rest } = context;
return {
...rest,
translation: translations[locale ?? 'en'],
};
}
/**
* Get the client context from the OpenAPI context.
*/
export function getOpenAPIClientContext(context: OpenAPIContext): OpenAPIClientContext {
export function getOpenAPIClientContext(context: OpenAPIUniversalContext): OpenAPIClientContext {
return {
translation: context.translation,
icons: context.icons,
defaultInteractiveOpened: context.defaultInteractiveOpened,
blockKey: context.blockKey,
id: context.id,
$$isClientContext$$: true,
};
}
+3 -1
View File
@@ -4,4 +4,6 @@ export * from './OpenAPIWebhook';
export * from './OpenAPIOperationContext';
export * from './resolveOpenAPIOperation';
export * from './resolveOpenAPIWebhook';
export type { OpenAPIOperationData, OpenAPIContext, OpenAPIWebhookData } from './types';
export type { OpenAPIOperationData, OpenAPIWebhookData } from './types';
export type { OpenAPIContextInput } from './context';
export { checkIsValidLocale } from './translations';
@@ -4,8 +4,12 @@ import { OpenAPIDisclosure } from '../OpenAPIDisclosure';
import { OpenAPIExample } from '../OpenAPIExample';
import { OpenAPIRootSchema } from '../OpenAPISchemaServer';
import { Section, SectionBody, StaticSection } from '../StaticSection';
import { getOpenAPIClientContext } from '../context';
import type { OpenAPIContext } from '../types';
import {
type OpenAPIContextInput,
getOpenAPIClientContext,
resolveOpenAPIContext,
} from '../context';
import { t } from '../translate';
import { getExampleFromSchema } from '../util/example';
/**
@@ -14,13 +18,13 @@ import { getExampleFromSchema } from '../util/example';
export function OpenAPISchemas(props: {
className?: string;
schemas: OpenAPISchema[];
context: OpenAPIContext;
context: OpenAPIContextInput;
/**
* Whether to show the schema directly if there is only one.
*/
grouped?: boolean;
}) {
const { schemas, context, grouped, className } = props;
const { schemas, context: contextInput, grouped, className } = props;
const firstSchema = schemas[0];
@@ -28,6 +32,7 @@ export function OpenAPISchemas(props: {
return null;
}
const context = resolveOpenAPIContext(contextInput);
const clientContext = getOpenAPIClientContext(context);
// If there is only one model and we are not grouping, we show it directly.
@@ -38,11 +43,16 @@ export function OpenAPISchemas(props: {
<div className="openapi-summary" id={context.id}>
{context.renderHeading({
title,
deprecated: Boolean(firstSchema.schema.deprecated),
stability: firstSchema.schema['x-stability'],
})}
</div>
<div className="openapi-columns">
<div className="openapi-column-spec">
<StaticSection className="openapi-parameters" header="Attributes">
<StaticSection
className="openapi-parameters"
header={t(context.translation, 'attributes')}
>
<OpenAPIRootSchema
schema={firstSchema.schema}
context={clientContext}
+80
View File
@@ -0,0 +1,80 @@
import React from 'react';
import type { Translation, TranslationKey } from './translations';
/**
* Translate a string.
*/
export function t(
translation: Translation,
id: TranslationKey,
...args: React.ReactNode[]
): React.ReactNode {
const string = translation[id];
if (!string) {
throw new Error(`Translation not found for "${id}"`);
}
// Now we are going to replace the arguments
// but we want to return a string as long as it's possible
// (eg. if there isn't any argument that is a ReactNode)
const parts: React.ReactNode[] = [];
let currentStringToReplace: string = string;
args.forEach((arg, i) => {
if (typeof arg === 'string') {
currentStringToReplace = currentStringToReplace.replace(`\${${i + 1}}`, arg);
} else {
const [partToPush, partToReplace] = currentStringToReplace.split(`\${${i + 1}}`);
if (partToPush === undefined || partToReplace === undefined) {
throw new Error(`Invalid translation "${id}"`);
}
parts.push(<React.Fragment key={`string-${i}`}>{partToPush}</React.Fragment>);
parts.push(<React.Fragment key={`arg-${i}`}>{arg}</React.Fragment>);
currentStringToReplace = partToReplace;
}
});
if (!parts.length) {
return currentStringToReplace;
}
return (
<>
{parts}
{currentStringToReplace}
</>
);
}
/**
* Version of `t` that returns a string.
*/
export function tString(
translation: Translation,
id: TranslationKey,
...args: React.ReactNode[]
): string {
const result = t(translation, id, ...args);
return reactToString(result);
}
function reactToString(el: React.ReactNode): string {
if (typeof el === 'string' || typeof el === 'number' || typeof el === 'boolean') {
return `${el}`;
}
if (el === null || el === undefined) {
return '';
}
if (Array.isArray(el)) {
return el.map(reactToString).join('');
}
if (typeof el === 'object' && 'props' in el) {
return el.props.children.map(reactToString).join('');
}
throw new Error(`Unsupported type ${typeof el}`);
}
@@ -0,0 +1,37 @@
export const de = {
required: 'Erforderlich',
deprecated: 'Veraltet',
deprecated_and_sunset_on: 'Diese Operation ist veraltet und wird am ${1} eingestellt.',
stability_experimental: 'Experimentell',
stability_alpha: 'Alpha',
stability_beta: 'Beta',
copy_to_clipboard: 'In die Zwischenablage kopieren',
copied: 'Kopiert',
no_content: 'Kein Inhalt',
unresolved_reference: 'Nicht aufgelöste Referenz',
circular_reference: 'Zirkuläre Referenz',
read_only: 'Nur lesen',
write_only: 'Nur schreiben',
optional: 'Optional',
min: 'Min',
max: 'Max',
nullable: 'Nullfähig',
body: 'Rumpf',
payload: 'Nutzlast',
headers: 'Kopfzeilen',
authorizations: 'Autorisierungen',
responses: 'Antworten',
path_parameters: 'Pfadparameter',
query_parameters: 'Abfrageparameter',
header_parameters: 'Header-Parameter',
attributes: 'Attribute',
test_it: 'Teste es',
information: 'Information',
success: 'Erfolg',
redirect: 'Umleitung',
error: 'Fehler',
show: 'Anzeigen',
hide: 'Verstecken',
available_items: 'Verfügbare Elemente',
child_attributes: 'Unterattribute',
};
@@ -0,0 +1,37 @@
export const en = {
required: 'Required',
deprecated: 'Deprecated',
deprecated_and_sunset_on: 'This operation is deprecated and will be sunset on ${1}.',
stability_experimental: 'Experimental',
stability_alpha: 'Alpha',
stability_beta: 'Beta',
copy_to_clipboard: 'Copy to clipboard',
copied: 'Copied',
no_content: 'No content',
unresolved_reference: 'Unresolved reference',
circular_reference: 'Circular reference',
read_only: 'Read-only',
write_only: 'Write-only',
optional: 'Optional',
min: 'Min',
max: 'Max',
nullable: 'Nullable',
body: 'Body',
payload: 'Payload',
headers: 'Headers',
authorizations: 'Authorizations',
responses: 'Responses',
path_parameters: 'Path parameters',
query_parameters: 'Query parameters',
header_parameters: 'Header parameters',
attributes: 'Attributes',
test_it: 'Test it',
information: 'Information',
success: 'Success',
redirect: 'Redirect',
error: 'Error',
show: 'Show',
hide: 'Hide',
available_items: 'Available items',
child_attributes: 'Child attributes',
};
@@ -0,0 +1,37 @@
export const es = {
required: 'Requerido',
deprecated: 'Obsoleto',
deprecated_and_sunset_on: 'Esta operación está obsoleta y se retirará el ${1}.',
stability_experimental: 'Experimental',
stability_alpha: 'Alfa',
stability_beta: 'Beta',
copy_to_clipboard: 'Copiar al portapapeles',
copied: 'Copiado',
no_content: 'Sin contenido',
unresolved_reference: 'Referencia no resuelta',
circular_reference: 'Referencia circular',
read_only: 'Solo lectura',
write_only: 'Solo escritura',
optional: 'Opcional',
min: 'Mín',
max: 'Máx',
nullable: 'Nulo',
body: 'Cuerpo',
payload: 'Caga útil',
headers: 'Encabezados',
authorizations: 'Autorizaciones',
responses: 'Respuestas',
path_parameters: 'Parámetros de ruta',
query_parameters: 'Parámetros de consulta',
header_parameters: 'Parámetros de encabezado',
attributes: 'Atributos',
test_it: 'Pruébalo',
information: 'Información',
success: 'Éxito',
redirect: 'Redirección',
error: 'Error',
show: 'Mostrar',
hide: 'Ocultar',
available_items: 'Elementos disponibles',
child_attributes: 'Atributos secundarios',
};
@@ -0,0 +1,37 @@
export const fr = {
required: 'Requis',
deprecated: 'Obsolète',
deprecated_and_sunset_on: 'Cette opération est obsolète et sera supprimée le ${1}.',
stability_experimental: 'Expérimental',
stability_alpha: 'Alpha',
stability_beta: 'Bêta',
copy_to_clipboard: 'Copier dans le presse-papiers',
copied: 'Copié',
no_content: 'Aucun contenu',
unresolved_reference: 'Référence non résolue',
circular_reference: 'Référence circulaire',
read_only: 'Lecture seule',
write_only: 'Écriture seule',
optional: 'Optionnel',
min: 'Min',
max: 'Max',
nullable: 'Nullable',
body: 'Corps',
payload: 'Charge utile',
headers: 'En-têtes',
authorizations: 'Autorisations',
responses: 'Réponses',
path_parameters: 'Paramètres de chemin',
query_parameters: 'Paramètres de requête',
header_parameters: 'Paramètres den-tête',
attributes: 'Attributs',
test_it: 'Tester',
information: 'Information',
success: 'Succès',
redirect: 'Redirection',
error: 'Erreur',
show: 'Afficher',
hide: 'Masquer',
available_items: 'Éléments disponibles',
child_attributes: 'Attributs enfants',
};
@@ -0,0 +1,33 @@
import { de } from './de';
import { en } from './en';
import { es } from './es';
import { fr } from './fr';
import { ja } from './ja';
import { nl } from './nl';
import { no } from './no';
import { pt_br } from './pt-br';
import type { Translation } from './types';
import { zh } from './zh';
export * from './types';
export const translations = {
en,
de,
es,
fr,
ja,
nl,
no,
'pt-br': pt_br,
zh,
} satisfies Record<string, Translation>;
export type TranslationLocale = keyof typeof translations;
/**
* Check if the locale is valid.
*/
export function checkIsValidLocale(locale: string): locale is TranslationLocale {
return Object.prototype.hasOwnProperty.call(translations, locale);
}
@@ -0,0 +1,37 @@
export const ja = {
required: '必須',
deprecated: '非推奨',
deprecated_and_sunset_on: 'この操作は非推奨であり、${1}に廃止されます。',
stability_experimental: '実験的',
stability_alpha: 'アルファ',
stability_beta: 'ベータ',
copy_to_clipboard: 'クリップボードにコピー',
copied: 'コピー済み',
no_content: 'コンテンツなし',
unresolved_reference: '未解決の参照',
circular_reference: '循環参照',
read_only: '読み取り専用',
write_only: '書き込み専用',
optional: 'オプション',
min: '最小',
max: '最大',
nullable: 'ヌル許容',
body: '本文',
payload: 'ペイロード',
headers: 'ヘッダー',
authorizations: '認可',
responses: 'レスポンス',
path_parameters: 'パスパラメータ',
query_parameters: 'クエリパラメータ',
header_parameters: 'ヘッダーパラメータ',
attributes: '属性',
test_it: 'テストする',
information: '情報',
success: '成功',
redirect: 'リダイレクト',
error: 'エラー',
show: '表示',
hide: '非表示',
available_items: '利用可能なアイテム',
child_attributes: '子属性',
};
@@ -0,0 +1,37 @@
export const nl = {
required: 'Vereist',
deprecated: 'Verouderd',
deprecated_and_sunset_on: 'Deze bewerking is verouderd en wordt op ${1} beëindigd.',
stability_experimental: 'Experimenteel',
stability_alpha: 'Alfa',
stability_beta: 'Bèta',
copy_to_clipboard: 'Kopiëren naar klembord',
copied: 'Gekopieerd',
no_content: 'Geen inhoud',
unresolved_reference: 'Onopgeloste verwijzing',
circular_reference: 'Circulaire verwijzing',
read_only: 'Alleen lezen',
write_only: 'Alleen schrijven',
optional: 'Optioneel',
min: 'Min',
max: 'Max',
nullable: 'Null toegestaan',
body: 'Body',
payload: 'Payload',
headers: 'Headers',
authorizations: 'Autorisaties',
responses: 'Reacties',
path_parameters: 'Padparameters',
query_parameters: 'Queryparameters',
header_parameters: 'Headerparameters',
attributes: 'Attributen',
test_it: 'Test het',
information: 'Informatie',
success: 'Succes',
redirect: 'Omleiding',
error: 'Fout',
show: 'Toon',
hide: 'Verbergen',
available_items: 'Beschikbare items',
child_attributes: 'Kindattributen',
};
@@ -0,0 +1,37 @@
export const no = {
required: 'Påkrevd',
deprecated: 'Foreldet',
deprecated_and_sunset_on: 'Denne operasjonen er foreldet og vil bli avviklet den ${1}.',
stability_experimental: 'Eksperimentell',
stability_alpha: 'Alfa',
stability_beta: 'Beta',
copy_to_clipboard: 'Kopier til utklippstavle',
copied: 'Kopiert',
no_content: 'Ingen innhold',
unresolved_reference: 'Uavklart referanse',
circular_reference: 'Sirkulær referanse',
read_only: 'Skrivebeskyttet',
write_only: 'Kun skriving',
optional: 'Valgfri',
min: 'Min',
max: 'Maks',
nullable: 'Kan være null',
body: 'Brødtekst',
payload: 'Nyttelast',
headers: 'Overskrifter',
authorizations: 'Autorisasjoner',
responses: 'Responser',
path_parameters: 'Sti-parametere',
query_parameters: 'Forespørselsparametere',
header_parameters: 'Header-parametere',
attributes: 'Attributter',
test_it: 'Test det',
information: 'Informasjon',
success: 'Suksess',
redirect: 'Viderekobling',
error: 'Feil',
show: 'Vis',
hide: 'Skjul',
available_items: 'Tilgjengelige elementer',
child_attributes: 'Barneattributter',
};
@@ -0,0 +1,37 @@
export const pt_br = {
required: 'Obrigatório',
deprecated: 'Obsoleto',
deprecated_and_sunset_on: 'Esta operação está obsoleta e será descontinuada em ${1}.',
stability_experimental: 'Experimental',
stability_alpha: 'Alfa',
stability_beta: 'Beta',
copy_to_clipboard: 'Copiar para a área de transferência',
copied: 'Copiado',
no_content: 'Sem conteúdo',
unresolved_reference: 'Referência não resolvida',
circular_reference: 'Referência circular',
read_only: 'Somente leitura',
write_only: 'Somente escrita',
optional: 'Opcional',
min: 'Mín',
max: 'Máx',
nullable: 'Nulo',
body: 'Corpo',
payload: 'Carga útil',
headers: 'Cabeçalhos',
authorizations: 'Autorizações',
responses: 'Respostas',
path_parameters: 'Parâmetros de rota',
query_parameters: 'Parâmetros de consulta',
header_parameters: 'Parâmetros de cabeçalho',
attributes: 'Atributos',
test_it: 'Testar',
information: 'Informação',
success: 'Sucesso',
redirect: 'Redirecionamento',
error: 'Erro',
show: 'Mostrar',
hide: 'Ocultar',
available_items: 'Itens disponíveis',
child_attributes: 'Atributos filhos',
};
@@ -0,0 +1,7 @@
import type { en } from './en';
export type TranslationKey = keyof typeof en;
export type Translation = {
[key in TranslationKey]: string;
};
@@ -0,0 +1,37 @@
export const zh = {
required: '必填',
deprecated: '已弃用',
deprecated_and_sunset_on: '此操作已弃用,将于 ${1} 停止使用。',
stability_experimental: '实验性',
stability_alpha: 'Alpha',
stability_beta: 'Beta',
copy_to_clipboard: '复制到剪贴板',
copied: '已复制',
no_content: '无内容',
unresolved_reference: '未解析的引用',
circular_reference: '循环引用',
read_only: '只读',
write_only: '只写',
optional: '可选',
min: '最小值',
max: '最大值',
nullable: '可为 null',
body: '请求体',
payload: '有效载荷',
headers: '头部信息',
authorizations: '授权',
responses: '响应',
path_parameters: '路径参数',
query_parameters: '查询参数',
header_parameters: '头参数',
attributes: '属性',
test_it: '测试一下',
information: '信息',
success: '成功',
redirect: '重定向',
error: '错误',
show: '显示',
hide: '隐藏',
available_items: '可用项',
child_attributes: '子属性',
};
-53
View File
@@ -4,59 +4,6 @@ import type {
OpenAPIV3,
} from '@gitbook/openapi-parser';
export interface OpenAPIClientContext {
/**
* Icons used in the block.
*/
icons: {
chevronDown: React.ReactNode;
chevronRight: React.ReactNode;
plus: React.ReactNode;
};
/**
* Force all sections to be opened by default.
* @default false
*/
defaultInteractiveOpened?: boolean;
/**
* The key of the block
*/
blockKey?: string;
/**
* Optional id attached to the heading and used as an anchor.
*/
id?: string;
}
export interface OpenAPIContext extends OpenAPIClientContext {
/**
* Render a code block.
*/
renderCodeBlock: (props: { code: string; syntax: string }) => React.ReactNode;
/**
* Render the heading of the operation.
*/
renderHeading: (props: {
deprecated?: boolean;
title: string;
stability?: string;
}) => React.ReactNode;
/**
* Render the document of the operation.
*/
renderDocument: (props: { document: object }) => React.ReactNode;
/**
* Specification URL.
*/
specUrl: string;
}
export type OpenAPISecurityWithRequired = OpenAPIV3.SecuritySchemeObject & { required?: boolean };
export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
+17 -7
View File
@@ -1,14 +1,21 @@
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
import { OpenAPIExample } from '../OpenAPIExample';
import type { OpenAPIContext } from '../context';
import { generateSchemaExample } from '../generateSchemaExample';
import type { OpenAPIContext } from '../types';
import { tString } from '../translate';
import { checkIsReference } from '../utils';
/**
* Generate an example from a reference object.
*/
export function getExampleFromReference(ref: OpenAPIV3.ReferenceObject): OpenAPIV3.ExampleObject {
return { summary: 'Unresolved reference', value: { $ref: ref.$ref } };
export function getExampleFromReference(
ref: OpenAPIV3.ReferenceObject,
context: OpenAPIContext
): OpenAPIV3.ExampleObject {
return {
summary: tString(context.translation, 'unresolved_reference'),
value: { $ref: ref.$ref },
};
}
/**
@@ -17,13 +24,16 @@ export function getExampleFromReference(ref: OpenAPIV3.ReferenceObject): OpenAPI
export function getExamplesFromMediaTypeObject(args: {
mediaType: string;
mediaTypeObject: OpenAPIV3.MediaTypeObject;
context: OpenAPIContext;
}): { key: string; example: OpenAPIV3.ExampleObject }[] {
const { mediaTypeObject, mediaType } = args;
const { mediaTypeObject, mediaType, context } = args;
if (mediaTypeObject.examples) {
return Object.entries(mediaTypeObject.examples).map(([key, example]) => {
return {
key,
example: checkIsReference(example) ? getExampleFromReference(example) : example,
example: checkIsReference(example)
? getExampleFromReference(example, context)
: example,
};
});
}
@@ -88,8 +98,8 @@ export function getExamples(props: {
mediaType: string;
context: OpenAPIContext;
}) {
const { mediaTypeObject, mediaType } = props;
const examples = getExamplesFromMediaTypeObject({ mediaTypeObject, mediaType });
const { mediaTypeObject, mediaType, context } = props;
const examples = getExamplesFromMediaTypeObject({ mediaTypeObject, mediaType, context });
const syntax = getSyntaxFromMediaType(mediaType);
return examples.map((example) => {
+10 -5
View File
@@ -1,5 +1,7 @@
import type { AnyObject, OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser';
import type { OpenAPIUniversalContext } from './context';
import { stringifyOpenAPI } from './stringifyOpenAPI';
import { tString } from './translate';
export function checkIsReference(input: unknown): input is OpenAPIV3.ReferenceObject {
return typeof input === 'object' && !!input && '$ref' in input;
@@ -182,18 +184,21 @@ export function getStatusCodeClassName(statusCode: number | string): string {
* 3xx: Redirect
* 4xx, 5xx: Error
*/
export function getStatusCodeDefaultLabel(statusCode: number | string): string {
export function getStatusCodeDefaultLabel(
statusCode: number | string,
context: OpenAPIUniversalContext
): string {
const category = getStatusCodeCategory(statusCode);
switch (category) {
case 1:
return 'Information';
return tString(context.translation, 'information');
case 2:
return 'Success';
return tString(context.translation, 'success');
case 3:
return 'Redirect';
return tString(context.translation, 'redirect');
case 4:
case 5:
return 'Error';
return tString(context.translation, 'error');
default:
return '';
}