Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .cursor/skills/docs-i18n-translate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ description: >-
Translate ComfyUI Mintlify docs from English MDX to ja/zh/ko using translate-i18n.ts.
Incremental hash sync, chunked long pages, changelog update_blocks, glossary terms.
Use when translating docs, updating zh/ja/ko changelog or pages, running pnpm translate,
translationSourceHash, glossary sync, docs.json i18n, or fixing truncated translations.
translationSourceHash, glossary sync, docs.json i18n, OpenAPI spec localization,
or fixing truncated translations.
---

# Docs i18n Translation
Expand All @@ -21,11 +22,18 @@ index.mdx, changelog/index.mdx, … ← English (edit here)
▼ pnpm translate
{ja,zh,ko}/… ← translated MDX (commit to git)
snippets/{ja,zh,ko}/…
openapi/cloud.{lang}.yaml ← translated OpenAPI (API Reference pages)
openapi/registry.{lang}.yaml
openapi/.i18n/*.json ← translation sidecar metadata
▼ optional
pnpm translate:sync-docs-json ← mirror nav paths in docs.json
```

OpenAPI endpoint pages (`api-reference/**`) are Mintlify-generated from specs in
`translation-config.json` → `openapi_specs`. `pnpm translate` copies each English
spec to `*.{lang}.yaml` and translates `summary` / `description` incrementally.

Incremental: each file stores `translationSourceHash` in frontmatter. Unchanged English → skip.

## Environment (`.env.local`)
Expand Down Expand Up @@ -57,6 +65,9 @@ Requires **Bun**.
| `pnpm translate:sync-hash` | Refresh hashes after manual zh/ja/ko edits (no API) |
| `pnpm translate:sync-docs-json` | Sync `docs.json` nav paths (labels preserved) |
| `pnpm translate:sync-docs-json -- --translate-nav-labels` | Also translate new EN nav labels |
| `pnpm translate:openapi` | OpenAPI specs only |
| `pnpm translate -- --no-openapi` | Skip OpenAPI during a normal run |
| `pnpm translate -- --fetch-openapi` | Refresh `openapi/registry.en.yaml` from API before translating |
| `pnpm glossary:sync` | Rebuild glossary from ComfyUI frontend |
| `pnpm glossary:sync:dry-run` | Preview glossary sync |

Expand Down
33 changes: 32 additions & 1 deletion .github/scripts/i18n/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +48 to +50

Copy link
Copy Markdown

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:

#!/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.md

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
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.

```

### 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.

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.

Suggested change
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.


```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/`
Expand Down Expand Up @@ -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) |
Expand All @@ -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 |
25 changes: 23 additions & 2 deletions .github/scripts/i18n/check-i18n-sync.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 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 allDiffNames = gitLines(`git diff --name-only ${baseSha} ${headSha}`);
const acmrtNames = gitLines(
Expand All @@ -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);
}
Expand Down Expand Up @@ -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 };
}
Expand Down
107 changes: 107 additions & 0 deletions .github/scripts/i18n/openapi-translate.test.ts
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" } } },
});
});
});
Loading
Loading