diff --git a/.github/actions/translation-tracker/README.md b/.github/actions/translation-tracker/README.md new file mode 100644 index 0000000000..48c5e00f8a --- /dev/null +++ b/.github/actions/translation-tracker/README.md @@ -0,0 +1,266 @@ +# p5.js Translation Tracker + +A comprehensive GitHub Action and command-line tool to track translation status across multiple languages in the p5.js website examples. + +## 🚀 Overview + +This tool helps maintain translation consistency by: +- **Week 1**: Basic file-based change detection using Git +- **Week 2**: GitHub API integration for accurate commit tracking and automated issue creation +- **Week 3**: Multi-language support with single issues per file and manual scanning capabilities + +## 📋 Features + +### ✅ Week 1 Features (File-based tracking) +- Git-based change detection for English example files +- File modification time comparison +- Support for all languages: Spanish (es), Hindi (hi), Korean (ko), Chinese Simplified (zh-Hans) +- Local testing capabilities + +### ✅ Week 2 Features (GitHub API integration) +- Real commit-based comparison using GitHub API +- Automated GitHub issue creation for outdated translations +- Enhanced issue templates with helpful links and timelines +- Branch detection (auto-detects current branch) +- Backward compatibility with Week 1 + +### ✅ Week 3 Features (Multi-language & refinement) +- **Single issue per file**: Creates one issue covering all affected languages instead of separate issues per language +- **Enhanced labeling**: Uses "needs translation" base label + specific language labels (e.g., "lang-es", "lang-ko") +- **Manual scanning**: Can scan all English example files to find outdated/missing translations +- **Comprehensive issue format**: Detailed status for each language with links and action checklists +- **Batched language updates**: Groups all translation issues by file for better organization + +## 🎯 Supported Languages + +- **es**: Spanish (Español) +- **hi**: Hindi (हिन्दी) +- **ko**: Korean (한국어) +- **zh-Hans**: Chinese Simplified (简体中文) + +## 📁 File Structure + +``` +.github/actions/translation-tracker/ +├── index.js # Main implementation (Week 1 + 2 + 3) +├── package.json # Dependencies +├── test-local.js # Testing script with examples +└── README.md # This documentation +``` + +## 🛠 Usage + +### Basic Usage (Week 1 Mode) + +```bash +# Run locally with file-based tracking +node .github/actions/translation-tracker/index.js +``` + +### GitHub API Mode (Week 2 Mode) + +```bash +# Run with GitHub token for commit-based tracking +GITHUB_TOKEN=your_token node .github/actions/translation-tracker/index.js +``` + +### Manual Scanning (Week 3 Feature) + +```bash +# Scan all English example files +GITHUB_TOKEN=token node -e " +const { main } = require('./.github/actions/translation-tracker/index.js'); +main(null, { scanAll: true, createIssues: false }); +" +``` + +### Create Issues (Week 2 + 3 Feature) + +```bash +# Create GitHub issues for outdated translations +GITHUB_TOKEN=token node -e " +const { main } = require('./.github/actions/translation-tracker/index.js'); +main(null, { scanAll: true, createIssues: true }); +" +``` + +## 🧪 Testing + +The project includes a comprehensive test suite: + +```bash +# Run all tests +node .github/actions/translation-tracker/test-local.js + +# Manual scan test +node .github/actions/translation-tracker/test-local.js manual + +# Week 2 features with GitHub API +GITHUB_TOKEN=token node .github/actions/translation-tracker/test-local.js week2 + +# Create test issues +GITHUB_TOKEN=token node .github/actions/translation-tracker/test-local.js issues +``` + +## 📝 Issue Format (Week 3) + +The tool creates comprehensive GitHub issues with: + +### 🔖 Labels +- `needs translation` (base label) +- `help wanted` +- Language-specific labels: `lang-es`, `lang-hi`, `lang-ko`, `lang-zh-Hans` + +### 📄 Issue Content +- **Timeline**: Shows when English and translation files were last updated +- **Outdated Translations**: Lists languages that need updates with commit comparison links +- **Missing Translations**: Lists languages where translation files don't exist +- **Action Checklist**: Step-by-step guide for translators +- **Quick Links**: Direct links to files and comparison views + +### Example Issue Structure: +```markdown +## 🌍 Translation Update Needed + +**File**: `src/content/examples/en/01_Shapes_And_Color/00_Shape_Primitives/description.mdx` +**Branch**: `week2` + +### 📅 Timeline +- **Latest English update**: 6/22/2025 by p5js-contributor + +### 🔄 Outdated Translations + +- **Spanish (Español)**: Last updated 6/9/2025 by spanish-translator + - [📝 View file](https://github.com/owner/repo/blob/week2/src/content/examples/es/...) + - [🔍 Compare changes](https://github.com/owner/repo/compare/abc123...def456) + +### ✅ Action Checklist + +**For translators / contributors:** + +- [ ] Review the recent English file changes and the current translations +- [ ] Confirm if translation already reflects the update — close the issue if so +- [ ] Update the translation files accordingly +- [ ] Maintain structure, code blocks, and formatting +- [ ] Ensure translation is accurate and culturally appropriate +``` + +## 🔧 Configuration + +### Environment Variables + +- `GITHUB_TOKEN`: Required for Week 2+ features (API access and issue creation) +- `GITHUB_REPOSITORY`: Auto-detected in GitHub Actions (format: `owner/repo`) +- `GITHUB_EVENT_NAME`: Auto-detected in GitHub Actions +- `GITHUB_REF_NAME`: Auto-detected branch name + +### Options Object + +```javascript +{ + enableWeek2: boolean, // Force Week 2 mode + githubToken: string, // GitHub token for API access + createIssues: boolean, // Whether to create GitHub issues + scanAll: boolean // Scan all files instead of just changed files +} +``` + +## 📊 Output Example + +``` +🎯 p5.js Translation Tracker - Week 2 Mode +═══════════════════════════════════════════════════════ +📅 Event: local +🏠 Working directory: /Users/user/p5.js-website-new +🌍 Tracking languages: es, hi, ko, zh-Hans +🔍 Scan mode: All files + +🔍 REPOSITORY STRUCTURE ANALYSIS +═══════════════════════════════════ +📁 Examples path: src/content/examples +🌐 Available languages: en, es, hi, ko, zh-Hans + en: 61 example files across 15 categories + es: 61 example files across 15 categories + hi: 61 example files across 15 categories + ko: 61 example files across 15 categories + zh-Hans: 61 example files across 15 categories + +📝 Checking translations for: src/content/examples/en/01_Shapes_And_Color/00_Shape_Primitives/description.mdx + 🔄 es: Needs update + ✅ hi: Up to date + 🔄 ko: Needs update + 🔄 zh-Hans: Needs update + +📝 Creating GitHub issue for src/content/examples/en/01_Shapes_And_Color/00_Shape_Primitives/description.mdx... + ✅ Created issue #123: Update translations for description.mdx + +📊 TRANSLATION STATUS SUMMARY (Week 2) +═══════════════════════════════════════ +🆕 Missing translations: 0 +🔄 Outdated translations: 3 +✅ Up-to-date translations: 1 +🎫 Issues created: 1 + - Issue #123: description.mdx (Affected: es, ko, zh-Hans) +``` + +## 🚀 GitHub Actions Integration + +Create `.github/workflows/translation-tracker.yml`: + +```yaml +name: Translation Tracker + +on: + push: + paths: + - 'src/content/examples/en/**/*.mdx' + schedule: + - cron: '0 0 * * 1' # Weekly scan + +jobs: + track-translations: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '18' + + - name: Install dependencies + run: cd .github/actions/translation-tracker && npm install + + - name: Track translation status + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: node .github/actions/translation-tracker/index.js +``` + +## 🔄 Migration from Previous Versions + +- **Week 1 → Week 2**: Fully backward compatible, automatically detects GitHub token +- **Week 2 → Week 3**: Automatically creates single issues instead of multiple issues per language +- **Legacy Mode**: Will continue working in Week 1 mode if no GitHub token is provided + +## 💡 Key Improvements in Week 3 + +1. **Single Issue Per File**: Instead of creating separate issues for each language, creates one issue that covers all affected languages for a specific file +2. **Better Organization**: Issues are grouped by file rather than scattered by language +3. **Enhanced Labels**: Uses "needs translation" + specific language labels for better filtering +4. **Manual Scanning**: Ability to scan all files on demand rather than just changed files +5. **Improved Issue Format**: More detailed and actionable issue templates + +## 🤝 Contributing + +The translation tracker is modular and extensible: + +- Add new languages by updating `SUPPORTED_LANGUAGES` array +- Modify issue templates in `formatMultiLanguageIssueBody()` method +- Extend file scanning logic in `getAllEnglishExampleFiles()` function +- Add new detection modes by extending the `main()` function options + +## 📚 Dependencies + +- `@actions/core`: GitHub Actions integration +- `@actions/github`: GitHub API wrapper +- `@octokit/rest`: GitHub REST API client +- Node.js built-in modules: `fs`, `path`, `child_process` \ No newline at end of file diff --git a/.github/actions/translation-tracker/index.js b/.github/actions/translation-tracker/index.js new file mode 100644 index 0000000000..2fef3e9d41 --- /dev/null +++ b/.github/actions/translation-tracker/index.js @@ -0,0 +1,777 @@ +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const { Octokit } = require('@octokit/rest'); + + +function getTranslationPath(englishFilePath, language) { + // Ensure we have a valid English path + if (!englishFilePath.includes('/en/')) { + throw new Error(`Invalid English file path: ${englishFilePath}. Must contain '/en/'`); + } + + // Split path into parts and replace 'en' directory with target language + const pathParts = englishFilePath.split('/'); + const enIndex = pathParts.findIndex(part => part === 'en'); + + if (enIndex === -1) { + throw new Error(`Could not find 'en' directory in path: ${englishFilePath}`); + } + + // Create new path with language replacement + const translationParts = [...pathParts]; + translationParts[enIndex] = language; + + return translationParts.join('/'); +} + + +const SUPPORTED_LANGUAGES = ['es', 'hi', 'ko', 'zh-Hans']; + + +class GitHubCommitTracker { + constructor(token, owner, repo) { + this.octokit = new Octokit({ auth: token }); + this.owner = owner; + this.repo = repo; + this.currentBranch = this.detectCurrentBranch(); + } + + /** + * Detect the current git branch + */ + detectCurrentBranch() { + try { + // GitHub Actions environment + if (process.env.GITHUB_HEAD_REF) { + return process.env.GITHUB_HEAD_REF; // For pull requests + } + + if (process.env.GITHUB_REF_NAME) { + return process.env.GITHUB_REF_NAME; // For push events + } + + // Git command fallback + try { + const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim(); + if (branch && branch !== 'HEAD') { + return branch; + } + } catch (gitError) { + // Silent fallback + } + + // Default fallback + return 'main'; + + } catch (error) { + return 'main'; + } + } + + /** + * Get the last commit for a specific file using GitHub API + */ + async getLastCommit(filePath) { + try { + const { data } = await this.octokit.rest.repos.listCommits({ + owner: this.owner, + repo: this.repo, + sha: this.currentBranch, + path: filePath, + per_page: 1 + }); + + if (data.length > 0) { + return { + sha: data[0].sha, + date: new Date(data[0].commit.committer.date), + message: data[0].commit.message, + author: data[0].commit.author.name, + url: data[0].html_url + }; + } + + return null; + } catch (error) { + console.log(`⚠️ Primary commit lookup failed for ${filePath} on branch '${this.currentBranch}': ${error.message}`); + + // Fallback to main branch if current branch fails + if (this.currentBranch !== 'main') { + try { + const { data } = await this.octokit.rest.repos.listCommits({ + owner: this.owner, + repo: this.repo, + sha: 'main', + path: filePath, + per_page: 1 + }); + + if (data.length > 0) { + return { + sha: data[0].sha, + date: new Date(data[0].commit.committer.date), + message: data[0].commit.message, + author: data[0].commit.author.name, + url: data[0].html_url + }; + } + } catch (fallbackError) { + console.log(`⚠️ Fallback to main branch also failed for ${filePath}: ${fallbackError.message}`); + } + } + + console.log(`❌ Could not get commit info for ${filePath} from any branch`); + return null; + } + } + + /** + * Get a recent diff for a file (head vs previous commit) and return a short patch snippet + */ + async getRecentDiffForFile(filePath) { + try { + // Get latest two commits for this file on current branch + const { data: commits } = await this.octokit.rest.repos.listCommits({ + owner: this.owner, + repo: this.repo, + sha: this.currentBranch, + path: filePath, + per_page: 2, + }); + + if (!commits || commits.length === 0) { + return null; + } + + const headSha = commits[0].sha; + let baseSha = commits.length > 1 ? commits[1].sha : null; + + // If only one commit is found for the file (new file), use the parent of head + if (!baseSha) { + try { + const { data: headCommit } = await this.octokit.rest.repos.getCommit({ + owner: this.owner, + repo: this.repo, + ref: headSha, + }); + baseSha = headCommit.parents && headCommit.parents.length > 0 ? headCommit.parents[0].sha : null; + } catch (parentErr) { + console.log(`⚠️ Could not resolve base commit for diff of ${filePath}: ${parentErr.message}`); + } + } + + if (!baseSha) { + return { + baseSha: null, + headSha, + compareUrl: `https://github.com/${this.owner}/${this.repo}/commit/${headSha}`, + patchSnippet: null, + isTruncated: false, + }; + } + + // Compare the two commits and extract the file patch + const { data: compare } = await this.octokit.rest.repos.compareCommits({ + owner: this.owner, + repo: this.repo, + base: baseSha, + head: headSha, + }); + + const changedFile = (compare.files || []).find((f) => f.filename === filePath); + const patch = changedFile && changedFile.patch ? changedFile.patch : null; + + let patchSnippet = null; + let isTruncated = false; + if (patch) { + const lines = patch.split('\n'); + const maxLines = 80; + if (lines.length > maxLines) { + patchSnippet = lines.slice(0, maxLines).join('\n'); + isTruncated = true; + } else { + patchSnippet = patch; + } + } + + return { + baseSha, + headSha, + compareUrl: `https://github.com/${this.owner}/${this.repo}/compare/${baseSha}...${headSha}`, + patchSnippet, + isTruncated, + }; + } catch (error) { + console.log(`⚠️ Failed to compute diff for ${filePath} on branch '${this.currentBranch}': ${error.message}`); + // Fallback to at least provide a compare link to branch head + return { + baseSha: null, + headSha: null, + compareUrl: `https://github.com/${this.owner}/${this.repo}/blob/${this.currentBranch}/${filePath}`, + patchSnippet: null, + isTruncated: false, + }; + } + } + + /** + * Create a GitHub issue for outdated translation + */ + async createTranslationIssue(englishFile, language, commitInfo) { + const issueTitle = `🌍 Update ${language.toUpperCase()} translation for ${path.basename(englishFile)}`; + const issueBody = this.formatIssueBody(englishFile, language, commitInfo); + + try { + const { data } = await this.octokit.rest.issues.create({ + owner: this.owner, + repo: this.repo, + title: issueTitle, + body: issueBody, + labels: ['translation', `lang-${language}`, 'help wanted'] + }); + + return data; + } catch (error) { + console.error(`❌ Error creating issue:`, error.message); + return null; + } + } + + /** + * Create a single GitHub issue for a file covering multiple languages + */ + async createMultiLanguageTranslationIssue(fileTranslations) { + const englishFile = fileTranslations.englishFile; + const issueTitle = `🌍 Update translations for ${path.basename(englishFile)}`; + // Fetch recent English diff (best-effort) + const englishDiff = await this.getRecentDiffForFile(englishFile); + const issueBody = this.formatMultiLanguageIssueBody(fileTranslations, englishDiff); + + // Create labels: "needs translation" + specific language labels + const labels = ['needs translation', 'help wanted']; + const affectedLanguages = [ + ...fileTranslations.outdatedLanguages.map(l => l.language), + ...fileTranslations.missingLanguages.map(l => l.language) + ]; + + // Add specific language labels (remove duplicates) + const uniqueLanguages = [...new Set(affectedLanguages)]; + uniqueLanguages.forEach(lang => { + labels.push(`lang-${lang}`); + }); + + try { + const { data } = await this.octokit.rest.issues.create({ + owner: this.owner, + repo: this.repo, + title: issueTitle, + body: issueBody, + labels: labels + }); + + return data; + } catch (error) { + console.error(`❌ Error creating multi-language issue:`, error.message); + return null; + } + } + + /** + * Format the issue body with helpful information + */ + formatIssueBody(englishFile, language, commitInfo) { + const translationPath = getTranslationPath(englishFile, language); + const englishCommit = commitInfo.english; + const translationCommit = commitInfo.translation; + + return `## 🌍 Translation Update Needed + +**File**: \`${englishFile}\` +**Language**: ${this.getLanguageDisplayName(language)} +**Translation file**: \`${translationPath}\` +**Branch**: \`${this.currentBranch}\` + +### 📅 Timeline +- **English last updated**: ${englishCommit.date.toLocaleDateString()} by ${englishCommit.author} +- **Translation last updated**: ${translationCommit ? translationCommit.date.toLocaleDateString() + ' by ' + translationCommit.author : 'Never translated'} + +### 🔗 Quick Links +- [📄 Current English file](https://github.com/${this.owner}/${this.repo}/blob/${this.currentBranch}/${englishFile}) +- [📝 Translation file](https://github.com/${this.owner}/${this.repo}/blob/${this.currentBranch}/${translationPath}) +- [🔍 Compare changes](https://github.com/${this.owner}/${this.repo}/compare/${translationCommit ? translationCommit.sha : 'HEAD'}...${englishCommit.sha}) + +### 📋 What to do +1. Review the English changes in the file +2. Update the ${this.getLanguageDisplayName(language)} translation accordingly +3. Maintain the same structure and formatting +4. Test the translation for accuracy and cultural appropriateness + +### 📝 Recent English Changes +**Last commit**: [${englishCommit.message}](${englishCommit.url}) + +--- +*This issue was automatically created by the p5.js Translation Tracker 🤖* +*Need help? Check our [translation guidelines](https://github.com/processing/p5.js-website/blob/main/contributor_docs/translation.md)*`; + } + + /** + * Format the issue body for multi-language updates + */ + formatMultiLanguageIssueBody(fileTranslations, englishDiff) { + const englishFile = fileTranslations.englishFile; + const outdatedLanguages = fileTranslations.outdatedLanguages; + const missingLanguages = fileTranslations.missingLanguages; + + let body = `## 🌍 Translation Update Needed + +**File**: \`${englishFile}\` +**Branch**: \`${this.currentBranch}\` + +### 📅 Timeline +- **Latest English update**: ${fileTranslations.englishCommit.date.toLocaleDateString()} by ${fileTranslations.englishCommit.author} + +`; + + // Outdated translations section + if (outdatedLanguages.length > 0) { + body += `### 🔄 Outdated Translations\n\n`; + outdatedLanguages.forEach(lang => { + const translationPath = lang.translationPath; + body += `- **${this.getLanguageDisplayName(lang.language)}**: Last updated ${lang.commitInfo.translation.date.toLocaleDateString()} by ${lang.commitInfo.translation.author}\n`; + body += ` - [📝 View file](https://github.com/${this.owner}/${this.repo}/blob/${this.currentBranch}/${translationPath})\n`; + body += ` - [🔍 Compare changes](https://github.com/${this.owner}/${this.repo}/compare/${lang.commitInfo.translation.sha}...${lang.commitInfo.english.sha})\n\n`; + }); + } + + // Missing translations section + if (missingLanguages.length > 0) { + body += `### ❌ Missing Translations\n\n`; + missingLanguages.forEach(lang => { + const translationPath = lang.translationPath; + body += `- **${this.getLanguageDisplayName(lang.language)}**: Translation file does not exist\n`; + body += ` - Expected location: \`${translationPath}\`\n\n`; + }); + } + + // Include an English diff snippet if available + if (englishDiff) { + body += `### 🧾 English Changes (Recent)\n\n`; + body += `- [🔍 View full diff](${englishDiff.compareUrl})\n`; + if (englishDiff.patchSnippet) { + body += `\n\n\u0060\u0060\u0060diff\n${englishDiff.patchSnippet}\n\u0060\u0060\u0060\n`; + if (englishDiff.isTruncated) { + body += `\n_(diff truncated — open the full diff link above for all changes)_\n`; + } + } + body += `\n`; + } + + body += `### 🔗 Quick Links +- [📄 Current English file](https://github.com/${this.owner}/${this.repo}/blob/${this.currentBranch}/${englishFile}) + +### ✅ Action Checklist + +**For translators / contributors:** + +- [ ] Review the recent English file changes and the current translations +- [ ] Confirm if translation already reflects the update — close the issue if so +- [ ] Update the translation files accordingly +- [ ] Maintain structure, code blocks, and formatting +- [ ] Ensure translation is accurate and culturally appropriate + +### 📝 Summary of English File Changes +**Last commit**: [${fileTranslations.englishCommit.message}](${fileTranslations.englishCommit.url}) + +${outdatedLanguages.length > 0 || missingLanguages.length > 0 ? `**Change Type**: English file was updated. ${outdatedLanguages.length > 0 ? `${outdatedLanguages.map(l => this.getLanguageDisplayName(l.language)).join(', ')} translation${outdatedLanguages.length > 1 ? 's' : ''} may be outdated.` : ''} ${missingLanguages.length > 0 ? `${missingLanguages.map(l => this.getLanguageDisplayName(l.language)).join(', ')} translation${missingLanguages.length > 1 ? 's are' : ' is'} missing.` : ''}` : ''} + +--- +ℹ️ **Need help?** See our [Translation Guidelines](https://github.com/processing/p5.js-website/blob/main/contributor_docs/translation.md) + +🤖 *This issue was auto-generated by the p5.js Translation Tracker*`; + return body; + } + + /** + * Get display name for language code + */ + getLanguageDisplayName(langCode) { + const languages = { + 'es': 'Spanish (Español)', + 'hi': 'Hindi (हिन्दी)', + 'ko': 'Korean (한국어)', + 'zh-Hans': 'Chinese Simplified (简体中文)' + }; + return languages[langCode] || langCode; + } +} + +/** + * Week 1: Get changed files from git or test files + * This is the core Week 1 functionality that remains unchanged + */ +function getChangedFiles(testFiles = null) { + // Allow passing test files for local development (Week 1 feature) + if (testFiles) { + console.log('🧪 Using provided test files for local testing'); + return testFiles.filter(file => + file.startsWith('src/content/examples/en') && file.endsWith('.mdx') + ); + } + + try { + // Different git commands for different event types + const gitCommand = process.env.GITHUB_EVENT_NAME === 'pull_request' + ? 'git diff --name-only origin/main...HEAD' // Compare with base branch for PRs + : 'git diff --name-only HEAD~1 HEAD'; // Compare with previous commit for pushes + + const changedFilesOutput = execSync(gitCommand, { encoding: 'utf8' }); + const allChangedFiles = changedFilesOutput.trim().split('\n').filter(file => file.length > 0); + + const changedExampleFiles = allChangedFiles.filter(file => + file.startsWith('src/content/examples/en') && file.endsWith('.mdx') + ); + + return changedExampleFiles; + } catch (error) { + console.error('❌ Error getting changed files:', error.message); + return []; + } +} + +/** + * Scan all English example files (for manual scanning) + */ +function getAllEnglishExampleFiles() { + const examplesPath = 'src/content/examples/en'; + const allFiles = []; + + try { + if (!fs.existsSync(examplesPath)) { + console.log(`❌ Examples path does not exist: ${examplesPath}`); + return []; + } + + const scanDirectory = (dir) => { + const items = fs.readdirSync(dir); + items.forEach(item => { + const itemPath = path.join(dir, item); + if (fs.statSync(itemPath).isDirectory()) { + scanDirectory(itemPath); + } else if (item.endsWith('.mdx')) { + allFiles.push(itemPath); + } + }); + }; + + scanDirectory(examplesPath); + console.log(`📊 Found ${allFiles.length} English example files to check`); + return allFiles; + } catch (error) { + console.error('❌ Error scanning English example files:', error.message); + return []; + } +} + + +function fileExists(filePath) { + try { + return fs.existsSync(filePath); + } catch (error) { + return false; + } +} + + +function getFileModTime(filePath) { + try { + return fs.statSync(filePath).mtime; + } catch (error) { + console.log(`⚠️ Could not get file timestamp for ${filePath}: ${error.message}`); + return null; + } +} + + +async function checkTranslationStatus(changedExampleFiles, githubTracker = null, createIssues = false) { + const translationStatus = { + needsUpdate: [], + missing: [], + upToDate: [], + issuesCreated: [] + }; + + // Group translation issues by file to create single issues per file + const fileTranslationMap = new Map(); + + for (const englishFile of changedExampleFiles) { + const fileName = englishFile.split('/').pop(); + + const fileTranslations = { + englishFile, + outdatedLanguages: [], + missingLanguages: [], + upToDateLanguages: [], + englishCommit: null + }; + + for (const language of SUPPORTED_LANGUAGES) { + const translationPath = getTranslationPath(englishFile, language); + const exists = fileExists(translationPath); + + if (!exists) { + const missingItem = { + englishFile, + language, + translationPath, + status: 'missing' + }; + translationStatus.missing.push(missingItem); + fileTranslations.missingLanguages.push(missingItem); + continue; + } + + + if (githubTracker) { + // Get English commit only once per file + if (!fileTranslations.englishCommit) { + fileTranslations.englishCommit = await githubTracker.getLastCommit(englishFile); + } + const englishCommit = fileTranslations.englishCommit; + const translationCommit = await githubTracker.getLastCommit(translationPath); + + if (!englishCommit) { + continue; + } + + if (!translationCommit) { + const missingItem = { + englishFile, + language, + translationPath, + status: 'missing' + }; + translationStatus.missing.push(missingItem); + fileTranslations.missingLanguages.push(missingItem); + continue; + } + + const isOutdated = englishCommit.date > translationCommit.date; + + if (isOutdated) { + const statusItem = { + englishFile, + language, + translationPath, + status: 'outdated', + commitInfo: { + english: englishCommit, + translation: translationCommit + } + }; + + translationStatus.needsUpdate.push(statusItem); + fileTranslations.outdatedLanguages.push(statusItem); + } else { + const upToDateItem = { + englishFile, + language, + translationPath, + status: 'up-to-date' + }; + translationStatus.upToDate.push(upToDateItem); + fileTranslations.upToDateLanguages.push(upToDateItem); + } + } else { + // Week 1: Fallback to file modification time comparison + const englishModTime = getFileModTime(englishFile); + if (!englishModTime) { + console.log(` ⚠️ Could not get modification time for English file`); + continue; + } + + const translationModTime = getFileModTime(translationPath); + const isOutdated = translationModTime < englishModTime; + + if (isOutdated) { + const statusItem = { + englishFile, + language, + translationPath, + status: 'outdated', + englishModTime, + translationModTime + }; + translationStatus.needsUpdate.push(statusItem); + fileTranslations.outdatedLanguages.push(statusItem); + } else { + const upToDateItem = { + englishFile, + language, + translationPath, + status: 'up-to-date' + }; + translationStatus.upToDate.push(upToDateItem); + fileTranslations.upToDateLanguages.push(upToDateItem); + } + } + } + + // Store file translations for potential issue creation + if (fileTranslations.outdatedLanguages.length > 0 || fileTranslations.missingLanguages.length > 0) { + fileTranslationMap.set(englishFile, fileTranslations); + } + } + + // Create single issues per file (covering all affected languages) + if (createIssues && githubTracker) { + for (const [englishFile, fileTranslations] of fileTranslationMap) { + const issue = await githubTracker.createMultiLanguageTranslationIssue(fileTranslations); + if (issue) { + const issueItem = { + englishFile, + affectedLanguages: [ + ...fileTranslations.outdatedLanguages.map(l => l.language), + ...fileTranslations.missingLanguages.map(l => l.language) + ], + issueNumber: issue.number, + issueUrl: issue.html_url + }; + translationStatus.issuesCreated.push(issueItem); + } + } + } + + return translationStatus; +} + + +// Removed verbose summary function + + +// Remove verbose repository exploration + + +async function main(testFiles = null, options = {}) { + const hasToken = !!process.env.GITHUB_TOKEN; + const isGitHubAction = !!process.env.GITHUB_ACTIONS; // Detect if running in GitHub Actions + + // Default behavior: scan all files UNLESS running in GitHub Actions or test mode + const scanAll = isGitHubAction ? false : (process.env.SCAN_ALL !== 'false'); + const isProduction = hasToken && !testFiles; + + if (testFiles) { + console.log(`🧪 Test mode: Checking ${testFiles.length} predefined files`); + } else if (isGitHubAction) { + console.log(`🚀 GitHub Actions: Checking changed files only`); + } else { + console.log(`🔍 Manual run: Scanning all ${scanAll ? 'files' : 'changed files'}`); + } + + // Initialize GitHub tracker if token is available + let githubTracker = null; + if (hasToken) { + try { + const [owner, repo] = (process.env.GITHUB_REPOSITORY || 'processing/p5.js-website').split('/'); + githubTracker = new GitHubCommitTracker(process.env.GITHUB_TOKEN, owner, repo); + console.log(`📡 Connected to ${owner}/${repo}`); + } catch (error) { + console.error('❌ GitHub API failed, using file-based tracking'); + } + } + + // Get files to check + let filesToCheck; + if (scanAll && !testFiles && !isGitHubAction) { + console.log('📊 Scanning all English example files...'); + filesToCheck = getAllEnglishExampleFiles(); + } else { + filesToCheck = getChangedFiles(testFiles); + } + + if (filesToCheck.length === 0) { + if (isGitHubAction) { + console.log('✅ No English example files changed in this push'); + } else { + console.log('✅ No files to check'); + } + return; + } + + console.log(`📝 Checking ${filesToCheck.length} English example file(s):`); + filesToCheck.forEach(file => console.log(` - ${file}`)); + + const createIssues = isProduction && githubTracker !== null; + const translationStatus = await checkTranslationStatus( + filesToCheck, + githubTracker, + createIssues + ); + + // Detailed results + const { needsUpdate, missing, upToDate, issuesCreated } = translationStatus; + + console.log('\n📊 Translation Status Summary:'); + console.log(` 🔄 Outdated: ${needsUpdate.length}`); + console.log(` ❌ Missing: ${missing.length}`); + console.log(` ✅ Up-to-date: ${upToDate.length}`); + + if (needsUpdate.length > 0) { + console.log('\n🔄 Files needing translation updates:'); + needsUpdate.forEach(item => { + const langName = githubTracker ? githubTracker.getLanguageDisplayName(item.language) : item.language; + if (githubTracker && item.commitInfo) { + console.log(` - ${item.englishFile} → ${langName}`); + console.log(` English: ${item.commitInfo.english.date.toLocaleDateString()} by ${item.commitInfo.english.author}`); + console.log(` Translation: ${item.commitInfo.translation.date.toLocaleDateString()} by ${item.commitInfo.translation.author}`); + } else { + console.log(` - ${item.englishFile} → ${langName}`); + if (item.englishModTime && item.translationModTime) { + console.log(` English: ${item.englishModTime.toLocaleDateString()}`); + console.log(` Translation: ${item.translationModTime.toLocaleDateString()}`); + } + } + }); + } + + if (missing.length > 0) { + console.log('\n❌ Missing translation files:'); + missing.forEach(item => { + const langName = githubTracker ? githubTracker.getLanguageDisplayName(item.language) : item.language; + console.log(` - ${item.englishFile} → ${langName}`); + console.log(` Expected: ${item.translationPath}`); + }); + } + + if (issuesCreated.length > 0) { + console.log(`\n🎫 GitHub issues created: ${issuesCreated.length}`); + issuesCreated.forEach(issue => { + console.log(` - Issue #${issue.issueNumber}: ${issue.englishFile}`); + console.log(` Languages: ${issue.affectedLanguages.map(lang => githubTracker.getLanguageDisplayName(lang)).join(', ')}`); + console.log(` URL: ${issue.issueUrl}`); + }); + } else if (needsUpdate.length > 0 || missing.length > 0) { + if (!hasToken) { + console.log(`\n💡 Run with GITHUB_TOKEN to create GitHub issues`); + } + } + + if (needsUpdate.length === 0 && missing.length === 0) { + console.log('\n✅ All translations are up to date!'); + } +} + +// Export for testing (simplified) +module.exports = { + main, + getChangedFiles, + getAllEnglishExampleFiles, + checkTranslationStatus, + GitHubCommitTracker, + SUPPORTED_LANGUAGES +}; + +// Run if called directly +if (require.main === module) { + main(); +} diff --git a/.github/actions/translation-tracker/package-lock.json b/.github/actions/translation-tracker/package-lock.json new file mode 100644 index 0000000000..8c8d568011 --- /dev/null +++ b/.github/actions/translation-tracker/package-lock.json @@ -0,0 +1,457 @@ +{ + "name": "p5js-translation-tracker", + "version": "0.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "p5js-translation-tracker", + "version": "0.2.0", + "license": "LGPL-2.1", + "dependencies": { + "@actions/core": "^1.10.0", + "@actions/github": "^5.1.1", + "@octokit/rest": "^20.0.2", + "simple-git": "^3.24.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@actions/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "dependencies": { + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/github": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-5.1.1.tgz", + "integrity": "sha512-Nk59rMDoJaV+mHCOJPXuvB1zIbomlKS0dmSIqPGxd0enAXBnOfn4VWF+CGtRCwXZG9Epa54tZA7VIRlJDS8A6g==", + "dependencies": { + "@actions/http-client": "^2.0.1", + "@octokit/core": "^3.6.0", + "@octokit/plugin-paginate-rest": "^2.17.0", + "@octokit/plugin-rest-endpoint-methods": "^5.13.0" + } + }, + "node_modules/@actions/http-client": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", + "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==" + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" + }, + "node_modules/@octokit/auth-token": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", + "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", + "dependencies": { + "@octokit/types": "^6.0.3" + } + }, + "node_modules/@octokit/core": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz", + "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==", + "dependencies": { + "@octokit/auth-token": "^2.4.4", + "@octokit/graphql": "^4.5.8", + "@octokit/request": "^5.6.3", + "@octokit/request-error": "^2.0.5", + "@octokit/types": "^6.0.3", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/endpoint": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", + "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", + "dependencies": { + "@octokit/types": "^6.0.3", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/graphql": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", + "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", + "dependencies": { + "@octokit/request": "^5.6.0", + "@octokit/types": "^6.0.3", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "12.11.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", + "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "2.21.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.21.3.tgz", + "integrity": "sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw==", + "dependencies": { + "@octokit/types": "^6.40.0" + }, + "peerDependencies": { + "@octokit/core": ">=2" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "5.16.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.16.2.tgz", + "integrity": "sha512-8QFz29Fg5jDuTPXVtey05BLm7OB+M8fnvE64RNegzX7U+5NUXcOcnpTIK0YfSHBg8gYd0oxIq3IZTe9SfPZiRw==", + "dependencies": { + "@octokit/types": "^6.39.0", + "deprecation": "^2.3.1" + }, + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/request": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", + "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", + "dependencies": { + "@octokit/endpoint": "^6.0.1", + "@octokit/request-error": "^2.1.0", + "@octokit/types": "^6.16.1", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/request-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", + "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", + "dependencies": { + "@octokit/types": "^6.0.3", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "node_modules/@octokit/rest": { + "version": "20.1.2", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.1.2.tgz", + "integrity": "sha512-GmYiltypkHHtihFwPRxlaorG5R9VAHuk/vbszVoRTGXnAsY60wYLkh/E2XiFmdZmqrisw+9FaazS1i5SbdWYgA==", + "dependencies": { + "@octokit/core": "^5.0.2", + "@octokit/plugin-paginate-rest": "11.4.4-cjs.2", + "@octokit/plugin-request-log": "^4.0.0", + "@octokit/plugin-rest-endpoint-methods": "13.3.2-cjs.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/core": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz", + "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "dependencies": { + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==" + }, + "node_modules/@octokit/rest/node_modules/@octokit/plugin-paginate-rest": { + "version": "11.4.4-cjs.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.4-cjs.2.tgz", + "integrity": "sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw==", + "dependencies": { + "@octokit/types": "^13.7.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/plugin-request-log": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-4.0.1.tgz", + "integrity": "sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "13.3.2-cjs.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.2-cjs.1.tgz", + "integrity": "sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ==", + "dependencies": { + "@octokit/types": "^13.8.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/types": { + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", + "dependencies": { + "@octokit/openapi-types": "^12.11.0" + } + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/simple-git": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.28.0.tgz", + "integrity": "sha512-Rs/vQRwsn1ILH1oBUy8NucJlXmnnLeLCfcvbSehkPzbv3wwoFWIdtfd6Ndo6ZPhlPsCZ60CPI4rxurnwAa+a2w==", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + } + } +} diff --git a/.github/actions/translation-tracker/package.json b/.github/actions/translation-tracker/package.json new file mode 100644 index 0000000000..a6ea6d8bf3 --- /dev/null +++ b/.github/actions/translation-tracker/package.json @@ -0,0 +1,27 @@ +{ + "name": "p5js-translation-tracker", + "version": "0.2.0", + "description": "GitHub Action to track translation status for p5.js examples and documentation", + "main": "index.js", + "scripts": { + "start": "node index.js", + "test": "node test-local.js" + }, + "keywords": [ + "p5.js", + "translation", + "documentation", + "github-actions", + "automation" + ], + "author": "Divyansh Srivastava", + "license": "LGPL-2.1", + "engines": { + "node": ">=18.0.0" + }, + "dependencies": { + "@actions/core": "^1.10.0", + "@actions/github": "^5.1.1", + "@octokit/rest": "^19.0.5" + } +} \ No newline at end of file diff --git a/.github/actions/translation-tracker/test-local.js b/.github/actions/translation-tracker/test-local.js new file mode 100644 index 0000000000..50a7a2c993 --- /dev/null +++ b/.github/actions/translation-tracker/test-local.js @@ -0,0 +1,14 @@ + +const { main } = require('./index.js'); + +// Simple test with predefined files +const testFiles = [ + 'src/content/examples/en/01_Shapes_And_Color/00_Shape_Primitives/description.mdx', + 'src/content/examples/en/02_Animation_And_Variables/00_Drawing_Lines/description.mdx', + 'src/content/examples/en/03_Imported_Media/00_Words/description.mdx' +]; + +console.log('🧪 Testing Translation Tracker with predefined files'); +console.log('===================================================='); + +main(testFiles, { createIssues: false }); \ No newline at end of file diff --git a/.github/workflows/translation-sync.yml b/.github/workflows/translation-sync.yml new file mode 100644 index 0000000000..c309e60012 --- /dev/null +++ b/.github/workflows/translation-sync.yml @@ -0,0 +1,45 @@ +name: Translation Sync Tracker + +on: + push: + branches: [main, week2] + paths: + - 'src/content/examples/en/**' + pull_request: + branches: [main, week2] + paths: + - 'src/content/examples/en/**' + workflow_dispatch: + inputs: + scan_all: + description: 'Scan all files instead of just changed files' + required: false + default: false + type: boolean + +jobs: + track-translation-changes: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 2 # Fetch previous commit to compare changes + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Install dependencies + run: npm ci + + - name: Install translation tracker dependencies + run: cd .github/actions/translation-tracker && npm install + + - name: Run translation tracker + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SCAN_ALL: ${{ inputs.scan_all || false }} + run: node .github/actions/translation-tracker/index.js \ No newline at end of file diff --git a/src/components/OutdatedTranslationBanner/index.astro b/src/components/OutdatedTranslationBanner/index.astro new file mode 100644 index 0000000000..6e1c552dbe --- /dev/null +++ b/src/components/OutdatedTranslationBanner/index.astro @@ -0,0 +1,51 @@ +--- +interface Props { + englishUrl?: string; + contributeUrl?: string; + title?: string; + message?: string; +} + +const { + englishUrl = '/en', + contributeUrl = 'https://github.com/processing/p5.js-website', + title = 'यह अनुवाद पुराना हो सकता है', + message = 'यह पृष्ठ अंग्रेज़ी संस्करण की तुलना में अद्यतन नहीं है। कृपया अंग्रेज़ी पृष्ठ देखें या अनुवाद सुधारने में मदद करें।', +} = Astro.props as Props; +--- + +
+ +
+
{title}
+

+ {message} + + अंग्रेज़ी संस्करण देखें + · + अनुवाद में योगदान दें +

+
+ + +
+ + diff --git a/src/content/examples/en/01_Shapes_And_Color/00_Shape_Primitives/description.mdx b/src/content/examples/en/01_Shapes_And_Color/00_Shape_Primitives/description.mdx index 428f348709..6ff16675b2 100644 --- a/src/content/examples/en/01_Shapes_And_Color/00_Shape_Primitives/description.mdx +++ b/src/content/examples/en/01_Shapes_And_Color/00_Shape_Primitives/description.mdx @@ -9,7 +9,7 @@ relatedReference: --- This program demonstrates the use of the basic shape -primitive functions +primitive functions. Manual trigger testing: this should be detected by git. square(), rect(), ellipse(), diff --git a/src/layouts/ExampleLayout.astro b/src/layouts/ExampleLayout.astro index cc31a5d2bf..48fe9ed151 100644 --- a/src/layouts/ExampleLayout.astro +++ b/src/layouts/ExampleLayout.astro @@ -10,6 +10,7 @@ import { import BaseLayout from "./BaseLayout.astro"; import EditableSketch from "@components/EditableSketch/index.astro"; import RelatedItems from "@components/RelatedItems/index.astro"; +import OutdatedTranslationBanner from "@components/OutdatedTranslationBanner/index.astro"; interface Props { example: CollectionEntry<"examples">; @@ -59,6 +60,9 @@ const { Content } = await example.render(); topic="examples" className="example" > + {currentLocale === 'hi' ? ( + + ) : null}