-
Notifications
You must be signed in to change notification settings - Fork 186
feat(i18n): add OpenAPI translation pipeline and localized specs #1249
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -45,6 +45,36 @@ pnpm translate:repair-fences # append missing closing ``` | |||||||||
| pnpm translate:repair-truncated -- --lang ko | ||||||||||
| pnpm translate:sync-hash # update hashes after manual translation edits | ||||||||||
| pnpm translate:sync-docs-json # sync docs.json navigation paths | ||||||||||
| pnpm translate -- --openapi-only # OpenAPI specs only | ||||||||||
| pnpm translate -- --no-openapi # skip OpenAPI specs | ||||||||||
| pnpm translate -- --fetch-openapi # refresh vendored specs from fetch_url | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| ### OpenAPI API Reference (Mintlify auto-generated pages) | ||||||||||
|
|
||||||||||
| Mintlify endpoint pages under `api-reference/**` are generated from OpenAPI | ||||||||||
| specs, not MDX. This pipeline copies each English spec to locale-specific files | ||||||||||
| (for example `openapi/cloud.zh.yaml`) and translates `summary` / `description` | ||||||||||
| fields incrementally. | ||||||||||
|
|
||||||||||
| Configure sources in `translation-config.json` → `openapi_specs`: | ||||||||||
|
|
||||||||||
| | Source | Output | Notes | | ||||||||||
| |--------|--------|-------| | ||||||||||
| | `openapi/cloud.en.yaml` | `openapi/cloud.{lang}.yaml` | Cloud API Reference tab | | ||||||||||
| | `openapi/registry.en.yaml` | `openapi/registry.{lang}.yaml` | Registry + Admin tabs; refresh with `--fetch-openapi` | | ||||||||||
|
|
||||||||||
| `docs.json` keeps English sources under `openapi/`. Sidecar hashes live in `openapi/.i18n/`. | ||||||||||
| `pnpm translate:sync-docs-json` localizes tab `openapi.source` values to the | ||||||||||
| matching `{lang}` file for zh / ja / ko. | ||||||||||
|
|
||||||||||
| Sidecar metadata lives beside each translated spec as `*.i18n.json` (block hashes | ||||||||||
| for incremental sync). Commit translated specs and sidecars with MDX translations. | ||||||||||
|
Comment on lines
+71
to
+72
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win Correct the generated sidecar path and filename.
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
Suggested change
🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| ```bash | ||||||||||
| pnpm translate:dry-run -- --openapi-only --lang zh # preview pending OpenAPI work | ||||||||||
| pnpm translate -- --openapi-only --lang zh # translate Cloud + Registry specs for zh | ||||||||||
| pnpm translate -- --fetch-openapi --openapi-only # pull latest Registry spec, then translate | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| Quality controls during/after a run write to `.github/i18n-logs/translate/` | ||||||||||
|
|
@@ -236,6 +266,7 @@ env → `frontend_locales_path` in `translation-config.json` → | |||||||||
| | File | Role | | ||||||||||
| |------|------| | ||||||||||
| | `translate-i18n.ts` | translation entry point | | ||||||||||
| | `openapi-translate.ts` | OpenAPI summary/description translation | | ||||||||||
| | `chunked-translate.ts` | split/reassemble long MDX (`heading_sections`, `update_blocks`) | | ||||||||||
| | `sync-hash-i18n.ts` | Refresh translation hashes after manual edits (no API) | | ||||||||||
| | `repair-fences-i18n.ts` | Append missing closing ``` in translations (no API) | | ||||||||||
|
|
@@ -245,4 +276,4 @@ env → `frontend_locales_path` in `translation-config.json` → | |||||||||
| | `sync-docs-json.mjs` / `nav-label-translate.mjs` | docs.json navigation sync | | ||||||||||
| | `check-translation-truncation.ts` | detect truncated output | | ||||||||||
| | `check-i18n-sync.mjs` | PR check: English changes have matching translations | | ||||||||||
| | `translation-config.json` | languages, skip paths, `preserve_terms`, frontend path | | ||||||||||
| | `translation-config.json` | languages, skip paths, `openapi_specs`, `preserve_terms`, frontend path | | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -69,6 +69,16 @@ const addedRaw = gitLines( | |
| const changedFiles = filterEnglish(changedRaw.filter((f) => f.endsWith(".mdx"))); | ||
| const deletedFiles = filterEnglish(deletedRaw.filter((f) => f.endsWith(".mdx"))); | ||
| const addedFiles = filterEnglish(addedRaw.filter((f) => f.endsWith(".mdx"))); | ||
| const openapiSources = (CONFIG.openapi_specs ?? []).map((spec) => spec.source); | ||
| const changedOpenApiSources = changedRaw.filter((file) => openapiSources.includes(file)); | ||
|
|
||
| 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}`); | ||
| } | ||
|
Comment on lines
+75
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
This is the same DRY concern raised on See the proposed consolidation in the comment on 🤖 Prompt for AI Agents |
||
|
|
||
| const allDiffNames = gitLines(`git diff --name-only ${baseSha} ${headSha}`); | ||
| const acmrtNames = gitLines( | ||
|
|
@@ -87,9 +97,10 @@ const renamedDestinations = renameLines | |
| if ( | ||
| changedFiles.length === 0 && | ||
| deletedFiles.length === 0 && | ||
| addedFiles.length === 0 | ||
| addedFiles.length === 0 && | ||
| changedOpenApiSources.length === 0 | ||
| ) { | ||
| console.log("No English MDX files changed outside translation directories. Skipping check."); | ||
| console.log("No English MDX or OpenAPI source files changed outside translation directories. Skipping check."); | ||
| writeOutput({ skipped: true, missingByLang: {}, movedFiles: [] }); | ||
| process.exit(0); | ||
| } | ||
|
|
@@ -154,6 +165,16 @@ for (const lang of languages) { | |
| missing.push(langFile); | ||
| } | ||
|
|
||
| for (const source of changedOpenApiSources) { | ||
| const langFile = localizedOpenApiSource(source, lang.code); | ||
| if (acmrtNames.includes(langFile)) { | ||
| console.log(`✅ [${lang.code}] Found OpenAPI translation update: ${langFile}`); | ||
| continue; | ||
| } | ||
| console.log(`❌ [${lang.code}] Missing OpenAPI translation update: ${langFile}`); | ||
| missing.push(langFile); | ||
| } | ||
|
|
||
| if (missing.length > 0) { | ||
| missingByLang[lang.code] = { name: lang.name, files: missing }; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| import { describe, expect, test } from "bun:test"; | ||
| import { | ||
| applyTranslations, | ||
| extractTranslatableStrings, | ||
| localizedOpenApiSource, | ||
| sidecarPathForOutput, | ||
| stringifyYamlSpec, | ||
| stripYamlHeader, | ||
| } from "./openapi-translate.ts"; | ||
|
|
||
| describe("localizedOpenApiSource", () => { | ||
| test("inserts language code before extension", () => { | ||
| expect(localizedOpenApiSource("openapi/cloud.en.yaml", "zh")).toBe( | ||
| "openapi/cloud.zh.yaml" | ||
| ); | ||
| expect(localizedOpenApiSource("openapi/registry.en.yaml", "ja")).toBe( | ||
| "openapi/registry.ja.yaml" | ||
| ); | ||
| expect(localizedOpenApiSource("openapi/cloud.en.yaml", "en")).toBe( | ||
| "openapi/cloud.en.yaml" | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe("sidecarPathForOutput", () => { | ||
| test("uses openapi/.i18n/ directory", () => { | ||
| expect(sidecarPathForOutput("openapi/cloud.zh.yaml")).toBe( | ||
| "openapi/.i18n/cloud.zh.json" | ||
| ); | ||
| expect(sidecarPathForOutput("openapi/registry.ko.yaml")).toBe( | ||
| "openapi/.i18n/registry.ko.json" | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe("extractTranslatableStrings", () => { | ||
| test("collects summary and description only", () => { | ||
| const spec = { | ||
| info: { title: "API", description: "Root description" }, | ||
| paths: { | ||
| "/users": { | ||
| get: { | ||
| summary: "List users", | ||
| description: "Returns all users", | ||
| operationId: "listUsers", | ||
| }, | ||
| }, | ||
| }, | ||
| components: { | ||
| schemas: { | ||
| User: { | ||
| type: "object", | ||
| properties: { | ||
| id: { type: "string", description: "User id" }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| expect(extractTranslatableStrings(spec)).toEqual({ | ||
| "info.description": "Root description", | ||
| "paths./users.get.summary": "List users", | ||
| "paths./users.get.description": "Returns all users", | ||
| "components.schemas.User.properties.id.description": "User id", | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe("stringifyYamlSpec", () => { | ||
| test("produces indented multi-line YAML", () => { | ||
| const yaml = stringifyYamlSpec({ | ||
| openapi: "3.0.3", | ||
| info: { title: "API", description: "Hello" }, | ||
| }); | ||
| expect(yaml.split("\n").length).toBeGreaterThan(3); | ||
| expect(yaml).toContain("openapi: 3.0.3"); | ||
| expect(yaml).toContain(" title: API"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("stripYamlHeader", () => { | ||
| test("preserves translation metadata comments", () => { | ||
| const { header, body } = stripYamlHeader( | ||
| "# translationSourceHash: abc\n# translationFrom: x.yaml\n\nopenapi: 3.0.3\n" | ||
| ); | ||
| expect(header).toContain("translationSourceHash"); | ||
| expect(body).toContain("openapi: 3.0.3"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("applyTranslations", () => { | ||
| test("replaces targeted strings without changing structure", () => { | ||
| const english = { | ||
| info: { description: "Hello" }, | ||
| paths: { "/x": { get: { summary: "Get X" } } }, | ||
| }; | ||
| const localized = applyTranslations(english, { | ||
| "info.description": "你好", | ||
| "paths./x.get.summary": "获取 X", | ||
| }); | ||
| expect(localized).toEqual({ | ||
| info: { description: "你好" }, | ||
| paths: { "/x": { get: { summary: "获取 X" } } }, | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: 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