diff --git a/.github/actions/translation-tracker/index.js b/.github/actions/translation-tracker/index.js index dc0d839d8c..220f5deddd 100644 --- a/.github/actions/translation-tracker/index.js +++ b/.github/actions/translation-tracker/index.js @@ -2,7 +2,10 @@ const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); const { Octokit } = require('@octokit/rest'); +const yaml = require('js-yaml'); +const SUPPORTED_LANGUAGES = ['es', 'hi', 'ko', 'zh-Hans']; +const CONTENT_TYPES = ['examples', 'reference', 'tutorials', 'text-detail', 'events', 'libraries']; function getTranslationPath(englishFilePath, language) { // Ensure we have a valid English path @@ -25,10 +28,43 @@ function getTranslationPath(englishFilePath, language) { return translationParts.join('/'); } +function getSlugFromEnglishPath(englishFilePath, contentType) { + const prefix = `src/content/${contentType}/en/`; + if (!englishFilePath.startsWith(prefix)) return null; + let relative = englishFilePath.substring(prefix.length); + + if (relative.endsWith('/description.mdx')) { + relative = relative.slice(0, -'/description.mdx'.length); + } else if (relative.endsWith('.mdx')) { + relative = relative.slice(0, -'.mdx'.length); + } else if (relative.endsWith('.yaml')) { + relative = relative.slice(0, -'.yaml'.length); + } + return relative; +} +function loadStewardsConfig() { + try { + const stewardsPath = path.join(process.cwd(), '.github', 'stewards.yml'); + if (fs.existsSync(stewardsPath)) { + const stewardsContent = fs.readFileSync(stewardsPath, 'utf8'); + return yaml.load(stewardsContent); + } + } catch (error) { + console.log(`⚠️ Could not load stewards config: ${error.message}`); + } + return null; +} -const SUPPORTED_LANGUAGES = ['es', 'hi', 'ko', 'zh-Hans']; - +function getStewardsForLanguage(stewardsConfig, language) { + if (!stewardsConfig || !stewardsConfig.stewards) return []; + + const stewards = stewardsConfig.stewards.filter(s => + s.languages && s.languages.includes(language) + ); + + return stewards.map(s => `@${s.github}`); +} class GitHubCommitTracker { constructor(token, owner, repo) { @@ -36,6 +72,7 @@ class GitHubCommitTracker { this.owner = owner; this.repo = repo; this.currentBranch = this.detectCurrentBranch(); + this.stewardsConfig = loadStewardsConfig(); } /** @@ -49,7 +86,7 @@ class GitHubCommitTracker { } if (process.env.GITHUB_REF_NAME) { - return process.env.GITHUB_REF_NAME; // For push events + return process.env.GITHUB_REF_NAME; } // Git command fallback @@ -216,32 +253,6 @@ class GitHubCommitTracker { } } - /** - * 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)}`; @@ -262,60 +273,52 @@ class GitHubCommitTracker { labels.push(`lang-${lang}`); }); + let assignees = []; + uniqueLanguages.forEach(lang => { + const stewards = getStewardsForLanguage(this.stewardsConfig, lang); + assignees.push(...stewards); + }); + assignees = [...new Set(assignees.map(a => a.replace('@', '')))]; + try { - const { data } = await this.octokit.rest.issues.create({ + const createParams = { owner: this.owner, repo: this.repo, title: issueTitle, body: issueBody, labels: labels - }); + }; + + if (assignees.length > 0) { + createParams.assignees = assignees; + } + + const { data } = await this.octokit.rest.issues.create(createParams); return data; } catch (error) { + // If assignees fail, try again without assignees + if (error.message.includes('assignees') && assignees.length > 0) { + try { + const { data } = await this.octokit.rest.issues.create({ + owner: this.owner, + repo: this.repo, + title: issueTitle, + body: issueBody, + labels: labels + }); + console.log(`⚠️ Issue created but stewards could not be assigned (not collaborators)`); + return data; + } catch (retryError) { + console.error(`❌ Error creating issue on retry:`, retryError.message); + return null; + } + } 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 */ @@ -339,9 +342,10 @@ class GitHubCommitTracker { 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`; + const stewards = getStewardsForLanguage(this.stewardsConfig, lang.language); + const stewardsText = stewards.length > 0 ? ` (cc ${stewards.join(', ')})` : ''; + body += `- **${this.getLanguageDisplayName(lang.language)}**: Last updated ${lang.commitInfo.translation.date.toLocaleDateString()} by ${lang.commitInfo.translation.author}${stewardsText}\n`; + body += ` - [📝 View file](https://github.com/${this.owner}/${this.repo}/blob/${this.currentBranch}/${translationPath})\n\n`; }); } @@ -350,24 +354,13 @@ class GitHubCommitTracker { body += `### ❌ Missing Translations\n\n`; missingLanguages.forEach(lang => { const translationPath = lang.translationPath; - body += `- **${this.getLanguageDisplayName(lang.language)}**: Translation file does not exist\n`; + const stewards = getStewardsForLanguage(this.stewardsConfig, lang.language); + const stewardsText = stewards.length > 0 ? ` (cc ${stewards.join(', ')})` : ''; + body += `- **${this.getLanguageDisplayName(lang.language)}**: Translation file does not exist${stewardsText}\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}) @@ -407,10 +400,6 @@ ${outdatedLanguages.length > 0 || missingLanguages.length > 0 ? `**Change Type** } } -/** - * Week 1: Get changed files from git or test files - * This is the core Week 1 functionality that remains unchanged - */ /** * Get changed files from git or test files (generalized for different content types) */ @@ -419,7 +408,7 @@ function getChangedFiles(testFiles = null, contentType = 'examples') { if (testFiles) { console.log('🧪 Using provided test files for local testing'); return testFiles.filter(file => - file.startsWith(`src/content/${contentType}/en`) && file.endsWith('.mdx') + file.startsWith(`src/content/${contentType}/en`) && (file.endsWith('.mdx') || file.endsWith('.yaml')) ); } @@ -433,7 +422,7 @@ function getChangedFiles(testFiles = null, contentType = 'examples') { const allChangedFiles = changedFilesOutput.trim().split('\n').filter(file => file.length > 0); const changedContentFiles = allChangedFiles.filter(file => - file.startsWith(`src/content/${contentType}/en`) && file.endsWith('.mdx') + file.startsWith(`src/content/${contentType}/en`) && (file.endsWith('.mdx') || file.endsWith('.yaml')) ); return changedContentFiles; @@ -462,7 +451,7 @@ function getAllEnglishContentFiles(contentType = 'examples') { const itemPath = path.join(dir, item); if (fs.statSync(itemPath).isDirectory()) { scanDirectory(itemPath); - } else if (item.endsWith('.mdx')) { + } else if (item.endsWith('.mdx') || item.endsWith('.yaml')) { allFiles.push(itemPath); } }); @@ -477,14 +466,6 @@ function getAllEnglishContentFiles(contentType = 'examples') { } } -/** - * Scan all English example files (for backward compatibility) - */ -function getAllEnglishExampleFiles() { - return getAllEnglishContentFiles('examples'); -} - - function fileExists(filePath) { try { return fs.existsSync(filePath); @@ -504,7 +485,7 @@ function getFileModTime(filePath) { } -async function checkTranslationStatus(changedExampleFiles, githubTracker = null, createIssues = false) { +async function checkTranslationStatus(changedFiles, githubTracker = null, createIssues = false) { const translationStatus = { needsUpdate: [], missing: [], @@ -516,7 +497,7 @@ async function checkTranslationStatus(changedExampleFiles, githubTracker = null, // Group translation issues by file to create single issues per file const fileTranslationMap = translationStatus.fileTranslationMap; - for (const englishFile of changedExampleFiles) { + for (const englishFile of changedFiles) { const fileTranslations = { englishFile, outdatedLanguages: [], @@ -687,103 +668,133 @@ async function main(testFiles = null, options = {}) { } } - // Get files to check (currently focused on examples) - const contentType = 'examples'; - let filesToCheck; - if (testFiles) { - filesToCheck = getChangedFiles(testFiles, contentType); - } else if (isGitHubAction) { - filesToCheck = getChangedFiles(null, contentType); - } else { - console.log(`📊 Scanning all English ${contentType} files...`); - filesToCheck = getAllEnglishContentFiles(contentType); - } - - if (filesToCheck.length === 0) { - if (isGitHubAction) { - console.log('✅ No English example files changed in this push'); + const allTranslationStatus = []; + + for (const contentType of CONTENT_TYPES) { + let filesToCheck; + if (testFiles) { + filesToCheck = getChangedFiles(testFiles, contentType); + } else if (isGitHubAction) { + filesToCheck = getChangedFiles(null, contentType); } else { - console.log('✅ No files to check'); + console.log(`📊 Scanning all English ${contentType} files...`); + filesToCheck = getAllEnglishContentFiles(contentType); } - 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 - ); + + if (filesToCheck.length === 0) { + continue; + } + + console.log(`\n📝 Checking ${filesToCheck.length} English ${contentType} file(s):`); + filesToCheck.forEach(file => console.log(` - ${file}`)); + + const createIssues = isProduction && githubTracker !== null; + const translationStatus = await checkTranslationStatus( + filesToCheck, + githubTracker, + createIssues + ); + + allTranslationStatus.push({ contentType, translationStatus }); - // 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()}`); + const { needsUpdate, missing, upToDate, issuesCreated } = translationStatus; + + console.log(`\n📊 Translation Status Summary for ${contentType}:`); + 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 (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!'); - } + + if (needsUpdate.length === 0 && missing.length === 0) { + console.log(`\n✅ All ${contentType} translations are up to date!`); + } + // Write manifest JSON for the site to consume + try { + const manifestDir = path.join(process.cwd(), 'public', 'translation-status'); + const manifestPath = path.join(manifestDir, `${contentType}.json`); + if (!fs.existsSync(manifestDir)) { + fs.mkdirSync(manifestDir, { recursive: true }); + } + const content = {}; + for (const [englishFile, fileTranslations] of translationStatus.fileTranslationMap) { + const slug = getSlugFromEnglishPath(englishFile, contentType); + if (!slug) continue; + const outdated = fileTranslations.outdatedLanguages.map(l => l.language); + const missingLangs = fileTranslations.missingLanguages.map(l => l.language); + const upToDateLangs = fileTranslations.upToDateLanguages.map(l => l.language); + content[slug] = { + englishFile, + outdated, + missing: missingLangs, + upToDate: upToDateLangs, + }; + } + const manifest = { + generatedAt: new Date().toISOString(), + branch: githubTracker ? githubTracker.currentBranch : null, + contentType, + [contentType]: content, + }; + fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8'); + console.log(`\n🗂️ Wrote ${contentType} translation manifest: ${manifestPath}`); + } catch (writeErr) { + console.log(`\n⚠️ Could not write ${contentType} translation manifest: ${writeErr.message}`); + } + } } // Export for testing (simplified) module.exports = { main, getChangedFiles, - getAllEnglishExampleFiles, getAllEnglishContentFiles, checkTranslationStatus, GitHubCommitTracker, - SUPPORTED_LANGUAGES + SUPPORTED_LANGUAGES, + CONTENT_TYPES }; // Run if called directly if (require.main === module) { main(); -} +} diff --git a/.github/actions/translation-tracker/package.json b/.github/actions/translation-tracker/package.json index f94aa54561..74594d8db0 100644 --- a/.github/actions/translation-tracker/package.json +++ b/.github/actions/translation-tracker/package.json @@ -22,6 +22,7 @@ "dependencies": { "@actions/core": "^1.10.0", "@actions/github": "^5.1.1", - "@octokit/rest": "^19.0.5" + "@octokit/rest": "^19.0.5", + "js-yaml": "^4.1.0" } -} \ No newline at end of file +} \ No newline at end of file diff --git a/.github/stewards.yml b/.github/stewards.yml new file mode 100644 index 0000000000..ce5653f10f --- /dev/null +++ b/.github/stewards.yml @@ -0,0 +1,22 @@ +stewards: + - name: Divyansh013 + github: Divyansh013 + languages: + - hi + - name: takshittt + github: takshittt + languages: + - hi + - name: hana-cho + github: hana-cho + languages: + - ko + - name: limzykenneth + github: limzykenneth + languages: + - zh-Hans + - name: lirenjie95 + github: lirenjie95 + languages: + - zh-Hans + diff --git a/.github/workflows/translation-sync.yml b/.github/workflows/translation-sync.yml index 926a6ec8dd..603d59685b 100644 --- a/.github/workflows/translation-sync.yml +++ b/.github/workflows/translation-sync.yml @@ -2,9 +2,23 @@ name: Translation Sync Tracker on: push: - branches: [staging] + branches: [main] paths: - 'src/content/examples/en/**' + - 'src/content/reference/en/**' + - 'src/content/tutorials/en/**' + - 'src/content/text-detail/en/**' + - 'src/content/events/en/**' + - 'src/content/libraries/en/**' + pull_request: + branches: [main] + paths: + - 'src/content/examples/en/**' + - 'src/content/reference/en/**' + - 'src/content/tutorials/en/**' + - 'src/content/text-detail/en/**' + - 'src/content/events/en/**' + - 'src/content/libraries/en/**' workflow_dispatch: jobs: diff --git a/src/components/OutdatedTranslationBanner/index.astro b/src/components/OutdatedTranslationBanner/index.astro new file mode 100644 index 0000000000..2140bdfd4b --- /dev/null +++ b/src/components/OutdatedTranslationBanner/index.astro @@ -0,0 +1,88 @@ +--- +interface Props { + englishUrl?: string; + contributeUrl?: string; + title?: string; + message?: string; + locale?: string; +} + +const { + englishUrl = '/en', + contributeUrl = 'https://github.com/processing/p5.js-website/tree/main?tab=readme-ov-file#content-changes', + title, + message, + locale = 'en', +} = Astro.props as Props; + +const copyByLocale: Record = { + 'hi': { + title: 'यह अनुवाद पुराना हो सकता है', + message: 'यह पृष्ठ अंग्रेज़ी संस्करण की तुलना में अद्यतन नहीं है।', + viewEnglish: 'अंग्रेज़ी संस्करण देखें', + contribute: 'अनुवाद में योगदान दें', + }, + 'es': { + title: 'Esta traducción podría estar desactualizada', + message: 'Esta página no está actualizada en comparación con la versión en inglés.', + viewEnglish: 'Ver versión en inglés', + contribute: 'Contribuir a la traducción', + }, + 'ko': { + title: '이 번역은 오래되었을 수 있습니다', + message: '이 페이지는 영어 버전과 비교하여 최신 상태가 아닙니다.', + viewEnglish: '영어 페이지 보기', + contribute: '번역에 기여하기', + }, + 'zh-Hans': { + title: '此翻译可能已过期', + message: '与英文版本相比,此页面不是最新的。', + viewEnglish: '查看英文页面', + contribute: '参与翻译', + }, +}; + +const fallback = { + title: 'This translation might be outdated', + message: 'This page is not updated compared to the English version.', + viewEnglish: 'View English page', + contribute: 'Contribute to translation', +}; + +const copy = copyByLocale[locale] || fallback; +const resolvedTitle = title ?? copy.title; +const resolvedMessage = message ?? copy.message; +--- + +
+ +
+
{resolvedTitle}
+

+ {resolvedMessage} + {copy.viewEnglish} + · + {copy.contribute} +

+
+ + +
+ 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..21fc53fd9d 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 @@ -17,4 +17,4 @@ primitive functions arc(), line(), triangle(), -and quad(). \ No newline at end of file +and quad(). diff --git a/src/layouts/EventLayout.astro b/src/layouts/EventLayout.astro index f446191678..d214e12ce8 100644 --- a/src/layouts/EventLayout.astro +++ b/src/layouts/EventLayout.astro @@ -8,6 +8,8 @@ import RelatedItems from "@components/RelatedItems/index.astro"; import { setJumpToState } from "../globals/state"; import { getCurrentLocale, getUiTranslator } from "../i18n/utils"; import { getRelatedEntriesinCollection } from "../pages/_utils"; +import OutdatedTranslationBanner from "@components/OutdatedTranslationBanner/index.astro"; +import { checkTranslationBanner } from "../utils/translationBanner"; interface Props { entry: CollectionEntry<"events">; @@ -31,9 +33,17 @@ const relatedEvents = ? await getRelatedEntriesinCollection( "events", currentLocale, - entry.data.relatedPastEvents.map((r) => r.slug) + entry.data.relatedPastEvents.map((r: any) => r.slug) ) : []; + +const { showBanner, englishUrl } = checkTranslationBanner( + 'events', + entry.id, + currentLocale, + Astro.url.pathname, + Astro.url.origin +); --- + {showBanner && } { entry.data.featuredImage && entry.data.featuredImageAlt && ( ; @@ -42,6 +44,14 @@ const relatedReferences = : []; const { Content } = await example.render(); + +const { showBanner, englishUrl } = checkTranslationBanner( + 'examples', + example.id, + currentLocale, + Astro.url.pathname, + Astro.url.origin +); --- + {showBanner && }
diff --git a/src/layouts/ReferenceItemLayout.astro b/src/layouts/ReferenceItemLayout.astro index 60d7046f9f..9c581c9fc8 100644 --- a/src/layouts/ReferenceItemLayout.astro +++ b/src/layouts/ReferenceItemLayout.astro @@ -21,6 +21,8 @@ import { setJumpToState } from "../globals/state"; import { p5Version } from "../globals/p5-version"; import flask from "@src/content/ui/images/icons/flask.svg?raw"; import warning from "@src/content/ui/images/icons/warning.svg?raw"; +import OutdatedTranslationBanner from "@components/OutdatedTranslationBanner/index.astro"; +import { checkTranslationBanner } from "../utils/translationBanner"; const { entry, relatedEntries } = Astro.props; const currentLocale = getCurrentLocale(Astro.url.pathname); @@ -81,6 +83,13 @@ const descriptionParts = description.split( /(
[\s\S]+?<\/code><\/pre>)/gm
 );
 
+const { showBanner, englishUrl } = checkTranslationBanner(
+  'reference',
+  entry.id,
+  currentLocale,
+  Astro.url.pathname,
+  Astro.url.origin
+);
 ---
 
 
@@ -93,6 +102,7 @@ const descriptionParts = description.split(
   topic="reference"
   className="reference-item"
 >
+  {showBanner && }
   
{entry.data.beta && ( diff --git a/src/layouts/TextDetailLayout.astro b/src/layouts/TextDetailLayout.astro index 3aac942cb0..ff6c268382 100644 --- a/src/layouts/TextDetailLayout.astro +++ b/src/layouts/TextDetailLayout.astro @@ -4,6 +4,8 @@ import BaseLayout from "./BaseLayout.astro"; import type { CollectionEntry } from "astro:content"; import { setJumpToState } from "../globals/state"; import { getCurrentLocale } from "../i18n/utils"; +import OutdatedTranslationBanner from "@components/OutdatedTranslationBanner/index.astro"; +import { checkTranslationBanner } from "../utils/translationBanner"; interface Props { page: CollectionEntry<"text-detail">; @@ -19,6 +21,14 @@ const pageTopic = Astro.url.pathname.includes("donate") const currentLocale = getCurrentLocale(Astro.url.pathname); setJumpToState(null); + +const { showBanner, englishUrl } = checkTranslationBanner( + 'text-detail', + page.id, + currentLocale, + Astro.url.pathname, + Astro.url.origin +); --- @@ -30,6 +40,7 @@ setJumpToState(null); className={pageTopic} subtitle={null} > + {showBanner && }
diff --git a/src/layouts/TutorialLayout.astro b/src/layouts/TutorialLayout.astro index e812a7030c..5cafe26ba4 100644 --- a/src/layouts/TutorialLayout.astro +++ b/src/layouts/TutorialLayout.astro @@ -11,6 +11,8 @@ import { generateJumpToState, getRelatedEntriesinCollection, } from "../pages/_utils"; +import OutdatedTranslationBanner from "@components/OutdatedTranslationBanner/index.astro"; +import { checkTranslationBanner } from "../utils/translationBanner"; const { entry } = Astro.props; const { Content, components } = await entry.render(); @@ -44,6 +46,14 @@ const relatedExamples = entry.data.relatedContent.examples.map((r: any) => r.slug) ) : []; + +const { showBanner, englishUrl } = checkTranslationBanner( + 'tutorials', + entry.id, + currentLocale, + Astro.url.pathname, + Astro.url.origin +); --- + {showBanner && } {entry.data.authors &&
By {entry.data.authors.join(", ")}
} {entry.data.authorsNote && {entry.data.authorsNote}}
diff --git a/src/utils/translationBanner.ts b/src/utils/translationBanner.ts new file mode 100644 index 0000000000..bcad017e69 --- /dev/null +++ b/src/utils/translationBanner.ts @@ -0,0 +1,59 @@ +import fs from 'fs'; +import path from 'path'; + +interface BannerCheckResult { + showBanner: boolean; + englishUrl: string; +} + +export function checkTranslationBanner( + contentType: string, + itemId: string, + currentLocale: string, + currentPathname: string, + origin: string +): BannerCheckResult { + let showBanner = false; + let englishUrl = currentPathname; + + if (currentLocale === 'en') { + return { showBanner: false, englishUrl }; + } + + englishUrl = currentPathname.replace(`/${currentLocale}/`, '/'); + + if (!englishUrl.startsWith('http')) { + englishUrl = `${origin}${englishUrl}`; + } + + try { + const manifestPath = path.join(process.cwd(), 'public', 'translation-status', `${contentType}.json`); + if (fs.existsSync(manifestPath)) { + const raw = fs.readFileSync(manifestPath, 'utf8'); + const manifest = JSON.parse(raw); + + const idNoLocale = itemId.replace(/^[\w-]+\//, ''); + const withoutExt = idNoLocale.replace(/\.(mdx?|ya?ml)$/, ''); + const keyWithDescription = withoutExt; + const keyWithoutDescription = withoutExt.replace(/\/description$/, ''); + + const entry = manifest[contentType]?.[keyWithoutDescription] || manifest[contentType]?.[keyWithDescription]; + + if (entry) { + const isOutdated = Array.isArray(entry.outdated) && entry.outdated.includes(currentLocale); + const isMissing = Array.isArray(entry.missing) && entry.missing.includes(currentLocale); + showBanner = isOutdated || isMissing; + + if (isMissing) { + const missingEnglishUrl = currentPathname.replace(`/${currentLocale}/`, '/'); + englishUrl = `${origin}${missingEnglishUrl}`; + } + } + } + } catch (e) { + console.error('Error checking translation banner:', e); + } + + return { showBanner, englishUrl }; +} +