From b2c10ad9b803199441d0b3098333764e8de6c445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Thu, 17 Apr 2025 14:05:36 +0200 Subject: [PATCH 1/2] Translate all OpenAPI blocks --- .changeset/light-moons-press.md | 6 ++ .../DocumentView/OpenAPI/OpenAPIOperation.tsx | 2 +- .../DocumentView/OpenAPI/OpenAPISchemas.tsx | 2 +- .../DocumentView/OpenAPI/OpenAPIWebhook.tsx | 2 +- .../DocumentView/OpenAPI/context.tsx | 17 +++- .../components/DocumentView/OpenAPI/style.css | 8 +- packages/gitbook/src/intl/server.ts | 15 +++- .../intl/translations/translations.test.ts | 0 .../react-openapi/src/InteractiveSection.tsx | 1 + .../react-openapi/src/OpenAPICodeSample.tsx | 19 ++++- .../src/OpenAPICodeSampleSelector.tsx | 18 ++++- .../react-openapi/src/OpenAPICopyButton.tsx | 9 ++- packages/react-openapi/src/OpenAPIExample.tsx | 12 ++- .../react-openapi/src/OpenAPIMediaType.tsx | 17 +++- .../react-openapi/src/OpenAPIOperation.tsx | 13 +-- .../src/OpenAPIOperationDescription.tsx | 34 ++++++++ .../src/OpenAPIOperationStability.tsx | 39 +++++++++ packages/react-openapi/src/OpenAPIPath.tsx | 5 +- .../react-openapi/src/OpenAPIRequestBody.tsx | 8 +- .../react-openapi/src/OpenAPIResponse.tsx | 2 +- .../src/OpenAPIResponseExample.tsx | 10 ++- .../react-openapi/src/OpenAPIResponses.tsx | 7 +- packages/react-openapi/src/OpenAPISchema.tsx | 39 ++++++--- .../react-openapi/src/OpenAPISchemaName.tsx | 37 ++++++--- .../react-openapi/src/OpenAPISchemaServer.tsx | 2 +- .../react-openapi/src/OpenAPISecurities.tsx | 41 +++++++--- packages/react-openapi/src/OpenAPISpec.tsx | 21 +++-- packages/react-openapi/src/OpenAPIWebhook.tsx | 9 ++- .../src/OpenAPIWebhookExample.tsx | 5 +- .../react-openapi/src/ScalarApiButton.tsx | 7 +- .../src/common/OpenAPIColumnSpec.tsx | 15 ++-- .../common/OpenAPIOperationDescription.tsx | 2 +- .../src/common/OpenAPISummary.tsx | 5 +- packages/react-openapi/src/context.ts | 39 ++++++++- packages/react-openapi/src/index.ts | 4 +- .../src/schemas/OpenAPISchemas.tsx | 20 +++-- packages/react-openapi/src/translate.tsx | 80 +++++++++++++++++++ packages/react-openapi/src/translations/de.ts | 37 +++++++++ packages/react-openapi/src/translations/en.ts | 37 +++++++++ packages/react-openapi/src/translations/es.ts | 37 +++++++++ packages/react-openapi/src/translations/fr.ts | 37 +++++++++ .../react-openapi/src/translations/index.ts | 33 ++++++++ packages/react-openapi/src/translations/ja.ts | 37 +++++++++ packages/react-openapi/src/translations/nl.ts | 37 +++++++++ packages/react-openapi/src/translations/no.ts | 37 +++++++++ .../react-openapi/src/translations/pt-br.ts | 37 +++++++++ .../react-openapi/src/translations/types.ts | 7 ++ packages/react-openapi/src/translations/zh.ts | 37 +++++++++ packages/react-openapi/src/types.ts | 53 ------------ packages/react-openapi/src/util/example.tsx | 24 ++++-- packages/react-openapi/src/utils.ts | 15 ++-- 51 files changed, 856 insertions(+), 181 deletions(-) create mode 100644 .changeset/light-moons-press.md delete mode 100644 packages/gitbook/src/intl/translations/translations.test.ts create mode 100644 packages/react-openapi/src/OpenAPIOperationDescription.tsx create mode 100644 packages/react-openapi/src/OpenAPIOperationStability.tsx create mode 100644 packages/react-openapi/src/translate.tsx create mode 100644 packages/react-openapi/src/translations/de.ts create mode 100644 packages/react-openapi/src/translations/en.ts create mode 100644 packages/react-openapi/src/translations/es.ts create mode 100644 packages/react-openapi/src/translations/fr.ts create mode 100644 packages/react-openapi/src/translations/index.ts create mode 100644 packages/react-openapi/src/translations/ja.ts create mode 100644 packages/react-openapi/src/translations/nl.ts create mode 100644 packages/react-openapi/src/translations/no.ts create mode 100644 packages/react-openapi/src/translations/pt-br.ts create mode 100644 packages/react-openapi/src/translations/types.ts create mode 100644 packages/react-openapi/src/translations/zh.ts diff --git a/.changeset/light-moons-press.md b/.changeset/light-moons-press.md new file mode 100644 index 0000000000..9bd0dea6f6 --- /dev/null +++ b/.changeset/light-moons-press.md @@ -0,0 +1,6 @@ +--- +"@gitbook/react-openapi": patch +"gitbook": patch +--- + +Translate OpenAPI blocks diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIOperation.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIOperation.tsx index 9a5426abb8..1fe281a751 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIOperation.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIOperation.tsx @@ -51,7 +51,7 @@ async function OpenAPIOperationBody(props: BlockProps return ( ); diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPISchemas.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPISchemas.tsx index cdda5541e9..06023a89ce 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPISchemas.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPISchemas.tsx @@ -51,7 +51,7 @@ async function OpenAPISchemasBody(props: BlockProps) { ); diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIWebhook.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIWebhook.tsx index 087f1ae946..3453cf8d77 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIWebhook.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIWebhook.tsx @@ -51,7 +51,7 @@ async function OpenAPIWebhookBody(props: BlockProps) { return ( ); diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx index d470b14cae..8bd950757f 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx @@ -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; 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, }; } diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index ad409378ed..39cccc4b38 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -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 { diff --git a/packages/gitbook/src/intl/server.ts b/packages/gitbook/src/intl/server.ts index 0aa201ef24..b13076f41d 100644 --- a/packages/gitbook/src/intl/server.ts +++ b/packages/gitbook/src/intl/server.ts @@ -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]; } diff --git a/packages/gitbook/src/intl/translations/translations.test.ts b/packages/gitbook/src/intl/translations/translations.test.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/react-openapi/src/InteractiveSection.tsx b/packages/react-openapi/src/InteractiveSection.tsx index bb41d9aafd..db8a2662cb 100644 --- a/packages/react-openapi/src/InteractiveSection.tsx +++ b/packages/react-openapi/src/InteractiveSection.tsx @@ -102,6 +102,7 @@ export function InteractiveSection(props: { ) : null} {header} + {/* biome-ignore lint/a11y/useKeyWithClickEvents: we prevent default here */}
+ ); } @@ -215,7 +221,14 @@ function OpenAPICodeSampleFooter(props: { ) : ( )} - {!hideTryItPanel && } + {!hideTryItPanel && ( + + )}
); } diff --git a/packages/react-openapi/src/OpenAPICodeSampleSelector.tsx b/packages/react-openapi/src/OpenAPICodeSampleSelector.tsx index 4625b7ac69..d8b80a9f7b 100644 --- a/packages/react-openapi/src/OpenAPICodeSampleSelector.tsx +++ b/packages/react-openapi/src/OpenAPICodeSampleSelector.tsx @@ -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 ( <> - + {items.length > 1 ? ( } + header={ + + } className="openapi-codesample" >
diff --git a/packages/react-openapi/src/OpenAPICopyButton.tsx b/packages/react-openapi/src/OpenAPICopyButton.tsx index 954ff9b6a9..df8d258e94 100644 --- a/packages/react-openapi/src/OpenAPICopyButton.tsx +++ b/packages/react-openapi/src/OpenAPICopyButton.tsx @@ -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')} ); diff --git a/packages/react-openapi/src/OpenAPIExample.tsx b/packages/react-openapi/src/OpenAPIExample.tsx index f9a2b85a81..7f9328d6ff 100644 --- a/packages/react-openapi/src/OpenAPIExample.tsx +++ b/packages/react-openapi/src/OpenAPIExample.tsx @@ -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 ; + return ; } 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 (
-            

No Content

+

{t(context.translation, 'no_content')}

); } diff --git a/packages/react-openapi/src/OpenAPIMediaType.tsx b/packages/react-openapi/src/OpenAPIMediaType.tsx index 56f6c47405..e929319f2f 100644 --- a/packages/react-openapi/src/OpenAPIMediaType.tsx +++ b/packages/react-openapi/src/OpenAPIMediaType.tsx @@ -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" > - + ); } @@ -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 ; + return ; } return selectedExample.body; diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index 32418c9823..856ef5770f 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -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 (
@@ -22,8 +25,8 @@ export function OpenAPIOperation(props: {
- - + +
diff --git a/packages/react-openapi/src/OpenAPIOperationDescription.tsx b/packages/react-openapi/src/OpenAPIOperationDescription.tsx new file mode 100644 index 0000000000..c0a2b248b3 --- /dev/null +++ b/packages/react-openapi/src/OpenAPIOperationDescription.tsx @@ -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; + context: OpenAPIContext; +}) { + const { operation } = props; + if (operation['x-gitbook-description-document']) { + return ( +
+ {props.context.renderDocument({ + document: operation['x-gitbook-description-document'], + })} +
+ ); + } + + const description = resolveDescription(operation); + if (!description) { + return null; + } + + return ( +
+ +
+ ); +} diff --git a/packages/react-openapi/src/OpenAPIOperationStability.tsx b/packages/react-openapi/src/OpenAPIOperationStability.tsx new file mode 100644 index 0000000000..a60f59c932 --- /dev/null +++ b/packages/react-openapi/src/OpenAPIOperationStability.tsx @@ -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 ( +
{stabilityLabel}
+ ); +} + +/** + * 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; + } +} diff --git a/packages/react-openapi/src/OpenAPIPath.tsx b/packages/react-openapi/src/OpenAPIPath.tsx index aa5857a5d6..90a1d66cda 100644 --- a/packages/react-openapi/src/OpenAPIPath.tsx +++ b/packages/react-openapi/src/OpenAPIPath.tsx @@ -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} diff --git a/packages/react-openapi/src/OpenAPIRequestBody.tsx b/packages/react-openapi/src/OpenAPIRequestBody.tsx index 1b23779c44..0316e85b00 100644 --- a/packages/react-openapi/src/OpenAPIRequestBody.tsx +++ b/packages/react-openapi/src/OpenAPIRequestBody.tsx @@ -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 ( ) : ( - getStatusCodeDefaultLabel(key) + getStatusCodeDefaultLabel(key, context) ); if (checkIsReference(responseObject)) { @@ -55,7 +56,7 @@ export function OpenAPIResponseExample(props: { statusCode: key, body: ( @@ -68,7 +69,7 @@ export function OpenAPIResponseExample(props: { key: key, label, statusCode: key, - body: , + body: , }; } @@ -127,6 +128,7 @@ function OpenAPIResponse(props: { selectIcon={context.icons.chevronDown} stateKey={createStateKey('response-media-types', context.blockKey)} items={tabs} + context={getOpenAPIClientContext(context)} /> ); } diff --git a/packages/react-openapi/src/OpenAPIResponses.tsx b/packages/react-openapi/src/OpenAPIResponses.tsx index 303a242bf2..71de271db4 100644 --- a/packages/react-openapi/src/OpenAPIResponses.tsx +++ b/packages/react-openapi/src/OpenAPIResponses.tsx @@ -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) )}
), @@ -88,7 +89,7 @@ export function OpenAPIResponses(props: { const state = useResponseExamplesState(context.blockKey, groups[0]?.key); return ( - + ; @@ -40,7 +41,7 @@ function OpenAPISchemaProperty(props: { return (
- + {(() => { 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 ( getDisclosureLabel(schema, isExpanded)} + label={(isExpanded) => + getDisclosureLabel({ schema, isExpanded, context }) + } > getDisclosureLabel(schema, isExpanded)} + label={(isExpanded) => getDisclosureLabel({ schema, isExpanded, context })} > {properties?.length ? ( { // 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} > {`${item.value}`} @@ -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' ? (
@@ -355,7 +365,7 @@ function OpenAPISchemaPresentation(props: { property: OpenAPISchemaPropertyEntry Pattern: {schema.pattern} ) : null} - +
); } @@ -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}`; } diff --git a/packages/react-openapi/src/OpenAPISchemaName.tsx b/packages/react-openapi/src/OpenAPISchemaName.tsx index 62934cb843..6e230419cd 100644 --- a/packages/react-openapi/src/OpenAPISchemaName.tsx +++ b/packages/react-openapi/src/OpenAPISchemaName.tsx @@ -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 ( @@ -30,33 +33,45 @@ export function OpenAPISchemaName(props: OpenAPISchemaNameProps) { {additionalItems} ) : null} - {schema?.readOnly ? read-only : null} + {schema?.readOnly ? ( + + {t(context.translation, 'read_only')} + + ) : null} {schema?.writeOnly ? ( - write-only + + {t(context.translation, 'write_only')} + ) : null} {required ? ( - required + + {t(context.translation, 'required')} + ) : ( - optional + + {t(context.translation, 'optional')} + )} - {schema?.deprecated ? Deprecated : null} + {schema?.deprecated ? ( + {t(context.translation, 'deprecated')} + ) : null} ); } -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; diff --git a/packages/react-openapi/src/OpenAPISchemaServer.tsx b/packages/react-openapi/src/OpenAPISchemaServer.tsx index d2396c705e..2c3fd66dad 100644 --- a/packages/react-openapi/src/OpenAPISchemaServer.tsx +++ b/packages/react-openapi/src/OpenAPISchemaServer.tsx @@ -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; diff --git a/packages/react-openapi/src/OpenAPISecurities.tsx b/packages/react-openapi/src/OpenAPISecurities.tsx index 1f35afbd70..3f86236506 100644 --- a/packages/react-openapi/src/OpenAPISecurities.tsx +++ b/packages/react-openapi/src/OpenAPISecurities.tsx @@ -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 (
- {getLabelForType(security)} + {getLabelForType(security, context)} {description ? ( ; + return ( + + ); case 'oauth2': - return ; + return ( + + ); case 'openIdConnect': - return ; + return ( + + ); default: // @ts-ignore return security.type; diff --git a/packages/react-openapi/src/OpenAPISpec.tsx b/packages/react-openapi/src/OpenAPISpec.tsx index e4988866aa..49c41cda40 100644 --- a/packages/react-openapi/src/OpenAPISpec.tsx +++ b/packages/react-openapi/src/OpenAPISpec.tsx @@ -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; } diff --git a/packages/react-openapi/src/OpenAPIWebhook.tsx b/packages/react-openapi/src/OpenAPIWebhook.tsx index 3878be56f6..c81d57ea49 100644 --- a/packages/react-openapi/src/OpenAPIWebhook.tsx +++ b/packages/react-openapi/src/OpenAPIWebhook.tsx @@ -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 (
diff --git a/packages/react-openapi/src/OpenAPIWebhookExample.tsx b/packages/react-openapi/src/OpenAPIWebhookExample.tsx index 7bb29b0383..5e66cfb566 100644 --- a/packages/react-openapi/src/OpenAPIWebhookExample.tsx +++ b/packages/react-openapi/src/OpenAPIWebhookExample.tsx @@ -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: , + body: , }; } @@ -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)} />
diff --git a/packages/react-openapi/src/ScalarApiButton.tsx b/packages/react-openapi/src/ScalarApiButton.tsx index a6d387beb6..59024d2025 100644 --- a/packages/react-openapi/src/ScalarApiButton.tsx +++ b/packages/react-openapi/src/ScalarApiButton.tsx @@ -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(null); return ( @@ -27,7 +30,7 @@ export function ScalarApiButton(props: { setIsOpen(true); }} > - Test it + {t(context.translation, 'test_it')} {operation['x-deprecated-sunset'] ? (
- This operation is deprecated and will be sunset on{' '} - - {operation['x-deprecated-sunset']} - - {'.'} + {t(context.translation, 'deprecated_and_sunset_on', [ + + {operation['x-deprecated-sunset']} + , + ])}
) : null} diff --git a/packages/react-openapi/src/common/OpenAPIOperationDescription.tsx b/packages/react-openapi/src/common/OpenAPIOperationDescription.tsx index 2da004fa38..53c8275236 100644 --- a/packages/react-openapi/src/common/OpenAPIOperationDescription.tsx +++ b/packages/react-openapi/src/common/OpenAPIOperationDescription.tsx @@ -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: { diff --git a/packages/react-openapi/src/common/OpenAPISummary.tsx b/packages/react-openapi/src/common/OpenAPISummary.tsx index 06ac0c79e8..8c81ae2b47 100644 --- a/packages/react-openapi/src/common/OpenAPISummary.tsx +++ b/packages/react-openapi/src/common/OpenAPISummary.tsx @@ -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 ? : null} + {'path' in data ? : null}
); } diff --git a/packages/react-openapi/src/context.ts b/packages/react-openapi/src/context.ts index 704b192e80..cf7519034d 100644 --- a/packages/react-openapi/src/context.ts +++ b/packages/react-openapi/src/context.ts @@ -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 { /** * 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 { + /** + * 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, }; } diff --git a/packages/react-openapi/src/index.ts b/packages/react-openapi/src/index.ts index b30a180cbf..f097eab6af 100644 --- a/packages/react-openapi/src/index.ts +++ b/packages/react-openapi/src/index.ts @@ -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'; diff --git a/packages/react-openapi/src/schemas/OpenAPISchemas.tsx b/packages/react-openapi/src/schemas/OpenAPISchemas.tsx index 353878a7e1..2283245fcb 100644 --- a/packages/react-openapi/src/schemas/OpenAPISchemas.tsx +++ b/packages/react-openapi/src/schemas/OpenAPISchemas.tsx @@ -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: {
{context.renderHeading({ title, + deprecated: Boolean(firstSchema.schema.deprecated), + stability: firstSchema.schema['x-stability'], })}
- + { + 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({partToPush}); + parts.push({arg}); + 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}`); +} diff --git a/packages/react-openapi/src/translations/de.ts b/packages/react-openapi/src/translations/de.ts new file mode 100644 index 0000000000..8fc792e8ea --- /dev/null +++ b/packages/react-openapi/src/translations/de.ts @@ -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', +}; diff --git a/packages/react-openapi/src/translations/en.ts b/packages/react-openapi/src/translations/en.ts new file mode 100644 index 0000000000..61f45e9e3f --- /dev/null +++ b/packages/react-openapi/src/translations/en.ts @@ -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', +}; diff --git a/packages/react-openapi/src/translations/es.ts b/packages/react-openapi/src/translations/es.ts new file mode 100644 index 0000000000..5faa36c674 --- /dev/null +++ b/packages/react-openapi/src/translations/es.ts @@ -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', +}; diff --git a/packages/react-openapi/src/translations/fr.ts b/packages/react-openapi/src/translations/fr.ts new file mode 100644 index 0000000000..ccaf05b3d9 --- /dev/null +++ b/packages/react-openapi/src/translations/fr.ts @@ -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 d’en-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', +}; diff --git a/packages/react-openapi/src/translations/index.ts b/packages/react-openapi/src/translations/index.ts new file mode 100644 index 0000000000..690b33187e --- /dev/null +++ b/packages/react-openapi/src/translations/index.ts @@ -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; + +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); +} diff --git a/packages/react-openapi/src/translations/ja.ts b/packages/react-openapi/src/translations/ja.ts new file mode 100644 index 0000000000..55b5b2a0a0 --- /dev/null +++ b/packages/react-openapi/src/translations/ja.ts @@ -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: '子属性', +}; diff --git a/packages/react-openapi/src/translations/nl.ts b/packages/react-openapi/src/translations/nl.ts new file mode 100644 index 0000000000..5186580c78 --- /dev/null +++ b/packages/react-openapi/src/translations/nl.ts @@ -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', +}; diff --git a/packages/react-openapi/src/translations/no.ts b/packages/react-openapi/src/translations/no.ts new file mode 100644 index 0000000000..a4efc3cfe7 --- /dev/null +++ b/packages/react-openapi/src/translations/no.ts @@ -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', +}; diff --git a/packages/react-openapi/src/translations/pt-br.ts b/packages/react-openapi/src/translations/pt-br.ts new file mode 100644 index 0000000000..7c1f86a7c0 --- /dev/null +++ b/packages/react-openapi/src/translations/pt-br.ts @@ -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', +}; diff --git a/packages/react-openapi/src/translations/types.ts b/packages/react-openapi/src/translations/types.ts new file mode 100644 index 0000000000..06e131d067 --- /dev/null +++ b/packages/react-openapi/src/translations/types.ts @@ -0,0 +1,7 @@ +import type { en } from './en'; + +export type TranslationKey = keyof typeof en; + +export type Translation = { + [key in TranslationKey]: string; +}; diff --git a/packages/react-openapi/src/translations/zh.ts b/packages/react-openapi/src/translations/zh.ts new file mode 100644 index 0000000000..414043fd32 --- /dev/null +++ b/packages/react-openapi/src/translations/zh.ts @@ -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: '子属性', +}; diff --git a/packages/react-openapi/src/types.ts b/packages/react-openapi/src/types.ts index 2d44354324..625bdd18b0 100644 --- a/packages/react-openapi/src/types.ts +++ b/packages/react-openapi/src/types.ts @@ -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 { diff --git a/packages/react-openapi/src/util/example.tsx b/packages/react-openapi/src/util/example.tsx index 8559d5e322..95b295e58a 100644 --- a/packages/react-openapi/src/util/example.tsx +++ b/packages/react-openapi/src/util/example.tsx @@ -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) => { diff --git a/packages/react-openapi/src/utils.ts b/packages/react-openapi/src/utils.ts index e4f5b0d7c5..0ac9b78d5e 100644 --- a/packages/react-openapi/src/utils.ts +++ b/packages/react-openapi/src/utils.ts @@ -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 ''; } From c1cdc2e415bd97248924f5143a829e450a0a8f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Fri, 18 Apr 2025 13:18:12 +0200 Subject: [PATCH 2/2] Turn off unstable tests --- packages/gitbook/e2e/customers.spec.ts | 34 +++++++++++++------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/gitbook/e2e/customers.spec.ts b/packages/gitbook/e2e/customers.spec.ts index 3d22108359..f0612ba8e0 100644 --- a/packages/gitbook/e2e/customers.spec.ts +++ b/packages/gitbook/e2e/customers.spec.ts @@ -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',