From e2b610a0f74d1b179272b61fa764aefe793db1a7 Mon Sep 17 00:00:00 2001 From: Gustavo Bordoni Date: Mon, 19 Oct 2026 04:54:10 +0000 Subject: [PATCH] Fix release: download translations one at a time pup i18n sends all 71 translation requests at once, and translate.wordpress.org now answers with 429 Too Many Requests, which failed the 0.10.0 release build three times. .github/scripts/i18n.ts reads the same .puprc i18n config, fetches one file at a time, and backs off on 429 and 5xx. Also strips trailing whitespace from zip.yml. --- .github/scripts/i18n.ts | 81 +++++++++++++++++++++++++++++++++++++++ .github/workflows/zip.yml | 11 +++--- 2 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 .github/scripts/i18n.ts diff --git a/.github/scripts/i18n.ts b/.github/scripts/i18n.ts new file mode 100644 index 00000000..5b0d60db --- /dev/null +++ b/.github/scripts/i18n.ts @@ -0,0 +1,81 @@ +// Downloads the translation files listed by the `i18n` entries in .puprc, the same files `pup i18n` fetches. +// pup sends every request at once, and translate.wordpress.org now answers that burst with 429 Too Many +// Requests, which fails the release. This fetches one file at a time and backs off on 429 and 5xx. +// Usage: bun .github/scripts/i18n.ts +import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; + +type I18nConfig = { + path: string; + url: string; + slug: string; + textdomain: string; + formats?: string[]; + file_format?: string; + filter?: { minimum_percentage?: number }; +}; + +type TranslationSet = { + locale: string; + slug: string; + wp_locale?: string; + current_count: number; + percent_translated: number; +}; + +const puprc = JSON.parse( readFileSync( ".puprc", "utf8" ) ); +const configs: I18nConfig[] = [ puprc.i18n ].flat().filter( Boolean ); +const attempts = 6; +const pause = 500; + +async function get( url: string ): Promise { + for ( let attempt = 1; ; attempt++ ) { + const res = await fetch( url ); + if ( res.ok || attempt === attempts || ( res.status !== 429 && res.status < 500 ) ) { + return res; + } + const wait = Number( res.headers.get( "retry-after" ) ) * 1000 || 2000 * 2 ** attempt; + console.log( ` ${ res.status } on ${ url }, retrying in ${ wait / 1000 }s` ); + await Bun.sleep( wait ); + } +} + +for ( const config of configs ) { + const projectUrl = `${ config.url }/api/projects/${ config.slug }`; + const fileFormat = config.file_format ?? "%textdomain%-%wp_locale%.%format%"; + const minimum = config.filter?.minimum_percentage ?? 30; + console.log( `Fetching language files for ${ config.textdomain } from ${ config.url }` ); + + const project = await get( projectUrl ); + if ( ! project.ok ) { + throw new Error( `Failed to fetch project data from ${ projectUrl }: ${ project.status }` ); + } + const sets: TranslationSet[] = ( ( await project.json() ).translation_sets ?? [] ).filter( + ( set: TranslationSet ) => set.current_count > 0 && set.percent_translated >= minimum + ); + if ( ! sets.length ) { + throw new Error( `No translation sets at ${ minimum }% or more in ${ projectUrl }` ); + } + + mkdirSync( config.path, { recursive: true } ); + for ( const set of sets ) { + for ( const format of config.formats ?? [ "mo" ] ) { + const url = `${ projectUrl }/${ set.locale }/${ set.slug }/export-translations?format=${ format }`; + const res = await get( url ); + const body = new Uint8Array( await res.arrayBuffer() ); + // pup treats anything under 200 bytes as a failed export. + if ( ! res.ok || body.byteLength < 200 ) { + throw new Error( `Failed to fetch ${ url }: ${ res.status }, ${ body.byteLength } bytes` ); + } + const file = fileFormat + .replace( "%domainPath%", config.path ) + .replace( "%textdomain%", config.textdomain ) + .replace( "%locale%", set.locale ) + .replace( "%wp_locale%", set.wp_locale ?? "" ) + .replace( "%format%", format ); + writeFileSync( `${ config.path }/${ file }`, body ); + console.log( ` ${ file } (${ set.percent_translated }%)` ); + await Bun.sleep( pause ); + } + } + console.log( `Saved ${ sets.length } translation sets to ${ config.path }/` ); +} diff --git a/.github/workflows/zip.yml b/.github/workflows/zip.yml index a0cab55a..456592fe 100644 --- a/.github/workflows/zip.yml +++ b/.github/workflows/zip.yml @@ -46,8 +46,9 @@ jobs: - name: pup check run: composer -- pup check - - name: pup i18n - run: composer -- pup i18n + # Replaces `pup i18n`: pup requests every locale at once and translate.wordpress.org answers with 429. + - name: download translations + run: bun .github/scripts/i18n.ts - name: get version if: ${{ inputs.production }} @@ -80,16 +81,16 @@ jobs: echo "Working directory: $(pwd)" echo "Listing all directories:" find . -type d -name "*pup*" -o -name "*zip*" | grep -v "node_modules" - + echo "Searching for any ZIP files:" find . -name "*.zip" | sort echo "Directory structure from root:" ls -la - + - name: Upload plugin artifact uses: actions/upload-artifact@v7 with: include-hidden-files: true name: ${{ env.ZIP_NAME }} - path: .pup-zip/ \ No newline at end of file + path: .pup-zip/