+ {message} + + अंग्रेज़ी संस्करण देखें + · + अनुवाद में योगदान दें +
+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;
+---
+
+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' ? (
+