From 43dbf45ee0f46ce9c16b0af9a00e59f7e3fff53a Mon Sep 17 00:00:00 2001 From: ComfyUI Wiki Date: Mon, 17 Aug 2026 15:52:15 +0800 Subject: [PATCH 1/6] fix(docs-generation): translation pipeline bug fixes and prompt template cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bugs fixed: - update_param_translations.py: --dry-run no longer writes files (the dry-run branch previously called the same writing code path) - update_param_translations.py: Outputs-section regex now includes Persian 'خروجی‌ها', so fa output names are synced like other locales - main.py: --translate --mode node --node X now actually translates the given node via --node-list (previously --node was silently ignored and the mode downgraded to a 20-node test batch); invalid translate mode combinations (changed/resume/fix) now fail fast with clear errors - batch_translate_docs.py: _fix_output_names_in_translation no longer misaligns English output names when the Outputs section contains intro/blank/non-data lines; data rows are detected by structure (header/separator/backtick cell) instead of fixed line offsets - translation_config.json: the grouped-inputs (### subheading) rule was pasted in Simplified Chinese into ALL 11 language prompts; it is now properly localized per language, duplicate rule number '5.' renumbered to 5/6, the final translate instruction moved to the end of the prompt (adjacent to the appended document), and es/fr grammar fixed Simplifications: - batch_translate_docs.py: reuse a single OpenAI client per run instead of creating one per node; openai import is now lazy so the module's post-processing helpers stay importable/testable without the package - update_param_translations.py: manual sys.argv parsing replaced with argparse (with language validation) - removed dead config: translation_rules.txt was never loaded anywhere (only an unused TRANSLATION_RULES constant referenced it) Tests: add tests/test_translation_fixes.py (6 cases: output-name row alignment, dry-run no-write, fa Outputs detection); all 39 tests pass. --- .../config/translation_config.json | 24 +-- docs-generation/config/translation_rules.txt | 122 ------------- docs-generation/lib/paths.py | 1 - docs-generation/main.py | 67 ++++++-- .../scripts/batch_translate_docs.py | 107 ++++++------ .../scripts/update_param_translations.py | 79 +++++---- .../tests/test_translation_fixes.py | 160 ++++++++++++++++++ 7 files changed, 321 insertions(+), 239 deletions(-) delete mode 100644 docs-generation/config/translation_rules.txt create mode 100644 docs-generation/tests/test_translation_fixes.py diff --git a/docs-generation/config/translation_config.json b/docs-generation/config/translation_config.json index 60d78ce18..8d628ac86 100644 --- a/docs-generation/config/translation_config.json +++ b/docs-generation/config/translation_config.json @@ -5,7 +5,7 @@ "heading_inputs": "输入", "heading_outputs": "输出", "disclaimer": "本文档由 AI 生成。如果您发现任何错误或有改进建议,欢迎贡献!", - "prompt_template": "你是一位专业的技术文档翻译专家,专门负责将 ComfyUI 节点文档从英文翻译成简体中文。\n\n## 翻译规则\n\n1. **必须保持不变的内容:**\n - 参数名必须保持英文,用反引号包裹:`image`、`seed`、`model`\n - 数据类型必须保持英文大写:IMAGE、STRING、INT、FLOAT、MODEL、CONDITIONING 等\n - Range 列中的值保持不变:数字、\"auto\"、选项名称\n - 不要翻译任何代码、文件路径\n\n2. **需要翻译的内容:**\n - 章节标题翻译为:{heading_overview}、{heading_inputs}、{heading_outputs}\n - 所有描述性文字和说明\n - 参数描述\n\n3. **翻译质量要求:**\n - 使用自然流畅的简体中文\n - 保持专业但易懂的语气\n - 确保技术准确性\n - 使用中文技术文档的标准术语\n\n4. **格式要求:**\n - 保持所有 Markdown 格式不变\n - 保持表格结构完整\n - 不要添加免责声明(系统将自动添加到文档底部)\n\n## 翻译示例\n\n**英文:**\nThis node combines two CLIP models by adding the second model to the first.\n\n**中文:**\n此节点通过将第二个 CLIP 模型添加到第一个模型来组合两个 CLIP 模型。\n\n请将以下英文文档翻译成简体中文,不要包含免责声明:\n\n5. **子标题翻译(grouped inputs 格式):** 如果文档包含三级子标题(### 级别),请翻译其内容:\n - `### Common Inputs` → 通用输入标题(如简体中文「通用输入」,日文「共通入力」,西班牙语「Entradas comunes」等,用目标语言的对应说法)\n - `### <模型名> Inputs`(如 `### Seedance 2.5 Inputs`)→ 模型名保持英文原文,只翻译 \"Inputs\" 部分(如「Seedance 2.5 输入」)\n - `### Reference Inputs` → 参考输入标题(如「参考输入」/「参照入力」等)\n - 保持子标题的 Markdown 层级(###)和分组结构完全不变\n - 表格内的 Growable slot、Reference、Up to 等描述词也要翻译\n\n5. **关键规则 - 输出名称:** 输出表(Outputs)的第一列是输出名(如 `positive`、`negative`、`latent`、`image`、`model`、`conditioning`),**必须保持英文不变**。翻译输出名会导致输出表中出现重复名称,破坏文档结构。" + "prompt_template": "你是一位专业的技术文档翻译专家,专门负责将 ComfyUI 节点文档从英文翻译成简体中文。\n\n## 翻译规则\n\n1. **必须保持不变的内容:**\n - 参数名必须保持英文,用反引号包裹:`image`、`seed`、`model`\n - 数据类型必须保持英文大写:IMAGE、STRING、INT、FLOAT、MODEL、CONDITIONING 等\n - Range 列中的值保持不变:数字、\"auto\"、选项名称\n - 不要翻译任何代码、文件路径\n\n2. **需要翻译的内容:**\n - 章节标题翻译为:{heading_overview}、{heading_inputs}、{heading_outputs}\n - 所有描述性文字和说明\n - 参数描述\n\n3. **翻译质量要求:**\n - 使用自然流畅的简体中文\n - 保持专业但易懂的语气\n - 确保技术准确性\n - 使用中文技术文档的标准术语\n\n4. **格式要求:**\n - 保持所有 Markdown 格式不变\n - 保持表格结构完整\n - 不要添加免责声明(系统将自动添加到文档底部)\n\n5. **子标题翻译(grouped inputs 格式):** 如果文档包含三级子标题(### 级别),请翻译其内容:\n - `### Common Inputs` → 通用输入标题(如「通用输入」)\n - `### <模型名> Inputs`(如 `### Seedance 2.5 Inputs`)→ 模型名保持英文原文,只翻译 \"Inputs\" 部分(如「Seedance 2.5 输入」)\n - `### Reference Inputs` → 参考输入标题(如「参考输入」)\n - 保持子标题的 Markdown 层级(###)和分组结构完全不变\n - 表格内的 Growable slot、Reference、Up to 等描述词也要翻译\n\n6. **关键规则 - 输出名称:** 输出表(Outputs)的第一列是输出名(如 `positive`、`negative`、`latent`、`image`、`model`、`conditioning`),**必须保持英文不变**。翻译输出名会导致输出表中出现重复名称,破坏文档结构。\n\n## 翻译示例\n\n**英文:**\nThis node combines two CLIP models by adding the second model to the first.\n\n**中文:**\n此节点通过将第二个 CLIP 模型添加到第一个模型来组合两个 CLIP 模型。\n\n请将以下英文文档翻译成简体中文,不要包含免责声明:\n" }, "es": { "name": "Español", @@ -13,7 +13,7 @@ "heading_inputs": "Entradas", "heading_outputs": "Salidas", "disclaimer": "Esta documentación fue generada por IA. Si encuentra algún error o tiene sugerencias de mejora, ¡no dude en contribuir!", - "prompt_template": "Eres un experto en traducción técnica especializado en documentación de nodos ComfyUI del inglés al español.\n\n## Reglas de Traducción\n\n1. **Contenido que NO debe traducirse:**\n - Nombres de parámetros entre comillas invertidas: `image`, `seed`, `model`\n - Tipos de datos en MAYÚSCULAS: IMAGE, STRING, INT, FLOAT, MODEL, CONDITIONING, etc.\n - Valores en columna Range: números, \"auto\", nombres de opciones\n - Código, rutas de archivos\n\n2. **Contenido que SÍ debe traducirse:**\n - Títulos de secciones: {heading_overview}, {heading_inputs}, {heading_outputs}\n - Todo el texto descriptivo y explicativo\n - Descripciones de parámetros\n\n3. **Calidad de traducción:**\n - Usar español estándar y neutral\n - Mantener tono profesional pero accesible\n - Asegurar precisión técnica\n - Usar terminología técnica estándar en español\n\n4. **Formato:**\n - Mantener todo el formato Markdown\n - Preservar estructura de tablas\n - No agregar ninguna nota o enlace al inicio del documento (será agregado automáticamente)\n\nPor favor traduce la siguiente documentación al español, sin incluir la aviso de IA:\n\n5. **CRÍTICO - Nombres de salida:** La primera columna de la tabla de Salidas contiene nombres de salida (ej. `positive`, `negative`, `latent`, `image`, `model`, `conditioning`) que DEBEN permanecer en inglés. Traducir estos nombres crea entradas duplicadas y rompe la estructura del documento.\n\n5. **子标题翻译(grouped inputs 格式):** 如果文档包含三级子标题(### 级别),请翻译其内容:\n - `### Common Inputs` → 通用输入标题(如简体中文「通用输入」,日文「共通入力」,西班牙语「Entradas comunes」等,用目标语言的对应说法)\n - `### <模型名> Inputs`(如 `### Seedance 2.5 Inputs`)→ 模型名保持英文原文,只翻译 \"Inputs\" 部分(如「Seedance 2.5 输入」)\n - `### Reference Inputs` → 参考输入标题(如「参考输入」/「参照入力」等)\n - 保持子标题的 Markdown 层级(###)和分组结构完全不变\n - 表格内的 Growable slot、Reference、Up to 等描述词也要翻译\n" + "prompt_template": "Eres un experto en traducción técnica especializado en documentación de nodos ComfyUI del inglés al español.\n\n## Reglas de Traducción\n\n1. **Contenido que NO debe traducirse:**\n - Nombres de parámetros entre comillas invertidas: `image`, `seed`, `model`\n - Tipos de datos en MAYÚSCULAS: IMAGE, STRING, INT, FLOAT, MODEL, CONDITIONING, etc.\n - Valores en columna Range: números, \"auto\", nombres de opciones\n - Código, rutas de archivos\n\n2. **Contenido que SÍ debe traducirse:**\n - Títulos de secciones: {heading_overview}, {heading_inputs}, {heading_outputs}\n - Todo el texto descriptivo y explicativo\n - Descripciones de parámetros\n\n3. **Calidad de traducción:**\n - Usar español estándar y neutral\n - Mantener tono profesional pero accesible\n - Asegurar precisión técnica\n - Usar terminología técnica estándar en español\n\n4. **Formato:**\n - Mantener todo el formato Markdown\n - Preservar estructura de tablas\n - No agregar ninguna nota o enlace al inicio del documento (será agregado automáticamente)\n\n5. **Subtítulos (formato de entradas agrupadas):** Si el documento contiene subtítulos de tercer nivel (###), tradúzcalos:\n - `### Common Inputs` → título de entradas comunes (p. ej. «Entradas comunes»)\n - `### Inputs` (p. ej. `### Seedance 2.5 Inputs`) → mantenga el nombre del modelo en inglés y traduzca solo \"Inputs\" (p. ej. «Entradas de Seedance 2.5»)\n - `### Reference Inputs` → título de entradas de referencia (p. ej. «Entradas de referencia»)\n - Mantenga el nivel Markdown (###) y la estructura de grupos sin cambios\n - Traduzca también los descriptores de la tabla como Growable slot, Reference, Up to, etc.\n\n6. **CRÍTICO - Nombres de salida:** La primera columna de la tabla de Salidas contiene nombres de salida (ej. `positive`, `negative`, `latent`, `image`, `model`, `conditioning`) que DEBEN permanecer en inglés. Traducir estos nombres crea entradas duplicadas y rompe la estructura del documento.\n\nPor favor, traduce la siguiente documentación al español, sin incluir el aviso de IA:\n" }, "fr": { "name": "Français", @@ -21,7 +21,7 @@ "heading_inputs": "Entrées", "heading_outputs": "Sorties", "disclaimer": "Cette documentation a été générée par IA. Si vous trouvez des erreurs ou avez des suggestions d'amélioration, n'hésitez pas à contribuer !", - "prompt_template": "Vous êtes un expert en traduction technique spécialisé dans la documentation des nœuds ComfyUI de l'anglais vers le français.\n\n## Règles de Traduction\n\n1. **Contenu à NE PAS traduire:**\n - Noms de paramètres entre backticks: `image`, `seed`, `model`\n - Types de données en MAJUSCULES: IMAGE, STRING, INT, FLOAT, MODEL, CONDITIONING, etc.\n - Valeurs dans la colonne Range: nombres, \"auto\", noms d'options\n - Code, chemins de fichiers\n\n2. **Contenu à traduire:**\n - Titres de sections: {heading_overview}, {heading_inputs}, {heading_outputs}\n - Tout le texte descriptif et explicatif\n - Descriptions des paramètres\n\n3. **Qualité de traduction:**\n - Utiliser le français standard\n - Maintenir un ton professionnel mais accessible\n - Assurer la précision technique\n - Utiliser la terminologie technique standard en français\n\n4. **Format:**\n - Conserver tout le formatage Markdown\n - Préserver la structure des tableaux\n - Ne pas ajouter de note ou lien au début du document (sera ajouté automatiquement)\n\nVeuillez traduire la documentation suivante en français, sans inclure la avertissement IA:\n\n5. **CRITIQUE - Noms de sortie:** La première colonne du tableau des Sorties contient des noms de sortie (ex: `positive`, `negative`, `latent`, `image`, `model`, `conditioning`) qui DOIVENT rester en anglais. Traduire ces noms crée des doublons et casse la structure du document.\n\n5. **子标题翻译(grouped inputs 格式):** 如果文档包含三级子标题(### 级别),请翻译其内容:\n - `### Common Inputs` → 通用输入标题(如简体中文「通用输入」,日文「共通入力」,西班牙语「Entradas comunes」等,用目标语言的对应说法)\n - `### <模型名> Inputs`(如 `### Seedance 2.5 Inputs`)→ 模型名保持英文原文,只翻译 \"Inputs\" 部分(如「Seedance 2.5 输入」)\n - `### Reference Inputs` → 参考输入标题(如「参考输入」/「参照入力」等)\n - 保持子标题的 Markdown 层级(###)和分组结构完全不变\n - 表格内的 Growable slot、Reference、Up to 等描述词也要翻译\n" + "prompt_template": "Vous êtes un expert en traduction technique spécialisé dans la documentation des nœuds ComfyUI de l'anglais vers le français.\n\n## Règles de Traduction\n\n1. **Contenu à NE PAS traduire:**\n - Noms de paramètres entre backticks: `image`, `seed`, `model`\n - Types de données en MAJUSCULES: IMAGE, STRING, INT, FLOAT, MODEL, CONDITIONING, etc.\n - Valeurs dans la colonne Range: nombres, \"auto\", noms d'options\n - Code, chemins de fichiers\n\n2. **Contenu à traduire:**\n - Titres de sections: {heading_overview}, {heading_inputs}, {heading_outputs}\n - Tout le texte descriptif et explicatif\n - Descriptions des paramètres\n\n3. **Qualité de traduction:**\n - Utiliser le français standard\n - Maintenir un ton professionnel mais accessible\n - Assurer la précision technique\n - Utiliser la terminologie technique standard en français\n\n4. **Format:**\n - Conserver tout le formatage Markdown\n - Préserver la structure des tableaux\n - Ne pas ajouter de note ou lien au début du document (sera ajouté automatiquement)\n\n5. **Sous-titres (format d'entrées groupées) :** Si le document contient des sous-titres de troisième niveau (###), traduisez-les :\n - `### Common Inputs` → titre des entrées communes (p. ex. « Entrées communes »)\n - `### Inputs` (p. ex. `### Seedance 2.5 Inputs`) → conservez le nom du modèle en anglais et traduisez uniquement « Inputs » (p. ex. « Entrées Seedance 2.5 »)\n - `### Reference Inputs` → titre des entrées de référence (p. ex. « Entrées de référence »)\n - Conservez le niveau Markdown (###) et la structure de groupement à l'identique\n - Traduisez également les descripteurs du tableau comme Growable slot, Reference, Up to, etc.\n\n6. **CRITIQUE - Noms de sortie :** La première colonne du tableau des Sorties contient des noms de sortie (ex : `positive`, `negative`, `latent`, `image`, `model`, `conditioning`) qui DOIVENT rester en anglais. Traduire ces noms crée des doublons et casse la structure du document.\n\nVeuillez traduire la documentation suivante en français, sans inclure l'avertissement IA :\n" }, "ja": { "name": "日本語", @@ -29,7 +29,7 @@ "heading_inputs": "入力", "heading_outputs": "出力", "disclaimer": "このドキュメントは AI によって生成されました。エラーを見つけた場合や改善のご提案がある場合は、ぜひ貢献してください!", - "prompt_template": "あなたは ComfyUI ノードドキュメントを英語から日本語に翻訳する技術翻訳の専門家です。\n\n## 翻訳ルール\n\n1. **翻訳してはいけない内容:**\n - バッククォートで囲まれたパラメータ名:`image`、`seed`、`model`\n - 大文字のデータ型:IMAGE、STRING、INT、FLOAT、MODEL、CONDITIONING など\n - Range列の値:数値、\"auto\"、オプション名\n - コード、ファイルパス\n\n2. **翻訳する内容:**\n - セクション見出し:{heading_overview}、{heading_inputs}、{heading_outputs}\n - すべての説明文\n - パラメータの説明\n\n3. **翻訳品質:**\n - 丁寧語(です・ます体)を使用\n - 専門的でありながらわかりやすい表現\n - 技術的な正確性を保つ\n - 標準的な日本語技術用語を使用\n\n4. **フォーマット:**\n - すべての Markdown フォーマットを保持\n - 表の構造を維持\n - 免責事項は追加しないでください(システムが文書末尾に自動追加します)\n\n以下の英語ドキュメントを日本語に翻訳してください(免責事項は含めないでください):\n\n5. **重要ルール - 出力名:** 出力テーブルの最初の列には出力名(例:`positive`、`negative`、`latent`、`image`、`model`、`conditioning`)が含まれています。これらは**英語のままにしてください**。出力名を翻訳すると重複エントリが発生し、文書構造が壊れます。\n\n5. **子标题翻译(grouped inputs 格式):** 如果文档包含三级子标题(### 级别),请翻译其内容:\n - `### Common Inputs` → 通用输入标题(如简体中文「通用输入」,日文「共通入力」,西班牙语「Entradas comunes」等,用目标语言的对应说法)\n - `### <模型名> Inputs`(如 `### Seedance 2.5 Inputs`)→ 模型名保持英文原文,只翻译 \"Inputs\" 部分(如「Seedance 2.5 输入」)\n - `### Reference Inputs` → 参考输入标题(如「参考输入」/「参照入力」等)\n - 保持子标题的 Markdown 层级(###)和分组结构完全不变\n - 表格内的 Growable slot、Reference、Up to 等描述词也要翻译\n" + "prompt_template": "あなたは ComfyUI ノードドキュメントを英語から日本語に翻訳する技術翻訳の専門家です。\n\n## 翻訳ルール\n\n1. **翻訳してはいけない内容:**\n - バッククォートで囲まれたパラメータ名:`image`、`seed`、`model`\n - 大文字のデータ型:IMAGE、STRING、INT、FLOAT、MODEL、CONDITIONING など\n - Range列の値:数値、\"auto\"、オプション名\n - コード、ファイルパス\n\n2. **翻訳する内容:**\n - セクション見出し:{heading_overview}、{heading_inputs}、{heading_outputs}\n - すべての説明文\n - パラメータの説明\n\n3. **翻訳品質:**\n - 丁寧語(です・ます体)を使用\n - 専門的でありながらわかりやすい表現\n - 技術的な正確性を保つ\n - 標準的な日本語技術用語を使用\n\n4. **フォーマット:**\n - すべての Markdown フォーマットを保持\n - 表の構造を維持\n - 免責事項は追加しないでください(システムが文書末尾に自動追加します)\n\n5. **サブ見出しの翻訳(grouped inputs 形式):** ドキュメントに第3レベルのサブ見出し(###)が含まれる場合は、その内容を翻訳してください:\n - `### Common Inputs` → 共通入力の見出し(例:「共通入力」)\n - `### <モデル名> Inputs`(例:`### Seedance 2.5 Inputs`)→ モデル名は英語のまま保持し、\"Inputs\" の部分だけを翻訳(例:「Seedance 2.5 入力」)\n - `### Reference Inputs` → 参照入力の見出し(例:「参照入力」)\n - サブ見出しの Markdown レベル(###)とグループ構造は完全に維持\n - 表内の Growable slot、Reference、Up to などの記述語も翻訳してください\n\n6. **重要ルール - 出力名:** 出力テーブルの最初の列には出力名(例:`positive`、`negative`、`latent`、`image`、`model`、`conditioning`)が含まれています。これらは**英語のままにしてください**。出力名を翻訳すると重複エントリが発生し、文書構造が壊れます。\n\n以下の英語ドキュメントを日本語に翻訳してください(免責事項は含めないでください):\n" }, "ko": { "name": "한국어", @@ -37,7 +37,7 @@ "heading_inputs": "입력", "heading_outputs": "출력", "disclaimer": "이 문서는 AI에 의해 생성되었습니다. 오류를 발견하거나 개선 제안이 있으시면 기여해 주세요!", - "prompt_template": "당신은 ComfyUI 노드 문서를 영어에서 한국어로 번역하는 기술 번역 전문가입니다.\n\n## 번역 규칙\n\n1. **번역하지 말아야 할 내용:**\n - 백틱으로 둘러싸인 매개변수 이름: `image`, `seed`, `model`\n - 대문자 데이터 타입: IMAGE, STRING, INT, FLOAT, MODEL, CONDITIONING 등\n - Range 열의 값: 숫자, \"auto\", 옵션 이름\n - 코드, 파일 경로\n\n2. **번역해야 할 내용:**\n - 섹션 제목: {heading_overview}, {heading_inputs}, {heading_outputs}\n - 모든 설명 텍스트\n - 매개변수 설명\n\n3. **번역 품질:**\n - 정중한 격식체(합니다체) 사용\n - 전문적이면서도 이해하기 쉬운 표현\n - 기술적 정확성 유지\n - 표준 한국어 기술 용어 사용\n\n4. **형식:**\n - 모든 Markdown 형식 유지\n - 표 구조 보존\n - 문서 시작 부분에 메모나 링크를 추가하지 마세요 (자동으로 추가됩니다)\n\n다음 영어 문서를 한국어로 번역해주세요 (문서 시작 부분의 메모는 포함하지 마세요):\n\n5. **중요 규칙 - 출력 이름:** 출력 테이블의 첫 번째 열에는 출력 이름(예: `positive`, `negative`, `latent`, `image`, `model`, `conditioning`)이 포함되어 있으며, **영어로 유지해야 합니다**. 출력 이름을 번역하면 중복 항목이 생성되고 문서 구조가 손상됩니다.\n\n5. **子标题翻译(grouped inputs 格式):** 如果文档包含三级子标题(### 级别),请翻译其内容:\n - `### Common Inputs` → 通用输入标题(如简体中文「通用输入」,日文「共通入力」,西班牙语「Entradas comunes」等,用目标语言的对应说法)\n - `### <模型名> Inputs`(如 `### Seedance 2.5 Inputs`)→ 模型名保持英文原文,只翻译 \"Inputs\" 部分(如「Seedance 2.5 输入」)\n - `### Reference Inputs` → 参考输入标题(如「参考输入」/「参照入力」等)\n - 保持子标题的 Markdown 层级(###)和分组结构完全不变\n - 表格内的 Growable slot、Reference、Up to 等描述词也要翻译\n" + "prompt_template": "당신은 ComfyUI 노드 문서를 영어에서 한국어로 번역하는 기술 번역 전문가입니다.\n\n## 번역 규칙\n\n1. **번역하지 말아야 할 내용:**\n - 백틱으로 둘러싸인 매개변수 이름: `image`, `seed`, `model`\n - 대문자 데이터 타입: IMAGE, STRING, INT, FLOAT, MODEL, CONDITIONING 등\n - Range 열의 값: 숫자, \"auto\", 옵션 이름\n - 코드, 파일 경로\n\n2. **번역해야 할 내용:**\n - 섹션 제목: {heading_overview}, {heading_inputs}, {heading_outputs}\n - 모든 설명 텍스트\n - 매개변수 설명\n\n3. **번역 품질:**\n - 정중한 격식체(합니다체) 사용\n - 전문적이면서도 이해하기 쉬운 표현\n - 기술적 정확성 유지\n - 표준 한국어 기술 용어 사용\n\n4. **형식:**\n - 모든 Markdown 형식 유지\n - 표 구조 보존\n - 문서 시작 부분에 메모나 링크를 추가하지 마세요 (자동으로 추가됩니다)\n\n5. **소제목 번역(grouped inputs 형식):** 문서에 3단계 소제목(###)이 포함된 경우 해당 내용을 번역하세요:\n - `### Common Inputs` → 공통 입력 제목(예: 「공통 입력」)\n - `### <모델명> Inputs`(예: `### Seedance 2.5 Inputs`) → 모델명은 영어 원문을 유지하고 \"Inputs\" 부분만 번역(예: 「Seedance 2.5 입력」)\n - `### Reference Inputs` → 참조 입력 제목(예: 「참조 입력」)\n - 소제목의 Markdown 수준(###)과 그룹 구조는 그대로 유지\n - 표 안의 Growable slot, Reference, Up to 등의 설명어도 번역하세요\n\n6. **중요 규칙 - 출력 이름:** 출력 테이블의 첫 번째 열에는 출력 이름(예: `positive`, `negative`, `latent`, `image`, `model`, `conditioning`)이 포함되어 있으며, **영어로 유지해야 합니다**. 출력 이름을 번역하면 중복 항목이 생성되고 문서 구조가 손상됩니다.\n\n다음 영어 문서를 한국어로 번역해주세요 (문서 시작 부분의 메모는 포함하지 마세요):\n" }, "ru": { "name": "Русский", @@ -45,7 +45,7 @@ "heading_inputs": "Входы", "heading_outputs": "Выходы", "disclaimer": "Эта документация была создана с помощью ИИ. Если вы обнаружите ошибки или у вас есть предложения по улучшению, пожалуйста, внесите свой вклад!", - "prompt_template": "Вы эксперт по техническому переводу документации узлов ComfyUI с английского на русский язык.\n\n## Правила Перевода\n\n1. **Что НЕ переводить:**\n - Имена параметров в обратных кавычках: `image`, `seed`, `model`\n - Типы данных ЗАГЛАВНЫМИ буквами: IMAGE, STRING, INT, FLOAT, MODEL, CONDITIONING и т.д.\n - Значения в колонке Range: числа, \"auto\", названия опций\n - Код, пути к файлам\n\n2. **Что переводить:**\n - Заголовки разделов: {heading_overview}, {heading_inputs}, {heading_outputs}\n - Весь описательный текст\n - Описания параметров\n\n3. **Качество перевода:**\n - Использовать стандартный русский язык\n - Поддерживать профессиональный, но доступный тон\n - Обеспечить техническую точность\n - Использовать стандартную техническую терминологию на русском\n\n4. **Формат:**\n - Сохранить всё форматирование Markdown\n - Сохранить структуру таблиц\n - Не добавляйте отказ от ответственности (он будет добавлен автоматически в конце документа)\n\nПожалуйста, переведите следующую документацию на русский язык (не включайте отказ от ответственности):\n\n5. **ВАЖНО - Имена выходов:** Первый столбец таблицы выходов содержит имена выходов (например, `positive`, `negative`, `latent`, `image`, `model`, `conditioning`), которые ДОЛЖНЫ оставаться на английском языке. Перевод этих имен создает дубликаты и нарушает структуру документа.\n\n5. **子标题翻译(grouped inputs 格式):** 如果文档包含三级子标题(### 级别),请翻译其内容:\n - `### Common Inputs` → 通用输入标题(如简体中文「通用输入」,日文「共通入力」,西班牙语「Entradas comunes」等,用目标语言的对应说法)\n - `### <模型名> Inputs`(如 `### Seedance 2.5 Inputs`)→ 模型名保持英文原文,只翻译 \"Inputs\" 部分(如「Seedance 2.5 输入」)\n - `### Reference Inputs` → 参考输入标题(如「参考输入」/「参照入力」等)\n - 保持子标题的 Markdown 层级(###)和分组结构完全不变\n - 表格内的 Growable slot、Reference、Up to 等描述词也要翻译\n" + "prompt_template": "Вы эксперт по техническому переводу документации узлов ComfyUI с английского на русский язык.\n\n## Правила Перевода\n\n1. **Что НЕ переводить:**\n - Имена параметров в обратных кавычках: `image`, `seed`, `model`\n - Типы данных ЗАГЛАВНЫМИ буквами: IMAGE, STRING, INT, FLOAT, MODEL, CONDITIONING и т.д.\n - Значения в колонке Range: числа, \"auto\", названия опций\n - Код, пути к файлам\n\n2. **Что переводить:**\n - Заголовки разделов: {heading_overview}, {heading_inputs}, {heading_outputs}\n - Весь описательный текст\n - Описания параметров\n\n3. **Качество перевода:**\n - Использовать стандартный русский язык\n - Поддерживать профессиональный, но доступный тон\n - Обеспечить техническую точность\n - Использовать стандартную техническую терминологию на русском\n\n4. **Формат:**\n - Сохранить всё форматирование Markdown\n - Сохранить структуру таблиц\n - Не добавляйте отказ от ответственности (он будет добавлен автоматически в конце документа)\n\n5. **Подзаголовки (формат сгруппированных входов):** Если документ содержит подзаголовки третьего уровня (###), переведите их содержимое:\n - `### Common Inputs` → заголовок общих входов (напр. «Общие входы»)\n - `### <ИмяМодели> Inputs` (напр. `### Seedance 2.5 Inputs`) → имя модели оставьте на английском, переведите только «Inputs» (напр. «Входы Seedance 2.5»)\n - `### Reference Inputs` → заголовок эталонных входов (напр. «Эталонные входы»)\n - Сохраняйте уровень Markdown (###) и структуру групп без изменений\n - Переводите также описания в таблице: Growable slot, Reference, Up to и т. п.\n\n6. **ВАЖНО - Имена выходов:** Первый столбец таблицы выходов содержит имена выходов (например, `positive`, `negative`, `latent`, `image`, `model`, `conditioning`), которые ДОЛЖНЫ оставаться на английском языке. Перевод этих имен создает дубликаты и нарушает структуру документа.\n\nПожалуйста, переведите следующую документацию на русский язык (не включайте отказ от ответственности):\n" }, "zh-TW": { "name": "繁體中文", @@ -53,7 +53,7 @@ "heading_inputs": "輸入", "heading_outputs": "輸出", "disclaimer": "本文檔由 AI 生成。如果您發現任何錯誤或有改進建議,歡迎貢獻!", - "prompt_template": "你是一位專業的技術文檔翻譯專家,專門負責將 ComfyUI 節點文檔從英文翻譯成繁體中文。\n\n## 翻譯規則\n\n1. **必須保持不變的內容:**\n - 參數名必須保持英文,用反引號包裹:`image`、`seed`、`model`\n - 資料類型必須保持英文大寫:IMAGE、STRING、INT、FLOAT、MODEL、CONDITIONING 等\n - Range 列中的值保持不變:數字、\"auto\"、選項名稱\n - 不要翻譯任何程式碼、檔案路徑\n\n2. **需要翻譯的內容:**\n - 章節標題翻譯為:{heading_overview}、{heading_inputs}、{heading_outputs}\n - 所有描述性文字和說明\n - 參數描述\n\n3. **翻譯質量要求:**\n - 使用自然流暢的繁體中文\n - 保持專業但易懂的語氣\n - 確保技術準確性\n - 使用繁體中文技術文檔的標準術語\n\n4. **格式要求:**\n - 保持所有 Markdown 格式不變\n - 保持表格結構完整\n - 不要添加免責聲明(系統將自動添加到文檔底部)\n\n## 翻譯示例\n\n**英文:**\nThis node combines two CLIP models by adding the second model to the first.\n\n**繁體中文:**\n此節點透過將第二個 CLIP 模型添加到第一個模型來組合兩個 CLIP 模型。\n\n請將以下英文文檔翻譯成繁體中文,不要包含免責聲明:\n\n5. **關鍵規則 - 輸出名稱:** 輸出表(Outputs)的第一列是輸出名(如 `positive`、`negative`、`latent`、`image`、`model`、`conditioning`),**必須保持英文不變**。翻譯輸出名會導致輸出表中出現重複名稱,破壞文檔結構。\n\n5. **子标题翻译(grouped inputs 格式):** 如果文档包含三级子标题(### 级别),请翻译其内容:\n - `### Common Inputs` → 通用输入标题(如简体中文「通用输入」,日文「共通入力」,西班牙语「Entradas comunes」等,用目标语言的对应说法)\n - `### <模型名> Inputs`(如 `### Seedance 2.5 Inputs`)→ 模型名保持英文原文,只翻译 \"Inputs\" 部分(如「Seedance 2.5 输入」)\n - `### Reference Inputs` → 参考输入标题(如「参考输入」/「参照入力」等)\n - 保持子标题的 Markdown 层级(###)和分组结构完全不变\n - 表格内的 Growable slot、Reference、Up to 等描述词也要翻译\n" + "prompt_template": "你是一位專業的技術文檔翻譯專家,專門負責將 ComfyUI 節點文檔從英文翻譯成繁體中文。\n\n## 翻譯規則\n\n1. **必須保持不變的內容:**\n - 參數名必須保持英文,用反引號包裹:`image`、`seed`、`model`\n - 資料類型必須保持英文大寫:IMAGE、STRING、INT、FLOAT、MODEL、CONDITIONING 等\n - Range 列中的值保持不變:數字、\"auto\"、選項名稱\n - 不要翻譯任何程式碼、檔案路徑\n\n2. **需要翻譯的內容:**\n - 章節標題翻譯為:{heading_overview}、{heading_inputs}、{heading_outputs}\n - 所有描述性文字和說明\n - 參數描述\n\n3. **翻譯質量要求:**\n - 使用自然流暢的繁體中文\n - 保持專業但易懂的語氣\n - 確保技術準確性\n - 使用繁體中文技術文檔的標準術語\n\n4. **格式要求:**\n - 保持所有 Markdown 格式不變\n - 保持表格結構完整\n - 不要添加免責聲明(系統將自動添加到文檔底部)\n\n5. **子標題翻譯(grouped inputs 格式):** 如果文檔包含三級子標題(### 級別),請翻譯其內容:\n - `### Common Inputs` → 通用輸入標題(如「通用輸入」)\n - `### <模型名> Inputs`(如 `### Seedance 2.5 Inputs`)→ 模型名保持英文原文,只翻譯 \"Inputs\" 部分(如「Seedance 2.5 輸入」)\n - `### Reference Inputs` → 參考輸入標題(如「參考輸入」)\n - 保持子標題的 Markdown 層級(###)和分組結構完全不變\n - 表格內的 Growable slot、Reference、Up to 等描述詞也要翻譯\n\n6. **關鍵規則 - 輸出名稱:** 輸出表(Outputs)的第一列是輸出名(如 `positive`、`negative`、`latent`、`image`、`model`、`conditioning`),**必須保持英文不變**。翻譯輸出名會導致輸出表中出現重複名稱,破壞文檔結構。\n\n## 翻譯示例\n\n**英文:**\nThis node combines two CLIP models by adding the second model to the first.\n\n**繁體中文:**\n此節點透過將第二個 CLIP 模型添加到第一個模型來組合兩個 CLIP 模型。\n\n請將以下英文文檔翻譯成繁體中文,不要包含免責聲明:\n" }, "ar": { "name": "العربية", @@ -61,7 +61,7 @@ "heading_inputs": "المدخلات", "heading_outputs": "المخرجات", "disclaimer": "تم إنشاء هذه الوثيقة بواسطة الذكاء الاصطناعي. إذا وجدت أي أخطاء أو لديك اقتراحات للتحسين، فلا تتردد في المساهمة!", - "prompt_template": "أنت خبير في الترجمة التقنية متخصص في توثيق عُقد ComfyUI من الإنجليزية إلى العربية.\n\n## قواعد الترجمة\n\n1. **المحتوى الذي يجب عدم ترجمته:**\n - أسماء المعاملات بين علامات الاقتباس الخلفية: `image`, `seed`, `model`\n - أنواع البيانات بالأحرف الكبيرة: IMAGE, STRING, INT, FLOAT, MODEL, CONDITIONING, إلخ\n - القيم في عمود Range: الأرقام، \"auto\"، أسماء الخيارات\n - الكود، مسارات الملفات\n\n2. **المحتوى الذي يجب ترجمته:**\n - عناوين الأقسام: {heading_overview}, {heading_inputs}, {heading_outputs}\n - جميع النصوص الوصفية والتوضيحية\n - أوصاف المعاملات\n\n3. **جودة الترجمة:**\n - استخدام اللغة العربية الفصحى المعاصرة\n - الحفاظ على نبرة احترافية ولكن سهلة الفهم\n - ضمان الدقة التقنية\n - استخدام المصطلحات التقنية العربية القياسية\n\n4. **التنسيق:**\n - الحفاظ على جميع تنسيقات Markdown\n - الحفاظ على بنية الجداول\n - عدم إضافة أي ملاحظة أو رابط في بداية الوثيقة (سيتم إضافتها تلقائيًا)\n\nالرجاء ترجمة الوثيقة التالية إلى العربية، دون تضمين الملاحظة الأولية للوثيقة:\n\n5. **قاعدة حاسمة - أسماء المخرجات:** العمود الأول من جدول المخرجات يحتوي على أسماء مخرجات (مثل `positive`، `negative`، `latent`، `image`، `model`، `conditioning`) التي يجب أن تبقى بالإنجليزية. ترجمة هذه الأسماء يؤدي إلى تكرار الإدخالات وكسر بنية المستند.\n\n5. **子标题翻译(grouped inputs 格式):** 如果文档包含三级子标题(### 级别),请翻译其内容:\n - `### Common Inputs` → 通用输入标题(如简体中文「通用输入」,日文「共通入力」,西班牙语「Entradas comunes」等,用目标语言的对应说法)\n - `### <模型名> Inputs`(如 `### Seedance 2.5 Inputs`)→ 模型名保持英文原文,只翻译 \"Inputs\" 部分(如「Seedance 2.5 输入」)\n - `### Reference Inputs` → 参考输入标题(如「参考输入」/「参照入力」等)\n - 保持子标题的 Markdown 层级(###)和分组结构完全不变\n - 表格内的 Growable slot、Reference、Up to 等描述词也要翻译\n" + "prompt_template": "أنت خبير في الترجمة التقنية متخصص في توثيق عُقد ComfyUI من الإنجليزية إلى العربية.\n\n## قواعد الترجمة\n\n1. **المحتوى الذي يجب عدم ترجمته:**\n - أسماء المعاملات بين علامات الاقتباس الخلفية: `image`, `seed`, `model`\n - أنواع البيانات بالأحرف الكبيرة: IMAGE, STRING, INT, FLOAT, MODEL, CONDITIONING, إلخ\n - القيم في عمود Range: الأرقام، \"auto\"، أسماء الخيارات\n - الكود، مسارات الملفات\n\n2. **المحتوى الذي يجب ترجمته:**\n - عناوين الأقسام: {heading_overview}, {heading_inputs}, {heading_outputs}\n - جميع النصوص الوصفية والتوضيحية\n - أوصاف المعاملات\n\n3. **جودة الترجمة:**\n - استخدام اللغة العربية الفصحى المعاصرة\n - الحفاظ على نبرة احترافية ولكن سهلة الفهم\n - ضمان الدقة التقنية\n - استخدام المصطلحات التقنية العربية القياسية\n\n4. **التنسيق:**\n - الحفاظ على جميع تنسيقات Markdown\n - الحفاظ على بنية الجداول\n - عدم إضافة أي ملاحظة أو رابط في بداية الوثيقة (سيتم إضافتها تلقائيًا)\n\n5. **العناوين الفرعية (تنسيق المدخلات المجمّعة):** إذا احتوت الوثيقة على عناوين فرعية من المستوى الثالث (###)، فترجم محتواها:\n - `### Common Inputs` → عنوان المدخلات العامة (مثل «المدخلات العامة»)\n - `### <اسم النموذج> Inputs` (مثل `### Seedance 2.5 Inputs`) → أبقِ اسم النموذج بالإنجليزية وترجم كلمة \"Inputs\" فقط (مثل «مدخلات Seedance 2.5»)\n - `### Reference Inputs` → عنوان المدخلات المرجعية (مثل «المدخلات المرجعية»)\n - حافظ على مستوى Markdown (###) وبنية التجميع دون تغيير\n - ترجم أيضًا الأوصاف داخل الجدول مثل Growable slot وReference وUp to\n\n6. **قاعدة حاسمة - أسماء المخرجات:** العمود الأول من جدول المخرجات يحتوي على أسماء مخرجات (مثل `positive`، `negative`، `latent`، `image`، `model`، `conditioning`) التي يجب أن تبقى بالإنجليزية. ترجمة هذه الأسماء يؤدي إلى تكرار الإدخالات وكسر بنية المستند.\n\nالرجاء ترجمة الوثيقة التالية إلى العربية، دون تضمين الملاحظة الأولية للوثيقة:\n" }, "tr": { "name": "Türkçe", @@ -69,7 +69,7 @@ "heading_inputs": "Girdiler", "heading_outputs": "Çıktılar", "disclaimer": "Bu belge yapay zeka tarafından oluşturulmuştur. Herhangi bir hata bulursanız veya iyileştirme önerileriniz varsa, katkıda bulunmaktan çekinmeyin!", - "prompt_template": "ComfyUI düğüm belgelerini İngilizceden Türkçeye çevirmede uzmanlaşmış teknik çeviri uzmanısınız.\n\n## Çeviri Kuralları\n\n1. **Çevrilmemesi gereken içerik:**\n - Ters tırnak içindeki parametre adları: `image`, `seed`, `model`\n - BÜYÜK harflerle veri türleri: IMAGE, STRING, INT, FLOAT, MODEL, CONDITIONING, vb.\n - Range sütunundaki değerler: sayılar, \"auto\", seçenek adları\n - Kod, dosya yolları\n\n2. **Çevrilmesi gereken içerik:**\n - Bölüm başlıkları: {heading_overview}, {heading_inputs}, {heading_outputs}\n - Tüm açıklayıcı metinler\n - Parametre açıklamaları\n\n3. **Çeviri kalitesi:**\n - Standart Türkçe kullanın\n - Profesyonel ama anlaşılır bir üslup koruyun\n - Teknik doğruluğu sağlayın\n - Standart Türkçe teknik terminolojiyi kullanın\n\n4. **Format:**\n - Tüm Markdown biçimlendirmesini koruyun\n - Tablo yapısını koruyun\n - Belgenin başına herhangi bir not veya bağlantı eklemeyin (otomatik olarak eklenecektir)\n\nLütfen aşağıdaki belgeyi Türkçeye çevirin (belgenin başlangıç notunu dahil etmeyin):\n\n5. **KRİTİK KURAL - Çıktı Adları:** Çıktılar tablosunun ilk sütunu, İngilizce kalması ZORUNLU olan çıktı adlarını içerir (örn. `positive`, `negative`, `latent`, `image`, `model`, `conditioning`). Çıktı adlarını çevirmek yinelenen girdilere ve belge yapısının bozulmasına neden olur.\n\n5. **子标题翻译(grouped inputs 格式):** 如果文档包含三级子标题(### 级别),请翻译其内容:\n - `### Common Inputs` → 通用输入标题(如简体中文「通用输入」,日文「共通入力」,西班牙语「Entradas comunes」等,用目标语言的对应说法)\n - `### <模型名> Inputs`(如 `### Seedance 2.5 Inputs`)→ 模型名保持英文原文,只翻译 \"Inputs\" 部分(如「Seedance 2.5 输入」)\n - `### Reference Inputs` → 参考输入标题(如「参考输入」/「参照入力」等)\n - 保持子标题的 Markdown 层级(###)和分组结构完全不变\n - 表格内的 Growable slot、Reference、Up to 等描述词也要翻译\n" + "prompt_template": "ComfyUI düğüm belgelerini İngilizceden Türkçeye çevirmede uzmanlaşmış teknik çeviri uzmanısınız.\n\n## Çeviri Kuralları\n\n1. **Çevrilmemesi gereken içerik:**\n - Ters tırnak içindeki parametre adları: `image`, `seed`, `model`\n - BÜYÜK harflerle veri türleri: IMAGE, STRING, INT, FLOAT, MODEL, CONDITIONING, vb.\n - Range sütunundaki değerler: sayılar, \"auto\", seçenek adları\n - Kod, dosya yolları\n\n2. **Çevrilmesi gereken içerik:**\n - Bölüm başlıkları: {heading_overview}, {heading_inputs}, {heading_outputs}\n - Tüm açıklayıcı metinler\n - Parametre açıklamaları\n\n3. **Çeviri kalitesi:**\n - Standart Türkçe kullanın\n - Profesyonel ama anlaşılır bir üslup koruyun\n - Teknik doğruluğu sağlayın\n - Standart Türkçe teknik terminolojiyi kullanın\n\n4. **Format:**\n - Tüm Markdown biçimlendirmesini koruyun\n - Tablo yapısını koruyun\n - Belgenin başına herhangi bir not veya bağlantı eklemeyin (otomatik olarak eklenecektir)\n\n5. **Alt başlıkların çevirisi (gruplanmış girdiler formatı):** Belge üçüncü seviye alt başlıklar (###) içeriyorsa içeriklerini çevirin:\n - `### Common Inputs` → ortak girdiler başlığı (örn. \"Ortak Girdiler\")\n - `### Inputs` (örn. `### Seedance 2.5 Inputs`) → model adını İngilizce bırakın, yalnızca \"Inputs\" kısmını çevirin (örn. \"Seedance 2.5 Girdileri\")\n - `### Reference Inputs` → referans girdileri başlığı (örn. \"Referans Girdileri\")\n - Alt başlıkların Markdown seviyesini (###) ve grup yapısını aynen koruyun\n - Tablodaki Growable slot, Reference, Up to gibi tanımlayıcıları da çevirin\n\n6. **KRİTİK KURAL - Çıktı Adları:** Çıktılar tablosunun ilk sütunu, İngilizce kalması ZORUNLU olan çıktı adlarını içerir (örn. `positive`, `negative`, `latent`, `image`, `model`, `conditioning`). Çıktı adlarını çevirmek yinelenen girdilere ve belge yapısının bozulmasına neden olur.\n\nLütfen aşağıdaki belgeyi Türkçeye çevirin (belgenin başlangıç notunu dahil etmeyin):\n" }, "pt-BR": { "name": "Português (BR)", @@ -77,7 +77,7 @@ "heading_inputs": "Entradas", "heading_outputs": "Saídas", "disclaimer": "Esta documentação foi gerada por IA. Se você encontrar erros ou tiver sugestões de melhoria, sinta-se à vontade para contribuir!", - "prompt_template": "Você é um especialista em tradução técnica especializado em documentação de nós ComfyUI do inglês para português brasileiro.\n\n## Regras de Tradução\n\n1. **Conteúdo que NÃO deve ser traduzido:**\n - Nomes de parâmetros entre crases: `image`, `seed`, `model`\n - Tipos de dados em MAIÚSCULAS: IMAGE, STRING, INT, FLOAT, MODEL, CONDITIONING, etc.\n - Valores na coluna Range: números, \"auto\", nomes de opções\n - Código, caminhos de arquivos\n\n2. **Conteúdo que DEVE ser traduzido:**\n - Títulos de seções: {heading_overview}, {heading_inputs}, {heading_outputs}\n - Todo o texto descritivo e explicativo\n - Descrições de parâmetros\n\n3. **Qualidade da tradução:**\n - Use português brasileiro padrão\n - Mantenha um tom profissional mas acessível\n - Garanta precisão técnica\n - Use terminologia técnica padrão em português brasileiro\n\n4. **Formato:**\n - Mantenha toda a formatação Markdown\n - Preserve a estrutura das tabelas\n - Não adicione nenhuma nota ou link no início do documento (será adicionado automaticamente)\n\nPor favor, traduza a seguinte documentação para português brasileiro, sem incluir a nota inicial do documento:\n\n5. **CRÍTICO - Nomes de saída:** A primeira coluna da tabela de Saídas contém nomes de saída (ex: `positive`, `negative`, `latent`, `image`, `model`, `conditioning`) que DEVEM permanecer em inglês. Traduzir esses nomes cria entradas duplicadas e quebra a estrutura do documento.\n\n5. **子标题翻译(grouped inputs 格式):** 如果文档包含三级子标题(### 级别),请翻译其内容:\n - `### Common Inputs` → 通用输入标题(如简体中文「通用输入」,日文「共通入力」,西班牙语「Entradas comunes」等,用目标语言的对应说法)\n - `### <模型名> Inputs`(如 `### Seedance 2.5 Inputs`)→ 模型名保持英文原文,只翻译 \"Inputs\" 部分(如「Seedance 2.5 输入」)\n - `### Reference Inputs` → 参考输入标题(如「参考输入」/「参照入力」等)\n - 保持子标题的 Markdown 层级(###)和分组结构完全不变\n - 表格内的 Growable slot、Reference、Up to 等描述词也要翻译\n" + "prompt_template": "Você é um especialista em tradução técnica especializado em documentação de nós ComfyUI do inglês para português brasileiro.\n\n## Regras de Tradução\n\n1. **Conteúdo que NÃO deve ser traduzido:**\n - Nomes de parâmetros entre crases: `image`, `seed`, `model`\n - Tipos de dados em MAIÚSCULAS: IMAGE, STRING, INT, FLOAT, MODEL, CONDITIONING, etc.\n - Valores na coluna Range: números, \"auto\", nomes de opções\n - Código, caminhos de arquivos\n\n2. **Conteúdo que DEVE ser traduzido:**\n - Títulos de seções: {heading_overview}, {heading_inputs}, {heading_outputs}\n - Todo o texto descritivo e explicativo\n - Descrições de parâmetros\n\n3. **Qualidade da tradução:**\n - Use português brasileiro padrão\n - Mantenha um tom profissional mas acessível\n - Garanta precisão técnica\n - Use terminologia técnica padrão em português brasileiro\n\n4. **Formato:**\n - Mantenha toda a formatação Markdown\n - Preserve a estrutura das tabelas\n - Não adicione nenhuma nota ou link no início do documento (será adicionado automaticamente)\n\n5. **Subtítulos (formato de entradas agrupadas):** Se o documento contiver subtítulos de terceiro nível (###), traduza o conteúdo deles:\n - `### Common Inputs` → título de entradas comuns (ex.: «Entradas comuns»)\n - `### Inputs` (ex.: `### Seedance 2.5 Inputs`) → mantenha o nome do modelo em inglês e traduza apenas «Inputs» (ex.: «Entradas do Seedance 2.5»)\n - `### Reference Inputs` → título de entradas de referência (ex.: «Entradas de referência»)\n - Mantenha o nível Markdown (###) e a estrutura de agrupamento inalterados\n - Traduza também os descritores da tabela como Growable slot, Reference, Up to etc.\n\n6. **CRÍTICO - Nomes de saída:** A primeira coluna da tabela de Saídas contém nomes de saída (ex: `positive`, `negative`, `latent`, `image`, `model`, `conditioning`) que DEVEM permanecer em inglês. Traduzir esses nomes cria entradas duplicadas e quebra a estrutura do documento.\n\nPor favor, traduza a seguinte documentação para português brasileiro, sem incluir a nota inicial do documento:\n" }, "fa": { "name": "فارسی", @@ -85,6 +85,6 @@ "heading_inputs": "ورودی‌ها", "heading_outputs": "خروجی‌ها", "disclaimer": "این مستند با هوش مصنوعی تهیه شده است. اگر خطایی دیدید یا پیشنهادی برای بهبود دارید، خوشحال می‌شویم مشارکت کنید!", - "prompt_template": "شما یک مترجم فنی متخصص هستید که اسناد گره‌های ComfyUI را از انگلیسی به فارسی برمی‌گردانید.\n\n## قوانین ترجمه\n\n1. **محتوایی که نباید ترجمه شود:**\n - نام پارامترها در بک‌تیک: `image`, `seed`, `model`\n - نوع‌های داده با حروف بزرگ انگلیسی: IMAGE, STRING, INT, FLOAT, MODEL, CONDITIONING و غیره\n - مقادیر ستون Range: اعداد، \"auto\", نام گزینه‌ها\n - کد، مسیر فایل\n\n2. **محتوایی که باید ترجمه شود:**\n - عناوین بخش‌ها: {heading_overview}, {heading_inputs}, {heading_outputs}\n - تمام متن توضیحی\n - توضیحات پارامترها\n\n3. **کیفیت ترجمه:**\n - فارسی رسمی معاصر و روان بنویسید\n - لحن حرفه‌ای ولی قابل‌فهم\n - دقت فنی را حفظ کنید\n\n4. **قالب:**\n - قالب Markdown و ساختار جدول دست‌نخورده بماند\n - توضیح یا لینک در ابتدای سند اضافه نکنید (به‌صورت خودکار افزوده می‌شود)\n\nلطفاً سند انگلیسی زیر را به فارسی برگردانید؛ یادداشت ابتدای سند را در خروجی نگنجانید:\n\n5. **قانون حیاتی - نام‌های خروجی:** ستون اول جدول خروجی‌ها شامل نام‌های خروجی است (مانند `positive`، `negative`، `latent`، `image`، `model`، `conditioning`) که باید به انگلیسی باقی بمانند. ترجمه این نام‌ها باعث ایجاد ورودی‌های تکراری و شکستن ساختار سند می‌شود.\n\n5. **子标题翻译(grouped inputs 格式):** 如果文档包含三级子标题(### 级别),请翻译其内容:\n - `### Common Inputs` → 通用输入标题(如简体中文「通用输入」,日文「共通入力」,西班牙语「Entradas comunes」等,用目标语言的对应说法)\n - `### <模型名> Inputs`(如 `### Seedance 2.5 Inputs`)→ 模型名保持英文原文,只翻译 \"Inputs\" 部分(如「Seedance 2.5 输入」)\n - `### Reference Inputs` → 参考输入标题(如「参考输入」/「参照入力」等)\n - 保持子标题的 Markdown 层级(###)和分组结构完全不变\n - 表格内的 Growable slot、Reference、Up to 等描述词也要翻译\n" + "prompt_template": "شما یک مترجم فنی متخصص هستید که اسناد گره‌های ComfyUI را از انگلیسی به فارسی برمی‌گردانید.\n\n## قوانین ترجمه\n\n1. **محتوایی که نباید ترجمه شود:**\n - نام پارامترها در بک‌تیک: `image`, `seed`, `model`\n - نوع‌های داده با حروف بزرگ انگلیسی: IMAGE, STRING, INT, FLOAT, MODEL, CONDITIONING و غیره\n - مقادیر ستون Range: اعداد، \"auto\", نام گزینه‌ها\n - کد، مسیر فایل\n\n2. **محتوایی که باید ترجمه شود:**\n - عناوین بخش‌ها: {heading_overview}, {heading_inputs}, {heading_outputs}\n - تمام متن توضیحی\n - توضیحات پارامترها\n\n3. **کیفیت ترجمه:**\n - فارسی رسمی معاصر و روان بنویسید\n - لحن حرفه‌ای ولی قابل‌فهم\n - دقت فنی را حفظ کنید\n\n4. **قالب:**\n - قالب Markdown و ساختار جدول دست‌نخورده بماند\n - توضیح یا لینک در ابتدای سند اضافه نکنید (به‌صورت خودکار افزوده می‌شود)\n\n5. **ترجمه زیرعنوان‌ها (قالب ورودی‌های گروه‌بندی‌شده):** اگر سند شامل زیرعنوان‌های سطح سوم (###) است، محتوای آن‌ها را ترجمه کنید:\n - `### Common Inputs` → عنوان ورودی‌های مشترک (مثل «ورودی‌های مشترک»)\n - `### <نام مدل> Inputs` (مثل `### Seedance 2.5 Inputs`) → نام مدل را به انگلیسی نگه دارید و فقط بخش \"Inputs\" را ترجمه کنید (مثل «ورودی‌های Seedance 2.5»)\n - `### Reference Inputs` → عنوان ورودی‌های مرجع (مثل «ورودی‌های مرجع»)\n - سطح Markdown (###) و ساختار گروه‌بندی را کاملاً حفظ کنید\n - واژه‌های توصیفی داخل جدول مثل Growable slot و Reference و Up to را نیز ترجمه کنید\n\n6. **قانون حیاتی - نام‌های خروجی:** ستون اول جدول خروجی‌ها شامل نام‌های خروجی است (مانند `positive`، `negative`، `latent`، `image`، `model`، `conditioning`) که باید به انگلیسی باقی بمانند. ترجمه این نام‌ها باعث ایجاد ورودی‌های تکراری و شکستن ساختار سند می‌شود.\n\nلطفاً سند انگلیسی زیر را به فارسی برگردانید؛ یادداشت ابتدای سند را در خروجی نگنجانید:\n" } -} \ No newline at end of file +} diff --git a/docs-generation/config/translation_rules.txt b/docs-generation/config/translation_rules.txt deleted file mode 100644 index 41e8f17a1..000000000 --- a/docs-generation/config/translation_rules.txt +++ /dev/null @@ -1,122 +0,0 @@ -# ComfyUI Node Documentation Translation Rules - -## Translation Principles - -1. **Accuracy First** - - Maintain technical accuracy - - Preserve all parameter names in backticks (`) - - Keep data types in ENGLISH (IMAGE, STRING, INT, FLOAT, etc.) - - Do not translate code, node names, or technical identifiers - -2. **Natural Language** - - Use natural, fluent expressions in target language - - Adapt metaphors and examples to local culture when appropriate - - Maintain professional but accessible tone - -3. **Consistency** - - Use consistent terminology throughout - - Follow language-specific conventions for technical documentation - - Maintain the same structure as the English version - -4. **Preserve Formatting** - - Keep all Markdown formatting intact - - Preserve table structures - - Maintain line breaks and spacing - - Keep backticks around parameter names - -## What to Translate - -✅ **DO translate:** -- Section headings (Overview, Inputs, Outputs) -- Parameter descriptions -- Explanatory text -- Usage notes and tips -- Constraint descriptions - -❌ **DO NOT translate:** -- Parameter names (keep in backticks: `image`, `seed`, `model`) -- Data types (IMAGE, STRING, INT, FLOAT, MODEL, CONDITIONING, etc.) -- Values in Range column (numbers, "auto", option names) -- Code snippets -- File paths -- URLs (except in the disclaimer footer added by automation) - -## Language-Specific Requirements - -### Chinese (zh) -- Use simplified Chinese characters -- Technical terms: use commonly accepted Chinese translations -- Headings: 概述, 输入, 输出 -- Tone: professional but accessible - -### Spanish (es) -- Use standard Spanish (neutral, not regional dialects) -- Headings: Descripción general, Entradas, Salidas -- Maintain formal "usted" form where appropriate - -### French (fr) -- Use standard French -- Headings: Aperçu général, Entrées, Sorties -- Maintain formal tone - -### Japanese (ja) -- Use polite form (です/ます体) -- Headings: 概要, 入力, 出力 -- Technical terms: use katakana for foreign technical terms when appropriate - -### Korean (ko) -- Use formal polite form (합니다체) -- Headings: 개요, 입력, 출력 -- Technical terms: use Hangul or keep English when standard - -### Russian (ru) -- Use standard Russian -- Headings: Обзор, Входы, Выходы -- Maintain formal tone - -## Table Structure - -Maintain the exact table structure: - -**Inputs:** -| Parameter | Description | Data Type | Required | Range | -|-----------|-------------|-----------|----------|-------| - -**Outputs:** -| Output Name | Description | Data Type | -|-------------|-------------|-----------| - -## Example Translation - -**English:** -> This node combines two CLIP models by adding the second model to the first. - -**Chinese:** -> 此节点通过将第二个 CLIP 模型添加到第一个模型来组合两个 CLIP 模型。 - -**Spanish:** -> Este nodo combina dos modelos CLIP añadiendo el segundo modelo al primero. - -**French:** -> Ce nœud combine deux modèles CLIP en ajoutant le second modèle au premier. - -**Japanese:** -> このノードは、2番目のCLIPモデルを1番目に追加することで、2つのCLIPモデルを結合します。 - -**Korean:** -> 이 노드는 두 번째 CLIP 모델을 첫 번째 모델에 추가하여 두 개의 CLIP 모델을 결합합니다。 - -**Russian:** -> Этот узел объединяет две модели CLIP, добавляя вторую модель к первой. - -## Quality Checklist - -Before finalizing translation, verify: -- [ ] All parameter names remain in English within backticks -- [ ] All data types remain in English (uppercase) -- [ ] Table structure is intact -- [ ] Markdown formatting is preserved -- [ ] Technical accuracy is maintained -- [ ] Language sounds natural to native speakers -- [ ] No English text remains except parameter names, data types, and values - diff --git a/docs-generation/lib/paths.py b/docs-generation/lib/paths.py index 5611c5c7c..393cc037b 100644 --- a/docs-generation/lib/paths.py +++ b/docs-generation/lib/paths.py @@ -19,7 +19,6 @@ ENV_FILE = REPO_ROOT / ".env" TRANSLATION_CONFIG = CONFIG_DIR / "translation_config.json" DOC_RULES = CONFIG_DIR / "doc_rules.txt" -TRANSLATION_RULES = CONFIG_DIR / "translation_rules.txt" ALL_NODES_INFO = DATA_DIR / "all_nodes_info.json" NODE_VERSIONS = DATA_DIR / "node_versions.json" diff --git a/docs-generation/main.py b/docs-generation/main.py index dbc4aa8a0..03d5ea768 100644 --- a/docs-generation/main.py +++ b/docs-generation/main.py @@ -51,6 +51,10 @@ python3 main.py --translate --lang zh --count 10 python3 main.py --translate --lang pt-BR --mode all + # Translate a single node (one language, or all languages) + python3 main.py --translate --lang zh --mode node --node KSampler + python3 main.py --translate --all-languages --mode node --node KSampler + # Translate to all supported languages python3 main.py --translate --all-languages --count 10 python3 main.py --translate --all-languages --mode all @@ -221,16 +225,20 @@ def prepare_translation(self, lang: str, mode: str, count: int = None, force_all f"Preparing {lang} translation batch ({mode} mode{' + force-all-nodes' if force_all_nodes else ''})" ) - def translate_docs(self, lang: str, mode: str, count: int = None, force: bool = False) -> bool: + def translate_docs(self, lang: str, mode: str, count: int = None, force: bool = False, node_name: str = None) -> bool: """Translate documentation to a specific language""" args = ["--lang", lang, "--mode", mode] - + if mode == "test" and count: args.extend(["--count", str(count)]) - + + if node_name: + # Single-node translation: bypass the batch file entirely + args.extend(["--node-list", node_name]) + if force: args.append("--force") - + return self.run_command( self.translate_script, args, @@ -267,6 +275,7 @@ def run_translation_workflow( skip_initial_scan: bool = False, skip_frontend_sync: bool = False, force_all_nodes: bool = False, + node_name: str = None, ): """Run translation workflow for a specific language""" timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") @@ -277,34 +286,40 @@ def run_translation_workflow( print(f"Started at: {timestamp}") print(f"Target language: {lang}") print(f"Mode: {mode}") - if mode == "test": + if node_name: + print(f"Node: {node_name}") + if mode == "test" and not node_name: print(f"Count: {count} nodes") print(f"Force retranslate: {force}") print(f"Prepare batch from all nodes with en.md: {force_all_nodes}") print("=" * 80) - + # Step 0a: Sync frontend translations (unless skipped for multi-language) if not skip_frontend_sync: print(f"\n🔄 Step 0a: Syncing frontend translations...") if not self.sync_frontend_translations(): print("\n⚠️ Warning: Frontend translation sync failed, but continuing...") - + # Step 0b: Scan to update missing_nodes_report.json (unless skipped for multi-language) if not skip_initial_scan: print(f"\n📊 Step 0b: Scanning to update missing translations...") if not self.scan_nodes(): print("\n❌ Translation workflow failed at Step 0b: Scan") return False - + # Step 1: Prepare translation batch (missing report or every node with en.md) - print(f"\n🔧 Step 1: Preparing {lang} translation batch...") - if not self.prepare_translation(lang, mode, count, force_all_nodes=force_all_nodes): - print("\n❌ Translation workflow failed at Step 1: Prepare") - return False - + # Skipped for single-node translation: --node-list is passed straight to the translator. + if node_name: + print(f"\n🔧 Step 1: Single node '{node_name}' — batch preparation skipped.") + else: + print(f"\n🔧 Step 1: Preparing {lang} translation batch...") + if not self.prepare_translation(lang, mode, count, force_all_nodes=force_all_nodes): + print("\n❌ Translation workflow failed at Step 1: Prepare") + return False + # Step 2: Translate documents (trusts batch list, updates JSON incrementally) print(f"\n🤖 Step 2: Translating to {lang}...") - if not self.translate_docs(lang, mode, count, force): + if not self.translate_docs(lang, mode, count, force, node_name=node_name): print("\n❌ Translation workflow failed at Step 2: Translate") return False @@ -328,7 +343,7 @@ def run_translation_workflow( return True - def run_all_languages_translation(self, mode: str = "test", count: int = 10, force: bool = False, force_all_nodes: bool = False): + def run_all_languages_translation(self, mode: str = "test", count: int = 10, force: bool = False, force_all_nodes: bool = False, node_name: str = None): """Run translation workflow for all supported languages""" timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") languages = ['zh', 'zh-TW', 'es', 'fr', 'ja', 'ko', 'ru', 'ar', 'tr', 'pt-BR', 'fa'] @@ -339,7 +354,9 @@ def run_all_languages_translation(self, mode: str = "test", count: int = 10, for print(f"Started at: {timestamp}") print(f"Languages: {', '.join(languages)}") print(f"Mode: {mode}") - if mode == "test": + if node_name: + print(f"Node: {node_name}") + if mode == "test" and not node_name: print(f"Count per language: {count} nodes") print(f"Force retranslate: {force}") print(f"Prepare batch from all nodes with en.md: {force_all_nodes}") @@ -372,6 +389,7 @@ def run_all_languages_translation(self, mode: str = "test", count: int = 10, for skip_initial_scan=True, skip_frontend_sync=True, force_all_nodes=force_all_nodes, + node_name=node_name, ) results[lang] = success @@ -1213,6 +1231,9 @@ def main(): # Run fix workflow (no AI) if args.mode == 'fix': + if args.translate: + print("❌ Error: --translate cannot be combined with --mode fix.") + sys.exit(1) fix_action = args.fix_action or 'doc-titles' if fix_action != 'doc-titles': print(f"❌ Error: unknown --fix-action {fix_action!r}") @@ -1235,22 +1256,32 @@ def main(): if args.also_translate_all: print("❌ Error: do not combine --translate with --also-translate-all.") sys.exit(1) + if args.mode not in ("test", "all", "node"): + print(f"❌ Error: --translate only supports --mode test/all/node (got '{args.mode}').") + sys.exit(1) + if args.mode == "node" and not args.node: + print("❌ Error: --node is required when using --translate --mode node") + sys.exit(1) + if args.node and args.mode != "node": + print("⚠️ Note: --node only applies with --mode node; ignoring it.") if args.all_languages: # Translate all languages success = workflow.run_all_languages_translation( - mode=args.mode if args.mode != 'node' else 'test', + mode=args.mode, count=args.count, force=args.force, force_all_nodes=args.force_all_translation_nodes, + node_name=args.node if args.mode == "node" else None, ) else: # Translate single language success = workflow.run_translation_workflow( lang=args.lang, - mode=args.mode if args.mode != 'node' else 'test', + mode=args.mode, count=args.count, force=args.force, force_all_nodes=args.force_all_translation_nodes, + node_name=args.node if args.mode == "node" else None, ) sys.exit(0 if success else 1) diff --git a/docs-generation/scripts/batch_translate_docs.py b/docs-generation/scripts/batch_translate_docs.py index a56ac838d..89aa50a1f 100644 --- a/docs-generation/scripts/batch_translate_docs.py +++ b/docs-generation/scripts/batch_translate_docs.py @@ -13,7 +13,6 @@ import logging from pathlib import Path from datetime import datetime -from openai import OpenAI import runtime # noqa: F401 from lib.doc_disclaimer import ( @@ -139,6 +138,20 @@ def strip_ai_preamble(content: str, lang: str) -> str: return '\n'.join(lines) +def _extract_table_data_rows(table_text: str) -> list[str]: + """Return the data rows of a markdown table (lines starting with '|'), + excluding the header row and the |---| separator row.""" + table_lines = [l for l in table_text.split('\n') if l.strip().startswith('|')] + data_rows = [] + for i, line in enumerate(table_lines): + if i == 0: + continue # header row + if re.match(r'^\|[\s:|-]+\|?$', line.strip()): + continue # separator row + data_rows.append(line) + return data_rows + + def _fix_output_names_in_translation(translated_content: str, full_en: str) -> str: """Parse the English en.md Outputs table and force output names in translated content to match the English original by row index. This prevents the AI from translating @@ -146,35 +159,23 @@ def _fix_output_names_in_translation(translated_content: str, full_en: str) -> s Works by: 1. Extracting output names from en.md Outputs table in order (row by row). - 2. Extracting output table lines from translated content. + 2. Extracting output data rows from translated content (rows whose first cell + is a backtick-wrapped name). 3. Replacing the first column (output name) of each row with its English counterpart. """ # Extract English output names en_outputs_match = re.search( - r'##\s+(?:Outputs|输出|輸出|出力|출력|Выходы|Salidas|Sorties|المخرجات|Çıktılar|خروجی‌ها)\s*\n\n(.*?)(?=\n##|\Z)', + r'##\s+(?:Outputs|输出|輸出|出力|출력|Выходы|Salidas|Sorties|المخرجات|Çıktılar|خروجی‌ها)\s*\n(.*?)(?=\n##|\Z)', full_en, re.DOTALL ) if not en_outputs_match: return translated_content # No outputs table in English doc, nothing to fix - en_table = en_outputs_match.group(1).strip() - en_lines = en_table.split('\n') - if len(en_lines) < 3: - return translated_content - - # Skip header row (| Name | Type | ...) and separator row (|---|---|...) - # Output names are backtick-wrapped in first column of data rows en_output_names = [] - for line in en_lines[2:]: # Skip header + separator - line = line.strip() - if not line.startswith('|'): - continue - parts = [p.strip() for p in line.split('|')] - if len(parts) >= 2: - # Extract backtick-wrapped name from first column - name = parts[1].strip('`').strip() - if name: - en_output_names.append(name) + for line in _extract_table_data_rows(en_outputs_match.group(1)): + m = re.match(r'^\|\s*`([^`]+)`', line.strip()) + if m: + en_output_names.append(m.group(1).strip()) if not en_output_names: return translated_content @@ -182,7 +183,7 @@ def _fix_output_names_in_translation(translated_content: str, full_en: str) -> s # Now find and fix the Outputs table in translated content # Match any known heading for "Outputs" in any language tr_outputs_match = re.search( - r'##\s+(?:输出|輸出|出力|출력|Выходы|Salidas|Sorties|Outputs|المخرجات|Çıktılar|خروجی‌ها)\s*\n\n(.*?)(?=\n##|\Z)', + r'##\s+(?:输出|輸出|出力|출력|Выходы|Salidas|Sorties|Outputs|المخرجات|Çıktılar|خروجی‌ها)\s*\n(.*?)(?=\n##|\Z)', translated_content, re.DOTALL ) if not tr_outputs_match: @@ -191,28 +192,34 @@ def _fix_output_names_in_translation(translated_content: str, full_en: str) -> s tr_table = tr_outputs_match.group(1) tr_lines = tr_table.split('\n') - # Build new table lines with English names enforced + # Build new table lines with English names enforced. Only rows whose first + # cell holds a backtick-wrapped name count as data rows, so the row index + # cannot drift when blank/intro lines appear inside the section. + header_seen = False new_lines = list(tr_lines) data_row_idx = 0 for i, line in enumerate(tr_lines): - if i < 2: # Keep header and separator rows as-is + stripped = line.strip() + if not stripped.startswith('|'): continue - line = line.strip() - if not line.startswith('|'): + if not header_seen: + header_seen = True # first table line is the header row continue - parts = line.split('|') - if len(parts) < 2: - continue - # Check if we have an English name for this row + if re.match(r'^\|[\s:|-]+\|?$', stripped): + continue # separator row + orig_name_match = re.match(r'^\|(\s*`[^`]*`\s*)\|', stripped) + if not orig_name_match: + continue # not a data row; do not advance the EN row index if data_row_idx < len(en_output_names): en_name = en_output_names[data_row_idx] - # Replace the output name column (first column after initial pipe) - # The backtick-wrapped name in the first data column - orig_name_match = re.match(r'^\|(\s*`[^`]*`\s*)', line) - if orig_name_match: - old = orig_name_match.group(1) - new = f' `{en_name}` ' - new_lines[i] = line.replace(old, new, 1) + # Replace the backtick-wrapped name in the first column of the + # ORIGINAL line (preserves leading whitespace and cell padding) + new_lines[i] = re.sub( + r'^(\s*\|\s*)`[^`]*`(\s*\|)', + lambda m: f"{m.group(1)}`{en_name}`{m.group(2)}", + line, + count=1, + ) data_row_idx += 1 new_table = '\n'.join(new_lines) @@ -220,7 +227,7 @@ def _fix_output_names_in_translation(translated_content: str, full_en: str) -> s return translated_content -def translate_document(node_name, target_lang, lang_config, api_key, base_url, model): +def translate_document(node_name, target_lang, lang_config, client, model): """Translate a single document""" # Read English source document @@ -240,10 +247,7 @@ def translate_document(node_name, target_lang, lang_config, api_key, base_url, m # Build prompt using language-specific template prompt_template = lang_config.get('prompt_template', '') full_prompt = prompt_template + "\n\n" + source_content - - # Call AI API - client = OpenAI(api_key=api_key, base_url=base_url) - + max_retries = 3 for attempt in range(max_retries): try: @@ -298,7 +302,7 @@ def translate_document(node_name, target_lang, lang_config, api_key, base_url, m else: raise -def process_node(node_name, target_lang, lang_config, api_key, base_url, model, force=False): +def process_node(node_name, target_lang, lang_config, client, model, force=False): """ Process a single node translation. When force=False, skips if target file already exists (do not overwrite). @@ -310,9 +314,9 @@ def process_node(node_name, target_lang, lang_config, api_key, base_url, model, try: logger.info(f"🤖 Translating: {node_name}") - + # Translate document - translated_content = translate_document(node_name, target_lang, lang_config, api_key, base_url, model) + translated_content = translate_document(node_name, target_lang, lang_config, client, model) # Save translated document output_dir = DOCS_PATH / node_name @@ -408,7 +412,13 @@ def main(): logger.info(f"⚙️ Batch size: {DEFAULT_BATCH_SIZE}") logger.info("=" * 80) logger.info("") - + + # Create the API client once and reuse it for every node in the batch. + # Imported lazily so the module's pure post-processing helpers stay + # importable (and testable) without the openai package installed. + from openai import OpenAI + client = OpenAI(api_key=DEFAULT_API_KEY, base_url=DEFAULT_BASE_URL) + # Translate documents success_count = 0 failed_count = 0 @@ -416,18 +426,17 @@ def main(): consecutive_failures = 0 MAX_CONSECUTIVE_FAILURES = 5 completed_nodes = [] # Track completed nodes for batch update - + for idx, node_name in enumerate(nodes_to_translate, 1): logger.info("") logger.info(f"[{idx}/{len(nodes_to_translate)}] Processing node: {node_name}") logger.info("-" * 60) - + result = process_node( node_name, target_lang, lang_config, - DEFAULT_API_KEY, - DEFAULT_BASE_URL, + client, DEFAULT_MODEL, force=force ) diff --git a/docs-generation/scripts/update_param_translations.py b/docs-generation/scripts/update_param_translations.py index e2294d7d9..63b8eb33e 100644 --- a/docs-generation/scripts/update_param_translations.py +++ b/docs-generation/scripts/update_param_translations.py @@ -8,6 +8,7 @@ localization does not depend on a local frontend checkout being up to date. """ +import argparse import json import os import re @@ -106,8 +107,11 @@ def update_parameter_name_in_row(row, old_param_name, new_param_name): return re.sub(pattern, f'| `{new_param_name}` |', row) return row -def update_doc_with_translations(doc_file, node_name, lang, frontend_translations): - """Update a documentation file with frontend translations""" +def update_doc_with_translations(doc_file, node_name, lang, frontend_translations, dry_run=False): + """Update a documentation file with frontend translations. + + When ``dry_run`` is True, compute changes but never write to disk. + """ # Get translations for this node and language if lang not in frontend_translations: @@ -167,7 +171,7 @@ def update_doc_with_translations(doc_file, node_name, lang, frontend_translation for i, line in enumerate(lines): # Detect if we're in the Outputs section - if re.match(r'##\s+(?:输出|輸出|出力|출력|Выходы|Salidas|Sorties|Outputs|المخرجات|Çıktılar)', line): + if re.match(r'##\s+(?:输出|輸出|出力|출력|Выходы|Salidas|Sorties|Outputs|المخرجات|Çıktılar|خروجی‌ها)', line): in_output_section = True output_row_index = 0 continue @@ -212,32 +216,39 @@ def update_doc_with_translations(doc_file, node_name, lang, frontend_translation content = '\n'.join(lines) - # Save if changes were made + # Save if changes were made (never write in dry-run mode) if content != original_content: - with open(doc_file, 'w', encoding='utf-8') as f: - f.write(content) + if not dry_run: + with open(doc_file, 'w', encoding='utf-8') as f: + f.write(content) return True, changes_made return False, [] def main(): """Main function""" - - # Parse arguments - target_lang = None - target_node = None - dry_run = False - force_refresh = False - for i, arg in enumerate(sys.argv[1:]): - if arg == '--lang': - target_lang = sys.argv[i + 2] if i + 2 < len(sys.argv) else None - elif arg == '--node': - target_node = sys.argv[i + 2] if i + 2 < len(sys.argv) else None - elif arg == '--dry-run': - dry_run = True - elif arg == '--refresh': - force_refresh = True + parser = argparse.ArgumentParser( + description="Update parameter names in docs to match frontend translations" + ) + parser.add_argument("--lang", default=None, + help=f"Target language (default: all of {' '.join(SUPPORTED_LANGS)})") + parser.add_argument("--node", default=None, help="Process a single node directory") + parser.add_argument("--dry-run", action="store_true", + help="Preview changes without writing files") + parser.add_argument("--refresh", action="store_true", + help="Force re-fetch of frontend translations from GitHub") + args = parser.parse_args() + + if args.lang and args.lang not in SUPPORTED_LANGS: + print(f"❌ Error: unknown language '{args.lang}'") + print(f" Supported: {', '.join(SUPPORTED_LANGS)}") + sys.exit(1) + + target_lang = args.lang + target_node = args.node + dry_run = args.dry_run + force_refresh = args.refresh print("=" * 80) print("Parameter Translation Updater") @@ -282,23 +293,17 @@ def main(): if not doc_file.exists(): continue - # Update document - if not dry_run: - updated, changes = update_doc_with_translations(doc_file, node_name, lang, frontend_trans) - - if updated: - print(f"✅ Updated {node_name} ({lang}): {', '.join(changes)}") - total_updated += 1 - else: - total_skipped += 1 + # Update document (update_doc_with_translations honors dry_run internally) + updated, changes = update_doc_with_translations( + doc_file, node_name, lang, frontend_trans, dry_run=dry_run + ) + + if updated: + prefix = "🔍 Would update" if dry_run else "✅ Updated" + print(f"{prefix} {node_name} ({lang}): {', '.join(changes)}") + total_updated += 1 else: - # Dry run - just check - _, changes = update_doc_with_translations(doc_file, node_name, lang, frontend_trans) - if changes: - print(f"🔍 Would update {node_name} ({lang}): {', '.join(changes)}") - total_updated += 1 - else: - total_skipped += 1 + total_skipped += 1 print("\n" + "=" * 80) print("📊 Summary") diff --git a/docs-generation/tests/test_translation_fixes.py b/docs-generation/tests/test_translation_fixes.py new file mode 100644 index 000000000..fa44806cc --- /dev/null +++ b/docs-generation/tests/test_translation_fixes.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Regression tests for the translation pipeline fixes. + +Covers: +- batch_translate_docs._fix_output_names_in_translation: EN output names must + align to translated rows by data-row index even when blank/intro lines or + non-backtick rows appear inside the Outputs section. +- update_param_translations.update_doc_with_translations(dry_run=True): must + never write to disk. +- update_param_translations Outputs-section detection for Persian (fa). +""" + +import os +import sys +import tempfile +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(REPO_ROOT)) +sys.path.insert(0, str(REPO_ROOT / "scripts")) + +from scripts import batch_translate_docs as btd # noqa: E402 +from scripts import update_param_translations as upt # noqa: E402 + + +class FixOutputNamesTests(unittest.TestCase): + EN_DOC = ( + "# KSampler\n\n" + "## Outputs\n\n" + "| Name | Data Type | Description |\n" + "|------|-----------|-------------|\n" + "| `positive` | CONDITIONING | Positive conditioning |\n" + "| `negative` | CONDITIONING | Negative conditioning |\n" + "| `latent` | LATENT | Denoised latent |\n" + ) + + def test_translated_names_replaced_by_row_index(self): + translated = ( + "## 输出\n\n" + "| 名称 | 数据类型 | 描述 |\n" + "|------|-----------|-------------|\n" + "| `正向` | CONDITIONING | 正向条件 |\n" + "| `负向` | CONDITIONING | 负向条件 |\n" + "| `潜空间` | LATENT | 去噪后的潜空间 |\n" + ) + out = btd._fix_output_names_in_translation(translated, self.EN_DOC) + self.assertIn("| `positive` |", out) + self.assertIn("| `negative` |", out) + self.assertIn("| `latent` |", out) + self.assertNotIn("`正向`", out) + + def test_intro_line_inside_section_does_not_misalign_rows(self): + """A non-table line between heading and table must not shift the index.""" + translated = ( + "## 输出\n\n" + "以下是该节点的输出:\n\n" + "| 名称 | 数据类型 | 描述 |\n" + "|------|-----------|-------------|\n" + "| `正向` | CONDITIONING | 正向条件 |\n" + "| `负向` | CONDITIONING | 负向条件 |\n" + "| `潜空间` | LATENT | 去噪后的潜空间 |\n" + ) + out = btd._fix_output_names_in_translation(translated, self.EN_DOC) + self.assertIn("| `positive` | CONDITIONING | 正向条件 |", out) + self.assertIn("| `negative` | CONDITIONING | 负向条件 |", out) + self.assertIn("| `latent` | LATENT | 去噪后的潜空间 |", out) + + def test_separator_like_row_does_not_count_as_data_row(self): + translated = ( + "## 输出\n\n" + "| 名称 | 数据类型 | 描述 |\n" + "|------|-----------|-------------|\n" + "| `正向` | CONDITIONING | 正向条件 |\n" + "| `负向` | CONDITIONING | 负向条件 |\n" + "| `潜空间` | LATENT | 去噪后的潜空间 |\n" + ) + out = btd._fix_output_names_in_translation(translated, self.EN_DOC) + rows = [l for l in out.split("\n") if l.strip().startswith("| `")] + self.assertEqual( + [r.split("`")[1] for r in rows], ["positive", "negative", "latent"] + ) + + def test_no_outputs_section_returns_content_unchanged(self): + translated = "## 输出\n\n没有表格。\n" + en = "# Node\n\n## Outputs\n\nNo table here.\n" + self.assertEqual( + btd._fix_output_names_in_translation(translated, en), translated + ) + + +class DryRunTests(unittest.TestCase): + def _translations(self, node, lang): + return { + lang: { + node: { + "inputs": {"seed": {"name": "semilla"}}, + "outputs": {}, + } + } + } + + def test_dry_run_does_not_write(self): + content = ( + "## Entradas\n\n" + "| Parameter | Description | Type |\n" + "|-----------|-------------|------|\n" + "| `seed` | desc | INT |\n" + ) + with tempfile.NamedTemporaryFile( + "w", suffix=".md", delete=False, encoding="utf-8" + ) as f: + f.write(content) + path = f.name + try: + updated, changes = upt.update_doc_with_translations( + path, "TestNode", "es", self._translations("TestNode", "es"), + dry_run=True, + ) + self.assertTrue(updated) + self.assertTrue(changes) + # File on disk must be untouched + self.assertEqual(Path(path).read_text(encoding="utf-8"), content) + finally: + os.unlink(path) + + def test_persian_outputs_section_detected(self): + """'## خروجی‌ها' must open the Outputs section for fa docs.""" + content = ( + "## خروجی‌ها\n\n" + "| Name | Type |\n" + "|------|------|\n" + "| `old_name` | IMAGE |\n" + ) + translations = { + "fa": { + "TestNode": { + "inputs": {}, + "outputs": {"0": {"name": "new_name"}}, + } + } + } + with tempfile.NamedTemporaryFile( + "w", suffix=".md", delete=False, encoding="utf-8" + ) as f: + f.write(content) + path = f.name + try: + updated, changes = upt.update_doc_with_translations( + path, "TestNode", "fa", translations + ) + final = Path(path).read_text(encoding="utf-8") + finally: + os.unlink(path) + self.assertTrue(updated) + self.assertIn("`new_name`", final) + + +if __name__ == "__main__": + unittest.main() From 982853376c639d2dcaf4d39a0f250f86636c0f5e Mon Sep 17 00:00:00 2001 From: ComfyUI Wiki Date: Mon, 17 Aug 2026 16:10:24 +0800 Subject: [PATCH 2/6] feat(docs-generation): add --concurrency to the translation pipeline - batch_translate_docs.py: new --concurrency N flag. Default 1 keeps the exact original sequential behavior (with the every-5-nodes rest); N>1 runs translations on a thread pool over the shared OpenAI client (httpx-based, thread-safe), skipping the periodic rest while keeping per-request retry/backoff. The consecutive-failure circuit breaker still works: on trip it cancels not-yet-started futures and aborts. - The concurrent runner is a module-level function (translate_nodes_concurrently) so it is unit-testable without the openai package or API access. - main.py: --concurrency is plumbed through --translate (single lang, all languages, single node) and the re-translation step of --mode changed; rejected when < 1, warned-and-ignored for non-translation modes. Tests: 3 new cases for the concurrent runner (all-success, circuit breaker trip, failure-counter reset); all 42 tests pass. --- docs-generation/main.py | 41 ++++- .../scripts/batch_translate_docs.py | 159 +++++++++++++----- .../tests/test_translation_fixes.py | 37 ++++ 3 files changed, 192 insertions(+), 45 deletions(-) diff --git a/docs-generation/main.py b/docs-generation/main.py index 03d5ea768..9a80f338e 100644 --- a/docs-generation/main.py +++ b/docs-generation/main.py @@ -225,7 +225,7 @@ def prepare_translation(self, lang: str, mode: str, count: int = None, force_all f"Preparing {lang} translation batch ({mode} mode{' + force-all-nodes' if force_all_nodes else ''})" ) - def translate_docs(self, lang: str, mode: str, count: int = None, force: bool = False, node_name: str = None) -> bool: + def translate_docs(self, lang: str, mode: str, count: int = None, force: bool = False, node_name: str = None, concurrency: int = 1) -> bool: """Translate documentation to a specific language""" args = ["--lang", lang, "--mode", mode] @@ -239,6 +239,9 @@ def translate_docs(self, lang: str, mode: str, count: int = None, force: bool = if force: args.append("--force") + if concurrency > 1: + args.extend(["--concurrency", str(concurrency)]) + return self.run_command( self.translate_script, args, @@ -276,6 +279,7 @@ def run_translation_workflow( skip_frontend_sync: bool = False, force_all_nodes: bool = False, node_name: str = None, + concurrency: int = 1, ): """Run translation workflow for a specific language""" timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") @@ -319,7 +323,7 @@ def run_translation_workflow( # Step 2: Translate documents (trusts batch list, updates JSON incrementally) print(f"\n🤖 Step 2: Translating to {lang}...") - if not self.translate_docs(lang, mode, count, force, node_name=node_name): + if not self.translate_docs(lang, mode, count, force, node_name=node_name, concurrency=concurrency): print("\n❌ Translation workflow failed at Step 2: Translate") return False @@ -343,7 +347,7 @@ def run_translation_workflow( return True - def run_all_languages_translation(self, mode: str = "test", count: int = 10, force: bool = False, force_all_nodes: bool = False, node_name: str = None): + def run_all_languages_translation(self, mode: str = "test", count: int = 10, force: bool = False, force_all_nodes: bool = False, node_name: str = None, concurrency: int = 1): """Run translation workflow for all supported languages""" timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") languages = ['zh', 'zh-TW', 'es', 'fr', 'ja', 'ko', 'ru', 'ar', 'tr', 'pt-BR', 'fa'] @@ -390,6 +394,7 @@ def run_all_languages_translation(self, mode: str = "test", count: int = 10, for skip_frontend_sync=True, force_all_nodes=force_all_nodes, node_name=node_name, + concurrency=concurrency, ) results[lang] = success @@ -496,7 +501,7 @@ def _load_changed_nodes_from_scan(self) -> list[str]: print(f" ⚠️ Could not read scan report: {e}") return [] - def run_changed_workflow(self, force: bool = False): + def run_changed_workflow(self, force: bool = False, concurrency: int = 1): """Run workflow for nodes with changed source code""" timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") @@ -549,9 +554,12 @@ def run_changed_workflow(self, force: bool = False): print(f"\n{'=' * 60}") print(f"🌐 Translating changed nodes to {lang}...") print(f"{'=' * 60}") + tr_args = ["--lang", lang, "--node-list", node_list_str, "--force"] + if concurrency > 1: + tr_args.extend(["--concurrency", str(concurrency)]) self.run_command( self.translate_script, - ["--lang", lang, "--node-list", node_list_str, "--force"], + tr_args, f"Re-translating changed nodes to {lang}" ) print(f"\n✅ Step 4 complete: {len(changed_nodes)} changed nodes re-translated across {len(languages)} languages.") @@ -1170,6 +1178,18 @@ def main(): ), ) + parser.add_argument( + '--concurrency', + type=int, + default=1, + metavar='N', + help=( + 'Parallel translation workers (default: 1 = sequential). Applies to --translate ' + 'and to the re-translation step of --mode changed. Use with care: raises API ' + 'request rate proportionally.' + ), + ) + parser.add_argument( '--interactive', '-i', action='store_true', @@ -1209,6 +1229,13 @@ def main(): success = run_interactive(workflow) sys.exit(0 if success else 1) + if args.concurrency < 1: + print("❌ Error: --concurrency must be >= 1") + sys.exit(1) + + if args.concurrency > 1 and not (args.translate or args.mode == 'changed'): + print("⚠️ Note: --concurrency only applies to translation; ignoring it.") + # Validate arguments if args.mode == 'node' and not args.node: print("❌ Error: --node is required when using --mode node") @@ -1272,6 +1299,7 @@ def main(): force=args.force, force_all_nodes=args.force_all_translation_nodes, node_name=args.node if args.mode == "node" else None, + concurrency=args.concurrency, ) else: # Translate single language @@ -1282,6 +1310,7 @@ def main(): force=args.force, force_all_nodes=args.force_all_translation_nodes, node_name=args.node if args.mode == "node" else None, + concurrency=args.concurrency, ) sys.exit(0 if success else 1) @@ -1296,7 +1325,7 @@ def main(): ) elif args.mode == 'changed': # Changed nodes workflow - success = workflow.run_changed_workflow(force=args.force) + success = workflow.run_changed_workflow(force=args.force, concurrency=args.concurrency) elif args.mode == 'regenerate-all': success = workflow.run_regenerate_all_workflow( prepare_limit=args.prepare_limit, diff --git a/docs-generation/scripts/batch_translate_docs.py b/docs-generation/scripts/batch_translate_docs.py index 89aa50a1f..9e0c10ae7 100644 --- a/docs-generation/scripts/batch_translate_docs.py +++ b/docs-generation/scripts/batch_translate_docs.py @@ -334,6 +334,52 @@ def process_node(node_name, target_lang, lang_config, client, model, force=False logger.error(f"❌ Translation failed {node_name}: {e}") return "failed" + +def translate_nodes_concurrently(nodes, concurrency, process_fn, max_consecutive_failures=5): + """Translate nodes with a thread pool. + + ``process_fn(node_name)`` must return "success" | "failed" | "skipped". + Returns (results_dict, aborted) where results_dict maps each outcome to a + list of node names (input order preserved per bucket) and ``aborted`` is + True when the consecutive-failure circuit breaker tripped (remaining + not-yet-started tasks are cancelled). + """ + from concurrent.futures import ThreadPoolExecutor, as_completed + + results = {"success": [], "failed": [], "skipped": []} + consecutive_failures = 0 + aborted = False + + with ThreadPoolExecutor(max_workers=concurrency) as pool: + # Submit in order, consume in completion order + future_to_node = {pool.submit(process_fn, n): n for n in nodes} + for fut in as_completed(future_to_node): + node_name = future_to_node[fut] + if fut.cancelled(): + continue # cancelled by the circuit breaker; not tallied + try: + result = fut.result() + except Exception as e: # defensive; process_node already catches + logger.error(f"❌ Translation crashed {node_name}: {e}") + result = "failed" + if result not in results: + result = "failed" + results[result].append(node_name) + logger.info(f"[{sum(len(v) for v in results.values())}/{len(nodes)}] {node_name}: {result}") + + if result == "failed": + consecutive_failures += 1 + else: + consecutive_failures = 0 # Reset counter on success/skip + + if consecutive_failures >= max_consecutive_failures: + for other in future_to_node: + other.cancel() # no-op for already-running futures + aborted = True + break + + return results, aborted + def main(): """Main function""" parser = argparse.ArgumentParser(description="Batch translate docs using prepared batch file") @@ -347,10 +393,15 @@ def main(): help="Comma-separated list of node names to translate (overrides batch file)") parser.add_argument("--node-list-file", type=str, default=None, help="Path to JSON file with {'nodes': ['NodeA', 'NodeB']} (overrides batch file)") + parser.add_argument("--concurrency", type=int, default=1, + help="Parallel translation workers (default: 1 = sequential, with the " + "usual every-5-nodes rest). >1 uses a thread pool and skips the " + "periodic rest; retries/backoff still apply per request.") args = parser.parse_args() target_lang = args.lang mode = args.mode force = args.force + concurrency = max(1, args.concurrency) # Load translation config with open(TRANSLATION_CONFIG_FILE, 'r', encoding='utf-8') as f: @@ -427,20 +478,10 @@ def main(): MAX_CONSECUTIVE_FAILURES = 5 completed_nodes = [] # Track completed nodes for batch update - for idx, node_name in enumerate(nodes_to_translate, 1): - logger.info("") - logger.info(f"[{idx}/{len(nodes_to_translate)}] Processing node: {node_name}") - logger.info("-" * 60) - - result = process_node( - node_name, - target_lang, - lang_config, - client, - DEFAULT_MODEL, - force=force - ) - + def _tally(result, node_name): + """Update counters for one finished node; return True if the + consecutive-failure circuit breaker tripped.""" + nonlocal success_count, failed_count, skipped_count, consecutive_failures if result == "success": success_count += 1 consecutive_failures = 0 # Reset counter on success @@ -448,34 +489,74 @@ def main(): elif result == "failed": failed_count += 1 consecutive_failures += 1 - - # Check if we've hit the consecutive failure limit - if consecutive_failures >= MAX_CONSECUTIVE_FAILURES: - logger.error("") - logger.error("=" * 80) - logger.error(f"❌ Consecutive failures reached {MAX_CONSECUTIVE_FAILURES}, terminating") - logger.error("=" * 80) - logger.error(f"Success: {success_count}, Failed: {failed_count}, Skipped: {skipped_count}") - logger.error("Please check:") - logger.error(" 1. API key is correctly configured") - logger.error(" 2. Network connection is stable") - logger.error(" 3. API has sufficient balance") - logger.error("=" * 80) - print() - print("=" * 80) - print(f"❌ Consecutive failures: {MAX_CONSECUTIVE_FAILURES}, terminated automatically") - print(f"Success: {success_count}, Failed: {failed_count}, Skipped: {skipped_count}") - print(f"📁 Log: {log_file}") - print("=" * 80) - sys.exit(1) elif result == "skipped": skipped_count += 1 consecutive_failures = 0 # Reset counter on skip - - # Rate limiting - if idx % DEFAULT_BATCH_SIZE == 0 and idx < len(nodes_to_translate): - logger.info("⏸️ Batch rest for 2 seconds...") - time.sleep(2) + return consecutive_failures >= MAX_CONSECUTIVE_FAILURES + + def _abort_with_failure_summary(): + logger.error("") + logger.error("=" * 80) + logger.error(f"❌ Consecutive failures reached {MAX_CONSECUTIVE_FAILURES}, terminating") + logger.error("=" * 80) + logger.error(f"Success: {success_count}, Failed: {failed_count}, Skipped: {skipped_count}") + logger.error("Please check:") + logger.error(" 1. API key is correctly configured") + logger.error(" 2. Network connection is stable") + logger.error(" 3. API has sufficient balance") + logger.error("=" * 80) + print() + print("=" * 80) + print(f"❌ Consecutive failures: {MAX_CONSECUTIVE_FAILURES}, terminated automatically") + print(f"Success: {success_count}, Failed: {failed_count}, Skipped: {skipped_count}") + print(f"📁 Log: {log_file}") + print("=" * 80) + sys.exit(1) + + aborted = False + + if concurrency > 1 and len(nodes_to_translate) > 1: + # Concurrent path: thread pool over the shared client (httpx-based, + # thread-safe). The periodic every-5-nodes rest is skipped here; + # per-request retry/backoff in translate_document still applies. + logger.info(f"⚡ Concurrency: {concurrency} workers") + results, aborted = translate_nodes_concurrently( + nodes_to_translate, + concurrency, + lambda n: process_node(n, target_lang, lang_config, client, DEFAULT_MODEL, force=force), + max_consecutive_failures=MAX_CONSECUTIVE_FAILURES, + ) + success_count = len(results["success"]) + failed_count = len(results["failed"]) + skipped_count = len(results["skipped"]) + completed_nodes = results["success"] + else: + # Sequential path (default): identical to the original behavior. + for idx, node_name in enumerate(nodes_to_translate, 1): + logger.info("") + logger.info(f"[{idx}/{len(nodes_to_translate)}] Processing node: {node_name}") + logger.info("-" * 60) + + result = process_node( + node_name, + target_lang, + lang_config, + client, + DEFAULT_MODEL, + force=force + ) + + if _tally(result, node_name): + aborted = True + break + + # Rate limiting + if idx % DEFAULT_BATCH_SIZE == 0 and idx < len(nodes_to_translate): + logger.info("⏸️ Batch rest for 2 seconds...") + time.sleep(2) + + if aborted: + _abort_with_failure_summary() logger.info("") logger.info("=" * 80) diff --git a/docs-generation/tests/test_translation_fixes.py b/docs-generation/tests/test_translation_fixes.py index fa44806cc..6d227891f 100644 --- a/docs-generation/tests/test_translation_fixes.py +++ b/docs-generation/tests/test_translation_fixes.py @@ -156,5 +156,42 @@ def test_persian_outputs_section_detected(self): self.assertIn("`new_name`", final) +class ConcurrencyTests(unittest.TestCase): + def test_all_success(self): + nodes = [f"Node{i}" for i in range(10)] + results, aborted = btd.translate_nodes_concurrently( + nodes, concurrency=4, process_fn=lambda n: "success" + ) + self.assertFalse(aborted) + self.assertEqual(sorted(results["success"]), sorted(nodes)) + self.assertEqual(results["failed"], []) + + def test_circuit_breaker_trips_on_consecutive_failures(self): + nodes = [f"Node{i}" for i in range(20)] + results, aborted = btd.translate_nodes_concurrently( + nodes, + concurrency=1, # deterministic ordering for the breaker test + process_fn=lambda n: "failed", + max_consecutive_failures=5, + ) + self.assertTrue(aborted) + # Breaker trips after 5 failures; with concurrency=1 nothing beyond + # the in-flight task can complete, so we must see far fewer than 20. + self.assertLess(len(results["failed"]), 20) + + def test_success_resets_consecutive_failure_counter(self): + # Fail 4x, succeed, fail 4x: never 5 in a row -> no abort. + outcomes = {"a": "failed", "b": "failed", "c": "failed", "d": "failed", + "e": "success", "f": "failed", "g": "failed", "h": "failed", + "i": "failed"} + results, aborted = btd.translate_nodes_concurrently( + list(outcomes), concurrency=1, process_fn=lambda n: outcomes[n], + max_consecutive_failures=5, + ) + self.assertFalse(aborted) + self.assertEqual(len(results["failed"]), 8) + self.assertEqual(results["success"], ["e"]) + + if __name__ == "__main__": unittest.main() From 3861b078215ed736b8cc440d9404493eb59b0a13 Mon Sep 17 00:00:00 2001 From: ComfyUI Wiki Date: Mon, 17 Aug 2026 17:40:12 +0800 Subject: [PATCH 3/6] fix(docs-generation): sync frontend param names after re-translating changed nodes run_changed_workflow Step 4 re-translated changed nodes per language but never ran the frontend-i18n param/output name correction, unlike the regular translation workflow (its Step 3). Re-translated docs therefore kept raw names instead of the labels users see in the UI. Now calls update_param_translations for each language right after its translation pass, warning-and-continuing on failure, matching the regular workflow. --- docs-generation/main.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs-generation/main.py b/docs-generation/main.py index 9a80f338e..da606dbcd 100644 --- a/docs-generation/main.py +++ b/docs-generation/main.py @@ -562,6 +562,11 @@ def run_changed_workflow(self, force: bool = False, concurrency: int = 1): tr_args, f"Re-translating changed nodes to {lang}" ) + # Same post-translation correction as the regular translation + # workflow (Step 3 there): sync param/output names from the + # frontend i18n so re-translated docs match the UI labels. + if not self.update_param_translations(lang): + print(f"\n⚠️ Warning: Parameter translation update failed for {lang}, but continuing...") print(f"\n✅ Step 4 complete: {len(changed_nodes)} changed nodes re-translated across {len(languages)} languages.") else: print("\n⏭️ Step 4: No changed nodes to re-translate (skipping).") From 224b12bfb689b3107107fa30dd6e7e9e64dc5789 Mon Sep 17 00:00:00 2001 From: ComfyUI Wiki Date: Mon, 17 Aug 2026 23:12:12 +0800 Subject: [PATCH 4/6] fix(docs-generation): address CodeRabbit review on PR #133 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical: - translate_docs: single-node runs now pass --mode all + --node-list to batch_translate_docs.py (its argparse only accepts test/all, so '--mode node' crashed the subprocess with exit 2) Major: - concurrent breaker: completion-order 'consecutive failures' is meaningless with parallel workers (trips on unrelated failures / stays silent during outages). translate_nodes_concurrently now trips on an interleaving-independent aggregate rule: >=5 failures AND failures >= half of completed tasks; pending futures still cancelled on trip - Outputs-heading regexes: add pt-BR 'Saídas' (translated-content regex in batch_translate_docs.py; both Outputs regexes in update_param_translations.py) - OpenAI client: pass base_url=None when API_BASE_URL is unset so the SDK falls back to OPENAI_BASE_URL / its default endpoint - update_param_translations --node: resolve and require containment in DOCS_ROOT; absolute paths and ../ traversal are rejected - dry-run no longer writes the translations cache: load_frontend_translations gains persist=False which fetches and merges in memory only Minor/Trivial: - cap --concurrency at 32 with a loud warning (both main.py and the translator, which can be invoked directly) - rename ambiguous loop var 'l' -> 'line' (Ruff E741) - translate_nodes_concurrently docstring: buckets are in completion order, not input order - dry-run summary now says 'Would update' instead of 'Updated' - tests: separator/non-backtick-row fixture now actually contains both cases; breaker assertions pin exact counts; breaker tests reworked for the aggregate rule (43 tests pass) --- docs-generation/main.py | 20 ++++--- .../scripts/batch_translate_docs.py | 38 ++++++++----- .../scripts/update_param_translations.py | 50 ++++++++++++----- .../tests/test_translation_fixes.py | 54 ++++++++++++++----- 4 files changed, 116 insertions(+), 46 deletions(-) diff --git a/docs-generation/main.py b/docs-generation/main.py index da606dbcd..0a5d27278 100644 --- a/docs-generation/main.py +++ b/docs-generation/main.py @@ -227,14 +227,15 @@ def prepare_translation(self, lang: str, mode: str, count: int = None, force_all def translate_docs(self, lang: str, mode: str, count: int = None, force: bool = False, node_name: str = None, concurrency: int = 1) -> bool: """Translate documentation to a specific language""" - args = ["--lang", lang, "--mode", mode] - - if mode == "test" and count: - args.extend(["--count", str(count)]) - if node_name: - # Single-node translation: bypass the batch file entirely - args.extend(["--node-list", node_name]) + # Single-node translation: bypass the batch file via --node-list. + # The translator only accepts --mode test/all; the mode is + # irrelevant with --node-list (no batch slicing), so pass "all". + args = ["--lang", lang, "--mode", "all", "--node-list", node_name] + else: + args = ["--lang", lang, "--mode", mode] + if mode == "test" and count: + args.extend(["--count", str(count)]) if force: args.append("--force") @@ -1238,6 +1239,11 @@ def main(): print("❌ Error: --concurrency must be >= 1") sys.exit(1) + MAX_CONCURRENCY = 32 + if args.concurrency > MAX_CONCURRENCY: + print(f"⚠️ Warning: --concurrency {args.concurrency} is too high; capping at {MAX_CONCURRENCY} to avoid API rate-limit storms.") + args.concurrency = MAX_CONCURRENCY + if args.concurrency > 1 and not (args.translate or args.mode == 'changed'): print("⚠️ Note: --concurrency only applies to translation; ignoring it.") diff --git a/docs-generation/scripts/batch_translate_docs.py b/docs-generation/scripts/batch_translate_docs.py index 9e0c10ae7..89a679166 100644 --- a/docs-generation/scripts/batch_translate_docs.py +++ b/docs-generation/scripts/batch_translate_docs.py @@ -141,7 +141,7 @@ def strip_ai_preamble(content: str, lang: str) -> str: def _extract_table_data_rows(table_text: str) -> list[str]: """Return the data rows of a markdown table (lines starting with '|'), excluding the header row and the |---| separator row.""" - table_lines = [l for l in table_text.split('\n') if l.strip().startswith('|')] + table_lines = [line for line in table_text.split('\n') if line.strip().startswith('|')] data_rows = [] for i, line in enumerate(table_lines): if i == 0: @@ -182,8 +182,9 @@ def _fix_output_names_in_translation(translated_content: str, full_en: str) -> s # Now find and fix the Outputs table in translated content # Match any known heading for "Outputs" in any language + # (includes pt-BR "Saídas"; heading values come from translation_config.json) tr_outputs_match = re.search( - r'##\s+(?:输出|輸出|出力|출력|Выходы|Salidas|Sorties|Outputs|المخرجات|Çıktılar|خروجی‌ها)\s*\n(.*?)(?=\n##|\Z)', + r'##\s+(?:输出|輸出|出力|출력|Выходы|Salidas|Sorties|Saídas|Outputs|المخرجات|Çıktılar|خروجی‌ها)\s*\n(.*?)(?=\n##|\Z)', translated_content, re.DOTALL ) if not tr_outputs_match: @@ -340,14 +341,21 @@ def translate_nodes_concurrently(nodes, concurrency, process_fn, max_consecutive ``process_fn(node_name)`` must return "success" | "failed" | "skipped". Returns (results_dict, aborted) where results_dict maps each outcome to a - list of node names (input order preserved per bucket) and ``aborted`` is - True when the consecutive-failure circuit breaker tripped (remaining - not-yet-started tasks are cancelled). + list of node names in COMPLETION order (nondeterministic for + concurrency > 1), and ``aborted`` is True when the circuit breaker tripped + (remaining not-yet-started tasks are cancelled). + + Breaker rule: with workers running in parallel, completions interleave, so + a completion-order "consecutive failures" counter is meaningless — it can + trip on unrelated failures during a mostly-healthy run, or stay silent + during a real outage because one interleaved success resets it. Instead we + trip on an aggregate condition that is independent of interleaving: + at least ``max_consecutive_failures`` failures AND failures are at least + half of everything completed so far (i.e. the API is mostly failing). """ from concurrent.futures import ThreadPoolExecutor, as_completed results = {"success": [], "failed": [], "skipped": []} - consecutive_failures = 0 aborted = False with ThreadPoolExecutor(max_workers=concurrency) as pool: @@ -367,12 +375,9 @@ def translate_nodes_concurrently(nodes, concurrency, process_fn, max_consecutive results[result].append(node_name) logger.info(f"[{sum(len(v) for v in results.values())}/{len(nodes)}] {node_name}: {result}") - if result == "failed": - consecutive_failures += 1 - else: - consecutive_failures = 0 # Reset counter on success/skip - - if consecutive_failures >= max_consecutive_failures: + completed = sum(len(v) for v in results.values()) + failed = len(results["failed"]) + if failed >= max_consecutive_failures and failed * 2 >= completed: for other in future_to_node: other.cancel() # no-op for already-running futures aborted = True @@ -401,7 +406,10 @@ def main(): target_lang = args.lang mode = args.mode force = args.force - concurrency = max(1, args.concurrency) + MAX_CONCURRENCY = 32 + if args.concurrency > MAX_CONCURRENCY: + print(f"⚠️ Warning: --concurrency {args.concurrency} is too high; capping at {MAX_CONCURRENCY} to avoid API rate-limit storms.") + concurrency = max(1, min(args.concurrency, MAX_CONCURRENCY)) # Load translation config with open(TRANSLATION_CONFIG_FILE, 'r', encoding='utf-8') as f: @@ -468,7 +476,9 @@ def main(): # Imported lazily so the module's pure post-processing helpers stay # importable (and testable) without the openai package installed. from openai import OpenAI - client = OpenAI(api_key=DEFAULT_API_KEY, base_url=DEFAULT_BASE_URL) + # base_url=None lets the SDK fall back to OPENAI_BASE_URL or its default + # endpoint when API_BASE_URL is unset (an empty string would not). + client = OpenAI(api_key=DEFAULT_API_KEY, base_url=DEFAULT_BASE_URL or None) # Translate documents success_count = 0 diff --git a/docs-generation/scripts/update_param_translations.py b/docs-generation/scripts/update_param_translations.py index 63b8eb33e..59c6fa2fc 100644 --- a/docs-generation/scripts/update_param_translations.py +++ b/docs-generation/scripts/update_param_translations.py @@ -36,9 +36,12 @@ # when (re)writing the file, even though this script only updates non-en docs. FETCH_LANGS = ['en'] + SUPPORTED_LANGS -def load_frontend_translations(force_refresh=False): +def load_frontend_translations(force_refresh=False, persist=True): """Load frontend translations from exported JSON, refreshing from GitHub - if the file is missing or stale (older than TRANSLATIONS_MAX_AGE_HOURS).""" + if the file is missing or stale (older than TRANSLATIONS_MAX_AGE_HOURS). + + With ``persist=False`` (dry-run), a refresh still fetches and merges in + memory but never writes the cache file — a dry run must stay dry.""" stale = False if not TRANSLATIONS_FILE.exists(): print(f"ℹ️ {TRANSLATIONS_FILE} not found") @@ -53,10 +56,24 @@ def load_frontend_translations(force_refresh=False): print("📡 Fetching frontend translations from GitHub (Comfy-Org/ComfyUI_frontend@master)...") data = fetch_remote_translations(FETCH_LANGS) if any(data.values()): - # Merge over the existing file (atomic write) so languages whose - # fetch failed and keys not managed here are preserved. - merged = save_translations(TRANSLATIONS_FILE, data) - print(f"✓ Refreshed {TRANSLATIONS_FILE} from GitHub") + if persist: + # Merge over the existing file (atomic write) so languages whose + # fetch failed and keys not managed here are preserved. + merged = save_translations(TRANSLATIONS_FILE, data) + print(f"✓ Refreshed {TRANSLATIONS_FILE} from GitHub") + return merged + # Dry-run: merge in memory only, never touch the cache file + merged = {} + if TRANSLATIONS_FILE.exists(): + try: + with open(TRANSLATIONS_FILE, 'r', encoding='utf-8') as f: + merged = json.load(f) + except (json.JSONDecodeError, OSError): + merged = {} + for lang, lang_data in data.items(): + if lang_data: + merged[lang] = lang_data + print("✓ Fetched translations from GitHub (in-memory only, dry-run)") return merged # Fetch failed: fall back to existing file if TRANSLATIONS_FILE.exists(): @@ -75,7 +92,7 @@ def extract_table_rows(content, table_type='inputs'): if table_type == 'inputs': pattern = r'##\s+(?:输入|輸入|入力|입력|Входы|Entradas|Entrées|Inputs|المدخلات|Girdiler|ورودی‌ها)\s*\n\n(.*?)(?=\n##|\Z)' else: - pattern = r'##\s+(?:输出|輸出|出力|출력|Выходы|Salidas|Sorties|Outputs|المخرجات|Çıktılar|خروجی‌ها)\s*\n\n(.*?)(?=\n##|\Z)' + pattern = r'##\s+(?:输出|輸出|出力|출력|Выходы|Salidas|Sorties|Saídas|Outputs|المخرجات|Çıktılar|خروجی‌ها)\s*\n\n(.*?)(?=\n##|\Z)' match = re.search(pattern, content, re.DOTALL) if not match: @@ -171,7 +188,7 @@ def update_doc_with_translations(doc_file, node_name, lang, frontend_translation for i, line in enumerate(lines): # Detect if we're in the Outputs section - if re.match(r'##\s+(?:输出|輸出|出力|출력|Выходы|Salidas|Sorties|Outputs|المخرجات|Çıktılar|خروجی‌ها)', line): + if re.match(r'##\s+(?:输出|輸出|出力|출력|Выходы|Salidas|Sorties|Saídas|Outputs|المخرجات|Çıktılar|خروجی‌ها)', line): in_output_section = True output_row_index = 0 continue @@ -261,14 +278,20 @@ def main(): print("=" * 80) print() - # Load frontend translations + # Load frontend translations (dry-run must not write the cache file) print("📖 Loading frontend translations...") - frontend_trans = load_frontend_translations(force_refresh=force_refresh) + frontend_trans = load_frontend_translations(force_refresh=force_refresh, persist=not dry_run) print(f" Loaded translations for {len(SUPPORTED_LANGS)} languages\n") # Get list of nodes to process if target_node: - node_dirs = [DOCS_ROOT / target_node] + # Constrain --node to DOCS_ROOT: absolute values or ../ traversal + # must not make the updater write outside the docs tree. + candidate = (DOCS_ROOT / target_node).resolve() + if not candidate.is_relative_to(DOCS_ROOT): + print(f"❌ Error: --node must be a node name inside {DOCS_ROOT}, got: {target_node}") + sys.exit(1) + node_dirs = [candidate] if not node_dirs[0].exists(): print(f"❌ Error: Node directory not found: {node_dirs[0]}") sys.exit(1) @@ -308,7 +331,10 @@ def main(): print("\n" + "=" * 80) print("📊 Summary") print("=" * 80) - print(f"✅ Updated: {total_updated}") + if dry_run: + print(f"🔍 Would update: {total_updated}") + else: + print(f"✅ Updated: {total_updated}") print(f"⏭️ Skipped: {total_skipped}") if dry_run: print("\n💡 Run without --dry-run to apply changes") diff --git a/docs-generation/tests/test_translation_fixes.py b/docs-generation/tests/test_translation_fixes.py index 6d227891f..891abe416 100644 --- a/docs-generation/tests/test_translation_fixes.py +++ b/docs-generation/tests/test_translation_fixes.py @@ -67,19 +67,27 @@ def test_intro_line_inside_section_does_not_misalign_rows(self): self.assertIn("| `latent` | LATENT | 去噪后的潜空间 |", out) def test_separator_like_row_does_not_count_as_data_row(self): + """A stray separator row and a non-backtick row inside the table must + not advance the EN row index (only real data rows count).""" translated = ( "## 输出\n\n" "| 名称 | 数据类型 | 描述 |\n" "|------|-----------|-------------|\n" "| `正向` | CONDITIONING | 正向条件 |\n" + "|------|-----------|-------------|\n" # stray separator row mid-table + "| 普通文本行 | CONDITIONING | 没有反引号的行 |\n" # non-backtick row "| `负向` | CONDITIONING | 负向条件 |\n" "| `潜空间` | LATENT | 去噪后的潜空间 |\n" ) out = btd._fix_output_names_in_translation(translated, self.EN_DOC) rows = [l for l in out.split("\n") if l.strip().startswith("| `")] + # The stray separator and the non-backtick row must not shift alignment: + # 负向 -> negative, 潜空间 -> latent (not negative/latent swapped early) self.assertEqual( [r.split("`")[1] for r in rows], ["positive", "negative", "latent"] ) + # The non-backtick row keeps its own text + self.assertIn("| 普通文本行 |", out) def test_no_outputs_section_returns_content_unchanged(self): translated = "## 输出\n\n没有表格。\n" @@ -166,7 +174,8 @@ def test_all_success(self): self.assertEqual(sorted(results["success"]), sorted(nodes)) self.assertEqual(results["failed"], []) - def test_circuit_breaker_trips_on_consecutive_failures(self): + def test_circuit_breaker_trips_on_aggregate_failures(self): + """All-failing run: breaker trips on exactly the 5th failure.""" nodes = [f"Node{i}" for i in range(20)] results, aborted = btd.translate_nodes_concurrently( nodes, @@ -175,22 +184,41 @@ def test_circuit_breaker_trips_on_consecutive_failures(self): max_consecutive_failures=5, ) self.assertTrue(aborted) - # Breaker trips after 5 failures; with concurrency=1 nothing beyond - # the in-flight task can complete, so we must see far fewer than 20. - self.assertLess(len(results["failed"]), 20) - - def test_success_resets_consecutive_failure_counter(self): - # Fail 4x, succeed, fail 4x: never 5 in a row -> no abort. - outcomes = {"a": "failed", "b": "failed", "c": "failed", "d": "failed", - "e": "success", "f": "failed", "g": "failed", "h": "failed", - "i": "failed"} + self.assertEqual(len(results["failed"]), 5) + + def test_interleaved_successes_do_not_prevent_or_fake_trips(self): + """The aggregate breaker is independent of completion interleaving: + scattered failures below the majority threshold never abort.""" + # 5 failures spread across 20 nodes -> at completion of node 12 + # (the 5th failure) failed=5 but 5*2 < 12, so no trip. + failing = {"Node0", "Node1", "Node9", "Node10", "Node11"} + nodes = [f"Node{i}" for i in range(20)] results, aborted = btd.translate_nodes_concurrently( - list(outcomes), concurrency=1, process_fn=lambda n: outcomes[n], + nodes, + concurrency=1, + process_fn=lambda n: "failed" if n in failing else "success", max_consecutive_failures=5, ) self.assertFalse(aborted) - self.assertEqual(len(results["failed"]), 8) - self.assertEqual(results["success"], ["e"]) + self.assertEqual(len(results["failed"]), 5) + self.assertEqual(len(results["success"]), 15) + + def test_majority_failure_rate_trips_breaker(self): + """Failures dominating completions trip the breaker even when not + consecutive in submission order.""" + # With concurrency=1 this is completion order too: 5 failures and + # 1 success interleaved; at the 5th failure failed=5, completed=6, + # 5*2 >= 6 -> trip. + order = ["f1", "ok1", "f2", "f3", "f4", "f5", "ok2", "ok3"] + results, aborted = btd.translate_nodes_concurrently( + order, + concurrency=1, + process_fn=lambda n: "failed" if n.startswith("f") else "success", + max_consecutive_failures=5, + ) + self.assertTrue(aborted) + self.assertEqual(len(results["failed"]), 5) + self.assertEqual(results["success"], ["ok1"]) if __name__ == "__main__": From 60ad2c065a6a698490a1bd63a4857591791d50e7 Mon Sep 17 00:00:00 2001 From: ComfyUI Wiki Date: Tue, 18 Aug 2026 03:29:07 +0800 Subject: [PATCH 5/6] fix(docs-generation): address second CodeRabbit pass on PR #133 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Node-name path containment for translation: a --node value like ../../outside was forwarded as --node-list and the translator built output paths from it, allowing writes outside DOCS_PATH. Validated in main.py (early, clear error) and in batch_translate_docs.py for every entry point (--node-list / --node-list-file / batch file). - update_param_translations --node: require an actual node directory — reject DOCS_ROOT itself and non-directory paths, which previously exited 0 after silently processing nothing. 43 tests pass. --- docs-generation/main.py | 9 ++++++++- .../scripts/batch_translate_docs.py | 19 +++++++++++++++++++ .../scripts/update_param_translations.py | 15 +++++++++------ 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/docs-generation/main.py b/docs-generation/main.py index 0a5d27278..d57e5bd25 100644 --- a/docs-generation/main.py +++ b/docs-generation/main.py @@ -98,7 +98,7 @@ import subprocess from pathlib import Path -from lib.paths import REPO_ROOT, load_dotenv +from lib.paths import REPO_ROOT, embedded_docs_dir, load_dotenv load_dotenv() from datetime import datetime @@ -228,6 +228,13 @@ def prepare_translation(self, lang: str, mode: str, count: int = None, force_all def translate_docs(self, lang: str, mode: str, count: int = None, force: bool = False, node_name: str = None, concurrency: int = 1) -> bool: """Translate documentation to a specific language""" if node_name: + # Guard: the node name becomes a path under the docs root in the + # translator. Reject absolute paths / traversal before forwarding. + docs_root = embedded_docs_dir().resolve() + node_dir = (docs_root / node_name).resolve() + if not node_dir.is_relative_to(docs_root) or node_dir == docs_root: + print(f"❌ Error: --node must be a node name inside {docs_root}, got: {node_name!r}") + return False # Single-node translation: bypass the batch file via --node-list. # The translator only accepts --mode test/all; the mode is # irrelevant with --node-list (no batch slicing), so pass "all". diff --git a/docs-generation/scripts/batch_translate_docs.py b/docs-generation/scripts/batch_translate_docs.py index 89a679166..aa05ab70d 100644 --- a/docs-generation/scripts/batch_translate_docs.py +++ b/docs-generation/scripts/batch_translate_docs.py @@ -425,6 +425,19 @@ def main(): # Determine which nodes to translate nodes_to_translate: list[str] = [] + # Node names become paths under DOCS_PATH (reading en.md, writing + # .md). Reject any value that resolves outside the docs root — + # covers --node-list, --node-list-file and batch-file entries alike. + docs_root = DOCS_PATH.resolve() + + def _invalid_nodes(names): + bad = [] + for n in names: + node_dir = (docs_root / n).resolve() + if not node_dir.is_relative_to(docs_root) or node_dir == docs_root: + bad.append(n) + return bad + if args.node_list: # Direct node list from CLI argument nodes_to_translate = [n.strip() for n in args.node_list.split(",") if n.strip()] @@ -455,6 +468,12 @@ def main(): nodes_to_translate = nodes_to_translate[:args.count] print(f"📊 Batch prepared: {batch_data.get('total', 0)} nodes") + + bad_nodes = _invalid_nodes(nodes_to_translate) + if bad_nodes: + print(f"❌ Error: node names must stay inside {docs_root}; invalid: {', '.join(bad_nodes)}") + sys.exit(1) + print(f"💡 {'Test' if mode == 'test' else 'Full'} mode: Translating {len(nodes_to_translate)} nodes") print() print(f"Target language: {lang_config['name']} ({target_lang})") diff --git a/docs-generation/scripts/update_param_translations.py b/docs-generation/scripts/update_param_translations.py index 59c6fa2fc..4f58ce73b 100644 --- a/docs-generation/scripts/update_param_translations.py +++ b/docs-generation/scripts/update_param_translations.py @@ -286,15 +286,18 @@ def main(): # Get list of nodes to process if target_node: # Constrain --node to DOCS_ROOT: absolute values or ../ traversal - # must not make the updater write outside the docs tree. + # must not make the updater write outside the docs tree. The target + # must also be an actual node directory (not a file, not DOCS_ROOT + # itself), otherwise the run would silently process nothing. candidate = (DOCS_ROOT / target_node).resolve() - if not candidate.is_relative_to(DOCS_ROOT): - print(f"❌ Error: --node must be a node name inside {DOCS_ROOT}, got: {target_node}") + if ( + not candidate.is_relative_to(DOCS_ROOT) + or candidate == DOCS_ROOT + or not candidate.is_dir() + ): + print(f"❌ Error: --node must be a node directory inside {DOCS_ROOT}, got: {target_node}") sys.exit(1) node_dirs = [candidate] - if not node_dirs[0].exists(): - print(f"❌ Error: Node directory not found: {node_dirs[0]}") - sys.exit(1) else: node_dirs = [d for d in DOCS_ROOT.iterdir() if d.is_dir()] From cace4392b9048f9f8092f4db23ca4a18a1a86e62 Mon Sep 17 00:00:00 2001 From: ComfyUI Wiki Date: Tue, 18 Aug 2026 09:37:36 +0800 Subject: [PATCH 6/6] fix(docs-generation): stop changed-node workflow when a language translation fails CodeRabbit PR #133 follow-up: the Step 4 re-translation result was ignored, so a translator failure (including a circuit-breaker abort) still ran update_param_translations on half-translated docs and the workflow could exit 0. Now returns False immediately on failure; the param correction only runs after a successful translation. --- docs-generation/main.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs-generation/main.py b/docs-generation/main.py index d57e5bd25..b4c92b031 100644 --- a/docs-generation/main.py +++ b/docs-generation/main.py @@ -565,11 +565,15 @@ def run_changed_workflow(self, force: bool = False, concurrency: int = 1): tr_args = ["--lang", lang, "--node-list", node_list_str, "--force"] if concurrency > 1: tr_args.extend(["--concurrency", str(concurrency)]) - self.run_command( + if not self.run_command( self.translate_script, tr_args, f"Re-translating changed nodes to {lang}" - ) + ): + # Includes circuit-breaker aborts (translator exits 1): + # do not run the param correction on half-translated docs. + print(f"\n❌ Changed-node translation failed for {lang}") + return False # Same post-translation correction as the regular translation # workflow (Step 3 there): sync param/output names from the # frontend i18n so re-translated docs match the UI labels.