Translation Status Check #185
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Translation Status Check | |
| on: | |
| workflow_run: | |
| workflows: | |
| - "CI DOC Checker" | |
| - "Translation Runner" | |
| types: [completed] | |
| jobs: | |
| report: | |
| runs-on: ubuntu-latest | |
| # Only run if the triggering workflow succeeded (prevents running on failed linting) | |
| if: ${{ github.event.workflow_run.conclusion == 'success' }} | |
| permissions: | |
| pull-requests: write | |
| contents: read | |
| steps: | |
| - name: Get PR Number | |
| id: pr_number | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const { data: pullRequests } = await github.rest.pulls.list({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| // Format as owner:branch to ensure we find PRs from external forks | |
| head: context.payload.workflow_run.head_repository.owner.login + ':' + context.payload.workflow_run.head_branch, | |
| state: 'open', | |
| }); | |
| if (pullRequests.length === 0) { | |
| core.setFailed('No open pull request found for this workflow_run.'); | |
| return; | |
| } | |
| core.setOutput('pr_number', pullRequests[0].number); | |
| # MOVED UP: Find existing comment first so we know if we should stay silent | |
| - name: Find Existing Comment | |
| uses: peter-evans/find-comment@v3 | |
| id: findcomment | |
| with: | |
| issue-number: ${{ steps.pr_number.outputs.pr_number }} | |
| comment-author: 'github-actions[bot]' | |
| body-includes: "### π Translation Required?" | |
| - name: Checkout PR Code | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: refs/pull/${{ steps.pr_number.outputs.pr_number }}/merge | |
| fetch-depth: 0 | |
| - name: Get PR Details | |
| id: pr_details | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const prNumber = '${{ steps.pr_number.outputs.pr_number }}'; | |
| const { data: pr } = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber, | |
| }); | |
| core.setOutput('base_sha', pr.base.sha); | |
| - name: Get Changed Files | |
| id: changed-files | |
| uses: tj-actions/changed-files@v44 | |
| with: | |
| base_sha: ${{ steps.pr_details.outputs.base_sha }} | |
| json: true | |
| files: | | |
| docs/en/**/*.md* | |
| docs/zh/**/*.md* | |
| docs/ja/**/*.md* | |
| - name: Generate Checklist Body | |
| id: checklist | |
| uses: actions/github-script@v7 | |
| env: | |
| HAS_COMMENT: ${{ steps.findcomment.outputs.comment-id != '' }} | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| let files = []; | |
| try { | |
| files = JSON.parse('${{ steps.changed-files.outputs.all_changed_files }}'); | |
| } catch (e) { console.log("No files detected"); } | |
| const hasComment = process.env.HAS_COMMENT === 'true'; | |
| const timestamp = new Date().toUTCString(); | |
| // 1. SILENT ABORT: No doc changes and no prior comment? Stay completely quiet. | |
| if (files.length === 0 && !hasComment) { | |
| console.log("No translatable files and no prior comment. Skipping all actions."); | |
| core.setOutput('skip_commenting', 'true'); | |
| core.setOutput('needs_translation', 'false'); | |
| return; | |
| } | |
| // We are proceeding, so don't skip commenting | |
| core.setOutput('skip_commenting', 'false'); | |
| let body = "### π Translation Required?\n"; | |
| // 2. SUCCESS UPDATE: No doc changes, but we DO have a comment to update. | |
| if (files.length === 0) { | |
| body += "\nβ **All translation files are up to date.**\n"; | |
| body += "No translation actions are required for this PR.\n"; | |
| body += `\n> π **Last updated:** ${timestamp}`; | |
| core.setOutput('body', body); | |
| core.setOutput('needs_translation', 'false'); | |
| return; | |
| } | |
| const getValidTargets = (sourceLang) => { | |
| if (sourceLang === 'en') return ['zh', 'ja']; | |
| if (sourceLang === 'zh') return ['en']; | |
| if (sourceLang === 'ja') return []; | |
| return []; | |
| }; | |
| const filesByBase = {}; | |
| files.forEach(file => { | |
| if (!fs.existsSync(file)) return; | |
| const parts = file.split('/'); | |
| if (parts.length < 3) return; | |
| const lang = parts[1]; | |
| if (!['en', 'zh', 'ja'].includes(lang)) return; | |
| const baseName = parts.slice(2).join('/'); | |
| if (!filesByBase[baseName]) { | |
| filesByBase[baseName] = { en: false, zh: false, ja: false, paths: {} }; | |
| } | |
| filesByBase[baseName][lang] = true; | |
| filesByBase[baseName].paths[lang] = file; | |
| }); | |
| let automatedCheckboxBuffer = ""; | |
| let manualTranslationBuffer = ""; | |
| let hasAutomatedTranslations = false; | |
| let hasManualTranslations = false; | |
| Object.entries(filesByBase).forEach(([baseName, langData]) => { | |
| const allLanguagesPresent = langData.en && langData.zh && langData.ja; | |
| if (allLanguagesPresent) return; | |
| ['en', 'zh', 'ja'].forEach(sourceLang => { | |
| if (!langData[sourceLang]) return; | |
| const file = langData.paths[sourceLang]; | |
| const validTargets = getValidTargets(sourceLang); | |
| if (validTargets.length === 0) { | |
| if (sourceLang === 'ja' && !langData.en && !langData.zh) { | |
| manualTranslationBuffer += `- \`${file}\` (Japanese file - requires English or Chinese source)\n`; | |
| hasManualTranslations = true; | |
| } | |
| return; | |
| } | |
| let automatedChecklist = ""; | |
| let manualChecklist = ""; | |
| validTargets.forEach(lang => { | |
| if (!langData[lang]) { | |
| const status = "(New)"; | |
| const canAutomate = (sourceLang === 'en' || (sourceLang === 'zh' && lang === 'en')); | |
| if (canAutomate) { | |
| automatedChecklist += `- [ ] **${lang}** for \`${file}\` ${status}\n`; | |
| } else { | |
| manualChecklist += `- **${lang}** for \`${file}\` ${status}\n`; | |
| } | |
| } | |
| }); | |
| if (automatedChecklist) { | |
| hasAutomatedTranslations = true; | |
| automatedCheckboxBuffer += `#### \`${file}\`\n${automatedChecklist}\n`; | |
| } | |
| if (manualChecklist) { | |
| hasManualTranslations = true; | |
| manualTranslationBuffer += `#### \`${file}\`\n${manualChecklist}\n`; | |
| } | |
| }); | |
| }); | |
| // 3. FINAL OUTPUT LOGIC | |
| if (!hasAutomatedTranslations && !hasManualTranslations) { | |
| body += "\nβ **All translation files are up to date.**\n"; | |
| body += "Great job! No translation actions are required for this PR.\n"; | |
| core.setOutput('needs_translation', 'false'); | |
| } else { | |
| core.setOutput('needs_translation', 'true'); | |
| body += "Thanks for your doc contribution! The following languages are missing or outdated for your changes.\n\n"; | |
| if (hasAutomatedTranslations) { | |
| body += "## π€ Automated Translations\n"; | |
| body += "If you are fluent in any of the missing languages you can add them. If not, a maintainer can generate the translations below.\n\n"; | |
| body += "**Maintainer:** Check the ones you wish to generate:\n\n"; | |
| body += automatedCheckboxBuffer; | |
| body += "---\n*Maintainers: Check boxes and reply with `/translate` to start.*\n\n"; | |
| } | |
| if (hasManualTranslations) { | |
| body += "## π€ Manual Translations Required\n"; | |
| body += "The following translations must be done manually by a human translator:\n\n"; | |
| body += manualTranslationBuffer; | |
| } | |
| } | |
| body += `\n\n> π **Last updated:** ${timestamp}`; | |
| core.setOutput('body', body); | |
| - name: Create or update comment | |
| # Only run if we actually have doc changes OR we need to update a previous comment | |
| if: steps.checklist.outputs.skip_commenting != 'true' | |
| uses: peter-evans/create-or-update-comment@v5 | |
| with: | |
| comment-id: ${{ steps.findcomment.outputs.comment-id }} | |
| issue-number: ${{ steps.pr_number.outputs.pr_number }} | |
| body: ${{ steps.checklist.outputs.body }} | |
| edit-mode: replace | |
| - name: Label PR | |
| if: steps.checklist.outputs.needs_translation == 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: ${{ steps.pr_number.outputs.pr_number }}, | |
| labels: ['docs-maintainer'] | |
| }); |