From 00d59988d8a706a40fe77fe63887d6fc94d4cd57 Mon Sep 17 00:00:00 2001 From: ComfyUI Wiki Date: Mon, 30 Mar 2026 00:20:07 +0800 Subject: [PATCH 1/5] Add automated translation sync workflow with DeepSeek API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .github/workflows/translation-sync.yml: triggers on push to main, detects which language files are out of sync, runs translation script, creates a PR for review - .github/scripts/translate-sync.py: diff-based translation — only translates changed sections (split by headings), skips files already updated in the same commit - .github/scripts/translation-config.json: language config, easily extensible to add new languages Co-Authored-By: Claude Sonnet 4.6 --- .github/scripts/translate-sync.py | 427 ++++++++++++++++++++++++ .github/scripts/translation-config.json | 23 ++ .github/workflows/translation-sync.yml | 121 +++++++ 3 files changed, 571 insertions(+) create mode 100644 .github/scripts/translate-sync.py create mode 100644 .github/scripts/translation-config.json create mode 100644 .github/workflows/translation-sync.yml diff --git a/.github/scripts/translate-sync.py b/.github/scripts/translate-sync.py new file mode 100644 index 00000000..c5b03569 --- /dev/null +++ b/.github/scripts/translate-sync.py @@ -0,0 +1,427 @@ +#!/usr/bin/env python3 +""" +Translation sync script for ComfyUI docs. + +Usage: + python translate-sync.py --before --after [--dry-run] + +For each changed English .mdx file, checks which language translations are +out of sync and translates only the changed sections using DeepSeek API. +""" + +import argparse +import json +import os +import re +import subprocess +import sys +from pathlib import Path +from typing import Optional + +try: + from openai import OpenAI +except ImportError: + print("Error: openai package required. Run: pip install openai") + sys.exit(1) + +SCRIPT_DIR = Path(__file__).parent +REPO_ROOT = SCRIPT_DIR.parent.parent +CONFIG_PATH = SCRIPT_DIR / "translation-config.json" + + +def load_config() -> dict: + with open(CONFIG_PATH) as f: + return json.load(f) + + +def get_changed_files(before_sha: str, after_sha: str, exclude_dirs: list[str]) -> list[str]: + """Get English .mdx files changed between two commits.""" + result = subprocess.run( + ["git", "diff", "--name-only", "--diff-filter=ACMRT", before_sha, after_sha], + capture_output=True, text=True, cwd=REPO_ROOT + ) + files = [] + for line in result.stdout.strip().splitlines(): + if not line.endswith(".mdx"): + continue + # Skip language dirs + if any(line.startswith(d + "/") for d in exclude_dirs): + continue + files.append(line) + return files + + +def get_files_changed_in_range(before_sha: str, after_sha: str) -> set[str]: + """Get all files (any language) changed in the commit range.""" + result = subprocess.run( + ["git", "diff", "--name-only", before_sha, after_sha], + capture_output=True, text=True, cwd=REPO_ROOT + ) + return set(result.stdout.strip().splitlines()) + + +def get_file_diff(before_sha: str, after_sha: str, filepath: str) -> str: + """Get the unified diff of a file between two commits.""" + result = subprocess.run( + ["git", "diff", before_sha, after_sha, "--", filepath], + capture_output=True, text=True, cwd=REPO_ROOT + ) + return result.stdout + + +def split_into_sections(content: str) -> list[dict]: + """ + Split MDX content into sections by top-level headings (## or ###). + Returns list of {"heading": str, "content": str, "level": int} + """ + lines = content.splitlines(keepends=True) + sections = [] + current_heading = None + current_level = 0 + current_lines = [] + frontmatter_done = False + frontmatter_lines = [] + in_frontmatter = False + + for i, line in enumerate(lines): + # Handle frontmatter + if i == 0 and line.strip() == "---": + in_frontmatter = True + frontmatter_lines.append(line) + continue + if in_frontmatter: + frontmatter_lines.append(line) + if line.strip() == "---": + in_frontmatter = False + frontmatter_done = True + continue + + heading_match = re.match(r'^(#{2,3})\s+(.+)', line) + if heading_match: + # Save current section + if current_lines: + sections.append({ + "heading": current_heading, + "level": current_level, + "content": "".join(current_lines) + }) + current_heading = heading_match.group(2).strip() + current_level = len(heading_match.group(1)) + current_lines = [line] + else: + current_lines.append(line) + + # Save last section + if current_lines: + sections.append({ + "heading": current_heading, + "level": current_level, + "content": "".join(current_lines) + }) + + return { + "frontmatter": "".join(frontmatter_lines), + "sections": sections + } + + +def get_changed_headings_from_diff(diff: str) -> set[str]: + """Extract headings of sections that were changed in the diff.""" + changed_headings = set() + current_heading = None + + for line in diff.splitlines(): + # Track which heading we're under in the diff + heading_match = re.match(r'^[+-]\s*(#{2,3})\s+(.+)', line) + if heading_match: + current_heading = heading_match.group(2).strip() + changed_headings.add(current_heading) + elif line.startswith('+') or line.startswith('-'): + if current_heading: + changed_headings.add(current_heading) + + return changed_headings + + +def translate_content( + client: OpenAI, + content: str, + target_language: str, + target_language_name: str, + preserve_terms: list[str], + is_full_file: bool = False, + existing_translation: Optional[str] = None +) -> str: + """Translate MDX content using DeepSeek API.""" + preserve_str = ", ".join(preserve_terms) + + if existing_translation: + system_prompt = f"""You are a technical documentation translator. Translate the given MDX content into {target_language_name}. + +Rules: +- Preserve ALL MDX component tags exactly: , , , , , , , , , , ,