Skip to content
Merged
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
24 changes: 22 additions & 2 deletions .cursor/skills/docs-i18n-translate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ Requires **Bun**.
| `pnpm translate:snippets` | Snippets only |
| `pnpm translate -- --pages-only` | Skip snippets |
| `pnpm translate:check-truncation` | Scan for truncated output |
| `pnpm translate:repair-fences` | Append missing closing ``` (no API) |
| `pnpm translate:repair-truncated -- --lang ko` | Re-translate flagged files |
| `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 glossary:sync` | Rebuild glossary from ComfyUI frontend |
Expand All @@ -70,6 +72,20 @@ pnpm translate -- changelog/index.mdx # or specific paths
pnpm translate:check-truncation # if long page / changelog
```

### Small English edits (manual translation)

When only a line or paragraph changed:

```bash
# 1. Edit English + update zh/ja/ko by hand (or ask Cursor to patch matching sections)
# 2. Sync hashes so translate skips the file
pnpm translate:sync-hash -- path/to/page.mdx
pnpm translate:sync-hash -- --verify path/to/page.mdx # optional sanity check
```

For larger or new sections, use `pnpm translate -- path/to/page.mdx` (chunked pages
only re-translate changed `##` sections when `auto_chunk` applies).

### Changelog (`changelog/index.mdx`)

- Strategy: `update_blocks` (configured in `translation-config.json`)
Expand All @@ -96,7 +112,7 @@ pnpm translate -- changelog/index.mdx --lang zh

| Strategy | When | Config |
|----------|------|--------|
| `heading_sections` | Long reference pages | `chunked_files` or `auto_chunk` (≥10k chars, ≥4 `##`) |
| `heading_sections` | Long reference pages | `chunked_files` or `auto_chunk` (≥3k chars, ≥2 `##`) |
| `update_blocks` | Changelog | `chunked_files` entry for `changelog/index.mdx` |

Checkpoints per block — safe to resume after interrupt.
Expand Down Expand Up @@ -134,7 +150,8 @@ pnpm glossary:sync # after frontend locale updates
When user updates English docs and needs translations:

- [ ] Identify changed files (or run `pnpm translate:dry-run`)
- [ ] Run `pnpm translate` for affected paths — **not** `cms:prepare` unless CMS/Strapi
- [ ] For small edits: hand-update translations, then `pnpm translate:sync-hash -- <path>`
- [ ] For larger edits: run `pnpm translate` for affected paths — **not** `cms:prepare` unless CMS/Strapi
- [ ] For changelog, translate **docs** `zh/changelog/` etc., not CMS staging
- [ ] After long pages, run `pnpm translate:check-truncation`
- [ ] Commit translated MDX + updated `translationSourceHash` / `translationBlockHashes`
Expand All @@ -148,6 +165,7 @@ When user updates English docs and needs translations:
|------|------|
| `.github/scripts/i18n/translate-i18n.ts` | Entry point |
| `.github/scripts/i18n/chunked-translate.ts` | Block splitting/reassembly |
| `.github/scripts/i18n/sync-hash-i18n.ts` | Hash-only sync after manual edits |
| `.github/scripts/i18n/translation-config.json` | Languages, skip paths, chunked files |
| `.github/scripts/i18n/glossary.mjs` | Term injection |
| `.github/scripts/i18n/README.md` | Full reference |
Expand All @@ -158,7 +176,9 @@ When user updates English docs and needs translations:
| Issue | Fix |
|-------|-----|
| File skipped | English hash unchanged — use `pnpm translate:force` or edit EN source |
| Manual translation done | `pnpm translate:sync-hash -- <path>` to refresh hashes |
| Truncated translation | `translate:repair-truncated` or add to `chunked_files` |
| Missing closing ``` only | `translate:repair-fences` (structural); re-translate if code inside block was cut |
| Wrong term | `glossary/overrides/{lang}.json` or `preserve_terms` |
| PR i18n comment | Run `pnpm translate` for listed files |
| Changelog date still English | Re-run translate for that block; dates derived from EN |
Expand Down
43 changes: 42 additions & 1 deletion .github/scripts/i18n/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,31 @@ pnpm translate -- --lang zh,ja # specific languages
pnpm translate -- installation/x.mdx # specific files
pnpm translate:snippets # snippets only
pnpm translate:check-truncation # scan for truncated output
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
```

Quality controls during/after a run write to `.github/i18n-logs/translate/`
(gitignored): semantic mismatches reported by the model, and a truncation scan
(`check-translation-truncation.ts`).

Truncation heuristics include **unclosed ` ``` ` blocks** (`unclosed_code_fence`),
fewer fence markers than English (`missing_code_fence`), and short body length.

```bash
pnpm translate:check-truncation # scan all languages
pnpm translate:check-truncation -- --lang ko
pnpm translate:repair-fences # append missing closing ```
pnpm translate:repair-fences -- --dry-run # preview fence fixes
pnpm translate:repair-truncated -- --lang ko # re-translate via API when content was cut
```

`repair-fences` is a **structural** fix (adds `\`\`\`` at the end). It does not
restore code lines lost inside the block. Use `repair-truncated` when the block
body itself was truncated.

### Long pages (chunked translation)

Very long MDX files (e.g. `tutorials/partner-nodes/pricing.mdx`) exceed model
Expand All @@ -60,7 +77,7 @@ output limits when translated in one shot. Two strategies avoid truncation:
| `update_blocks` | Changelog | `<Update label="…">` blocks | Per-block content hash in `translationBlockHashes` (new labels + EN edits) |

Configure explicit paths in `translation-config.json` → `chunked_files`, or rely
on `auto_chunk` (default: body ≥ 10k chars and ≥ 4 `##` sections) to auto-enable
on `auto_chunk` (default: body ≥ 3k chars and ≥ 2 `##` sections) to auto-enable
`heading_sections`.

Changelog `<Update description="…">` dates are **derived from English** and
Expand All @@ -87,6 +104,28 @@ pnpm translate:check-truncation -- --lang ko
pnpm translate:repair-truncated -- --lang ko # force re-translate flagged files
```

### Sync hashes after manual edits

When you edit English and update zh/ja/ko translations by hand (or with Cursor), refresh
metadata so `pnpm translate` skips those files:

```bash
pnpm translate:sync-hash -- path/to/page.mdx # specific file(s)
pnpm translate:sync-hash -- --lang zh path/to/page.mdx
pnpm translate:sync-hash -- --dry-run path/to/page.mdx # preview
pnpm translate:sync-hash -- --verify path/to/page.mdx # warn if EN blocks still look pending
pnpm translate:sync-hash # all files with hash drift
```

This updates `translationSourceHash` (and `translationBlockHashes` on chunked pages)
from the English source. It does **not** call the translation API or change prose.

Example:

```bash
pnpm translate:sync-hash -- installation/system_requirements.mdx
```

## Quality review (LLM-as-a-judge)

`review-i18n.ts` scores existing translations with an **independent** (and
Expand Down Expand Up @@ -198,6 +237,8 @@ env → `frontend_locales_path` in `translation-config.json` →
|------|------|
| `translate-i18n.ts` | translation entry point |
| `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) |
| `sync-glossary.mjs` | rebuild the glossary frontend mirror |
| `glossary.mjs` | load glossary layers, select + inject terms |
| `i18n-config.mjs` | shared path rules from `translation-config.json` |
Expand Down
54 changes: 52 additions & 2 deletions .github/scripts/i18n/check-translation-truncation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,53 @@ function enFenceCount(enBody: string): number {
return countFencePairs(enBody);
}

/** True when body has an odd number of ``` line markers (unclosed block). */
export function hasUnclosedCodeFence(body: string): boolean {
return countCodeFences(body).open;
}

/** Append a closing ``` when the body ends inside a code block. */
export function repairUnclosedCodeFence(body: string): {
body: string;
repaired: boolean;
openLang: string;
openLine: number;
} {
const fence = countCodeFences(body);
if (!fence.open) {
return { body, repaired: false, openLang: "", openLine: 0 };
}
const trimmed = body.endsWith("\n") ? body : `${body}\n`;
return {
body: `${trimmed}\`\`\`\n`,
repaired: true,
openLang: fence.openLang,
openLine: fence.openLine,
};
}

/** Fix an MDX file whose body ends with an unclosed ``` block. Frontmatter is preserved. */
export function repairTargetContent(targetContent: string): {
content: string;
repaired: boolean;
detail: string;
} {
const { frontmatter, body } = parseFrontmatterAndBody(targetContent);
const result = repairUnclosedCodeFence(body);
if (!result.repaired) {
return { content: targetContent, repaired: false, detail: "" };
}
const newBody = result.body;
const raw = frontmatter ? `${frontmatter}\n${newBody}` : newBody;
const content = raw.endsWith("\n") ? raw : `${raw}\n`;
Comment on lines +139 to +146

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

Extra blank line inserted between frontmatter and body on every fence repair.

parseFrontmatterAndBody already restores the newline consumed by its \n? regex group (frontmatter: ${match[1]}\n``), so plain frontmatter + body concatenation faithfully reproduces the original spacing. Here, `${frontmatter}\n${newBody}` adds one more `\n` on top of that, so every repaired file gains a spurious extra blank line between frontmatter and content (regardless of whether the source had a blank line there or not). Harmless for MDX rendering (consecutive blank lines collapse), but it needlessly bloats the diff/committed file beyond the actual fence fix.

🔧 Proposed fix
   const newBody = result.body;
-  const raw = frontmatter ? `${frontmatter}\n${newBody}` : newBody;
+  const raw = frontmatter ? `${frontmatter}${newBody}` : newBody;
📝 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
const { frontmatter, body } = parseFrontmatterAndBody(targetContent);
const result = repairUnclosedCodeFence(body);
if (!result.repaired) {
return { content: targetContent, repaired: false, detail: "" };
}
const newBody = result.body;
const raw = frontmatter ? `${frontmatter}\n${newBody}` : newBody;
const content = raw.endsWith("\n") ? raw : `${raw}\n`;
const { frontmatter, body } = parseFrontmatterAndBody(targetContent);
const result = repairUnclosedCodeFence(body);
if (!result.repaired) {
return { content: targetContent, repaired: false, detail: "" };
}
const newBody = result.body;
const raw = frontmatter ? `${frontmatter}${newBody}` : newBody;
const content = raw.endsWith("\n") ? raw : `${raw}\n`;
🤖 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-translation-truncation.ts around lines 139 - 146,
The fence-repair path in check-translation-truncation.ts is inserting an extra
blank line between frontmatter and body because parseFrontmatterAndBody already
preserves the trailing newline. Update the concatenation in the repair flow
(around result.repaired / newBody) so the rebuilt content uses frontmatter plus
newBody directly, matching the original spacing and avoiding the unnecessary
diff bloat.

const langHint = result.openLang ? `\`${result.openLang}\` ` : "";
return {
content,
repaired: true,
detail: `appended closing \`\`\` for ${langHint}block opened near line ${result.openLine}`,
};
}

function targetPath(enRel: string, lang: LangConfig): string {
if (enRel.startsWith("snippets/")) {
return join(ROOT, lang.snippets_dir, enRel.slice("snippets/".length));
Expand Down Expand Up @@ -354,7 +401,8 @@ export async function writeTruncationReport(
"changelog missing <Update> blocks.",
"",
"Repair:",
" npm run translate:repair-truncated -- --lang ko",
" npm run translate:repair-fences # append missing closing ```",
" npm run translate:repair-truncated -- --lang ko # re-translate truncated files",
"",
`Note: semantic AI review notes (mismatch) are separate — see ${TRANSLATE_LOG_REL}/mismatches.txt`,
" (only written when `npm run translate` runs and the model reports issues).",
Expand Down Expand Up @@ -416,7 +464,9 @@ async function main() {
console.log(`\nLog: ${TXT_PATH}`);
console.log(`JSON: ${JSON_PATH}`);
if (issues.length > 0) {
console.log("\nRepair: npm run translate:repair-truncated -- --lang <code>");
console.log("\nRepair:");
console.log(" npm run translate:repair-fences");
console.log(" npm run translate:repair-truncated -- --lang <code>");
}
}

Expand Down
135 changes: 135 additions & 0 deletions .github/scripts/i18n/repair-fences-i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/usr/bin/env bun
/**
* Append missing closing ``` markers in translated MDX (structural fix only).
*
* Safe to run when `translate:check-truncation` reports `unclosed_code_fence`.
* Does not restore truncated code inside the block — use `translate:repair-truncated`
* when content was cut off, not just the closing fence.
*
* Usage:
* pnpm translate:repair-fences
* pnpm translate:repair-fences -- --lang ko
* pnpm translate:repair-fences -- --dry-run path/to/page.mdx
*/

import { readFile, writeFile } from "fs/promises";
import { join } from "path";
import {
loadI18nConfig,
REPO_ROOT,
parseLangArg as parseLangArgFromConfig,
} from "./i18n-config.mjs";
import {
hasUnclosedCodeFence,
repairTargetContent,
scanTruncationIssues,
writeTruncationReport,
} from "./check-translation-truncation.ts";
import { parseFrontmatterAndBody } from "./chunked-translate.ts";

interface LangConfig {
code: string;
name: string;
dir: string;
snippets_dir: string;
}

const config = loadI18nConfig() as {
languages: LangConfig[];
skip_paths: string[];
};

async function readFileOr(path: string, fallback = ""): Promise<string> {
try {
return await readFile(path, "utf-8");
} catch {
return fallback;
}
}
Comment on lines +42 to +48

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Silent error swallowing in readFileOr may hide real failures.

Any read error (permissions, I/O failure, encoding issue), not just "file missing", falls back to an empty string, which is then silently skipped at line 83-86 as if the target simply didn't exist yet. A truly missing target vs. a broken read are treated identically with no diagnostic output.

🤖 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/repair-fences-i18n.ts around lines 42 - 48, The
readFileOr helper is swallowing every read failure and treating it like a
missing file, which hides real errors. Update readFileOr to distinguish “file
not found” from other read problems, and only return the fallback for the
missing-file case; for permissions/I/O/encoding failures, surface the error or
log it with enough context before rethrowing. Keep the existing callers in
repair-fences-i18n.ts unchanged except for handling the non-missing failure path
around readFileOr.


function parseLangArg(args: string[]): LangConfig[] {
try {
return parseLangArgFromConfig(args, config.languages) as LangConfig[];
} catch (err: unknown) {
console.error(err instanceof Error ? err.message : String(err));
process.exit(1);
}
}

async function main() {
const args = process.argv.slice(2);
const dryRun = args.includes("--dry-run");
const snippetsMode = args.includes("--snippets");
const langs = parseLangArg(args);
const fileArgs = args.filter((a, i) => !a.startsWith("--") && args[i - 1] !== "--lang");

const issues = await scanTruncationIssues({ langs, snippetsMode, fileArgs });
const fenceIssues = issues.filter((i) => i.reasons.includes("unclosed_code_fence"));

if (fenceIssues.length === 0) {
console.log("No unclosed code fences found.");
return;
}

console.log(`Found ${fenceIssues.length} file(s) with unclosed \`\`\` blocks${dryRun ? " [dry-run]" : ""}:`);

let wouldRepair = 0;
let repaired = 0;
let skipped = 0;
const repairedPairs: { lang: string; enRel: string }[] = [];

for (const issue of fenceIssues) {
const targetPath = join(REPO_ROOT, issue.targetRel);
const targetContent = await readFileOr(targetPath);
if (!targetContent.trim()) {
skipped++;
continue;
}

const { body } = parseFrontmatterAndBody(targetContent);
if (!hasUnclosedCodeFence(body)) {
console.log(` [${issue.lang}] ${issue.enRel}: already balanced, skipping`);
skipped++;
continue;
}

const result = repairTargetContent(targetContent);
if (!result.repaired) {
skipped++;
continue;
}
Comment on lines +88 to +99

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 | 💤 Low value

Redundant fence detection before repair.

hasUnclosedCodeFence(body) (line 89) duplicates work already performed inside repairTargetContent (which internally calls repairUnclosedCodeFence, per the upstream snippet). Since repairTargetContent already returns repaired: false when there's nothing to fix, the earlier manual parse/check is unnecessary and just doubles the frontmatter/body parsing work per file. Not wrong, just gets a fence around the pun-free zone unnecessarily.

♻️ Simplify by relying on repairTargetContent's own result
-    const { body } = parseFrontmatterAndBody(targetContent);
-    if (!hasUnclosedCodeFence(body)) {
-      console.log(`  [${issue.lang}] ${issue.enRel}: already balanced, skipping`);
-      skipped++;
-      continue;
-    }
-
     const result = repairTargetContent(targetContent);
     if (!result.repaired) {
+      console.log(`  [${issue.lang}] ${issue.enRel}: already balanced, skipping`);
       skipped++;
       continue;
     }
📝 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
const { body } = parseFrontmatterAndBody(targetContent);
if (!hasUnclosedCodeFence(body)) {
console.log(` [${issue.lang}] ${issue.enRel}: already balanced, skipping`);
skipped++;
continue;
}
const result = repairTargetContent(targetContent);
if (!result.repaired) {
skipped++;
continue;
}
const result = repairTargetContent(targetContent);
if (!result.repaired) {
console.log(` [${issue.lang}] ${issue.enRel}: already balanced, skipping`);
skipped++;
continue;
}
🤖 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/repair-fences-i18n.ts around lines 88 - 99, The
pre-check in the i18n repair flow is duplicating work already handled by
repairTargetContent. Remove the separate
parseFrontmatterAndBody/hasUnclosedCodeFence check and rely on
repairTargetContent(targetContent) to decide whether anything needs fixing via
its repaired result. Keep the existing logging and skipped handling in the
repair loop, using repairTargetContent and repairUnclosedCodeFence as the main
path.


if (dryRun) {
console.log(` [${issue.lang}] would repair: ${issue.targetRel}`);
console.log(` ${result.detail}`);
wouldRepair++;
} else {
await writeFile(targetPath, result.content);
console.log(` [${issue.lang}] repaired: ${issue.targetRel}`);
console.log(` ${result.detail}`);
repaired++;
repairedPairs.push({ lang: issue.lang, enRel: issue.enRel });
}
}

if (!dryRun && repairedPairs.length > 0) {
const remaining = await scanTruncationIssues({
langs,
snippetsMode,
pairs: repairedPairs.map((p) => ({ langCode: p.lang, enRel: p.enRel })),
});
await writeTruncationReport(remaining, { replacePairs: repairedPairs });
}

console.log(
`\nDone: ${dryRun ? wouldRepair : repaired} repaired, ${skipped} skipped` +
(fenceIssues.some((i) => i.reasons.includes("missing_code_fence")) && !dryRun
? "\nNote: files with missing_code_fence (fewer fences than EN) may need translate:repair-truncated."
: "")
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
Loading
Loading