feat(i18n): add OpenAPI translation pipeline and localized specs#1249
feat(i18n): add OpenAPI translation pipeline and localized specs#1249comfyui-wiki wants to merge 2 commits into
Conversation
Wire cloud and registry OpenAPI specs into pnpm translate with per-language YAML under openapi/, Mintlify docs.json paths per locale, and pretty-printed output for readable zh/ja/ko API reference pages.
📝 WalkthroughWalkthroughChangesOpenAPI specifications now support locale-specific translation, incremental hash tracking, optional registry refreshes, localized documentation references, synchronization checks, and dedicated translation and reformatting commands. OpenAPI translation engine
Translation CLI and configuration
Locale synchronization and validation
Workflow documentation
Sequence Diagram(s)sequenceDiagram
participant Translator
participant translate_i18n as translate-i18n.ts
participant specs as OpenAPI specs
participant docs_json as docs.json
Translator->>translate_i18n: Run OpenAPI translation
translate_i18n->>specs: Read, translate, and write locale files
specs-->>translate_i18n: Return translation state
Translator->>docs_json: Run documentation synchronization
docs_json->>specs: Reference locale-specific sources
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/scripts/i18n/check-i18n-sync.mjs:
- Around line 75-81: Consolidate the duplicated localizedOpenApiSource logic by
moving it into a shared .mjs utility usable by Node scripts, then import and use
that utility in localizedOpenApiSource within check-i18n-sync.mjs and the
corresponding sync-docs-json.mjs helper; remove the duplicate implementations
while preserving existing English and extension handling.
In @.github/scripts/i18n/openapi-translate.ts:
- Around line 237-250: Prevent dry-run executions from writing fetched OpenAPI
sources: update ensureEnglishOpenApiSource and its callers to receive dryRun,
and skip writeSpecFile when dryRun is true while still allowing fetch/parse as
needed. Alternatively, disable fetchOpenApi whenever dry-run is active, ensuring
registry files are never overwritten.
- Around line 317-321: The unchanged check should treat retained English
translations as complete instead of reprocessing them. In the logic computing
unchanged in the translation workflow, replace the value-difference requirement
with an own-property check on existingStrings for path, while preserving the
force and matching sidecar block-hash conditions.
- Around line 89-112: The current string-based path handling in
extractTranslatableStrings and parsePath loses OpenAPI key boundaries when keys
contain dots or brackets, causing setAtPath to miss translations. Replace the
path representation with a lossless segment array or JSON Pointer, update
parsePath and the corresponding setAtPath logic to preserve literal key names,
then regenerate the translation sidecars.
- Line 237: Bound the registry request in the fetch logic surrounding
fetch(spec.fetch_url) by using an AbortController with a finite timeout and
passing its signal to fetch. Ensure the timeout is cleared after completion and
translate an abort into a clear timeout error so the translation run fails
promptly.
In @.github/scripts/i18n/README.md:
- Around line 71-72: Correct the README documentation to state that generated
sidecars are written under openapi/.i18n/ and use the cloud.zh.json filename
pattern, rather than being colocated with translated specs as *.i18n.json;
update the “Sidecar metadata” guidance accordingly.
- Around line 48-50: The README’s sidecar path documentation is incorrect:
update the note describing translated OpenAPI JSON sidecars to state that they
are written under `openapi/.i18n/<basename>.json`, rather than beside each
translated specification.
In @.github/scripts/i18n/sync-docs-json.mjs:
- Around line 322-329: Consolidate the duplicated OpenAPI localization logic
into the shared i18n-config.mjs module, using one consistent name such as
localizedOpenApiSource. Remove the local implementation from sync-docs-json.mjs
and check-i18n-sync.mjs, import the shared utility there, and update
openapi-translate.ts to reuse or clearly reference the shared implementation so
all callers have a single source of truth.
In @.github/workflows/i18n-sync-check.yml:
- Around line 10-11: Replace the hardcoded OpenAPI paths in the workflow trigger
list with glob patterns covering all English YAML and JSON sources, such as
openapi/*.en.yaml and openapi/*.en.json, so new translation sources are included
automatically. Also add job-level concurrency using the i18n-sync-check-${{
github.head_ref }} group with cancel-in-progress enabled.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: b5845f0e-482a-4786-b0bf-bf8356ed9edf
📒 Files selected for processing (30)
.cursor/skills/docs-i18n-translate/SKILL.md.github/scripts/i18n/README.md.github/scripts/i18n/check-i18n-sync.mjs.github/scripts/i18n/openapi-translate.test.ts.github/scripts/i18n/openapi-translate.ts.github/scripts/i18n/reformat-openapi.ts.github/scripts/i18n/sync-docs-json.mjs.github/scripts/i18n/translate-i18n.ts.github/scripts/i18n/translation-config.json.github/workflows/i18n-sync-check.ymldevelopment/cloud/openapi.mdxdocs.jsonja/development/cloud/openapi.mdxko/development/cloud/openapi.mdxopenapi/.i18n/cloud.ja.jsonopenapi/.i18n/cloud.ko.jsonopenapi/.i18n/cloud.zh.jsonopenapi/.i18n/registry.ja.jsonopenapi/.i18n/registry.ko.jsonopenapi/.i18n/registry.zh.jsonopenapi/cloud.en.yamlopenapi/cloud.ja.yamlopenapi/cloud.ko.yamlopenapi/cloud.zh.yamlopenapi/registry.en.yamlopenapi/registry.ja.yamlopenapi/registry.ko.yamlopenapi/registry.zh.yamlpackage.jsonzh/development/cloud/openapi.mdx
| function localizedOpenApiSource(source, langCode) { | ||
| if (!source || langCode === "en") return source; | ||
| if (/\.en\.(ya?ml|json)$/i.test(source)) { | ||
| return source.replace(/\.en\.(ya?ml|json)$/i, (_, ext) => `.${langCode}.${ext}`); | ||
| } | ||
| return source.replace(/\.(ya?ml|json)$/i, (_, ext) => `.${langCode}.${ext}`); | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
localizedOpenApiSource duplicates the same function in openapi-translate.ts and sync-docs-json.mjs.
This is the same DRY concern raised on sync-docs-json.mjs lines 322-329. The function here is identical to the exported localizedOpenApiSource in openapi-translate.ts (line 49) and the localizeOpenApiSource in sync-docs-json.mjs (line 323). Since check-i18n-sync.mjs runs under node (per the workflow), it cannot import the .ts file directly, but it could import from a shared .mjs utility.
See the proposed consolidation in the comment on sync-docs-json.mjs.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/scripts/i18n/check-i18n-sync.mjs around lines 75 - 81, Consolidate
the duplicated localizedOpenApiSource logic by moving it into a shared .mjs
utility usable by Node scripts, then import and use that utility in
localizedOpenApiSource within check-i18n-sync.mjs and the corresponding
sync-docs-json.mjs helper; remove the duplicate implementations while preserving
existing English and extension handling.
| const childPath = path ? `${path}.${key}` : key; | ||
| if ( | ||
| TRANSLATABLE_KEYS.has(key) && | ||
| typeof child === "string" && | ||
| child.trim().length > 0 | ||
| ) { | ||
| out[childPath] = child; | ||
| } else { | ||
| Object.assign(out, extractTranslatableStrings(child, childPath)); | ||
| } | ||
| } | ||
|
|
||
| return out; | ||
| } | ||
|
|
||
| function parsePath(path: string): Array<string | number> { | ||
| const parts: Array<string | number> = []; | ||
| const re = /([^.\[\]]+)|\[(\d+)\]/g; | ||
| let match: RegExpExecArray | null; | ||
| while ((match = re.exec(path)) !== null) { | ||
| if (match[1] != null) parts.push(match[1]); | ||
| else if (match[2] != null) parts.push(Number(match[2])); | ||
| } | ||
| return parts; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Use a lossless path format.
parsePath treats . and [] as syntax, but OpenAPI keys may contain them. For example, paths./v1/foo.bar.get.summary is split incorrectly, so setAtPath silently leaves that translation untranslated. Use JSON Pointer or segment arrays, then regenerate the sidecars.
Also applies to: 124-135
🧰 Tools
🪛 OpenGrep (1.23.0)
[ERROR] 108-108: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.
(coderabbit.command-injection.exec-js)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/scripts/i18n/openapi-translate.ts around lines 89 - 112, The current
string-based path handling in extractTranslatableStrings and parsePath loses
OpenAPI key boundaries when keys contain dots or brackets, causing setAtPath to
miss translations. Replace the path representation with a lossless segment array
or JSON Pointer, update parsePath and the corresponding setAtPath logic to
preserve literal key names, then regenerate the translation sidecars.
| if (!spec.fetch_url) return; | ||
|
|
||
| console.log(`[openapi] Fetching ${spec.source} from ${spec.fetch_url} ...`); | ||
| const response = await fetch(spec.fetch_url); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- file info ---'
wc -l .github/scripts/i18n/openapi-translate.ts
echo '--- around target lines ---'
sed -n '200,280p' .github/scripts/i18n/openapi-translate.ts
echo '--- search timeout usage in repo ---'
rg -n "AbortSignal\.timeout|signal:\s*AbortSignal|fetch\(" .github/scripts -SRepository: Comfy-Org/docs
Length of output: 4183
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- references to openapi-translate ---'
rg -n "openapi-translate|ensureEnglishOpenApiSource|fetchOpenApi" . -S
echo '--- workflow timeouts / job wrappers near references ---'
rg -n "timeout-minutes|timeout\s*:" .github/workflows .github -SRepository: Comfy-Org/docs
Length of output: 192
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- github files ---'
git ls-files .github | sed -n '1,200p'
echo '--- search for script invocation patterns ---'
rg -n "openapi|i18n|bun .*openapi|scripts/i18n" .github -S
echo '--- workflows with timeouts ---'
rg -n "timeout-minutes|timeout:" .github/workflows -SRepository: Comfy-Org/docs
Length of output: 27238
Bound the registry fetch. In .github/scripts/i18n/openapi-translate.ts:237, fetch(spec.fetch_url) can hang the whole translation run if the upstream stalls. Add an abort timeout and surface a timeout error so the job doesn’t sit there playing possum.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/scripts/i18n/openapi-translate.ts at line 237, Bound the registry
request in the fetch logic surrounding fetch(spec.fetch_url) by using an
AbortController with a finite timeout and passing its signal to fetch. Ensure
the timeout is cleared after completion and translate an abort into a clear
timeout error so the translation run fails promptly.
| const response = await fetch(spec.fetch_url); | ||
| if (!response.ok) { | ||
| throw new Error(`Failed to fetch ${spec.fetch_url}: HTTP ${response.status}`); | ||
| } | ||
|
|
||
| const format = spec.fetch_format ?? "json"; | ||
| let parsed: Record<string, unknown>; | ||
| if (format === "yaml") { | ||
| parsed = Bun.YAML.parse(await response.text()) as Record<string, unknown>; | ||
| } else { | ||
| parsed = (await response.json()) as Record<string, unknown>; | ||
| } | ||
|
|
||
| await writeSpecFile(absPath, parsed); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Do not write fetched sources during a dry run.
--dry-run --fetch-openapi reaches this write path and overwrites openapi/registry.en.yaml. Pass dryRun into ensureEnglishOpenApiSource and skip the refresh write, or suppress fetchOpenApi when dry-run is active.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/scripts/i18n/openapi-translate.ts around lines 237 - 250, Prevent
dry-run executions from writing fetched OpenAPI sources: update
ensureEnglishOpenApiSource and its callers to receive dryRun, and skip
writeSpecFile when dryRun is true while still allowing fetch/parse as needed.
Alternatively, disable fetchOpenApi whenever dry-run is active, ensuring
registry files are never overwritten.
| const unchanged = | ||
| !options.force && | ||
| sidecar?.blockHashes?.[path] === hash && | ||
| existingStrings[path] && | ||
| existingStrings[path] !== enText; |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win
Treat successfully retained English values as complete.
The existingStrings[path] !== enText check makes values that legitimately remain identical to English pending forever, even after their sidecar hash is recorded. This repeats model calls on every run. Check for an own output entry instead of requiring a different value.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/scripts/i18n/openapi-translate.ts around lines 317 - 321, The
unchanged check should treat retained English translations as complete instead
of reprocessing them. In the logic computing unchanged in the translation
workflow, replace the value-difference requirement with an own-property check on
existingStrings for path, while preserving the force and matching sidecar
block-hash conditions.
| pnpm translate -- --openapi-only # OpenAPI specs only | ||
| pnpm translate -- --no-openapi # skip OpenAPI specs | ||
| pnpm translate -- --fetch-openapi # refresh vendored specs from fetch_url |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== files ==\n'
git ls-files '.github/scripts/i18n/README.md' 'SKILL.md' 'package.json' 'pnpm-workspace.yaml' '.github/scripts/i18n/*' | sed 's#^`#-` #'
printf '\n== search translate/openapi refs ==\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' --glob '!coverage' \
'translate:openapi|openapi-only|no-openapi|fetch-openapi|openapi/.i18n|vendored specs|fetch_url|sidecar' \
.github/scripts/i18n README.md SKILL.md package.json . 2>/dev/null || true
printf '\n== package scripts ==\n'
python3 - <<'PY'
import json, pathlib
for p in ['package.json', 'pnpm-workspace.yaml']:
path = pathlib.Path(p)
if path.exists():
print(f'--- {p} ---')
if p.endswith('.json'):
data = json.loads(path.read_text())
scripts = data.get('scripts', {})
for k,v in scripts.items():
if 'translate' in k or 'translate' in str(v) or 'openapi' in str(v):
print(k, '=>', v)
else:
print(path.read_text())
PY
printf '\n== README excerpt ==\n'
sed -n '1,140p' .github/scripts/i18n/README.md
printf '\n== SKILL excerpt ==\n'
sed -n '1,220p' SKILL.mdRepository: Comfy-Org/docs
Length of output: 19010
Correct the sidecar path note. Lines 67-72 describe the JSON sidecar as living beside each translated spec, but it is written under openapi/.i18n/<basename>.json.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/scripts/i18n/README.md around lines 48 - 50, The README’s sidecar
path documentation is incorrect: update the note describing translated OpenAPI
JSON sidecars to state that they are written under
`openapi/.i18n/<basename>.json`, rather than beside each translated
specification.
| Sidecar metadata lives beside each translated spec as `*.i18n.json` (block hashes | ||
| for incremental sync). Commit translated specs and sidecars with MDX translations. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Correct the generated sidecar path and filename.
openapi-translate.ts writes sidecars under openapi/.i18n/, for example openapi/.i18n/cloud.zh.json. They are not beside the translated spec and are not named cloud.zh.i18n.json.
Suggested correction
-Sidecar metadata lives beside each translated spec as `*.i18n.json` (block hashes
-for incremental sync). Commit translated specs and sidecars with MDX translations.
+Sidecar metadata lives under `openapi/.i18n/` as `<spec>.<lang>.json` (block hashes
+for incremental sync). Commit translated specs and sidecars with MDX translations.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Sidecar metadata lives beside each translated spec as `*.i18n.json` (block hashes | |
| for incremental sync). Commit translated specs and sidecars with MDX translations. | |
| Sidecar metadata lives under `openapi/.i18n/` as `<spec>.<lang>.json` (block hashes | |
| for incremental sync). Commit translated specs and sidecars with MDX translations. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/scripts/i18n/README.md around lines 71 - 72, Correct the README
documentation to state that generated sidecars are written under openapi/.i18n/
and use the cloud.zh.json filename pattern, rather than being colocated with
translated specs as *.i18n.json; update the “Sidecar metadata” guidance
accordingly.
| /** @param {string} source @param {string} langCode */ | ||
| export function localizeOpenApiSource(source, langCode) { | ||
| if (!source || langCode === "en") return source; | ||
| if (/\.en\.(ya?ml|json)$/i.test(source)) { | ||
| return source.replace(/\.en\.(ya?ml|json)$/i, (_, ext) => `.${langCode}.${ext}`); | ||
| } | ||
| return source.replace(/\.(ya?ml|json)$/i, (_, ext) => `.${langCode}.${ext}`); | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Consolidate duplicated localizeOpenApiSource to avoid drift and naming inconsistency.
This function is identical in logic to localizedOpenApiSource in openapi-translate.ts (line 49, exported) and check-i18n-sync.mjs (line 75, local). The name differs (localize vs localized), which could cause confusion when cross-referencing. If the regex or suffix convention changes, all three copies must be updated in sync.
Consider extracting this into the shared i18n-config.mjs module (already imported by other scripts) and importing it in both .mjs files. The TypeScript copy in openapi-translate.ts can re-export from the shared module or remain standalone with a comment linking the two.
When it comes to DRY, don't be shy — a single source of truth keeps the bugs from multiplying.
♻️ Proposed shared utility in i18n-config.mjs
# In i18n-config.mjs, add:
+export function localizedOpenApiSource(source, langCode) {
+ if (!source || langCode === "en") return source;
+ if (/\.en\.(ya?ml|json)$/i.test(source)) {
+ return source.replace(/\.en\.(ya?ml|json)$/i, (_, ext) => `.${langCode}.${ext}`);
+ }
+ return source.replace(/\.(ya?ml|json)$/i, (_, ext) => `.${langCode}.${ext}`);
+}# In sync-docs-json.mjs:
-import { ... } from "./i18n-config.mjs";
+import { ..., localizedOpenApiSource } from "./i18n-config.mjs";
-export function localizeOpenApiSource(source, langCode) {
- if (!source || langCode === "en") return source;
- if (/\.en\.(ya?ml|json)$/i.test(source)) {
- return source.replace(/\.en\.(ya?ml|json)$/i, (_, ext) => `.${langCode}.${ext}`);
- }
- return source.replace(/\.(ya?ml|json)$/i, (_, ext) => `.${langCode}.${ext}`);
-}# In check-i18n-sync.mjs:
+import { localizedOpenApiSource } from "./i18n-config.mjs";
-function localizedOpenApiSource(source, langCode) {
- if (!source || langCode === "en") return source;
- if (/\.en\.(ya?ml|json)$/i.test(source)) {
- return source.replace(/\.en\.(ya?ml|json)$/i, (_, ext) => `.${langCode}.${ext}`);
- }
- return source.replace(/\.(ya?ml|json)$/i, (_, ext) => `.${langCode}.${ext}`);
-}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/scripts/i18n/sync-docs-json.mjs around lines 322 - 329, Consolidate
the duplicated OpenAPI localization logic into the shared i18n-config.mjs
module, using one consistent name such as localizedOpenApiSource. Remove the
local implementation from sync-docs-json.mjs and check-i18n-sync.mjs, import the
shared utility there, and update openapi-translate.ts to reuse or clearly
reference the shared implementation so all callers have a single source of
truth.
| - 'openapi/cloud.en.yaml' | ||
| - 'openapi/registry.en.yaml' |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
Consider a glob pattern for OpenAPI path triggers to reduce future maintenance.
Hardcoding openapi/cloud.en.yaml and openapi/registry.en.yaml means any new OpenAPI source file added to translation-config.json also requires updating this workflow. A glob like openapi/*.en.yaml and openapi/*.en.json would auto-cover new English sources.
The zizmor hint about missing job-level concurrency limits is also worth addressing (e.g., concurrency: { group: i18n-sync-check-${{ github.head_ref }}, cancel-in-progress: true }), though that's pre-existing and not introduced by this PR.
🧰 Tools
🪛 zizmor (1.26.1)
[warning] 3-11: insufficient job-level concurrency limits (concurrency-limits): workflow is missing concurrency setting
(concurrency-limits)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/i18n-sync-check.yml around lines 10 - 11, Replace the
hardcoded OpenAPI paths in the workflow trigger list with glob patterns covering
all English YAML and JSON sources, such as openapi/*.en.yaml and
openapi/*.en.json, so new translation sources are included automatically. Also
add job-level concurrency using the i18n-sync-check-${{ github.head_ref }} group
with cancel-in-progress enabled.
Source: Linters/SAST tools
Wire cloud and registry OpenAPI specs into pnpm translate with per-language YAML under openapi/, Mintlify docs.json paths per locale, and pretty-printed output for readable zh/ja/ko API reference pages.