Skip to content

feat(i18n): use translate=no attribute in code sensitive renderings - #2697

Open
displague wants to merge 4 commits into
Redocly:mainfrom
displague:translate-no
Open

feat(i18n): use translate=no attribute in code sensitive renderings#2697
displague wants to merge 4 commits into
Redocly:mainfrom
displague:translate-no

Conversation

@displague

@displague displague commented Jun 5, 2025

Copy link
Copy Markdown

What/Why/How?

Adds translate="no", a W3C standard HTML attribute described at https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/translate, and described in the issue linked below. This hints localization tools (Google Translate and others) to avoid translation of data/type sensitive sections of Redoc rendered OpenAPI specs. Many sites may exclude their Redoc pages from translation today because the outcome would be unpredictable. By adding translate="no" the translators and translation tools will know to leave these sections untranslated.

Reference

Fixes #2696

Tests

  • npm test (unit + license check): 290 tests, 29 suites, 52 snapshots — all passing.
  • npm run lint: clean (one pre-existing warning in src/components/RedocStandalone.tsx, untouched by this PR).
  • npm run prettier: no changes.
  • npm run ts-check: clean.
  • Added src/components/__tests__/translateNo.test.tsx, which asserts the attribute in both directions: code-sensitive surfaces are marked, and Redoc-authored prose is not. Each assertion was verified to be load-bearing by stripping the attribute from the sources under test and confirming the tests fail.
  • Added src/services/__tests__/mdtranslate.test.ts for the Markdown renderer, including a check that surrounding prose stays translatable.
  • Manually reviewed the rendered spec (museum.yaml) via npm start.

npm run e2e was not run: @redocly/workerize-loader is listed in package.json but missing from the local install, so the standalone bundle does not boot and all 26 specs fail. I confirmed this is pre-existing and unrelated by building and running e2e on an unmodified upstream/main, which fails identically. The Markdown change wraps <pre>/<code>, so I checked Markdown/styled.elements.tsx for >/sibling selectors — all rules are descendant-based (code {}, pre {}, pre code {}), so styling is unaffected.

Screenshots (optional)

Check yourself

  • Code is linted
  • Tested
  • All new/updated code is covered with tests

@displague displague left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is in draft and not considered to be complete. I believe I've seen <div> used for some text elements in Redoc renderings. In the prompt for this change I hinted that likely candidates for change would be <span> elements.

I don't believe <code> needs to be decorated the same way, but we could easily add that if we can not assume translation services block those elements.

Comment thread src/components/ApiInfo/ApiInfo.tsx Outdated
Comment thread src/utils/jsonToHtml.ts
Comment thread src/utils/jsonToHtml.ts
Comment thread src/utils/jsonToHtml.ts Outdated
@displague

displague commented Jun 23, 2025

Copy link
Copy Markdown
Author

I see the translate=no appearing in the right-side panel, the JSON example blocks, but I'm not seeing it in the left side where the descriptions are. I would appreciate any pointers.

@displague
displague marked this pull request as ready for review June 30, 2025 13:58
@displague
displague requested a review from a team as a code owner June 30, 2025 13:58
displague and others added 2 commits September 4, 2026 15:00
Signed-off-by: Marques Johansson <marques@displague.com>
- Restore the original `Field.tsx` button structure; the property name and
  parent-name prefix stay inside the clickable button, only `translate="no"`
  is added.
- Mark the whole `.redoc-json` block with `translate="no"` rather than relying
  solely on per-token spans.
- Cover the left-hand (description column) values that were still translatable:
  type names/formats, schema patterns, example/default/const values, extension
  values, constraints, and enum values. Applied via `.attrs()` on the shared
  styled elements in `common-elements/fields.ts` so every call site is covered
  centrally, and drop the now-redundant inline attribute on `ConstraintItem`.
- Leave prose translatable: `TypePrefix` ("Array of"), `TypeTitle` (schema
  title), `RecursiveLabel`, deprecation/required labels, and descriptions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@displague

displague commented Sep 4, 2026

Copy link
Copy Markdown
Author

I'm not seeing it in the left side where the descriptions are

Found it — rebased onto latest main and pushed.

The left/description column renders through the shared styled elements in src/common-elements/fields.ts, which had no coverage at all, so nothing there was getting the attribute. Rather than annotating each call site, I applied it centrally with .attrs({ translate: 'no' }):

  • TypeName — and TypeFormat, which aliases it, so <format> / <contentEncoding> / <contentMediaType> are covered too
  • PatternLabel — regex patterns
  • ExampleValuedefault:, example:, const:, and array-form enum values
  • ExtensionValue — inherits from ExampleValue
  • ConstraintItem — the inline attribute from the first pass is now redundant and was dropped

Plus <td translate="no"> for the value column of the x-enumDescriptions table.

Left translatable on purpose: TypePrefix ("Array of"), TypeTitle (author-written schema title), RecursiveLabel, required/deprecated labels, and descriptions.

Two other fixes in this push:

  • Field.tsx had an unrequested restructure that moved the property name and parent-name prefix out of the clickable <button> — same class of edit as the ApiInfo.tsx one flagged above. Reverted to the original structure; it now only adds the attribute.
  • .redoc-json now carries translate="no" on the container, rather than relying solely on per-token spans.

276 tests pass; 5 snapshots updated to record the new attribute.

Fix a false positive:

- `ConstraintItem` is Redoc-authored English prose from `humanizeConstraints()`
  ("non-empty", "unique", ">= 5 characters", "[ 1 .. 10 ] items", "multiple of
  3"), not spec-derived code. It must stay translatable, so the attribute added
  in the first pass is removed.

Close the remaining gaps:

- `StyledPre` — every Prism-highlighted sample: request samples (curl/JS/Python
  etc.), non-JSON payload/response examples, external-example error output. This
  was the largest omission. Applied to `StyledPre` rather than the shared
  `PrismDiv` base, since `StyledMarkdownBlock` also derives from `PrismDiv` and
  renders prose.
- Markdown `code`/`codespan` — fenced blocks and inline spans in descriptions
  are still code; the surrounding prose stays translatable. Verified DOMPurify
  preserves `translate` under `options.sanitize`.
- Security identifiers, previously bare `<code>`: API key parameter name, HTTP
  auth scheme, bearer format, OAuth flow type, authorization/token/refresh/
  connect URLs, and scope names (plus the `ScopeName` element).
- HTTP verbs in the sidebar and callback titles, matching `HttpVerb` in
  `Endpoint`. The `type="hook"` badge is only marked when it shows the verb, so
  the `l('webhook')` label stays translatable.
- Callback names — OpenAPI runtime expressions, e.g. `{$request.body#/url}`.
- Response status codes (`Code`).
- Media type labels (`MimeLabel`, both definitions).
- `x-` extension names.
- `fieldParentsName` prefix in `Field.tsx`, which rendered parent property names
  as bare text outside the marked span.

Deliberately left translatable: `TypePrefix` ("Array of"), `TypeTitle`, schema
titles, `RecursiveLabel`, required/optional/deprecated labels, constraints, all
`l()` labels, `UnderlinedHeader` section headings, sidebar `sidebarLabel`
summaries, `info.title`, and descriptions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@displague

Copy link
Copy Markdown
Author

Did a full pass over every rendering surface rather than just the <span sites the original prompt targeted. Two corrections and a set of gaps.

One false positive, removed

ConstraintItem should not be translate="no". Its content comes from humanizeConstraints() in src/utils/openapi.ts, which emits Redoc-authored English prose — non-empty, unique, >= 5 characters, [ 1 .. 10 ] items, multiple of 3, decimal places <= 2. That is UI text that should be translated. It was marked in the first pass; reverted.

The largest gap: StyledPre

src/common-elements/samples.tsx. Every Prism-highlighted sample renders through it — all request samples (curl / JS / Python), non-JSON payload and response examples, and external-example error output. Nothing in the first pass touched it; only JSON examples were covered, because those route through jsonToHTML instead.

One implementation note: StyledPre and StyledMarkdownBlock both derive from PrismDiv, so the attribute goes on StyledPre. Putting it on the shared base would have suppressed translation of every description in the document.

Other gaps closed

  • Security identifiersSecurityDetails.tsx, OAuthFlow.tsx and RequiredScopesRow.tsx used bare <code> for the API key parameter name, HTTP auth scheme, bearer format, OAuth flow type, authorization / token / refresh / connect URLs, and scope names. Plus the ScopeName styled element.
  • HTTP verbs in the sidebar and callback titlesEndpoint's HttpVerb was covered but OperationBadge was not. Only the verb usages are marked: the type="hook" badge renders l('webhook') when showWebhookVerb is off, so it is marked conditionally.
  • Callback names — OpenAPI runtime expressions such as {$request.body#/callbackUrl}.
  • fieldParentsName prefix in Field.tsx — parent property names were rendered as bare text outside the .property-name span, so for parent.child the prefix was translated while the leaf was not.
  • Response status codes (Code), media type labels (MimeLabel, both definitions), and x- extension names.
  • Code inside descriptions — overrode renderer.code / renderer.codespan in MarkdownRenderer so fenced blocks and inline spans are excluded while the surrounding prose still translates. Confirmed DOMPurify preserves translate under options.sanitize before relying on this.

Deliberately left translatable

TypePrefix ("Array of"), TypeTitle, RecursiveLabel, required / optional / deprecated labels, constraints, every l() label, UnderlinedHeader section headings ("query Parameters", "Response Schema"), sidebar sidebarLabel summaries, info.title, response summaries, and descriptions.

Two judgment calls left open

TypeName is not pure code. displayType (services/models/Schema.ts) joins type keywords with the English word ' or ', and in the oneOf path embeds typePrefix and schema.title. Marking TypeName blocks translation of those too. I kept it marked, since the OAS type keywords are the code-sensitive part and the actual complaint in #2696, but splitting them properly means restructuring displayType into parts — a larger change than this PR should carry. Happy to do it here if you would prefer.

Dropdown options are mixed. The same component renders media types (code), discriminator schema titles, and example names. I marked MimeLabel, which is unambiguously media types, and left Dropdown alone rather than guess.

Verification

279 tests pass across 28 suites; 5 snapshots updated. tsc --noEmit reports no errors in src. Added src/services/__tests__/mdtranslate.test.ts for the markdown renderer, including an assertion that surrounding prose stays unmarked. I inspected the ConstraintItem snapshot diff specifically to confirm the only changes there were attribute removals on that element.

@displague displague changed the title feat: use translate=no attribute in code sensitive renderings feat(i18n): use translate=no attribute in code sensitive renderings Sep 4, 2026
Snapshots recorded the attribute incidentally, but nothing asserted it, and
several changed surfaces (code samples, media type labels, callback titles,
extension keys) had no assertion at all.

Adds `translateNo.test.tsx` with both directions:

- code-sensitive surfaces are marked: type names, patterns, example/default
  values, Prism-highlighted samples via `SourceCode`, the `.redoc-json`
  container, OAuth scope names, flow type and URLs, callback verb and name, and
  `x-` extension keys;
- prose stays translatable: humanized constraints, `TypePrefix`, `TypeTitle`,
  and `RecursiveLabel`.

The negative cases are the regression guard for the `ConstraintItem` fix —
`humanizeConstraints()` emits English prose, so marking it was wrong.

Verified each assertion is load-bearing by stripping the attribute from the
sources under test and confirming 10/11 fail, then separately marking
`TypePrefix`/`TypeTitle`/`RecursiveLabel` to confirm the 11th fails too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@displague

Copy link
Copy Markdown
Author

@AlexVarchuk I compared notes (with Claude) against two other PRs in the a11n and i18n space and made some improvements here to get the PR to cover the right elements and to get it more inline with repo expectations. I'm hoping these updates and review comments are in the direction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add translate=no attribute or notranslate classes in non-descriptive rendered fields

1 participant