Locale Download POEditor #160
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: Locale Download POEditor | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| on: | |
| schedule: | |
| # Run daily at midnight UTC | |
| - cron: "0 0 * * *" | |
| workflow_dispatch: | |
| # Allow manual triggering from GitHub UI | |
| inputs: | |
| operation_mode: | |
| description: 'Operation to perform' | |
| required: false | |
| type: choice | |
| options: | |
| - 'download-and-audit' | |
| - 'audit-only' | |
| default: 'download-and-audit' | |
| skip_audit: | |
| description: 'Skip locale audit step for faster execution (ignored if audit-only mode)' | |
| required: false | |
| type: boolean | |
| default: false | |
| jobs: | |
| download: | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| node-version: ['22.x'] | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| # fetch full history so things like auto-changelog work properly | |
| fetch-depth: 0 | |
| # Use token for write access | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Display operation mode | |
| run: | | |
| echo "🔧 **Operation Mode**: ${{ inputs.operation_mode || 'download-and-audit' }}" | |
| case "${{ inputs.operation_mode || 'download-and-audit' }}" in | |
| "audit-only") | |
| echo "📋 Running audit-only mode - will check locale completeness without downloading" | |
| ;; | |
| "download-and-audit") | |
| echo "🌍 Running full mode - will download locales and run audit" | |
| echo "📋 Audit will be ${{ inputs.skip_audit == true && 'skipped' || 'included' }}" | |
| ;; | |
| esac | |
| - name: Setup Node.js ${{ matrix.node-version }} | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ matrix.node-version }} | |
| cache: 'npm' | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Get package version | |
| id: package-version | |
| uses: martinbeentjes/npm-get-version-action@v1.3.1 | |
| - name: Get current date | |
| id: date | |
| run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT | |
| - name: Cache node modules | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.npm | |
| key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} | |
| restore-keys: | | |
| ${{ runner.os }}-node- | |
| - name: Install dependencies | |
| run: | | |
| npm ci | |
| npm install grunt-cli --save-dev | |
| - name: Check POEditor Token | |
| if: inputs.operation_mode != 'audit-only' | |
| run: | | |
| if [ -z "${{ secrets.POEDITOR_TOKEN }}" ]; then | |
| echo "❌ POEDITOR_TOKEN secret is not set" | |
| echo "Set this secret in Settings → Secrets and variables → Actions with a valid POEditor API token." | |
| exit 1 | |
| fi | |
| echo "✅ POEditor token is configured" | |
| - name: Create BuildConfig file | |
| if: inputs.operation_mode != 'audit-only' | |
| uses: jsdaniell/create-json@v1.2.3 | |
| with: | |
| name: "BuildConfig.json" | |
| json: '{"POEditor": { "id": "77079", "token": "${{ secrets.POEDITOR_TOKEN }}"}}' | |
| - name: Download locales from POEditor | |
| if: inputs.operation_mode != 'audit-only' | |
| run: | | |
| set -e # Exit on any command failure | |
| echo "🌍 Downloading locales from POEditor..." | |
| # Run the complete locale download workflow | |
| npm run locale:download | |
| echo "✅ Locale download completed successfully" | |
| - name: Validate locale download integrity | |
| if: inputs.operation_mode != 'audit-only' | |
| run: | | |
| echo "🔍 Validating locale download integrity..." | |
| # Check if any JSON files were created | |
| json_count=$(find src/locale/i18n -name "*.json" -type f | wc -l) | |
| echo "Found $json_count JSON locale files" | |
| if [ "$json_count" -eq 0 ]; then | |
| echo "❌ ERROR: No JSON locale files found after download!" | |
| exit 1 | |
| fi | |
| # Check for completely empty JSON files (which shouldn't exist) | |
| # Exclude en_US.json as it's the base locale and will always be empty | |
| empty_files=$(find src/locale/i18n -name "*.json" -type f -empty ! -name "en_US.json") | |
| if [ -n "$empty_files" ]; then | |
| echo "❌ ERROR: Found empty JSON locale files:" | |
| echo "$empty_files" | |
| echo "This indicates a download failure for these locales." | |
| exit 1 | |
| fi | |
| # Check for JSON files that only contain whitespace or are malformed | |
| # Exclude en_US.json as it's the base locale | |
| invalid_files="" | |
| for json_file in src/locale/i18n/*.json; do | |
| # Skip en_US.json (base locale, expected to be empty) | |
| if [ "$(basename "$json_file")" = "en_US.json" ]; then | |
| continue | |
| fi | |
| if [ -f "$json_file" ]; then | |
| # Check if file contains valid JSON (at minimum empty object {}) | |
| if ! python3 -m json.tool "$json_file" > /dev/null 2>&1; then | |
| # If not valid JSON, check if it's just empty/whitespace | |
| if [ ! -s "$json_file" ] || [ "$(cat "$json_file" | tr -d '[:space:]')" = "" ]; then | |
| invalid_files="$invalid_files\n$(basename "$json_file")" | |
| fi | |
| fi | |
| fi | |
| done | |
| if [ -n "$invalid_files" ]; then | |
| echo "❌ ERROR: Found invalid/empty JSON locale files:" | |
| echo -e "$invalid_files" | |
| echo "This indicates POEditor download failures. Failing the job." | |
| exit 1 | |
| fi | |
| echo "✅ Locale download integrity validation passed" | |
| - name: Check for locale regressions | |
| if: inputs.operation_mode != 'audit-only' | |
| run: | | |
| echo "🔍 Checking for locale regressions (files that lost content)..." | |
| # Get list of files that were deleted or significantly reduced | |
| regression_files="" | |
| # Check git diff for files that went from having content to being empty | |
| deleted_content=$(git diff --name-only --diff-filter=M | grep "src/locale/i18n/.*\.json$" || true) | |
| if [ -n "$deleted_content" ]; then | |
| for file in $deleted_content; do | |
| # Check if file exists and is now empty/small but was larger before | |
| if [ -f "$file" ]; then | |
| current_size=$(wc -c < "$file" 2>/dev/null || echo "0") | |
| # Get the size of the file in the previous commit | |
| previous_size=$(git show HEAD~1:"$file" 2>/dev/null | wc -c || echo "0") | |
| # If current file is empty or very small (< 50 bytes) but previous was substantial (> 500 bytes) | |
| if [ "$current_size" -lt 50 ] && [ "$previous_size" -gt 500 ]; then | |
| regression_files="$regression_files\n - $file (was ${previous_size} bytes, now ${current_size} bytes)" | |
| fi | |
| fi | |
| done | |
| fi | |
| if [ -n "$regression_files" ]; then | |
| echo "❌ ERROR: Detected locale regression - files lost substantial content:" | |
| echo -e "$regression_files" | |
| echo "" | |
| echo "This suggests POEditor download failed for these locales." | |
| echo "The job will fail to prevent loss of existing translations." | |
| exit 1 | |
| fi | |
| echo "✅ No locale regressions detected" | |
| - name: Show download summary | |
| if: inputs.operation_mode != 'audit-only' | |
| run: | | |
| echo "📊 Download Summary:" | |
| echo "Total JSON files: $(find src/locale/i18n -name "*.json" -type f | wc -l)" | |
| echo "Non-empty JSON files: $(find src/locale/i18n -name "*.json" -type f ! -empty | wc -l)" | |
| echo "Files with substantial content (>100 bytes): $(find src/locale/i18n -name "*.json" -type f -exec wc -c {} \; | awk '$1 > 100' | wc -l)" | |
| echo "" | |
| echo "Git status after download:" | |
| git status --porcelain src/locale/i18n/ | |
| # Check if base term updates exist | |
| if [ -d "locale/base-term-updates" ]; then | |
| base_updates_count=$(find locale/base-term-updates -name "*.json" -type f 2>/dev/null | wc -l) | |
| if [ "$base_updates_count" -gt 0 ]; then | |
| echo "" | |
| echo "📋 Base Term Updates:" | |
| echo "Generated files: $base_updates_count" | |
| find locale/base-term-updates -name "*.json" -type f -exec ls -lh {} \; | |
| fi | |
| fi | |
| - name: Check for changes | |
| if: inputs.operation_mode != 'audit-only' | |
| id: changes | |
| run: | | |
| if git diff --quiet; then | |
| echo "No locale changes detected" | |
| echo "has_changes=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "Locale changes detected" | |
| echo "has_changes=true" >> $GITHUB_OUTPUT | |
| echo "Changed files:" | |
| git diff --name-only | |
| fi | |
| - name: Generate base term updates for variants | |
| if: inputs.operation_mode != 'audit-only' | |
| run: | | |
| echo "🔄 Generating base term updates for locale variants..." | |
| node locale/scripts/locale-populate-variants.js | |
| echo "✅ Base term updates generated" | |
| - name: Configure Git | |
| if: inputs.operation_mode != 'audit-only' | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Commit base term updates | |
| if: inputs.operation_mode != 'audit-only' | |
| run: | | |
| if [ -d "locale/base-term-updates" ] && [ -n "$(find locale/base-term-updates -name '*.json' -type f 2>/dev/null)" ]; then | |
| echo "📝 Committing base term updates..." | |
| git add locale/base-term-updates/ | |
| # Check if there are staged changes to commit | |
| if ! git diff --cached --quiet; then | |
| git commit -m "📋 Base term updates: Add missing translation terms for locale variants" | |
| echo "✅ Base term updates committed" | |
| else | |
| echo "ℹ️ No new base term updates to commit" | |
| fi | |
| else | |
| echo "ℹ️ No base term updates generated" | |
| fi | |
| - name: Generate missing terms for all locales | |
| if: inputs.operation_mode != 'audit-only' | |
| run: | | |
| echo "📋 Generating missing translation terms for all locales..." | |
| npm run locale:missing | |
| echo "✅ Missing terms files generated" | |
| - name: Commit missing terms for POEditor | |
| if: inputs.operation_mode != 'audit-only' | |
| run: | | |
| if [ -d "locale/missing-terms" ] && [ -n "$(find locale/missing-terms -name 'poeditor-*.json' -type f 2>/dev/null)" ]; then | |
| echo "📝 Committing missing terms files..." | |
| git add locale/missing-terms/poeditor-*.json | |
| # Check if there are staged changes to commit | |
| if ! git diff --cached --quiet; then | |
| git commit -m "📊 Missing translation terms: Updated for all locales from master list" | |
| echo "✅ Missing terms files committed" | |
| else | |
| echo "ℹ️ No new missing terms to commit" | |
| fi | |
| else | |
| echo "ℹ️ No missing terms files generated" | |
| fi | |
| - name: Create Pull Request | |
| if: inputs.operation_mode != 'audit-only' && (steps.changes.outputs.has_changes == 'true' || github.event_name == 'workflow_dispatch') | |
| uses: peter-evans/create-pull-request@v6 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| branch: 'locale/${{ steps.package-version.outputs.current-version}}' | |
| delete-branch: true | |
| labels: | | |
| localization | |
| milestone: ${{ steps.package-version.outputs.current-version}} | |
| title: "🌍 POEditor Locale Update - ${{ steps.date.outputs.date }}" | |
| commit-message: | | |
| 🌍 Locale update from POEditor on ${{ steps.date.outputs.date }} | |
| - Updated translations from POEditor | |
| - Version: ${{ steps.package-version.outputs.current-version}} | |
| - Triggered: ${{ github.event_name == 'workflow_dispatch' && 'Manual' || 'Scheduled' }} | |
| body: | | |
| ## 🌍 Automatic Locale Update | |
| This PR contains updated translations downloaded from POEditor, base term updates for locale variants, and missing term reports for all locales. | |
| **Details:** | |
| - 📅 **Date**: ${{ steps.date.outputs.date }} | |
| - 🏷️ **Version**: ${{ steps.package-version.outputs.current-version}} | |
| - 🚀 **Trigger**: ${{ github.event_name == 'workflow_dispatch' && 'Manual run' || 'Scheduled daily run' }} | |
| **What's included:** | |
| - Updated translation files from POEditor | |
| - Generated locale JSON files | |
| - 📋 Base term updates for locale variants (in `locale/base-term-updates/`) | |
| - 📊 Missing terms for all locales (in `locale/missing-terms/poeditor-*.json`) | |
| - Locale audit results | |
| **Base Term Updates:** | |
| Syncs missing translations from base locales (Portuguese, Spanish) to their variants via POEditor. | |
| **Missing Terms Reports:** | |
| For each incomplete locale, a `poeditor-{locale}.json` file contains all missing/untranslated terms from the master English list. These can be imported into POEditor to help translators complete the translations. | |
| This PR was automatically created by the POEditor workflow. | |
| --- | |
| **Manual Run Options:** | |
| ${{ inputs.operation_mode == 'audit-only' && '- 🔍 Audit-only mode (no downloads)' || inputs.skip_audit == true && '- ⏩ Audit skipped' || '- 🔍 Full download and audit' }} | |
| - name: Run locale audit | |
| if: inputs.operation_mode == 'audit-only' || (inputs.operation_mode != 'audit-only' && inputs.skip_audit != true) | |
| run: | | |
| echo "🔍 Running locale audit..." | |
| npm run locale:audit | |
| echo "✅ Locale audit completed" | |
| - name: Report results | |
| run: | | |
| echo "## 📊 Workflow Results" | |
| echo "- **Operation mode**: ${{ inputs.operation_mode || 'download-and-audit' }}" | |
| echo "- **Download validation**: ${{ inputs.operation_mode == 'audit-only' && 'Skipped (audit-only)' || 'Passed' }}" | |
| echo "- **Regression check**: ${{ inputs.operation_mode == 'audit-only' && 'Skipped (audit-only)' || 'Passed' }}" | |
| echo "- **Base term updates**: Generated and committed" | |
| echo "- **Missing terms report**: Generated for all locales" | |
| echo "- **Changes detected**: ${{ inputs.operation_mode == 'audit-only' && 'N/A (audit-only)' || steps.changes.outputs.has_changes }}" | |
| echo "- **PR created**: ${{ inputs.operation_mode == 'audit-only' && 'No (audit-only)' || ((steps.changes.outputs.has_changes == 'true' || github.event_name == 'workflow_dispatch') && 'Yes' || 'No') }}" | |
| echo "- **Branch**: ${{ inputs.operation_mode == 'audit-only' && 'N/A' || format('locale/{0}', steps.package-version.outputs.current-version) }}" | |
| echo "- **Audit run**: ${{ (inputs.operation_mode == 'audit-only' || (inputs.operation_mode != 'audit-only' && inputs.skip_audit != true)) && 'Yes' || 'Skipped' }}" | |
| echo "- **Trigger**: ${{ github.event_name }}" |