From 4a1521de09c6ae3a5c65ae6e04d399ccbfe2ab2d Mon Sep 17 00:00:00 2001 From: decolua Date: Fri, 6 Mar 2026 11:56:16 +0700 Subject: [PATCH] feat: add auto README translation workflow with streaming - Add GitHub Actions workflow to auto-translate README.md - Support Vietnamese and Simplified Chinese - Use GLM-5 API with streaming mode - Auto-commit translations to i18n/ folder - Trigger on README.md changes or manual dispatch Made-with: Cursor --- .github/scripts/translate-readme.js | 188 +++++++++++++++++++++++++ .github/workflows/translate-readme.yml | 38 +++++ 2 files changed, 226 insertions(+) create mode 100755 .github/scripts/translate-readme.js create mode 100644 .github/workflows/translate-readme.yml diff --git a/.github/scripts/translate-readme.js b/.github/scripts/translate-readme.js new file mode 100755 index 0000000..295f25c --- /dev/null +++ b/.github/scripts/translate-readme.js @@ -0,0 +1,188 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +// ============ CONFIGURATION ============ +const API_ENDPOINT = process.env.GLM_API_ENDPOINT || 'https://api.z.ai/api/anthropic/v1/messages'; +const API_MODEL = process.env.GLM_API_MODEL || 'glm-5'; +const API_KEY = process.env.GLM_API_KEY; +const MAX_TOKENS = parseInt(process.env.GLM_MAX_TOKENS || '32000'); +const TEMPERATURE = parseFloat(process.env.GLM_TEMPERATURE || '0.3'); + +const SUPPORTED_LANGUAGES = { + vi: 'Vietnamese', + 'zh-CN': 'Simplified Chinese' +}; + +// ============ VALIDATION ============ +if (!API_KEY) { + console.error('Error: GLM_API_KEY environment variable not set'); + process.exit(1); +} + +const targetLangs = process.argv.slice(2); +if (targetLangs.length === 0) { + console.error('Usage: node translate-readme.js [lang2] ...'); + console.error(`Supported languages: ${Object.keys(SUPPORTED_LANGUAGES).join(', ')}`); + process.exit(1); +} + +for (const lang of targetLangs) { + if (!SUPPORTED_LANGUAGES[lang]) { + console.error(`Unsupported language: ${lang}`); + process.exit(1); + } +} + +// ============ TRANSLATION FUNCTION ============ +async function translateToLanguage(readmeContent, targetLang) { + const langName = SUPPORTED_LANGUAGES[targetLang]; + console.log(`\n[${targetLang}] Translating to ${langName}...`); + console.log(`[${targetLang}] README size: ${readmeContent.length} characters`); + + const prompt = `Translate this entire Markdown document to ${langName}. + +CRITICAL RULES: +- Keep ALL markdown syntax EXACTLY as is (##, \`\`\`, -, *, |, tables, etc.) +- Do NOT modify code blocks, ASCII diagrams, or code fences +- Only translate human-readable text content +- Keep all URLs, links, and technical terms unchanged + +${readmeContent}`; + + const response = await fetch(API_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': API_KEY, + 'anthropic-version': '2023-06-01' + }, + body: JSON.stringify({ + model: API_MODEL, + messages: [{ role: 'user', content: prompt }], + temperature: TEMPERATURE, + max_tokens: MAX_TOKENS, + stream: true + }) + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`[${targetLang}] API Error: ${response.status} ${error}`); + } + + console.log(`[${targetLang}] Receiving translation stream...`); + + let translatedContent = ''; + let chunkCount = 0; + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value, { stream: true }); + const lines = chunk.split('\n'); + + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = line.slice(6); + if (data === '[DONE]') continue; + + try { + const parsed = JSON.parse(data); + if (parsed.type === 'content_block_delta' && parsed.delta?.text) { + translatedContent += parsed.delta.text; + chunkCount++; + if (chunkCount % 100 === 0) { + process.stdout.write(`\r[${targetLang}] Received ${translatedContent.length} chars...`); + } + } + } catch (e) { + // Skip invalid JSON + } + } + } + } + + process.stdout.write('\n'); + + console.log(`\n[${targetLang}] Stream complete, received ${translatedContent.length} characters`); + + if (!translatedContent) { + throw new Error(`[${targetLang}] No translation received`); + } + + console.log(`[${targetLang}] Fixing image paths...`); + + // Fix image paths + translatedContent = translatedContent + .replace(/!\[([^\]]*)\]\(\.\/images\//g, '![$1](../images/') + .replace(/!\[([^\]]*)\]\(\.\/public\//g, '![$1](../public/') + .replace(/ r.status === 'rejected').length; + if (failed > 0) { + console.log(`\n⚠️ ${failed} translation(s) failed`); + process.exit(1); + } + + console.log('\n✅ All translations completed successfully!'); +} + +main().catch(err => { + console.error('Fatal error:', err); + process.exit(1); +}); diff --git a/.github/workflows/translate-readme.yml b/.github/workflows/translate-readme.yml new file mode 100644 index 0000000..a425f59 --- /dev/null +++ b/.github/workflows/translate-readme.yml @@ -0,0 +1,38 @@ +name: Translate README + +on: + push: + branches: + - master + paths: + - 'README.md' + workflow_dispatch: + +jobs: + translate: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Translate README to all languages + env: + GLM_API_KEY: ${{ secrets.GLM_API_KEY }} + run: | + node .github/scripts/translate-readme.js vi zh-CN + + - name: Commit translations + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add i18n/ + git diff --staged --quiet || git commit -m "chore: auto-translate README to vi, zh-CN" + git push