From e5739de123c6e913f57eec9617ffefe82cb1e228 Mon Sep 17 00:00:00 2001 From: Matthieu MOREL Date: Tue, 7 Apr 2026 07:50:28 +0200 Subject: [PATCH 1/4] feat(i18n): add internationalization plugin with locale auto-detection and built-in translations Signed-off-by: Matthieu MOREL --- docs/usage/i18n.md | 252 ++++++++++++++++++ src/core/components/auth/api-key-auth.jsx | 18 +- src/core/components/auth/authorize-btn.jsx | 12 +- .../auth/authorize-operation-btn.jsx | 12 +- src/core/components/auth/auths.jsx | 20 +- src/core/components/auth/basic-auth.jsx | 18 +- src/core/components/auth/oauth2.jsx | 48 ++-- src/core/components/clear.jsx | 9 +- src/core/components/copy-to-clipboard-btn.jsx | 10 +- src/core/components/errors.jsx | 40 ++- src/core/components/execute.jsx | 12 +- src/core/components/headers.jsx | 19 +- src/core/components/live-response.jsx | 46 ++-- src/core/components/operation-tag.jsx | 6 +- src/core/components/param-body.jsx | 16 +- src/core/components/parameters/parameters.jsx | 21 +- src/core/components/response-body.jsx | 24 +- src/core/components/response.jsx | 18 +- src/core/components/responses.jsx | 22 +- src/core/components/try-it-out-button.jsx | 11 +- src/core/config/defaults.js | 5 + src/core/containers/filter.jsx | 10 +- src/core/plugins/i18n/actions.js | 20 ++ src/core/plugins/i18n/fn.js | 40 +++ src/core/plugins/i18n/index.js | 75 ++++++ src/core/plugins/i18n/locales/de.js | 115 ++++++++ src/core/plugins/i18n/locales/en.js | 115 ++++++++ src/core/plugins/i18n/locales/es.js | 116 ++++++++ src/core/plugins/i18n/locales/fr.js | 115 ++++++++ src/core/plugins/i18n/locales/index.js | 21 ++ src/core/plugins/i18n/locales/is.js | 115 ++++++++ src/core/plugins/i18n/locales/ko.js | 114 ++++++++ src/core/plugins/i18n/locales/pt.js | 115 ++++++++ src/core/plugins/i18n/locales/ru.js | 115 ++++++++ src/core/plugins/i18n/locales/zh.js | 114 ++++++++ src/core/plugins/i18n/reducers.js | 14 + src/core/plugins/i18n/selectors.js | 8 + .../json-schema-5/components/models.jsx | 12 +- .../request-snippets/request-snippets.jsx | 12 +- src/core/presets/base/index.js | 2 + .../plugins/top-bar/components/TopBar.jsx | 23 +- test/unit/core/plugins/i18n/fn.test.js | 127 +++++++++ test/unit/core/plugins/i18n/locales.test.js | 45 ++++ test/unit/core/plugins/i18n/reducers.test.js | 100 +++++++ 44 files changed, 2034 insertions(+), 148 deletions(-) create mode 100644 docs/usage/i18n.md create mode 100644 src/core/plugins/i18n/actions.js create mode 100644 src/core/plugins/i18n/fn.js create mode 100644 src/core/plugins/i18n/index.js create mode 100644 src/core/plugins/i18n/locales/de.js create mode 100644 src/core/plugins/i18n/locales/en.js create mode 100644 src/core/plugins/i18n/locales/es.js create mode 100644 src/core/plugins/i18n/locales/fr.js create mode 100644 src/core/plugins/i18n/locales/index.js create mode 100644 src/core/plugins/i18n/locales/is.js create mode 100644 src/core/plugins/i18n/locales/ko.js create mode 100644 src/core/plugins/i18n/locales/pt.js create mode 100644 src/core/plugins/i18n/locales/ru.js create mode 100644 src/core/plugins/i18n/locales/zh.js create mode 100644 src/core/plugins/i18n/reducers.js create mode 100644 src/core/plugins/i18n/selectors.js create mode 100644 test/unit/core/plugins/i18n/fn.test.js create mode 100644 test/unit/core/plugins/i18n/locales.test.js create mode 100644 test/unit/core/plugins/i18n/reducers.test.js diff --git a/docs/usage/i18n.md b/docs/usage/i18n.md new file mode 100644 index 00000000000..177de392967 --- /dev/null +++ b/docs/usage/i18n.md @@ -0,0 +1,252 @@ +# Internationalization (i18n) + +Swagger UI ships with a built-in internationalization system based on a lightweight key/message lookup. No additional third-party i18n libraries are required. + +--- + +## Quick start + +### Display the UI in a specific language + +Pass a `locale` option when initialising Swagger UI. Swagger UI ships built-in translations for several languages — when a matching locale is detected or configured, those translations are loaded automatically with no extra setup: + +```js +const ui = SwaggerUIBundle({ + url: "https://petstore.swagger.io/v2/swagger.json", + dom_id: "#swagger-ui", + locale: "fr", // French translations are loaded automatically +}) +``` + +### Auto-detect the browser language + +Set `locale` to `null` (the default) and Swagger UI will read `navigator.languages[0]` / `navigator.language` and normalise it to a BCP 47 base language code (e.g. `"fr-CA"` → `"fr"`). If a built-in or user-registered locale matches, it is used; otherwise the UI falls back to English. + +```js +SwaggerUIBundle({ + url: "...", + dom_id: "#swagger-ui", + locale: null, // default — auto-detect from browser +}) +``` + +--- + +## Built-in locales + +The following locales are bundled with Swagger UI and loaded automatically when the detected or configured locale matches: + +| Code | Language | +|------|----------| +| `de` | German (Deutsch) | +| `en` | English *(default / fallback)* | +| `es` | Spanish (Español) | +| `fr` | French (Français) | +| `is` | Icelandic (Íslenska) | +| `ko` | Korean (한국어) | +| `pt` | Portuguese / Brazilian Portuguese (Português do Brasil) | +| `ru` | Russian (Русский) | +| `zh` | Chinese Simplified (简体中文) | + +All locale files are located in `src/core/plugins/i18n/locales/`. + +### Adding or extending translations + +Call `loadMessages` at any time to register additional keys or override existing ones. Partial overrides are supported — any key absent from the registered messages falls through to the English fallback: + +```js +const ui = SwaggerUIBundle({ url: "...", dom_id: "#swagger-ui", locale: "fr" }) + +// Override a subset of French translations +ui.i18nActions.loadMessages("fr", { + "button.execute": "Lancer", + "button.try_it_out": "Tester", +}) + +// Switch locale at runtime +ui.i18nActions.setLocale("de") +``` + +--- + +## Configuration options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `locale` | `string \| null` | `null` | BCP 47 language tag (e.g. `"en"`, `"fr"`, `"de"`). `null` means auto-detect from the browser. | + +--- + +## Key catalog + +All translatable strings are identified by dot-namespaced keys. The full catalog is defined in [`src/core/plugins/i18n/locales/en.js`](../../src/core/plugins/i18n/locales/en.js). + +### Buttons / actions + +| Key | Default (English) | +|-----|-------------------| +| `button.authorize` | Authorize | +| `button.cancel` | Cancel | +| `button.clear` | Clear | +| `button.close` | Close | +| `button.copy_to_clipboard` | Copy to clipboard | +| `button.download_file` | Download file | +| `button.edit` | Edit | +| `button.execute` | Execute | +| `button.explore` | Explore | +| `button.hide` | Hide | +| `button.logout` | Logout | +| `button.reset` | Reset | +| `button.show` | Show | +| `button.try_it_out` | Try it out | + +### Section / column labels + +| Key | Default (English) | +|-----|-------------------| +| `label.callbacks` | Callbacks | +| `label.code` | Code | +| `label.description` | Description | +| `label.details` | Details | +| `label.examples` | Examples | +| `label.headers` | Headers: | +| `label.links` | Links | +| `label.media_type` | Media type | +| `label.models` | Models | +| `label.name` | Name | +| `label.no_links` | No links | +| `label.no_parameters` | No parameters | +| `label.parameter_content_type` | Parameter content type | +| `label.parameters` | Parameters | +| `label.request_body` | Request body | +| `label.request_duration` | Request duration | +| `label.request_url` | Request URL | +| `label.response_body` | Response body | +| `label.response_content_type` | Response content type | +| `label.response_headers` | Response headers | +| `label.responses` | Responses | +| `label.schemas` | Schemas | +| `label.server_response` | Server response | +| `label.snippets` | Snippets | +| `label.type` | Type | +| `label.undocumented` | Undocumented | + +### Authentication + +| Key | Default (English) | +|-----|-------------------| +| `auth.api_key_in` | In: | +| `auth.api_key_name` | Name: | +| `auth.api_key_value` | Value: | +| `auth.application` | Application: | +| `auth.authorization_header` | Authorization header | +| `auth.authorization_url` | Authorization URL: | +| `auth.authorized` | Authorized | +| `auth.basic_authorization_title` | Basic authorization | +| `auth.client_credentials_location` | Client credentials location: | +| `auth.client_id` | client_id: | +| `auth.client_secret` | client_secret: | +| `auth.flow` | Flow: | +| `auth.openid_connect_url` | OpenID Connect URL: | +| `auth.password` | password: | +| `auth.password_cap` | Password: | +| `auth.request_body_option` | Request body | +| `auth.scopes_description` | Scopes are used to grant … | +| `auth.scopes_required` | API requires the following scopes … | +| `auth.scopes_title` | Scopes: | +| `auth.select_all` | select all | +| `auth.select_none` | select none | +| `auth.token_url` | Token URL: | +| `auth.username` | username: | +| `auth.username_cap` | Username: | + +### Accessibility (aria-labels / titles) + +| Key | Default (English) | +|-----|-------------------| +| `aria.apply_credentials` | Apply credentials | +| `aria.apply_oauth2_credentials` | Apply given OAuth2 credentials | +| `aria.authorization_button_locked` | authorization button locked | +| `aria.authorization_button_unlocked` | authorization button unlocked | +| `aria.collapse_operation` | Collapse operation | +| `aria.expand_operation` | Expand operation | +| `aria.remove_authorization` | Remove authorization | +| `aria.request_content_type` | Request content type | +| `aria.response_content_type` | Response content type | + +### Errors + +| Key | Default (English) | +|-----|-------------------| +| `errors.jump_to_line` | Jump to line {{line}} | +| `errors.title` | Errors | + +### Placeholders + +| Key | Default (English) | +|-----|-------------------| +| `placeholder.filter_by_tag` | Filter by tag | + +### Response + +Response-related messages are defined in the English catalog and include whitespace-sensitive fragments used when rendering inline elements such as `Accept`. To avoid publishing an incomplete or stale subset here, refer to `locales/en.js` as the canonical source for `response.*` keys and their exact default strings. + +### Topbar + +| Key | Default (English) | +|-----|-------------------| +| `topbar.select_definition` | Select a definition | + +--- + +## Variable interpolation + +Some messages contain `{{varName}}` placeholders that are replaced at runtime: + +| Key | Variable | +|-----|----------| +| `errors.jump_to_line` | `line` — the line number | + +Example override with interpolation preserved: + +```js +ui.i18nActions.loadMessages("de", { + "errors.jump_to_line": "Gehe zu Zeile {{line}}" +}) +``` + +--- + +## Programmatic API + +The i18n system is exposed through the Redux system just like any other plugin. + +### Dispatch actions + +```js +// Change the active locale at runtime +getSystem().i18nActions.setLocale("de") + +// Register or extend a locale's messages +getSystem().i18nActions.loadMessages("de", { + "button.execute": "Ausführen" +}) +``` + +### Call the translation function directly + +```js +const t = getSystem().t +console.log(t("button.execute")) // "Execute" (or translated equivalent) +console.log(t("errors.jump_to_line", { line: 42 })) // "Jump to line 42" +``` + +--- + +## Architecture notes + +- The i18n system is implemented as a Redux **plugin** (`I18nPlugin`) registered early in the base preset, making it available to all other plugins and components. +- The `t(key, vars?)` function is injected into `rootInjects`, so every connected component receives it as a direct prop alongside `getComponent`, `getConfigs`, etc. +- The **English locale** (`locales/en.js`) is the canonical catalog and ultimate fallback. If a key is absent from the active locale messages, the English string is used. If the key is absent from English too, the raw key string is returned. +- Locale is normalised to the BCP 47 base language code (`"fr-CA"` → `"fr"`). +- All existing config options and behaviour remain unchanged when no `locale` is supplied. diff --git a/src/core/components/auth/api-key-auth.jsx b/src/core/components/auth/api-key-auth.jsx index 03c1e73cce9..d4952e44de7 100644 --- a/src/core/components/auth/api-key-auth.jsx +++ b/src/core/components/auth/api-key-auth.jsx @@ -1,5 +1,6 @@ import React from "react" import PropTypes from "prop-types" +import { fallbackT } from "core/plugins/i18n/fn" export default class ApiKeyAuth extends React.Component { static propTypes = { @@ -9,7 +10,12 @@ export default class ApiKeyAuth extends React.Component { schema: PropTypes.object.isRequired, name: PropTypes.string.isRequired, onChange: PropTypes.func, - authSelectors: PropTypes.object.isRequired + authSelectors: PropTypes.object.isRequired, + t: PropTypes.func, + } + + static defaultProps = { + t: fallbackT, } constructor(props, context) { @@ -40,7 +46,7 @@ export default class ApiKeyAuth extends React.Component { } render() { - let { schema, getComponent, errSelectors, name, authSelectors } = this.props + let { schema, getComponent, errSelectors, name, authSelectors, t } = this.props const Input = getComponent("Input") const Row = getComponent("Row") const Col = getComponent("Col") @@ -57,18 +63,18 @@ export default class ApiKeyAuth extends React.Component { { name || schema.get("name") } (apiKey) - { value &&
Authorized
} + { value &&
{t("auth.authorized")}
} -

Name: { schema.get("name") }

+

{t("auth.api_key_name")} { schema.get("name") }

-

In: { schema.get("in") }

+

{t("auth.api_key_in")} { schema.get("in") }

- + { value ? ****** : diff --git a/src/core/components/auth/authorize-btn.jsx b/src/core/components/auth/authorize-btn.jsx index 21b9ba2c0b5..9ce235ca333 100644 --- a/src/core/components/auth/authorize-btn.jsx +++ b/src/core/components/auth/authorize-btn.jsx @@ -1,16 +1,22 @@ import React from "react" import PropTypes from "prop-types" +import { fallbackT } from "core/plugins/i18n/fn" export default class AuthorizeBtn extends React.Component { static propTypes = { onClick: PropTypes.func, isAuthorized: PropTypes.bool, showPopup: PropTypes.bool, - getComponent: PropTypes.func.isRequired + getComponent: PropTypes.func.isRequired, + t: PropTypes.func, + } + + static defaultProps = { + t: fallbackT, } render() { - let { isAuthorized, showPopup, onClick, getComponent } = this.props + let { isAuthorized, showPopup, onClick, getComponent, t } = this.props //must be moved out of button component const AuthorizationPopup = getComponent("authorizationPopup", true) @@ -20,7 +26,7 @@ export default class AuthorizeBtn extends React.Component { return (
{ showPopup && } diff --git a/src/core/components/auth/authorize-operation-btn.jsx b/src/core/components/auth/authorize-operation-btn.jsx index fcb1fdfd4b7..270c4c04c3a 100644 --- a/src/core/components/auth/authorize-operation-btn.jsx +++ b/src/core/components/auth/authorize-operation-btn.jsx @@ -1,11 +1,17 @@ import React from "react" import PropTypes from "prop-types" +import { fallbackT } from "core/plugins/i18n/fn" export default class AuthorizeOperationBtn extends React.Component { static propTypes = { isAuthorized: PropTypes.bool.isRequired, onClick: PropTypes.func, - getComponent: PropTypes.func.isRequired + getComponent: PropTypes.func.isRequired, + t: PropTypes.func, + } + + static defaultProps = { + t: fallbackT, } onClick =(e) => { @@ -18,14 +24,14 @@ export default class AuthorizeOperationBtn extends React.Component { } render() { - let { isAuthorized, getComponent } = this.props + let { isAuthorized, getComponent, t } = this.props const LockAuthOperationIcon = getComponent("LockAuthOperationIcon", true) const UnlockAuthOperationIcon = getComponent("UnlockAuthOperationIcon", true) return ( diff --git a/src/core/components/auth/auths.jsx b/src/core/components/auth/auths.jsx index a29b57359f3..f2d826004cd 100644 --- a/src/core/components/auth/auths.jsx +++ b/src/core/components/auth/auths.jsx @@ -1,6 +1,7 @@ import React from "react" import PropTypes from "prop-types" import ImPropTypes from "react-immutable-proptypes" +import { fallbackT } from "core/plugins/i18n/fn" export default class Auths extends React.Component { static propTypes = { @@ -9,7 +10,12 @@ export default class Auths extends React.Component { authSelectors: PropTypes.object.isRequired, authActions: PropTypes.object.isRequired, errSelectors: PropTypes.object.isRequired, - specSelectors: PropTypes.object.isRequired + specSelectors: PropTypes.object.isRequired, + t: PropTypes.func, + } + + static defaultProps = { + t: fallbackT, } constructor(props, context) { @@ -55,7 +61,7 @@ export default class Auths extends React.Component { } render() { - let { definitions, getComponent, authSelectors, errSelectors } = this.props + let { definitions, getComponent, authSelectors, errSelectors, t } = this.props const AuthItem = getComponent("AuthItem") const Oauth2 = getComponent("oauth2", true) const Button = getComponent("Button") @@ -89,10 +95,10 @@ export default class Auths extends React.Component { }
{ - nonOauthDefinitions.size === authorizedAuth.size ? - : + nonOauthDefinitions.size === authorizedAuth.size ? + : } - +
} @@ -100,8 +106,8 @@ export default class Auths extends React.Component { { oauthDefinitions && oauthDefinitions.size ?
-

Scopes are used to grant an application different levels of access to data on behalf of the end user. Each API may declare one or more scopes.

-

API requires the following scopes. Select which ones you want to grant to Swagger UI.

+

{t("auth.scopes_description")}

+

{t("auth.scopes_required")}

{ definitions.filter( schema => schema.get("type") === "oauth2") diff --git a/src/core/components/auth/basic-auth.jsx b/src/core/components/auth/basic-auth.jsx index b8c69bd5a2a..4b0122ebd0b 100644 --- a/src/core/components/auth/basic-auth.jsx +++ b/src/core/components/auth/basic-auth.jsx @@ -1,6 +1,7 @@ import React from "react" import PropTypes from "prop-types" import ImPropTypes from "react-immutable-proptypes" +import { fallbackT } from "core/plugins/i18n/fn" export default class BasicAuth extends React.Component { static propTypes = { @@ -10,7 +11,12 @@ export default class BasicAuth extends React.Component { onChange: PropTypes.func.isRequired, name: PropTypes.string.isRequired, errSelectors: PropTypes.object.isRequired, - authSelectors: PropTypes.object.isRequired + authSelectors: PropTypes.object.isRequired, + t: PropTypes.func, + } + + static defaultProps = { + t: fallbackT, } constructor(props, context) { @@ -48,7 +54,7 @@ export default class BasicAuth extends React.Component { } render() { - let { schema, getComponent, name, errSelectors, authSelectors } = this.props + let { schema, getComponent, name, errSelectors, authSelectors, t } = this.props const Input = getComponent("Input") const Row = getComponent("Row") const Col = getComponent("Col") @@ -61,13 +67,13 @@ export default class BasicAuth extends React.Component { return (
-

Basic authorization

- { username &&
Authorized
} +

{t("auth.basic_authorization_title")}

+ { username &&
{t("auth.authorized")}
} - + { username ? { username } : @@ -83,7 +89,7 @@ export default class BasicAuth extends React.Component { } - + { username ? ****** : diff --git a/src/core/components/auth/oauth2.jsx b/src/core/components/auth/oauth2.jsx index dfb6845ba27..7ad5548967f 100644 --- a/src/core/components/auth/oauth2.jsx +++ b/src/core/components/auth/oauth2.jsx @@ -1,6 +1,7 @@ import React from "react" import PropTypes from "prop-types" import oauth2Authorize from "core/oauth2-authorize" +import { fallbackT } from "core/plugins/i18n/fn" export default class Oauth2 extends React.Component { static propTypes = { @@ -14,7 +15,12 @@ export default class Oauth2 extends React.Component { oas3Selectors: PropTypes.object.isRequired, specSelectors: PropTypes.object.isRequired, errActions: PropTypes.object.isRequired, - getConfigs: PropTypes.any + getConfigs: PropTypes.any, + t: PropTypes.func, + } + + static defaultProps = { + t: fallbackT, } constructor(props, context) { @@ -109,7 +115,7 @@ export default class Oauth2 extends React.Component { render() { let { - schema, getComponent, authSelectors, errSelectors, name, specSelectors + schema, getComponent, authSelectors, errSelectors, name, specSelectors, t } = this.props const Input = getComponent("Input") const Row = getComponent("Row") @@ -147,21 +153,21 @@ export default class Oauth2 extends React.Component { return (

{name} (OAuth2, { flowToDisplay })

- { !this.state.appName ? null :
Application: { this.state.appName }
} + { !this.state.appName ? null :
{t("auth.application")} { this.state.appName }
} { description && } - { isAuthorized &&
Authorized
} + { isAuthorized &&
{t("auth.authorized")}
} - { oidcUrl &&

OpenID Connect URL: { oidcUrl }

} - { ( flow === AUTH_FLOW_IMPLICIT || flow === AUTH_FLOW_ACCESS_CODE ) &&

Authorization URL: { schema.get("authorizationUrl") }

} - { ( flow === AUTH_FLOW_PASSWORD || flow === AUTH_FLOW_ACCESS_CODE || flow === AUTH_FLOW_APPLICATION ) &&

Token URL: { schema.get("tokenUrl") }

} -

Flow: { flowToDisplay }

+ { oidcUrl &&

{t("auth.openid_connect_url")} { oidcUrl }

} + { ( flow === AUTH_FLOW_IMPLICIT || flow === AUTH_FLOW_ACCESS_CODE ) &&

{t("auth.authorization_url")} { schema.get("authorizationUrl") }

} + { ( flow === AUTH_FLOW_PASSWORD || flow === AUTH_FLOW_ACCESS_CODE || flow === AUTH_FLOW_APPLICATION ) &&

{t("auth.token_url")} { schema.get("tokenUrl") }

} +

{t("auth.flow")} { flowToDisplay }

{ flow !== AUTH_FLOW_PASSWORD ? null : - + { isAuthorized ? { this.state.username } : @@ -173,7 +179,7 @@ export default class Oauth2 extends React.Component { } - + { isAuthorized ? ****** : @@ -182,13 +188,13 @@ export default class Oauth2 extends React.Component { } - + { isAuthorized ? { this.state.passwordType } : } @@ -198,7 +204,7 @@ export default class Oauth2 extends React.Component { { ( flow === AUTH_FLOW_APPLICATION || flow === AUTH_FLOW_IMPLICIT || flow === AUTH_FLOW_ACCESS_CODE || flow === AUTH_FLOW_PASSWORD ) && ( !isAuthorized || isAuthorized && this.state.clientId) && - + { isAuthorized ? ****** : @@ -215,7 +221,7 @@ export default class Oauth2 extends React.Component { { ( (flow === AUTH_FLOW_APPLICATION || flow === AUTH_FLOW_ACCESS_CODE || flow === AUTH_FLOW_PASSWORD) && - + { isAuthorized ? ****** : @@ -233,9 +239,9 @@ export default class Oauth2 extends React.Component { { !isAuthorized && scopes && scopes.size ?

- Scopes: - select all - select none + {t("auth.scopes_title")} + {t("auth.select_all")} + {t("auth.select_none")}

{ scopes.map((description, name) => { return ( @@ -270,11 +276,11 @@ export default class Oauth2 extends React.Component { }
{ isValid && - ( isAuthorized ? - : + ( isAuthorized ? + : ) } - +
diff --git a/src/core/components/clear.jsx b/src/core/components/clear.jsx index d4e0ec859ff..2c299608ce1 100644 --- a/src/core/components/clear.jsx +++ b/src/core/components/clear.jsx @@ -1,5 +1,6 @@ import React, { Component } from "react" import PropTypes from "prop-types" +import { fallbackT } from "core/plugins/i18n/fn" export default class Clear extends Component { @@ -10,9 +11,10 @@ export default class Clear extends Component { } render(){ + const { t } = this.props return ( ) } @@ -21,5 +23,10 @@ export default class Clear extends Component { specActions: PropTypes.object.isRequired, path: PropTypes.string.isRequired, method: PropTypes.string.isRequired, + t: PropTypes.func, + } + + static defaultProps = { + t: fallbackT, } } diff --git a/src/core/components/copy-to-clipboard-btn.jsx b/src/core/components/copy-to-clipboard-btn.jsx index 9c44487e2c5..e31fbf6cffe 100644 --- a/src/core/components/copy-to-clipboard-btn.jsx +++ b/src/core/components/copy-to-clipboard-btn.jsx @@ -1,6 +1,7 @@ import React from "react" import { CopyToClipboard } from "react-copy-to-clipboard" import PropTypes from "prop-types" +import { fallbackT } from "core/plugins/i18n/fn" /** * @param {{ getComponent: func, textToCopy: string }} props @@ -9,12 +10,12 @@ import PropTypes from "prop-types" */ export default class CopyToClipboardBtn extends React.Component { render() { - let { getComponent } = this.props + let { getComponent, t } = this.props const CopyIcon = getComponent("CopyIcon") return ( -
+
@@ -25,5 +26,10 @@ export default class CopyToClipboardBtn extends React.Component { static propTypes = { getComponent: PropTypes.func.isRequired, textToCopy: PropTypes.string.isRequired, + t: PropTypes.func, + } + + static defaultProps = { + t: fallbackT, } } diff --git a/src/core/components/errors.jsx b/src/core/components/errors.jsx index afe2baaf794..b5576103555 100644 --- a/src/core/components/errors.jsx +++ b/src/core/components/errors.jsx @@ -1,6 +1,7 @@ import React from "react" import PropTypes from "prop-types" import { List } from "immutable" +import { fallbackT } from "core/plugins/i18n/fn" export default class Errors extends React.Component { @@ -10,11 +11,15 @@ export default class Errors extends React.Component { layoutSelectors: PropTypes.object.isRequired, layoutActions: PropTypes.object.isRequired, getComponent: PropTypes.func.isRequired, + t: PropTypes.func, } - render() { - let { editorActions, errSelectors, layoutSelectors, layoutActions, getComponent } = this.props + static defaultProps = { + t: fallbackT, + } + render() { + let { editorActions, errSelectors, layoutSelectors, layoutActions, getComponent, t } = this.props const Collapse = getComponent("Collapse") if(editorActions && editorActions.jumpToLine) { @@ -38,18 +43,18 @@ export default class Errors extends React.Component { return (
         
-

Errors

- +

{t("errors.title")}

+
{ sortedJSErrors.map((err, i) => { let type = err.get("type") if(type === "thrown" || type === "auth") { - return + return } if(type === "spec") { - return + return } }) }
@@ -59,7 +64,7 @@ export default class Errors extends React.Component { } } -const ThrownErrorItem = ( { error, jumpToLine } ) => { +const ThrownErrorItem = ( { error, jumpToLine, t } ) => { if(!error) { return null } @@ -76,7 +81,7 @@ const ThrownErrorItem = ( { error, jumpToLine } ) => { { error.get("message") }
- { errorLine && jumpToLine ? Jump to line { errorLine } : null } + { errorLine && jumpToLine ? {t("errors.jump_to_line", { line: errorLine })} : null }
} @@ -84,7 +89,7 @@ const ThrownErrorItem = ( { error, jumpToLine } ) => { ) } -const SpecErrorItem = ( { error, jumpToLine = null } ) => { +const SpecErrorItem = ( { error, jumpToLine, t } ) => { let locationMessage = null if(error.get("path")) { @@ -105,7 +110,7 @@ const SpecErrorItem = ( { error, jumpToLine = null } ) => { { error.get("message") }
@@ -123,10 +128,21 @@ function toTitleCase(str) { ThrownErrorItem.propTypes = { error: PropTypes.object.isRequired, - jumpToLine: PropTypes.func + jumpToLine: PropTypes.func, + t: PropTypes.func, +} + +ThrownErrorItem.defaultProps = { + t: fallbackT, } SpecErrorItem.propTypes = { error: PropTypes.object.isRequired, - jumpToLine: PropTypes.func + jumpToLine: PropTypes.func, + t: PropTypes.func, +} + +SpecErrorItem.defaultProps = { + jumpToLine: null, + t: fallbackT, } diff --git a/src/core/components/execute.jsx b/src/core/components/execute.jsx index e6b3e1b0bed..bdbac7914e9 100644 --- a/src/core/components/execute.jsx +++ b/src/core/components/execute.jsx @@ -1,5 +1,6 @@ import React, { Component } from "react" import PropTypes from "prop-types" +import { fallbackT } from "core/plugins/i18n/fn" export default class Execute extends Component { @@ -12,7 +13,12 @@ export default class Execute extends Component { oas3Selectors: PropTypes.object.isRequired, oas3Actions: PropTypes.object.isRequired, onExecute: PropTypes.func, - disabled: PropTypes.bool + disabled: PropTypes.bool, + t: PropTypes.func, + } + + static defaultProps = { + t: fallbackT, } handleValidateParameters = () => { @@ -93,10 +99,10 @@ export default class Execute extends Component { onChangeProducesWrapper = ( val ) => this.props.specActions.changeProducesValue([this.props.path, this.props.method], val) render(){ - const { disabled } = this.props + const { disabled, t } = this.props return ( ) } diff --git a/src/core/components/headers.jsx b/src/core/components/headers.jsx index c717b61e571..87b1473ba8a 100644 --- a/src/core/components/headers.jsx +++ b/src/core/components/headers.jsx @@ -1,18 +1,23 @@ import React from "react" import PropTypes from "prop-types" import Im from "immutable" +import { fallbackT } from "core/plugins/i18n/fn" const propClass = "header-example" export default class Headers extends React.Component { static propTypes = { headers: PropTypes.object.isRequired, - getComponent: PropTypes.func.isRequired + getComponent: PropTypes.func.isRequired, + t: PropTypes.func, } - render() { - let { headers, getComponent } = this.props + static defaultProps = { + t: fallbackT, + } + render() { + let { headers, getComponent, t } = this.props const Property = getComponent("Property") const Markdown = getComponent("Markdown", true) @@ -21,13 +26,13 @@ export default class Headers extends React.Component { return (
-

Headers:

+

{t("label.headers")}

- - - + + + diff --git a/src/core/components/live-response.jsx b/src/core/components/live-response.jsx index 38ddcafa6df..c2f2543fd26 100644 --- a/src/core/components/live-response.jsx +++ b/src/core/components/live-response.jsx @@ -1,28 +1,39 @@ import React from "react" import PropTypes from "prop-types" import ImPropTypes from "react-immutable-proptypes" +import { fallbackT } from "core/plugins/i18n/fn" -const Headers = ( { headers } )=>{ +const Headers = ( { headers, t } )=>{ return (
-
Response headers
+
{t("label.response_headers")}
{headers}
) } Headers.propTypes = { - headers: PropTypes.array.isRequired + headers: PropTypes.array.isRequired, + t: PropTypes.func, } -const Duration = ( { duration } ) => { +Headers.defaultProps = { + t: fallbackT, +} + +const Duration = ( { duration, t } ) => { return (
-
Request duration
+
{t("label.request_duration")}
{duration} ms
) } Duration.propTypes = { - duration: PropTypes.number.isRequired + duration: PropTypes.number.isRequired, + t: PropTypes.func, +} + +Duration.defaultProps = { + t: fallbackT, } @@ -34,7 +45,12 @@ export default class LiveResponse extends React.Component { displayRequestDuration: PropTypes.bool.isRequired, specSelectors: PropTypes.object.isRequired, getComponent: PropTypes.func.isRequired, - getConfigs: PropTypes.func.isRequired + getConfigs: PropTypes.func.isRequired, + t: PropTypes.func, + } + + static defaultProps = { + t: fallbackT, } shouldComponentUpdate(nextProps) { @@ -47,7 +63,7 @@ export default class LiveResponse extends React.Component { } render() { - const { response, getComponent, getConfigs, displayRequestDuration, specSelectors, path, method } = this.props + const { response, getComponent, getConfigs, displayRequestDuration, specSelectors, path, method, t } = this.props const { showMutatedRequest, requestSnippetsEnabled } = getConfigs() const curlRequest = showMutatedRequest ? specSelectors.mutatedRequestFor(path, method) : specSelectors.requestFor(path, method) @@ -79,17 +95,17 @@ export default class LiveResponse extends React.Component { } { url &&
-

Request URL

+

{t("label.request_url")}

{url}
} -

Server response

+

{t("label.server_response")}

NameDescriptionType{t("label.name")}{t("label.description")}{t("label.type")}
- - + + @@ -98,7 +114,7 @@ export default class LiveResponse extends React.Component { { status } { notDocumented ?
- Undocumented + {t("label.undocumented")}
: null } @@ -118,10 +134,10 @@ export default class LiveResponse extends React.Component { : null } { - hasHeaders ? : null + hasHeaders ? : null } { - displayRequestDuration && duration ? : null + displayRequestDuration && duration ? : null } diff --git a/src/core/components/operation-tag.jsx b/src/core/components/operation-tag.jsx index fe9124bb4f3..fd9576b58d3 100644 --- a/src/core/components/operation-tag.jsx +++ b/src/core/components/operation-tag.jsx @@ -3,6 +3,7 @@ import PropTypes from "prop-types" import ImPropTypes from "react-immutable-proptypes" import Im from "immutable" import { createDeepLinkPath, escapeDeepLinkPath, isFunc } from "core/utils" +import { fallbackT } from "core/plugins/i18n/fn" import { safeBuildUrl, sanitizeUrl } from "core/utils/url" /* eslint-disable react/jsx-no-bind */ @@ -12,6 +13,7 @@ export default class OperationTag extends React.Component { static defaultProps = { tagObj: Im.fromJS({}), tag: "", + t: fallbackT, } static propTypes = { @@ -28,6 +30,7 @@ export default class OperationTag extends React.Component { specUrl: PropTypes.string.isRequired, children: PropTypes.element, + t: PropTypes.func, } render() { @@ -41,6 +44,7 @@ export default class OperationTag extends React.Component { getConfigs, getComponent, specUrl, + t, } = this.props let { @@ -105,7 +109,7 @@ export default class OperationTag extends React.Component { } diff --git a/src/core/components/parameters/parameters.jsx b/src/core/components/parameters/parameters.jsx index 0a9bd517b24..a918a2b2bf5 100644 --- a/src/core/components/parameters/parameters.jsx +++ b/src/core/components/parameters/parameters.jsx @@ -3,6 +3,7 @@ import PropTypes from "prop-types" import { Map, List } from "immutable" import ImPropTypes from "react-immutable-proptypes" import createHtmlReadyId from "core/utils/create-html-ready-id" +import { fallbackT } from "core/plugins/i18n/fn" export default class Parameters extends Component { @@ -32,6 +33,7 @@ export default class Parameters extends Component { pathMethod: PropTypes.array.isRequired, getConfigs: PropTypes.func.isRequired, specPath: ImPropTypes.list.isRequired, + t: PropTypes.func, } @@ -42,6 +44,7 @@ export default class Parameters extends Component { allowTryItOut: true, onChangeKey: [], specPath: [], + t: fallbackT, } onChange = (param, value, isXml) => { @@ -110,6 +113,7 @@ export default class Parameters extends Component { oas3Actions, oas3Selectors, operation, + t, } = this.props const ParameterRow = getComponent("parameterRow") @@ -145,20 +149,20 @@ export default class Parameters extends Component {
this.toggleTab("parameters")} className={`tab-item ${this.state.parametersVisible && "active"}`}> -

Parameters

+

{t("label.parameters")}

{operation.get("callbacks") ? (
this.toggleTab("callbacks")} className={`tab-item ${this.state.callbackVisible && "active"}`}> -

Callbacks

+

{t("label.callbacks")}

) : null }
) : (
-

Parameters

+

{t("label.parameters")}

)} {allowTryItOut ? ( @@ -172,13 +176,13 @@ export default class Parameters extends Component { ) : null} {this.state.parametersVisible ?
- {!groupedParametersArr.length ?

No parameters

: + {!groupedParametersArr.length ?

{t("label.no_parameters")}

:
CodeDetails{t("label.code")}{t("label.details")}
- - + + @@ -218,8 +222,7 @@ export default class Parameters extends Component { isOAS3 && requestBody && this.state.parametersVisible &&
-

Request - body

+

{t("label.request_body")}

diff --git a/src/core/components/response-body.jsx b/src/core/components/response-body.jsx index 4b99f41f8bb..5078c679095 100644 --- a/src/core/components/response-body.jsx +++ b/src/core/components/response-body.jsx @@ -5,6 +5,7 @@ import toLower from "lodash/toLower" import { extractFileNameFromContentDispositionHeader } from "core/utils" import { getKnownSyntaxHighlighterLanguage } from "core/utils/jsonParse" import win from "core/window" +import { fallbackT } from "core/plugins/i18n/fn" export default class ResponseBody extends React.PureComponent { state = { @@ -16,7 +17,12 @@ export default class ResponseBody extends React.PureComponent { contentType: PropTypes.string, getComponent: PropTypes.func.isRequired, headers: PropTypes.object, - url: PropTypes.string + url: PropTypes.string, + t: PropTypes.func, + } + + static defaultProps = { + t: fallbackT, } updateParsedContent = (prevContent) => { @@ -50,7 +56,7 @@ export default class ResponseBody extends React.PureComponent { } render() { - let { content, contentType, url, headers={}, getComponent } = this.props + let { content, contentType, url, headers={}, getComponent, t } = this.props const { parsedContent } = this.state const HighlightCode = getComponent("HighlightCode", true) const downloadName = "response_" + new Date().getTime() @@ -85,12 +91,12 @@ export default class ResponseBody extends React.PureComponent { } if(win.navigator && win.navigator.msSaveOrOpenBlob) { - bodyEl = + bodyEl = } else { - bodyEl = + bodyEl = } } else { - bodyEl =
Download headers detected but your browser does not support downloading binary via XHR (Blob).
+ bodyEl =
{ t("response.no_blob_support") }
} // Anything else (CORS) @@ -104,7 +110,7 @@ export default class ResponseBody extends React.PureComponent { try { body = JSON.stringify(JSON.parse(content), null, " ") } catch (error) { - body = "can't parse JSON. Raw result:\n\n" + content + body = t("response.json_parse_error") + content } bodyEl = {body} @@ -145,7 +151,7 @@ export default class ResponseBody extends React.PureComponent { // in `updateParsedContent`, so let's display it bodyEl =

- Unrecognized response type; displaying content as text. + { t("response.unrecognized_type_display_as_text") }

{parsedContent}
@@ -153,7 +159,7 @@ export default class ResponseBody extends React.PureComponent { } else { // Give up bodyEl =

- Unrecognized response type; unable to display. + { t("response.unrecognized_type_unable_to_display") }

} } else { @@ -162,7 +168,7 @@ export default class ResponseBody extends React.PureComponent { } return ( !bodyEl ? null :
-
Response body
+
{t("label.response_body")}
{ bodyEl }
) diff --git a/src/core/components/response.jsx b/src/core/components/response.jsx index b1d0b971cf6..0705a61d9bf 100644 --- a/src/core/components/response.jsx +++ b/src/core/components/response.jsx @@ -3,6 +3,7 @@ import PropTypes from "prop-types" import ImPropTypes from "react-immutable-proptypes" import cx from "classnames" import { fromJS, Seq, Iterable, Map } from "immutable" +import { fallbackT } from "core/plugins/i18n/fn" import { getExtensions, fromJSOrdered, stringify } from "core/utils" import { getKnownSyntaxHighlighterLanguage } from "core/utils/jsonParse" @@ -44,12 +45,14 @@ export default class Response extends React.Component { contentType: PropTypes.string, activeExamplesKey: PropTypes.string, controlsAcceptHeader: PropTypes.bool, - onContentTypeChange: PropTypes.func + onContentTypeChange: PropTypes.func, + t: PropTypes.func, } static defaultProps = { response: fromJS({}), - onContentTypeChange: () => {} + onContentTypeChange: () => {}, + t: fallbackT, } _onContentTypeChange = (value) => { @@ -87,6 +90,7 @@ export default class Response extends React.Component { contentType, controlsAcceptHeader, oas3Actions, + t, } = this.props let { inferSchema, getSampleSchema } = fn @@ -194,7 +198,7 @@ export default class Response extends React.Component { })} > - Media type + {t("label.media_type")} {controlsAcceptHeader ? ( - Controls Accept header. + {t("response.controls_accept_header_prefix")} + Accept + {t("response.controls_accept_header_suffix")} ) : null}
{Map.isMap(examplesForMediaType) && !examplesForMediaType.isEmpty() ? (
- Examples + {t("label.examples")} { return }) - : No links} + : {t("label.no_links")}} : null} ) diff --git a/src/core/components/responses.jsx b/src/core/components/responses.jsx index fe1f7ddcb8b..56794bf97cc 100644 --- a/src/core/components/responses.jsx +++ b/src/core/components/responses.jsx @@ -3,6 +3,7 @@ import { fromJS, Iterable } from "immutable" import PropTypes from "prop-types" import ImPropTypes from "react-immutable-proptypes" import { defaultStatusCode, getAcceptControllingResponse, isExtension } from "core/utils" +import { fallbackT } from "core/plugins/i18n/fn" import createHtmlReadyId from "core/utils/create-html-ready-id" export default class Responses extends React.Component { @@ -21,13 +22,15 @@ export default class Responses extends React.Component { oas3Actions: PropTypes.object.isRequired, oas3Selectors: PropTypes.object.isRequired, specPath: ImPropTypes.list.isRequired, - fn: PropTypes.object.isRequired + fn: PropTypes.object.isRequired, + t: PropTypes.func, } static defaultProps = { tryItOutResponse: null, produces: fromJS(["application/json"]), - displayRequestDuration: false + displayRequestDuration: false, + t: fallbackT, } // These performance-enhancing checks were disabled as part of Multiple Examples @@ -73,6 +76,7 @@ export default class Responses extends React.Component { method, oas3Selectors, oas3Actions, + t, } = this.props let defaultCode = defaultStatusCode( responses ) @@ -94,12 +98,12 @@ export default class Responses extends React.Component { return (!nonExtensionResponses || !nonExtensionResponses.size) ? null : (
-

Responses

+

{t("label.responses")}

{ specSelectors.isOAS3() ? null :
} @@ -125,9 +129,9 @@ export default class Responses extends React.Component {
NameDescription{t("label.name")}{t("label.description")}
- - - { specSelectors.isOAS3() ? : null } + + + { specSelectors.isOAS3() ? : null } diff --git a/src/core/components/try-it-out-button.jsx b/src/core/components/try-it-out-button.jsx index 176ce314c81..08f25464066 100644 --- a/src/core/components/try-it-out-button.jsx +++ b/src/core/components/try-it-out-button.jsx @@ -1,5 +1,6 @@ import React from "react" import PropTypes from "prop-types" +import { fallbackT } from "core/plugins/i18n/fn" export default class TryItOutButton extends React.Component { @@ -10,6 +11,7 @@ export default class TryItOutButton extends React.Component { enabled: PropTypes.bool, // Try it out is enabled, ie: the user has access to the form hasUserEditedBody: PropTypes.bool, // Try it out is enabled, ie: the user has access to the form isOAS3: PropTypes.bool, // Try it out is enabled, ie: the user has access to the form + t: PropTypes.func, } static defaultProps = { @@ -19,21 +21,22 @@ export default class TryItOutButton extends React.Component { enabled: false, hasUserEditedBody: false, isOAS3: false, + t: fallbackT, } render() { - const { onTryoutClick, onCancelClick, onResetClick, enabled, hasUserEditedBody, isOAS3 } = this.props + const { onTryoutClick, onCancelClick, onResetClick, enabled, hasUserEditedBody, isOAS3, t } = this.props const showReset = isOAS3 && hasUserEditedBody return (
{ - enabled ? - : + enabled ? + : } { - showReset && + showReset && }
) diff --git a/src/core/config/defaults.js b/src/core/config/defaults.js index 96102dda55e..ee0d8760f68 100644 --- a/src/core/config/defaults.js +++ b/src/core/config/defaults.js @@ -96,6 +96,11 @@ const defaultOptions = Object.freeze({ ], uncaughtExceptionHandler: null, + + // ── Internationalization ───────────────────────────────────────────────── + // BCP 47 locale tag (e.g. "en", "fr", "de"). null = auto-detect from + // navigator.languages / navigator.language, falling back to "en". + locale: null, }) export default defaultOptions diff --git a/src/core/containers/filter.jsx b/src/core/containers/filter.jsx index 440b1ac4ad9..5dfa2df4d12 100644 --- a/src/core/containers/filter.jsx +++ b/src/core/containers/filter.jsx @@ -1,5 +1,6 @@ import React from "react" import PropTypes from "prop-types" +import { fallbackT } from "core/plugins/i18n/fn" export default class FilterContainer extends React.Component { @@ -8,6 +9,11 @@ export default class FilterContainer extends React.Component { layoutSelectors: PropTypes.object.isRequired, layoutActions: PropTypes.object.isRequired, getComponent: PropTypes.func.isRequired, + t: PropTypes.func, + } + + static defaultProps = { + t: fallbackT, } onFilterChange = (e) => { @@ -16,7 +22,7 @@ export default class FilterContainer extends React.Component { } render () { - const {specSelectors, layoutSelectors, getComponent} = this.props + const {specSelectors, layoutSelectors, getComponent, t} = this.props const Col = getComponent("Col") const isLoading = specSelectors.loadingStatus() === "loading" @@ -32,7 +38,7 @@ export default class FilterContainer extends React.Component { {filter === false ? null :
- diff --git a/src/core/plugins/i18n/actions.js b/src/core/plugins/i18n/actions.js new file mode 100644 index 00000000000..a1562d952d7 --- /dev/null +++ b/src/core/plugins/i18n/actions.js @@ -0,0 +1,20 @@ +/** + * @prettier + */ + +export const SET_LOCALE = "i18n_set_locale" +export const LOAD_MESSAGES = "i18n_load_messages" + +export function setLocale(locale) { + return { + type: SET_LOCALE, + payload: locale, + } +} + +export function loadMessages(locale, messages) { + return { + type: LOAD_MESSAGES, + payload: { locale, messages }, + } +} diff --git a/src/core/plugins/i18n/fn.js b/src/core/plugins/i18n/fn.js new file mode 100644 index 00000000000..046c6468a87 --- /dev/null +++ b/src/core/plugins/i18n/fn.js @@ -0,0 +1,40 @@ +/** + * @prettier + */ +import en from "./locales/en" + +/** + * Pure translation helper. + * Looks up `key` in `localeMsgs` (a plain JS object), then falls back to + * `fallbackMsgs`. Interpolates `{{varName}}` placeholders with values from + * `vars`. Returns the key itself when no match is found. + * + * @param {object} localeMsgs - Key→value map for the active locale (may be null/undefined) + * @param {object} fallbackMsgs - Key→value map for the fallback locale (usually "en") + * @param {string} key - Message key, e.g. "button.cancel" + * @param {object} [vars] - Interpolation variables, e.g. { line: 42 } + * @returns {string} + */ +export function translate(localeMsgs, fallbackMsgs, key, vars) { + const own = Object.prototype.hasOwnProperty + const raw = + localeMsgs && own.call(localeMsgs, key) + ? localeMsgs[key] + : fallbackMsgs && own.call(fallbackMsgs, key) + ? fallbackMsgs[key] + : key + + if (!vars) return String(raw) + return String(raw).replace(/\{\{(\w+)\}\}/g, (_, k) => + own.call(vars, k) ? String(vars[k]) : `{{${k}}}` + ) +} + +/** + * Default translation function that uses the built-in English locale. + * Used as a fallback in components when no `t` prop is injected (e.g. in + * unit tests that do not go through the Redux system). + */ +export function fallbackT(key, vars) { + return translate(null, en, key, vars) +} diff --git a/src/core/plugins/i18n/index.js b/src/core/plugins/i18n/index.js new file mode 100644 index 00000000000..21e696a9186 --- /dev/null +++ b/src/core/plugins/i18n/index.js @@ -0,0 +1,75 @@ +/** + * @prettier + */ +import reducers from "./reducers" +import * as actions from "./actions" +import * as selectors from "./selectors" +import en from "./locales/en" +import builtinLocales from "./locales" +import win from "core/window" + +export default function I18nPlugin() { + return { + afterLoad(system) { + // ── 1. Load built-in English messages ──────────────────────────────── + system.i18nActions.loadMessages("en", en) + + // ── 2. Determine locale ────────────────────────────────────────────── + const { locale: configLocale } = system.getConfigs() + let locale + if (configLocale) { + // Normalize configured locale to base language code, same as auto-detection + locale = configLocale.split("-")[0].toLowerCase() + } else { + const browserLang = + (win.navigator && + win.navigator.languages && + win.navigator.languages[0]) || + (win.navigator && win.navigator.language) || + "en" + locale = browserLang.split("-")[0].toLowerCase() + } + system.i18nActions.setLocale(locale) + + // ── 3. Auto-load matching built-in locale (if any) ─────────────────── + if (locale !== "en" && builtinLocales[locale]) { + system.i18nActions.loadMessages(locale, builtinLocales[locale]) + } + + // ── 4. Register the t() translation function ───────────────────────── + this.rootInjects = this.rootInjects || {} + const own = Object.prototype.hasOwnProperty + this.rootInjects.t = (key, vars) => { + const allMessages = system.i18nSelectors.getMessages() + const currentLocale = system.i18nSelectors.getLocale() + + // Use Immutable-native lookups — avoids expensive .toJS() on every call + const localeMap = allMessages.get(currentLocale) + const enMap = allMessages.get("en") + + let raw + if (localeMap && localeMap.has(key)) { + raw = localeMap.get(key) + } else if (enMap && enMap.has(key)) { + raw = enMap.get(key) + } else { + // Ultimate fallback: static en object (always available without Redux) + raw = own.call(en, key) ? en[key] : key + } + + if (!vars) return String(raw) + return String(raw).replace(/\{\{(\w+)\}\}/g, (_, k) => + own.call(vars, k) ? String(vars[k]) : `{{${k}}}` + ) + } + }, + + statePlugins: { + i18n: { + reducers, + actions, + selectors, + }, + }, + } +} diff --git a/src/core/plugins/i18n/locales/de.js b/src/core/plugins/i18n/locales/de.js new file mode 100644 index 00000000000..9b7588ad464 --- /dev/null +++ b/src/core/plugins/i18n/locales/de.js @@ -0,0 +1,115 @@ +/** + * @prettier + */ + +/** + * German (Deutsch) message catalog. + */ +const de = { + // ── Buttons / actions ───────────────────────────────────────────────────── + "button.authorize": "Autorisieren", + "button.cancel": "Abbrechen", + "button.clear": "Löschen", + "button.close": "Schließen", + "button.copy_to_clipboard": "In die Zwischenablage kopieren", + "button.download_file": "Datei herunterladen", + "button.edit": "Bearbeiten", + "button.execute": "Ausführen", + "button.explore": "Erkunden", + "button.hide": "Verbergen", + "button.logout": "Abmelden", + "button.reset": "Zurücksetzen", + "button.show": "Anzeigen", + "button.try_it_out": "Ausprobieren", + + // ── Section / column labels ──────────────────────────────────────────────── + "label.callbacks": "Callbacks", + "label.code": "Code", + "label.description": "Beschreibung", + "label.details": "Details", + "label.examples": "Beispiele", + "label.headers": "Header:", + "label.links": "Links", + "label.media_type": "Medientyp", + "label.models": "Modelle", + "label.name": "Name", + "label.no_links": "Keine Links", + "label.no_parameters": "Keine Parameter", + "label.parameter_content_type": "Parameter-Inhaltstyp", + "label.parameters": "Parameter", + "label.request_body": "Anfrage-Body", + "label.request_duration": "Anfragedauer", + "label.request_url": "Anfrage-URL", + "label.response_body": "Antwort-Body", + "label.response_content_type": "Antwort-Inhaltstyp", + "label.response_headers": "Antwortheader", + "label.responses": "Antworten", + "label.schemas": "Schemas", + "label.server_response": "Serverantwort", + "label.snippets": "Snippets", + "label.type": "Typ", + "label.undocumented": "Nicht dokumentiert", + + // ── Authentication ───────────────────────────────────────────────────────── + "auth.api_key_in": "In:", + "auth.api_key_name": "Name:", + "auth.api_key_value": "Wert:", + "auth.application": "Anwendung:", + "auth.authorization_header": "Autorisierungsheader", + "auth.authorization_url": "Autorisierungs-URL:", + "auth.authorized": "Autorisiert", + "auth.basic_authorization_title": "Basisautorisierung", + "auth.client_credentials_location": "Speicherort der Client-Anmeldedaten:", + "auth.client_id": "client_id:", + "auth.client_secret": "client_secret:", + "auth.flow": "Flow:", + "auth.openid_connect_url": "OpenID Connect-URL:", + "auth.password": "Passwort:", + "auth.password_cap": "Passwort:", + "auth.request_body_option": "Anfrage-Body", + "auth.scopes_description": + "Scopes werden verwendet, um einer Anwendung verschiedene Zugriffsebenen auf Daten im Namen des Endbenutzers zu gewähren. Jede API kann einen oder mehrere Scopes deklarieren.", + "auth.scopes_required": + "Die API erfordert die folgenden Scopes. Wählen Sie aus, welche Sie Swagger UI gewähren möchten.", + "auth.scopes_title": "Scopes:", + "auth.select_all": "Alle auswählen", + "auth.select_none": "Keine auswählen", + "auth.token_url": "Token-URL:", + "auth.username": "Benutzername:", + "auth.username_cap": "Benutzername:", + + // ── Accessibility (aria-labels / titles) ─────────────────────────────────── + "aria.apply_credentials": "Anmeldedaten anwenden", + "aria.apply_oauth2_credentials": "Angegebene OAuth2-Anmeldedaten anwenden", + "aria.authorization_button_locked": "Autorisierungsschaltfläche gesperrt", + "aria.authorization_button_unlocked": "Autorisierungsschaltfläche entsperrt", + "aria.collapse_operation": "Operation einklappen", + "aria.expand_operation": "Operation ausklappen", + "aria.remove_authorization": "Autorisierung entfernen", + "aria.request_content_type": "Anfrage-Inhaltstyp", + "aria.response_content_type": "Antwort-Inhaltstyp", + + // ── Errors ──────────────────────────────────────────────────────────────── + "errors.jump_to_line": "Zur Zeile {{line}} springen", + "errors.title": "Fehler", + + // ── Placeholders ────────────────────────────────────────────────────────── + "placeholder.filter_by_tag": "Nach Tag filtern", + + // ── Response ────────────────────────────────────────────────────────────── + "response.controls_accept_header_prefix": "Steuert den ", + "response.controls_accept_header_suffix": "-Header.", + "response.json_parse_error": + "JSON konnte nicht verarbeitet werden. Rohergebnis:\n\n", + "response.no_blob_support": + "Download-Header erkannt, aber Ihr Browser unterstützt kein Herunterladen von Binärdaten via XHR (Blob).", + "response.unrecognized_type_display_as_text": + "Unbekannter Antworttyp; Inhalt wird als Text angezeigt.", + "response.unrecognized_type_unable_to_display": + "Unbekannter Antworttyp; kann nicht angezeigt werden.", + + // ── Topbar ──────────────────────────────────────────────────────────────── + "topbar.select_definition": "Definition auswählen", +} + +export default de diff --git a/src/core/plugins/i18n/locales/en.js b/src/core/plugins/i18n/locales/en.js new file mode 100644 index 00000000000..b1bb5a1fedf --- /dev/null +++ b/src/core/plugins/i18n/locales/en.js @@ -0,0 +1,115 @@ +/** + * @prettier + */ + +/** + * English message catalog — the canonical reference and ultimate fallback. + * Keys use a namespaced dot-notation: .. + */ +const en = { + // ── Buttons / actions ───────────────────────────────────────────────────── + "button.authorize": "Authorize", + "button.cancel": "Cancel", + "button.clear": "Clear", + "button.close": "Close", + "button.copy_to_clipboard": "Copy to clipboard", + "button.download_file": "Download file", + "button.edit": "Edit", + "button.execute": "Execute", + "button.explore": "Explore", + "button.hide": "Hide", + "button.logout": "Logout", + "button.reset": "Reset", + "button.show": "Show", + "button.try_it_out": "Try it out", + + // ── Section / column labels ──────────────────────────────────────────────── + "label.callbacks": "Callbacks", + "label.code": "Code", + "label.description": "Description", + "label.details": "Details", + "label.examples": "Examples", + "label.headers": "Headers:", + "label.links": "Links", + "label.media_type": "Media type", + "label.models": "Models", + "label.name": "Name", + "label.no_links": "No links", + "label.no_parameters": "No parameters", + "label.parameter_content_type": "Parameter content type", + "label.parameters": "Parameters", + "label.request_body": "Request body", + "label.request_duration": "Request duration", + "label.request_url": "Request URL", + "label.response_body": "Response body", + "label.response_content_type": "Response content type", + "label.response_headers": "Response headers", + "label.responses": "Responses", + "label.schemas": "Schemas", + "label.server_response": "Server response", + "label.snippets": "Snippets", + "label.type": "Type", + "label.undocumented": "Undocumented", + + // ── Authentication ───────────────────────────────────────────────────────── + "auth.api_key_in": "In:", + "auth.api_key_name": "Name:", + "auth.api_key_value": "Value:", + "auth.application": "Application:", + "auth.authorization_header": "Authorization header", + "auth.authorization_url": "Authorization URL:", + "auth.authorized": "Authorized", + "auth.basic_authorization_title": "Basic authorization", + "auth.client_credentials_location": "Client credentials location:", + "auth.client_id": "client_id:", + "auth.client_secret": "client_secret:", + "auth.flow": "Flow:", + "auth.openid_connect_url": "OpenID Connect URL:", + "auth.password": "password:", + "auth.password_cap": "Password:", + "auth.request_body_option": "Request body", + "auth.scopes_description": + "Scopes are used to grant an application different levels of access to data on behalf of the end user. Each API may declare one or more scopes.", + "auth.scopes_required": + "API requires the following scopes. Select which ones you want to grant to Swagger UI.", + "auth.scopes_title": "Scopes:", + "auth.select_all": "select all", + "auth.select_none": "select none", + "auth.token_url": "Token URL:", + "auth.username": "username:", + "auth.username_cap": "Username:", + + // ── Accessibility (aria-labels / titles) ─────────────────────────────────── + "aria.apply_credentials": "Apply credentials", + "aria.apply_oauth2_credentials": "Apply given OAuth2 credentials", + "aria.authorization_button_locked": "authorization button locked", + "aria.authorization_button_unlocked": "authorization button unlocked", + "aria.collapse_operation": "Collapse operation", + "aria.expand_operation": "Expand operation", + "aria.remove_authorization": "Remove authorization", + "aria.request_content_type": "Request content type", + "aria.response_content_type": "Response content type", + + // ── Errors ──────────────────────────────────────────────────────────────── + "errors.jump_to_line": "Jump to line {{line}}", + "errors.title": "Errors", + + // ── Placeholders ────────────────────────────────────────────────────────── + "placeholder.filter_by_tag": "Filter by tag", + + // ── Response ────────────────────────────────────────────────────────────── + "response.controls_accept_header_prefix": "Controls ", + "response.controls_accept_header_suffix": " header.", + "response.json_parse_error": "Can't parse JSON. Raw result:\n\n", + "response.no_blob_support": + "Download headers detected but your browser does not support downloading binary via XHR (Blob).", + "response.unrecognized_type_display_as_text": + "Unrecognized response type; displaying content as text.", + "response.unrecognized_type_unable_to_display": + "Unrecognized response type; unable to display.", + + // ── Topbar ──────────────────────────────────────────────────────────────── + "topbar.select_definition": "Select a definition", +} + +export default en diff --git a/src/core/plugins/i18n/locales/es.js b/src/core/plugins/i18n/locales/es.js new file mode 100644 index 00000000000..6c29bf7795e --- /dev/null +++ b/src/core/plugins/i18n/locales/es.js @@ -0,0 +1,116 @@ +/** + * @prettier + */ + +/** + * Spanish (Español) message catalog. + */ +const es = { + // ── Buttons / actions ───────────────────────────────────────────────────── + "button.authorize": "Autorizar", + "button.cancel": "Cancelar", + "button.clear": "Limpiar", + "button.close": "Cerrar", + "button.copy_to_clipboard": "Copiar al portapapeles", + "button.download_file": "Descargar archivo", + "button.edit": "Editar", + "button.execute": "Ejecutar", + "button.explore": "Explorar", + "button.hide": "Ocultar", + "button.logout": "Cerrar sesión", + "button.reset": "Restablecer", + "button.show": "Mostrar", + "button.try_it_out": "Pruébalo", + + // ── Section / column labels ──────────────────────────────────────────────── + "label.callbacks": "Callbacks", + "label.code": "Código", + "label.description": "Descripción", + "label.details": "Detalles", + "label.examples": "Ejemplos", + "label.headers": "Cabeceras:", + "label.links": "Vínculos", + "label.media_type": "Tipo de medio", + "label.models": "Modelos", + "label.name": "Nombre", + "label.no_links": "Sin vínculos", + "label.no_parameters": "Sin parámetros", + "label.parameter_content_type": "Tipo de contenido del parámetro", + "label.parameters": "Parámetros", + "label.request_body": "Cuerpo de la solicitud", + "label.request_duration": "Duración de la solicitud", + "label.request_url": "URL de la solicitud", + "label.response_body": "Cuerpo de la respuesta", + "label.response_content_type": "Tipo de contenido de la respuesta", + "label.response_headers": "Cabeceras de respuesta", + "label.responses": "Respuestas", + "label.schemas": "Esquemas", + "label.server_response": "Respuesta del servidor", + "label.snippets": "Fragmentos", + "label.type": "Tipo", + "label.undocumented": "No documentado", + + // ── Authentication ───────────────────────────────────────────────────────── + "auth.api_key_in": "En:", + "auth.api_key_name": "Nombre:", + "auth.api_key_value": "Valor:", + "auth.application": "Aplicación:", + "auth.authorization_header": "Cabecera de autorización", + "auth.authorization_url": "URL de autorización:", + "auth.authorized": "Autorizado", + "auth.basic_authorization_title": "Autorización básica", + "auth.client_credentials_location": "Ubicación de credenciales del cliente:", + "auth.client_id": "client_id:", + "auth.client_secret": "client_secret:", + "auth.flow": "Flujo:", + "auth.openid_connect_url": "URL de OpenID Connect:", + "auth.password": "contraseña:", + "auth.password_cap": "Contraseña:", + "auth.request_body_option": "Cuerpo de la solicitud", + "auth.scopes_description": + "Los scopes se utilizan para otorgar a una aplicación diferentes niveles de acceso a los datos en nombre del usuario final. Cada API puede declarar uno o más scopes.", + "auth.scopes_required": + "La API requiere los siguientes scopes. Seleccione cuáles desea otorgar a Swagger UI.", + "auth.scopes_title": "Scopes:", + "auth.select_all": "seleccionar todos", + "auth.select_none": "seleccionar ninguno", + "auth.token_url": "URL del token:", + "auth.username": "nombre de usuario:", + "auth.username_cap": "Nombre de usuario:", + + // ── Accessibility (aria-labels / titles) ─────────────────────────────────── + "aria.apply_credentials": "Aplicar credenciales", + "aria.apply_oauth2_credentials": + "Aplicar las credenciales OAuth2 proporcionadas", + "aria.authorization_button_locked": "botón de autorización bloqueado", + "aria.authorization_button_unlocked": "botón de autorización desbloqueado", + "aria.collapse_operation": "Contraer operación", + "aria.expand_operation": "Expandir operación", + "aria.remove_authorization": "Eliminar autorización", + "aria.request_content_type": "Tipo de contenido de la solicitud", + "aria.response_content_type": "Tipo de contenido de la respuesta", + + // ── Errors ──────────────────────────────────────────────────────────────── + "errors.jump_to_line": "Saltar a la línea {{line}}", + "errors.title": "Errores", + + // ── Placeholders ────────────────────────────────────────────────────────── + "placeholder.filter_by_tag": "Filtrar por etiqueta", + + // ── Response ────────────────────────────────────────────────────────────── + "response.controls_accept_header_prefix": "Controla la cabecera ", + "response.controls_accept_header_suffix": ".", + "response.json_parse_error": + "No se puede analizar el JSON. Resultado sin formato:\n\n", + "response.no_blob_support": + "Encabezados de descarga detectados, pero su navegador no es compatible con la descarga de datos binarios mediante XHR (Blob).", + "response.unrecognized_type_display_as_text": + "Tipo de respuesta no reconocido; mostrando contenido como texto.", + "response.unrecognized_type_unable_to_display": + "Tipo de respuesta no reconocido; no se puede mostrar.", + + // ── Topbar ──────────────────────────────────────────────────────────────── + "topbar.select_definition": "Seleccionar una definición", +} + +export default es diff --git a/src/core/plugins/i18n/locales/fr.js b/src/core/plugins/i18n/locales/fr.js new file mode 100644 index 00000000000..c72d7ecc92b --- /dev/null +++ b/src/core/plugins/i18n/locales/fr.js @@ -0,0 +1,115 @@ +/** + * @prettier + */ + +/** + * French (Français) message catalog. + */ +const fr = { + // ── Buttons / actions ───────────────────────────────────────────────────── + "button.authorize": "Autoriser", + "button.cancel": "Annuler", + "button.clear": "Effacer", + "button.close": "Fermer", + "button.copy_to_clipboard": "Copier dans le presse-papiers", + "button.download_file": "Télécharger le fichier", + "button.edit": "Modifier", + "button.execute": "Exécuter", + "button.explore": "Explorer", + "button.hide": "Masquer", + "button.logout": "Se déconnecter", + "button.reset": "Réinitialiser", + "button.show": "Afficher", + "button.try_it_out": "Essayer", + + // ── Section / column labels ──────────────────────────────────────────────── + "label.callbacks": "Callbacks", + "label.code": "Code", + "label.description": "Description", + "label.details": "Détails", + "label.examples": "Exemples", + "label.headers": "En-têtes :", + "label.links": "Liens", + "label.media_type": "Type de média", + "label.models": "Modèles", + "label.name": "Nom", + "label.no_links": "Aucun lien", + "label.no_parameters": "Aucun paramètre", + "label.parameter_content_type": "Type de contenu du paramètre", + "label.parameters": "Paramètres", + "label.request_body": "Corps de la requête", + "label.request_duration": "Durée de la requête", + "label.request_url": "URL de la requête", + "label.response_body": "Corps de la réponse", + "label.response_content_type": "Type de contenu de la réponse", + "label.response_headers": "En-têtes de réponse", + "label.responses": "Réponses", + "label.schemas": "Schémas", + "label.server_response": "Réponse du serveur", + "label.snippets": "Extraits", + "label.type": "Type", + "label.undocumented": "Non documenté", + + // ── Authentication ───────────────────────────────────────────────────────── + "auth.api_key_in": "Dans :", + "auth.api_key_name": "Nom :", + "auth.api_key_value": "Valeur :", + "auth.application": "Application :", + "auth.authorization_header": "En-tête d'autorisation", + "auth.authorization_url": "URL d'autorisation :", + "auth.authorized": "Autorisé", + "auth.basic_authorization_title": "Autorisation de base", + "auth.client_credentials_location": "Emplacement des identifiants client :", + "auth.client_id": "client_id :", + "auth.client_secret": "client_secret :", + "auth.flow": "Flux :", + "auth.openid_connect_url": "URL OpenID Connect :", + "auth.password": "mot de passe :", + "auth.password_cap": "Mot de passe :", + "auth.request_body_option": "Corps de la requête", + "auth.scopes_description": + "Les scopes sont utilisés pour accorder à une application différents niveaux d'accès aux données au nom de l'utilisateur final. Chaque API peut déclarer un ou plusieurs scopes.", + "auth.scopes_required": + "L'API nécessite les scopes suivants. Sélectionnez ceux que vous souhaitez accorder à Swagger UI.", + "auth.scopes_title": "Scopes :", + "auth.select_all": "tout sélectionner", + "auth.select_none": "ne rien sélectionner", + "auth.token_url": "URL du token :", + "auth.username": "nom d'utilisateur :", + "auth.username_cap": "Nom d'utilisateur :", + + // ── Accessibility (aria-labels / titles) ─────────────────────────────────── + "aria.apply_credentials": "Appliquer les identifiants", + "aria.apply_oauth2_credentials": "Appliquer les identifiants OAuth2 fournis", + "aria.authorization_button_locked": "bouton d'autorisation verrouillé", + "aria.authorization_button_unlocked": "bouton d'autorisation déverrouillé", + "aria.collapse_operation": "Réduire l'opération", + "aria.expand_operation": "Développer l'opération", + "aria.remove_authorization": "Supprimer l'autorisation", + "aria.request_content_type": "Type de contenu de la requête", + "aria.response_content_type": "Type de contenu de la réponse", + + // ── Errors ──────────────────────────────────────────────────────────────── + "errors.jump_to_line": "Aller à la ligne {{line}}", + "errors.title": "Erreurs", + + // ── Placeholders ────────────────────────────────────────────────────────── + "placeholder.filter_by_tag": "Filtrer par étiquette", + + // ── Response ────────────────────────────────────────────────────────────── + "response.controls_accept_header_prefix": "Contrôle l'en-tête ", + "response.controls_accept_header_suffix": ".", + "response.json_parse_error": + "Impossible d'analyser le JSON. Résultat brut\u00a0:\n\n", + "response.no_blob_support": + "En-têtes de téléchargement détectés, mais votre navigateur ne prend pas en charge le téléchargement de données binaires via XHR (Blob).", + "response.unrecognized_type_display_as_text": + "Type de réponse non reconnu\u00a0; affichage du contenu en texte.", + "response.unrecognized_type_unable_to_display": + "Type de réponse non reconnu\u00a0; impossible d'afficher.", + + // ── Topbar ──────────────────────────────────────────────────────────────── + "topbar.select_definition": "Sélectionner une définition", +} + +export default fr diff --git a/src/core/plugins/i18n/locales/index.js b/src/core/plugins/i18n/locales/index.js new file mode 100644 index 00000000000..713ef3d68b0 --- /dev/null +++ b/src/core/plugins/i18n/locales/index.js @@ -0,0 +1,21 @@ +/** + * @prettier + */ + +/** + * Built-in locale catalogs bundled with Swagger UI. + * Exported as a plain object keyed by BCP 47 base language code. + */ +import en from "./en" +import de from "./de" +import es from "./es" +import fr from "./fr" +import is from "./is" +import ko from "./ko" +import pt from "./pt" +import ru from "./ru" +import zh from "./zh" + +const builtinLocales = { en, de, es, fr, is, ko, pt, ru, zh } + +export default builtinLocales diff --git a/src/core/plugins/i18n/locales/is.js b/src/core/plugins/i18n/locales/is.js new file mode 100644 index 00000000000..ddd5e23df8d --- /dev/null +++ b/src/core/plugins/i18n/locales/is.js @@ -0,0 +1,115 @@ +/** + * @prettier + */ + +/** + * Icelandic (Íslenska) message catalog. + */ +const is = { + // ── Buttons / actions ───────────────────────────────────────────────────── + "button.authorize": "Heimila", + "button.cancel": "Hætta við", + "button.clear": "Hreinsa", + "button.close": "Loka", + "button.copy_to_clipboard": "Afrita á klippiborð", + "button.download_file": "Sækja skrá", + "button.edit": "Breyta", + "button.execute": "Keyra", + "button.explore": "Kanna", + "button.hide": "Fela", + "button.logout": "Útskrá", + "button.reset": "Endurstilla", + "button.show": "Sýna", + "button.try_it_out": "Prófaðu", + + // ── Section / column labels ──────────────────────────────────────────────── + "label.callbacks": "Callbacks", + "label.code": "Kóði", + "label.description": "Lýsing", + "label.details": "Upplýsingar", + "label.examples": "Dæmi", + "label.headers": "Hausar:", + "label.links": "Tenglar", + "label.media_type": "Miðilsgerð", + "label.models": "Líkön", + "label.name": "Nafn", + "label.no_links": "Engir tenglar", + "label.no_parameters": "Engar breytur", + "label.parameter_content_type": "Innihaldsgerð breytu", + "label.parameters": "Breytur", + "label.request_body": "Meginmál beiðni", + "label.request_duration": "Lengd beiðni", + "label.request_url": "Slóð beiðni", + "label.response_body": "Meginmál svars", + "label.response_content_type": "Innihaldsgerð svars", + "label.response_headers": "Hausar svars", + "label.responses": "Svör", + "label.schemas": "Skema", + "label.server_response": "Svar þjóns", + "label.snippets": "Brot", + "label.type": "Tegund", + "label.undocumented": "Óskráð", + + // ── Authentication ───────────────────────────────────────────────────────── + "auth.api_key_in": "Í:", + "auth.api_key_name": "Nafn:", + "auth.api_key_value": "Gildi:", + "auth.application": "Forrit:", + "auth.authorization_header": "Heimildarhausur", + "auth.authorization_url": "Heimildarvefslóð:", + "auth.authorized": "Heimilt", + "auth.basic_authorization_title": "Grunnheimild", + "auth.client_credentials_location": "Staðsetning skilríkja biðlara:", + "auth.client_id": "client_id:", + "auth.client_secret": "client_secret:", + "auth.flow": "Flæði:", + "auth.openid_connect_url": "OpenID Connect vefslóð:", + "auth.password": "lykilorð:", + "auth.password_cap": "Lykilorð:", + "auth.request_body_option": "Meginmál beiðni", + "auth.scopes_description": + "Scopes eru notaðar til að veita forriti mismunandi aðgangsstig að gögnum fyrir hönd notandans. Sérhver API getur lýst yfir einum eða fleiri scopes.", + "auth.scopes_required": + "API krefst eftirfarandi scopes. Veldu þær sem þú vilt veita Swagger UI.", + "auth.scopes_title": "Scopes:", + "auth.select_all": "velja allt", + "auth.select_none": "velja ekkert", + "auth.token_url": "Token vefslóð:", + "auth.username": "notandanafn:", + "auth.username_cap": "Notandanafn:", + + // ── Accessibility (aria-labels / titles) ─────────────────────────────────── + "aria.apply_credentials": "Nota skilríki", + "aria.apply_oauth2_credentials": "Nota tilgreind OAuth2-skilríki", + "aria.authorization_button_locked": "heimildarhnappur læstur", + "aria.authorization_button_unlocked": "heimildarhnappur opinn", + "aria.collapse_operation": "Fella saman aðgerð", + "aria.expand_operation": "Víkka út aðgerð", + "aria.remove_authorization": "Fjarlægja heimild", + "aria.request_content_type": "Innihaldsgerð beiðni", + "aria.response_content_type": "Innihaldsgerð svars", + + // ── Errors ──────────────────────────────────────────────────────────────── + "errors.jump_to_line": "Fara í línu {{line}}", + "errors.title": "Villur", + + // ── Placeholders ────────────────────────────────────────────────────────── + "placeholder.filter_by_tag": "Sía eftir merki", + + // ── Response ────────────────────────────────────────────────────────────── + "response.controls_accept_header_prefix": "Stýrir ", + "response.controls_accept_header_suffix": "-hausi.", + "response.json_parse_error": + "Ekki tókst að þátta JSON. Hrá niðurstaða:\n\n", + "response.no_blob_support": + "Niðurhalshaus greindir, en vafrinn þinn styður ekki niðurhal á tvíundagögnum í gegnum XHR (Blob).", + "response.unrecognized_type_display_as_text": + "Óþekkt svargerð; sýni efni sem texta.", + "response.unrecognized_type_unable_to_display": + "Óþekkt svargerð; get ekki sýnt.", + + // ── Topbar ──────────────────────────────────────────────────────────────── + "topbar.select_definition": "Velja skilgreiningu", +} + +export default is diff --git a/src/core/plugins/i18n/locales/ko.js b/src/core/plugins/i18n/locales/ko.js new file mode 100644 index 00000000000..ca3c2325b2b --- /dev/null +++ b/src/core/plugins/i18n/locales/ko.js @@ -0,0 +1,114 @@ +/** + * @prettier + */ + +/** + * Korean (한국어) message catalog. + */ +const ko = { + // ── Buttons / actions ───────────────────────────────────────────────────── + "button.authorize": "인증", + "button.cancel": "취소", + "button.clear": "지우기", + "button.close": "닫기", + "button.copy_to_clipboard": "클립보드에 복사", + "button.download_file": "파일 다운로드", + "button.edit": "수정", + "button.execute": "실행", + "button.explore": "탐색", + "button.hide": "숨기기", + "button.logout": "로그아웃", + "button.reset": "초기화", + "button.show": "표시", + "button.try_it_out": "직접 해보기", + + // ── Section / column labels ──────────────────────────────────────────────── + "label.callbacks": "콜백", + "label.code": "코드", + "label.description": "설명", + "label.details": "세부 정보", + "label.examples": "예시", + "label.headers": "헤더:", + "label.links": "링크", + "label.media_type": "미디어 유형", + "label.models": "모델", + "label.name": "이름", + "label.no_links": "링크 없음", + "label.no_parameters": "파라미터 없음", + "label.parameter_content_type": "파라미터 콘텐츠 유형", + "label.parameters": "파라미터", + "label.request_body": "요청 본문", + "label.request_duration": "요청 시간", + "label.request_url": "요청 URL", + "label.response_body": "응답 본문", + "label.response_content_type": "응답 콘텐츠 유형", + "label.response_headers": "응답 헤더", + "label.responses": "응답", + "label.schemas": "스키마", + "label.server_response": "서버 응답", + "label.snippets": "스니펫", + "label.type": "유형", + "label.undocumented": "미문서화", + + // ── Authentication ───────────────────────────────────────────────────────── + "auth.api_key_in": "위치:", + "auth.api_key_name": "이름:", + "auth.api_key_value": "값:", + "auth.application": "애플리케이션:", + "auth.authorization_header": "인증 헤더", + "auth.authorization_url": "인증 URL:", + "auth.authorized": "인증됨", + "auth.basic_authorization_title": "기본 인증", + "auth.client_credentials_location": "클라이언트 자격증명 위치:", + "auth.client_id": "client_id:", + "auth.client_secret": "client_secret:", + "auth.flow": "흐름:", + "auth.openid_connect_url": "OpenID Connect URL:", + "auth.password": "비밀번호:", + "auth.password_cap": "비밀번호:", + "auth.request_body_option": "요청 본문", + "auth.scopes_description": + "스코프는 최종 사용자를 대신하여 애플리케이션에 데이터에 대한 다양한 수준의 액세스를 부여하는 데 사용됩니다. 각 API는 하나 이상의 스코프를 선언할 수 있습니다.", + "auth.scopes_required": + "API에는 다음 스코프가 필요합니다. Swagger UI에 부여할 스코프를 선택하세요.", + "auth.scopes_title": "스코프:", + "auth.select_all": "전체 선택", + "auth.select_none": "전체 해제", + "auth.token_url": "토큰 URL:", + "auth.username": "사용자 이름:", + "auth.username_cap": "사용자 이름:", + + // ── Accessibility (aria-labels / titles) ─────────────────────────────────── + "aria.apply_credentials": "자격증명 적용", + "aria.apply_oauth2_credentials": "주어진 OAuth2 자격증명 적용", + "aria.authorization_button_locked": "인증 버튼 잠김", + "aria.authorization_button_unlocked": "인증 버튼 잠금 해제", + "aria.collapse_operation": "작업 축소", + "aria.expand_operation": "작업 확장", + "aria.remove_authorization": "인증 제거", + "aria.request_content_type": "요청 콘텐츠 유형", + "aria.response_content_type": "응답 콘텐츠 유형", + + // ── Errors ──────────────────────────────────────────────────────────────── + "errors.jump_to_line": "{{line}}번 줄로 이동", + "errors.title": "오류", + + // ── Placeholders ────────────────────────────────────────────────────────── + "placeholder.filter_by_tag": "태그로 필터링", + + // ── Response ────────────────────────────────────────────────────────────── + "response.controls_accept_header_prefix": "", + "response.controls_accept_header_suffix": " 헤더를 제어합니다.", + "response.json_parse_error": "JSON을 파싱할 수 없습니다. 원시 결과:\n\n", + "response.no_blob_support": + "다운로드 헤더가 감지되었지만 브라우저가 XHR (Blob)을 통한 바이너리 다운로드를 지원하지 않습니다.", + "response.unrecognized_type_display_as_text": + "인식되지 않는 응답 유형입니다. 내용을 텍스트로 표시합니다.", + "response.unrecognized_type_unable_to_display": + "인식되지 않는 응답 유형입니다. 표시할 수 없습니다.", + + // ── Topbar ──────────────────────────────────────────────────────────────── + "topbar.select_definition": "정의 선택", +} + +export default ko diff --git a/src/core/plugins/i18n/locales/pt.js b/src/core/plugins/i18n/locales/pt.js new file mode 100644 index 00000000000..d68afa0eabb --- /dev/null +++ b/src/core/plugins/i18n/locales/pt.js @@ -0,0 +1,115 @@ +/** + * @prettier + */ + +/** + * Portuguese / Brazilian Portuguese (Português do Brasil) message catalog. + */ +const pt = { + // ── Buttons / actions ───────────────────────────────────────────────────── + "button.authorize": "Autorizar", + "button.cancel": "Cancelar", + "button.clear": "Limpar", + "button.close": "Fechar", + "button.copy_to_clipboard": "Copiar para a área de transferência", + "button.download_file": "Baixar arquivo", + "button.edit": "Editar", + "button.execute": "Executar", + "button.explore": "Explorar", + "button.hide": "Ocultar", + "button.logout": "Sair", + "button.reset": "Redefinir", + "button.show": "Mostrar", + "button.try_it_out": "Experimente", + + // ── Section / column labels ──────────────────────────────────────────────── + "label.callbacks": "Callbacks", + "label.code": "Código", + "label.description": "Descrição", + "label.details": "Detalhes", + "label.examples": "Exemplos", + "label.headers": "Cabeçalhos:", + "label.links": "Links", + "label.media_type": "Tipo de mídia", + "label.models": "Modelos", + "label.name": "Nome", + "label.no_links": "Sem links", + "label.no_parameters": "Sem parâmetros", + "label.parameter_content_type": "Tipo de conteúdo do parâmetro", + "label.parameters": "Parâmetros", + "label.request_body": "Corpo da requisição", + "label.request_duration": "Duração da requisição", + "label.request_url": "URL da requisição", + "label.response_body": "Corpo da resposta", + "label.response_content_type": "Tipo de conteúdo da resposta", + "label.response_headers": "Cabeçalhos da resposta", + "label.responses": "Respostas", + "label.schemas": "Esquemas", + "label.server_response": "Resposta do servidor", + "label.snippets": "Trechos", + "label.type": "Tipo", + "label.undocumented": "Não documentado", + + // ── Authentication ───────────────────────────────────────────────────────── + "auth.api_key_in": "Em:", + "auth.api_key_name": "Nome:", + "auth.api_key_value": "Valor:", + "auth.application": "Aplicação:", + "auth.authorization_header": "Cabeçalho de autorização", + "auth.authorization_url": "URL de autorização:", + "auth.authorized": "Autorizado", + "auth.basic_authorization_title": "Autorização básica", + "auth.client_credentials_location": "Localização das credenciais do cliente:", + "auth.client_id": "client_id:", + "auth.client_secret": "client_secret:", + "auth.flow": "Fluxo:", + "auth.openid_connect_url": "URL do OpenID Connect:", + "auth.password": "senha:", + "auth.password_cap": "Senha:", + "auth.request_body_option": "Corpo da requisição", + "auth.scopes_description": + "Os escopos são usados para conceder a um aplicativo diferentes níveis de acesso aos dados em nome do usuário final. Cada API pode declarar um ou mais escopos.", + "auth.scopes_required": + "A API requer os seguintes escopos. Selecione quais você deseja conceder ao Swagger UI.", + "auth.scopes_title": "Escopos:", + "auth.select_all": "selecionar todos", + "auth.select_none": "selecionar nenhum", + "auth.token_url": "URL do token:", + "auth.username": "nome de usuário:", + "auth.username_cap": "Nome de usuário:", + + // ── Accessibility (aria-labels / titles) ─────────────────────────────────── + "aria.apply_credentials": "Aplicar credenciais", + "aria.apply_oauth2_credentials": "Aplicar as credenciais OAuth2 fornecidas", + "aria.authorization_button_locked": "botão de autorização bloqueado", + "aria.authorization_button_unlocked": "botão de autorização desbloqueado", + "aria.collapse_operation": "Recolher operação", + "aria.expand_operation": "Expandir operação", + "aria.remove_authorization": "Remover autorização", + "aria.request_content_type": "Tipo de conteúdo da requisição", + "aria.response_content_type": "Tipo de conteúdo da resposta", + + // ── Errors ──────────────────────────────────────────────────────────────── + "errors.jump_to_line": "Ir para a linha {{line}}", + "errors.title": "Erros", + + // ── Placeholders ────────────────────────────────────────────────────────── + "placeholder.filter_by_tag": "Filtrar por tag", + + // ── Response ────────────────────────────────────────────────────────────── + "response.controls_accept_header_prefix": "Controla o cabeçalho ", + "response.controls_accept_header_suffix": ".", + "response.json_parse_error": + "Não foi possível analisar o JSON. Resultado bruto:\n\n", + "response.no_blob_support": + "Cabeçalhos de download detectados, mas seu navegador não suporta download de dados binários via XHR (Blob).", + "response.unrecognized_type_display_as_text": + "Tipo de resposta não reconhecido; exibindo conteúdo como texto.", + "response.unrecognized_type_unable_to_display": + "Tipo de resposta não reconhecido; não é possível exibir.", + + // ── Topbar ──────────────────────────────────────────────────────────────── + "topbar.select_definition": "Selecionar uma definição", +} + +export default pt diff --git a/src/core/plugins/i18n/locales/ru.js b/src/core/plugins/i18n/locales/ru.js new file mode 100644 index 00000000000..020b0f604b6 --- /dev/null +++ b/src/core/plugins/i18n/locales/ru.js @@ -0,0 +1,115 @@ +/** + * @prettier + */ + +/** + * Russian (Русский) message catalog. + */ +const ru = { + // ── Buttons / actions ───────────────────────────────────────────────────── + "button.authorize": "Авторизовать", + "button.cancel": "Отмена", + "button.clear": "Очистить", + "button.close": "Закрыть", + "button.copy_to_clipboard": "Копировать в буфер обмена", + "button.download_file": "Скачать файл", + "button.edit": "Редактировать", + "button.execute": "Выполнить", + "button.explore": "Исследовать", + "button.hide": "Скрыть", + "button.logout": "Выйти", + "button.reset": "Сбросить", + "button.show": "Показать", + "button.try_it_out": "Попробовать", + + // ── Section / column labels ──────────────────────────────────────────────── + "label.callbacks": "Коллбэки", + "label.code": "Код", + "label.description": "Описание", + "label.details": "Детали", + "label.examples": "Примеры", + "label.headers": "Заголовки:", + "label.links": "Ссылки", + "label.media_type": "Тип медиа", + "label.models": "Модели", + "label.name": "Имя", + "label.no_links": "Нет ссылок", + "label.no_parameters": "Нет параметров", + "label.parameter_content_type": "Тип содержимого параметра", + "label.parameters": "Параметры", + "label.request_body": "Тело запроса", + "label.request_duration": "Продолжительность запроса", + "label.request_url": "URL запроса", + "label.response_body": "Тело ответа", + "label.response_content_type": "Тип содержимого ответа", + "label.response_headers": "Заголовки ответа", + "label.responses": "Ответы", + "label.schemas": "Схемы", + "label.server_response": "Ответ сервера", + "label.snippets": "Фрагменты", + "label.type": "Тип", + "label.undocumented": "Не задокументировано", + + // ── Authentication ───────────────────────────────────────────────────────── + "auth.api_key_in": "В:", + "auth.api_key_name": "Имя:", + "auth.api_key_value": "Значение:", + "auth.application": "Приложение:", + "auth.authorization_header": "Заголовок авторизации", + "auth.authorization_url": "URL авторизации:", + "auth.authorized": "Авторизован", + "auth.basic_authorization_title": "Базовая авторизация", + "auth.client_credentials_location": "Местонахождение учётных данных клиента:", + "auth.client_id": "client_id:", + "auth.client_secret": "client_secret:", + "auth.flow": "Поток:", + "auth.openid_connect_url": "URL OpenID Connect:", + "auth.password": "пароль:", + "auth.password_cap": "Пароль:", + "auth.request_body_option": "Тело запроса", + "auth.scopes_description": + "Области используются для предоставления приложению разных уровней доступа к данным от имени конечного пользователя. Каждый API может объявлять одну или несколько областей.", + "auth.scopes_required": + "API требует следующих областей. Выберите те, которые вы хотите предоставить Swagger UI.", + "auth.scopes_title": "Области:", + "auth.select_all": "выбрать все", + "auth.select_none": "снять выбор", + "auth.token_url": "URL токена:", + "auth.username": "имя пользователя:", + "auth.username_cap": "Имя пользователя:", + + // ── Accessibility (aria-labels / titles) ─────────────────────────────────── + "aria.apply_credentials": "Применить учётные данные", + "aria.apply_oauth2_credentials": "Применить указанные учётные данные OAuth2", + "aria.authorization_button_locked": "кнопка авторизации заблокирована", + "aria.authorization_button_unlocked": "кнопка авторизации разблокирована", + "aria.collapse_operation": "Свернуть операцию", + "aria.expand_operation": "Развернуть операцию", + "aria.remove_authorization": "Удалить авторизацию", + "aria.request_content_type": "Тип содержимого запроса", + "aria.response_content_type": "Тип содержимого ответа", + + // ── Errors ──────────────────────────────────────────────────────────────── + "errors.jump_to_line": "Перейти к строке {{line}}", + "errors.title": "Ошибки", + + // ── Placeholders ────────────────────────────────────────────────────────── + "placeholder.filter_by_tag": "Фильтровать по тегу", + + // ── Response ────────────────────────────────────────────────────────────── + "response.controls_accept_header_prefix": "Управляет заголовком ", + "response.controls_accept_header_suffix": ".", + "response.json_parse_error": + "Не удаётся разобрать JSON. Необработанный результат:\n\n", + "response.no_blob_support": + "Обнаружены заголовки загрузки, но ваш браузер не поддерживает загрузку двоичных данных через XHR (Blob).", + "response.unrecognized_type_display_as_text": + "Нераспознанный тип ответа; содержимое отображается как текст.", + "response.unrecognized_type_unable_to_display": + "Нераспознанный тип ответа; отображение невозможно.", + + // ── Topbar ──────────────────────────────────────────────────────────────── + "topbar.select_definition": "Выбрать определение", +} + +export default ru diff --git a/src/core/plugins/i18n/locales/zh.js b/src/core/plugins/i18n/locales/zh.js new file mode 100644 index 00000000000..cd21adf3e40 --- /dev/null +++ b/src/core/plugins/i18n/locales/zh.js @@ -0,0 +1,114 @@ +/** + * @prettier + */ + +/** + * Chinese Simplified (简体中文) message catalog. + */ +const zh = { + // ── Buttons / actions ───────────────────────────────────────────────────── + "button.authorize": "授权", + "button.cancel": "取消", + "button.clear": "清除", + "button.close": "关闭", + "button.copy_to_clipboard": "复制到剪贴板", + "button.download_file": "下载文件", + "button.edit": "编辑", + "button.execute": "执行", + "button.explore": "探索", + "button.hide": "隐藏", + "button.logout": "退出登录", + "button.reset": "重置", + "button.show": "显示", + "button.try_it_out": "试一下", + + // ── Section / column labels ──────────────────────────────────────────────── + "label.callbacks": "回调", + "label.code": "代码", + "label.description": "描述", + "label.details": "详情", + "label.examples": "示例", + "label.headers": "标头:", + "label.links": "链接", + "label.media_type": "媒体类型", + "label.models": "模型", + "label.name": "名称", + "label.no_links": "无链接", + "label.no_parameters": "无参数", + "label.parameter_content_type": "参数内容类型", + "label.parameters": "参数", + "label.request_body": "请求体", + "label.request_duration": "请求时长", + "label.request_url": "请求 URL", + "label.response_body": "响应体", + "label.response_content_type": "响应内容类型", + "label.response_headers": "响应头", + "label.responses": "响应", + "label.schemas": "模式", + "label.server_response": "服务器响应", + "label.snippets": "代码片段", + "label.type": "类型", + "label.undocumented": "未文档化", + + // ── Authentication ───────────────────────────────────────────────────────── + "auth.api_key_in": "位置:", + "auth.api_key_name": "名称:", + "auth.api_key_value": "值:", + "auth.application": "应用程序:", + "auth.authorization_header": "授权请求头", + "auth.authorization_url": "授权 URL:", + "auth.authorized": "已授权", + "auth.basic_authorization_title": "基本授权", + "auth.client_credentials_location": "客户端凭据位置:", + "auth.client_id": "client_id:", + "auth.client_secret": "client_secret:", + "auth.flow": "流程:", + "auth.openid_connect_url": "OpenID Connect URL:", + "auth.password": "密码:", + "auth.password_cap": "密码:", + "auth.request_body_option": "请求体", + "auth.scopes_description": + "范围用于代表最终用户向应用程序授予对数据的不同访问级别。每个 API 可以声明一个或多个范围。", + "auth.scopes_required": + "API 需要以下范围。请选择您想要授予 Swagger UI 的范围。", + "auth.scopes_title": "范围:", + "auth.select_all": "全选", + "auth.select_none": "取消全选", + "auth.token_url": "令牌 URL:", + "auth.username": "用户名:", + "auth.username_cap": "用户名:", + + // ── Accessibility (aria-labels / titles) ─────────────────────────────────── + "aria.apply_credentials": "应用凭据", + "aria.apply_oauth2_credentials": "应用给定的 OAuth2 凭据", + "aria.authorization_button_locked": "授权按钮已锁定", + "aria.authorization_button_unlocked": "授权按钮已解锁", + "aria.collapse_operation": "折叠操作", + "aria.expand_operation": "展开操作", + "aria.remove_authorization": "删除授权", + "aria.request_content_type": "请求内容类型", + "aria.response_content_type": "响应内容类型", + + // ── Errors ──────────────────────────────────────────────────────────────── + "errors.jump_to_line": "跳转到第 {{line}} 行", + "errors.title": "错误", + + // ── Placeholders ────────────────────────────────────────────────────────── + "placeholder.filter_by_tag": "按标签过滤", + + // ── Response ────────────────────────────────────────────────────────────── + "response.controls_accept_header_prefix": "控制 ", + "response.controls_accept_header_suffix": " 请求头。", + "response.json_parse_error": "无法解析 JSON。原始结果:\n\n", + "response.no_blob_support": + "检测到下载标头,但您的浏览器不支持通过 XHR (Blob) 下载二进制文件。", + "response.unrecognized_type_display_as_text": + "无法识别的响应类型;将内容显示为文本。", + "response.unrecognized_type_unable_to_display": + "无法识别的响应类型;无法显示。", + + // ── Topbar ──────────────────────────────────────────────────────────────── + "topbar.select_definition": "选择一个定义", +} + +export default zh diff --git a/src/core/plugins/i18n/reducers.js b/src/core/plugins/i18n/reducers.js new file mode 100644 index 00000000000..2f1de565be4 --- /dev/null +++ b/src/core/plugins/i18n/reducers.js @@ -0,0 +1,14 @@ +/** + * @prettier + */ +import { Map } from "immutable" +import { SET_LOCALE, LOAD_MESSAGES } from "./actions" + +export default { + [SET_LOCALE]: (state, { payload: locale }) => state.set("locale", locale), + + [LOAD_MESSAGES]: (state, { payload: { locale, messages } }) => + state.updateIn(["messages", locale], (existing = Map()) => + existing.merge(Map(messages)) + ), +} diff --git a/src/core/plugins/i18n/selectors.js b/src/core/plugins/i18n/selectors.js new file mode 100644 index 00000000000..c9e0c16b8ad --- /dev/null +++ b/src/core/plugins/i18n/selectors.js @@ -0,0 +1,8 @@ +/** + * @prettier + */ +import { Map } from "immutable" + +export const getLocale = (state) => state.get("locale", "en") + +export const getMessages = (state) => state.get("messages", Map()) diff --git a/src/core/plugins/json-schema-5/components/models.jsx b/src/core/plugins/json-schema-5/components/models.jsx index e9023f36f3e..ff77d70b927 100644 --- a/src/core/plugins/json-schema-5/components/models.jsx +++ b/src/core/plugins/json-schema-5/components/models.jsx @@ -1,6 +1,7 @@ import React, { Component } from "react" import Im, { Map } from "immutable" import PropTypes from "prop-types" +import { fallbackT } from "core/plugins/i18n/fn" /* eslint-disable react/jsx-no-bind */ @@ -11,7 +12,12 @@ export default class Models extends Component { specActions: PropTypes.object.isRequired, layoutSelectors: PropTypes.object, layoutActions: PropTypes.object, - getConfigs: PropTypes.func.isRequired + getConfigs: PropTypes.func.isRequired, + t: PropTypes.func, + } + + static defaultProps = { + t: fallbackT, } getSchemaBasePath = () => { @@ -45,7 +51,7 @@ export default class Models extends Component { } render(){ - let { specSelectors, getComponent, layoutSelectors, layoutActions, getConfigs } = this.props + let { specSelectors, getComponent, layoutSelectors, layoutActions, getConfigs, t } = this.props let definitions = specSelectors.definitions() let { docExpansion, defaultModelsExpandDepth } = getConfigs() if (!definitions.size || defaultModelsExpandDepth < 0) return null @@ -68,7 +74,7 @@ export default class Models extends Component { className="models-control" onClick={() => layoutActions.show(specPathBase, !showModels)} > - {isOAS3 ? "Schemas" : "Models"} + {isOAS3 ? t("label.schemas") : t("label.models")} {showModels ? : } diff --git a/src/core/plugins/request-snippets/request-snippets.jsx b/src/core/plugins/request-snippets/request-snippets.jsx index 3fbb127a3bc..eb01dcc64f2 100644 --- a/src/core/plugins/request-snippets/request-snippets.jsx +++ b/src/core/plugins/request-snippets/request-snippets.jsx @@ -2,6 +2,7 @@ import React, { useRef, useEffect, useState } from "react" import classNames from "classnames" import PropTypes from "prop-types" import { CopyToClipboard } from "react-copy-to-clipboard" +import { fallbackT } from "core/plugins/i18n/fn" const style = { cursor: "pointer", @@ -33,7 +34,7 @@ const activeStyle = { borderBottom: "none" } -const RequestSnippets = ({ request, requestSnippetsSelectors, getComponent }) => { +const RequestSnippets = ({ request, requestSnippetsSelectors, getComponent, t }) => { const rootRef = useRef(null) const ArrowIcon = getComponent("ArrowUpIcon") @@ -104,11 +105,11 @@ const RequestSnippets = ({ request, requestSnippetsSelectors, getComponent }) =>

handleSetIsExpanded()} style={{ cursor: "pointer" }} - >Snippets

+ >{t("label.snippets")} @@ -158,6 +159,11 @@ RequestSnippets.propTypes = { requestSnippetsSelectors: PropTypes.object.isRequired, getComponent: PropTypes.func.isRequired, requestSnippetsActions: PropTypes.object, + t: PropTypes.func, +} + +RequestSnippets.defaultProps = { + t: fallbackT, } export default RequestSnippets diff --git a/src/core/presets/base/index.js b/src/core/presets/base/index.js index 15b7f6acd24..ff3a8f42104 100644 --- a/src/core/presets/base/index.js +++ b/src/core/presets/base/index.js @@ -6,6 +6,7 @@ import ConfigsPlugin from "core/plugins/configs" import DeepLinkingPlugin from "core/plugins/deep-linking" import ErrPlugin from "core/plugins/err" import FilterPlugin from "core/plugins/filter" +import I18nPlugin from "core/plugins/i18n" import IconsPlugin from "core/plugins/icons" import LayoutPlugin from "core/plugins/layout" import LogsPlugin from "core/plugins/logs" @@ -29,6 +30,7 @@ import FormComponentsPlugin from "core/presets/base/plugins/form-components" const BasePreset = () => [ ConfigsPlugin, UtilPlugin, + I18nPlugin, LogsPlugin, ViewPlugin, ViewLegacyPlugin, diff --git a/src/standalone/plugins/top-bar/components/TopBar.jsx b/src/standalone/plugins/top-bar/components/TopBar.jsx index c19f5af228d..99a1f6e2aaf 100644 --- a/src/standalone/plugins/top-bar/components/TopBar.jsx +++ b/src/standalone/plugins/top-bar/components/TopBar.jsx @@ -1,5 +1,6 @@ import React, { cloneElement } from "react" import PropTypes from "prop-types" +import { fallbackT } from "core/plugins/i18n/fn" import {parseSearch, serializeSearch} from "core/utils" @@ -7,7 +8,16 @@ class TopBar extends React.Component { static propTypes = { layoutActions: PropTypes.object.isRequired, - authActions: PropTypes.object.isRequired + authActions: PropTypes.object.isRequired, + specSelectors: PropTypes.object.isRequired, + specActions: PropTypes.object.isRequired, + getComponent: PropTypes.func.isRequired, + getConfigs: PropTypes.func.isRequired, + t: PropTypes.func, + } + + static defaultProps = { + t: fallbackT, } constructor(props, context) { @@ -133,7 +143,7 @@ class TopBar extends React.Component { }) control.push( -
@@ -221,7 +221,7 @@ export default class Oauth2 extends React.Component { { ( (flow === AUTH_FLOW_APPLICATION || flow === AUTH_FLOW_ACCESS_CODE || flow === AUTH_FLOW_PASSWORD) && - + { isAuthorized ? ****** : diff --git a/src/core/plugins/i18n/locales/ca.js b/src/core/plugins/i18n/locales/ca.js index cede6d278bb..9d6b0fa4d75 100644 --- a/src/core/plugins/i18n/locales/ca.js +++ b/src/core/plugins/i18n/locales/ca.js @@ -60,8 +60,6 @@ const ca = { "auth.authorized": "Autoritzat", "auth.basic_authorization_title": "Autorització bàsica", "auth.client_credentials_location": "Ubicació de les credencials del client:", - "auth.client_id": "client_id:", - "auth.client_secret": "client_secret:", "auth.flow": "Flux:", "auth.openid_connect_url": "URL de connexió OpenID:", "auth.password": "contrasenya:", diff --git a/src/core/plugins/i18n/locales/de.js b/src/core/plugins/i18n/locales/de.js index 9b7588ad464..89d664c06cf 100644 --- a/src/core/plugins/i18n/locales/de.js +++ b/src/core/plugins/i18n/locales/de.js @@ -60,8 +60,6 @@ const de = { "auth.authorized": "Autorisiert", "auth.basic_authorization_title": "Basisautorisierung", "auth.client_credentials_location": "Speicherort der Client-Anmeldedaten:", - "auth.client_id": "client_id:", - "auth.client_secret": "client_secret:", "auth.flow": "Flow:", "auth.openid_connect_url": "OpenID Connect-URL:", "auth.password": "Passwort:", diff --git a/src/core/plugins/i18n/locales/en.js b/src/core/plugins/i18n/locales/en.js index b1bb5a1fedf..60b5989e251 100644 --- a/src/core/plugins/i18n/locales/en.js +++ b/src/core/plugins/i18n/locales/en.js @@ -61,8 +61,6 @@ const en = { "auth.authorized": "Authorized", "auth.basic_authorization_title": "Basic authorization", "auth.client_credentials_location": "Client credentials location:", - "auth.client_id": "client_id:", - "auth.client_secret": "client_secret:", "auth.flow": "Flow:", "auth.openid_connect_url": "OpenID Connect URL:", "auth.password": "password:", diff --git a/src/core/plugins/i18n/locales/es.js b/src/core/plugins/i18n/locales/es.js index 6c29bf7795e..f52b7b66759 100644 --- a/src/core/plugins/i18n/locales/es.js +++ b/src/core/plugins/i18n/locales/es.js @@ -60,8 +60,6 @@ const es = { "auth.authorized": "Autorizado", "auth.basic_authorization_title": "Autorización básica", "auth.client_credentials_location": "Ubicación de credenciales del cliente:", - "auth.client_id": "client_id:", - "auth.client_secret": "client_secret:", "auth.flow": "Flujo:", "auth.openid_connect_url": "URL de OpenID Connect:", "auth.password": "contraseña:", diff --git a/src/core/plugins/i18n/locales/fr.js b/src/core/plugins/i18n/locales/fr.js index c72d7ecc92b..1688322cb21 100644 --- a/src/core/plugins/i18n/locales/fr.js +++ b/src/core/plugins/i18n/locales/fr.js @@ -60,8 +60,6 @@ const fr = { "auth.authorized": "Autorisé", "auth.basic_authorization_title": "Autorisation de base", "auth.client_credentials_location": "Emplacement des identifiants client :", - "auth.client_id": "client_id :", - "auth.client_secret": "client_secret :", "auth.flow": "Flux :", "auth.openid_connect_url": "URL OpenID Connect :", "auth.password": "mot de passe :", diff --git a/src/core/plugins/i18n/locales/is.js b/src/core/plugins/i18n/locales/is.js index ddd5e23df8d..106570cfe99 100644 --- a/src/core/plugins/i18n/locales/is.js +++ b/src/core/plugins/i18n/locales/is.js @@ -60,8 +60,6 @@ const is = { "auth.authorized": "Heimilt", "auth.basic_authorization_title": "Grunnheimild", "auth.client_credentials_location": "Staðsetning skilríkja biðlara:", - "auth.client_id": "client_id:", - "auth.client_secret": "client_secret:", "auth.flow": "Flæði:", "auth.openid_connect_url": "OpenID Connect vefslóð:", "auth.password": "lykilorð:", diff --git a/src/core/plugins/i18n/locales/it.js b/src/core/plugins/i18n/locales/it.js index af9e039752a..501edc6ba2c 100644 --- a/src/core/plugins/i18n/locales/it.js +++ b/src/core/plugins/i18n/locales/it.js @@ -61,8 +61,6 @@ const it = { "auth.basic_authorization_title": "Autorizzazione di base", "auth.client_credentials_location": "Posizione delle credenziali del client:", - "auth.client_id": "client_id:", - "auth.client_secret": "client_secret:", "auth.flow": "Flusso:", "auth.openid_connect_url": "URL OpenID Connect:", "auth.password": "password:", diff --git a/src/core/plugins/i18n/locales/ja.js b/src/core/plugins/i18n/locales/ja.js index c4e519aff05..162d6922a6a 100644 --- a/src/core/plugins/i18n/locales/ja.js +++ b/src/core/plugins/i18n/locales/ja.js @@ -60,8 +60,6 @@ const ja = { "auth.authorized": "認証済み", "auth.basic_authorization_title": "基本認証", "auth.client_credentials_location": "クライアント認証情報の場所:", - "auth.client_id": "client_id:", - "auth.client_secret": "client_secret:", "auth.flow": "フロー:", "auth.openid_connect_url": "OpenID Connect URL:", "auth.password": "パスワード:", diff --git a/src/core/plugins/i18n/locales/ka.js b/src/core/plugins/i18n/locales/ka.js index bfdcab809b4..3bdc53eea9d 100644 --- a/src/core/plugins/i18n/locales/ka.js +++ b/src/core/plugins/i18n/locales/ka.js @@ -61,8 +61,6 @@ const ka = { "auth.authorized": "ავტორიზებულია", "auth.basic_authorization_title": "საბაზო ავტორიზაცია", "auth.client_credentials_location": "კლიენტის სერთიფიკატების მდებარეობა:", - "auth.client_id": "client_id:", - "auth.client_secret": "client_secret:", "auth.flow": "ნაკადი:", "auth.openid_connect_url": "OpenID Connect URL:", "auth.password": "პაროლი:", diff --git a/src/core/plugins/i18n/locales/ko.js b/src/core/plugins/i18n/locales/ko.js index ca3c2325b2b..e0909042a2b 100644 --- a/src/core/plugins/i18n/locales/ko.js +++ b/src/core/plugins/i18n/locales/ko.js @@ -60,8 +60,6 @@ const ko = { "auth.authorized": "인증됨", "auth.basic_authorization_title": "기본 인증", "auth.client_credentials_location": "클라이언트 자격증명 위치:", - "auth.client_id": "client_id:", - "auth.client_secret": "client_secret:", "auth.flow": "흐름:", "auth.openid_connect_url": "OpenID Connect URL:", "auth.password": "비밀번호:", diff --git a/src/core/plugins/i18n/locales/pl.js b/src/core/plugins/i18n/locales/pl.js index 8f5fd82301b..41343dcbfa0 100644 --- a/src/core/plugins/i18n/locales/pl.js +++ b/src/core/plugins/i18n/locales/pl.js @@ -61,8 +61,6 @@ const pl = { "auth.basic_authorization_title": "Autoryzacja podstawowa", "auth.client_credentials_location": "Lokalizacja danych uwierzytelniających klienta:", - "auth.client_id": "client_id:", - "auth.client_secret": "client_secret:", "auth.flow": "Przepływ:", "auth.openid_connect_url": "URL OpenID Connect:", "auth.password": "hasło:", diff --git a/src/core/plugins/i18n/locales/pt.js b/src/core/plugins/i18n/locales/pt.js index d68afa0eabb..b367afbd876 100644 --- a/src/core/plugins/i18n/locales/pt.js +++ b/src/core/plugins/i18n/locales/pt.js @@ -60,8 +60,6 @@ const pt = { "auth.authorized": "Autorizado", "auth.basic_authorization_title": "Autorização básica", "auth.client_credentials_location": "Localização das credenciais do cliente:", - "auth.client_id": "client_id:", - "auth.client_secret": "client_secret:", "auth.flow": "Fluxo:", "auth.openid_connect_url": "URL do OpenID Connect:", "auth.password": "senha:", diff --git a/src/core/plugins/i18n/locales/ru.js b/src/core/plugins/i18n/locales/ru.js index 020b0f604b6..5e2c6bffe6d 100644 --- a/src/core/plugins/i18n/locales/ru.js +++ b/src/core/plugins/i18n/locales/ru.js @@ -60,8 +60,6 @@ const ru = { "auth.authorized": "Авторизован", "auth.basic_authorization_title": "Базовая авторизация", "auth.client_credentials_location": "Местонахождение учётных данных клиента:", - "auth.client_id": "client_id:", - "auth.client_secret": "client_secret:", "auth.flow": "Поток:", "auth.openid_connect_url": "URL OpenID Connect:", "auth.password": "пароль:", diff --git a/src/core/plugins/i18n/locales/tr.js b/src/core/plugins/i18n/locales/tr.js index f0d5b55a4d6..b6d9538cc1b 100644 --- a/src/core/plugins/i18n/locales/tr.js +++ b/src/core/plugins/i18n/locales/tr.js @@ -60,8 +60,6 @@ const tr = { "auth.authorized": "Yetkilendirildi", "auth.basic_authorization_title": "Temel yetkilendirme", "auth.client_credentials_location": "İstemci kimlik bilgilerinin konumu:", - "auth.client_id": "client_id:", - "auth.client_secret": "client_secret:", "auth.flow": "Akış:", "auth.openid_connect_url": "OpenID Connect URL'i:", "auth.password": "şifre:", diff --git a/src/core/plugins/i18n/locales/zh.js b/src/core/plugins/i18n/locales/zh.js index cd21adf3e40..11f59c86f64 100644 --- a/src/core/plugins/i18n/locales/zh.js +++ b/src/core/plugins/i18n/locales/zh.js @@ -60,8 +60,6 @@ const zh = { "auth.authorized": "已授权", "auth.basic_authorization_title": "基本授权", "auth.client_credentials_location": "客户端凭据位置:", - "auth.client_id": "client_id:", - "auth.client_secret": "client_secret:", "auth.flow": "流程:", "auth.openid_connect_url": "OpenID Connect URL:", "auth.password": "密码:",
CodeDescriptionLinks{t("label.code")}{t("label.description")}{t("label.links")}