Skip to content

Chore: Build pipeline fix and dev env cleanup#2933

Merged
bartech merged 20 commits intotrunkfrom
chore/dev-env-cleanup
Mar 6, 2026
Merged

Chore: Build pipeline fix and dev env cleanup#2933
bartech merged 20 commits intotrunkfrom
chore/dev-env-cleanup

Conversation

@bartech
Copy link
Copy Markdown
Collaborator

@bartech bartech commented Feb 25, 2026

Description

Fixes the build i18n translation that resulted with pushing broken translation files
See https://plugins.trac.wordpress.org/browser/woocommerce-services/#tags/3.5.0/i18n/languages
All .po, .mo files of 162 bytes size contents is

<html>
<head><title>429 Too Many Requests</title></head>
<body>
<center><h1>429 Too Many Requests</h1></center>
<hr><center>nginx</center>
</body>
</html>

Modernises the local development environment and static-analysis toolchain without touching any plugin functionality. The goal is to bring all dev tooling up to current standards so new contributors can spin up quickly and PHPCS/PHPUnit reflect the actual PHP/WP versions we target.

Also includes several build-pipeline fixes discovered while validating the changes in CI.

How to test:

  • Translations changes
    • see if pluging build works
    • see if i18n/languages folder contain only .json files.
    • test built plugin package, see if it activates and still work
  • docker env changes
    • run npm run up it should work

Changes at a glance:

  • Docker: switch WordPress container from PHP 7.3 → PHP 8.4 (Dockerfile-php84), update WP-CLI user from xfswww-data, add --ssl=0 to wp db check calls to avoid TLS handshake noise.
  • PHPCS rules (phpcs.xml.dist, .phpcs.security.xml): bump minimum WP version 4.7 → 6.7, PHP compatibility target 5.6– → 7.4–, exclude docker/ and wp-calypso/ directories, remove stale rule exceptions (DisallowShortArraySyntax / NotHyphenatedLowercase for classes/), fix text_domain property syntax, add trailing newline.
  • Composer (composer.json, composer.lock): bump all Jetpack and WPCS packages to latest stable versions, add new dev dependencies (phpstan, php-parallel-lint, php-stubs/wordpress-stubs, phpcs-variable-analysis, ergebnis/composer-normalize), normalise JSON indentation (tabs), add missing allow-plugins entries.
  • Composer scripts (composer.json): fix phpcs and phpcbf script definitions so they execute correctly.
  • .gitignore: add .phpunit.result.cache.
  • i18n/: remove stale generated artefacts (woocommerce-services.pot, strings.php).
  • Build fix (package.json, npm-shrinkwrap.json): add react-modal to dev dependencies — it was missing, causing webpack to fail in CI.
  • i18n build pipeline (tasks/i18n.js, tasks/i18n-download.js): two fixes found when running npm run build in CI:
    1. tasks/i18n.js — create i18n/languages/ directory if it doesn't exist. CI starts from a clean checkout so the gitignored i18n/ directory is absent, causing a hard crash before this fix.
    2. tasks/i18n-download.js — overhauled to avoid translate.wordpress.org rate limiting (HTTP 429) that was writing HTML error pages into .po/.mo files:
      • Fetches translation metadata from api.wordpress.org/translations/plugins/1.0/ in a single call to get last-updated timestamps per locale.
      • Skips files whose local mtime is already ≥ the remote timestamp; for locales absent from the metadata API, falls back to an existence + validity check.
      • Validates existing files before skipping: checks MO magic bytes and scans the first 256 bytes of PO files for HTML — detects and replaces files corrupted by past 429 responses.
      • Serialises downloads with a 1.5 s inter-request delay and a 30 s retry on 429 (up to 3 retries).

Related issue(s)

N/A — this is a standalone dev-environment housekeeping PR.

Steps to reproduce & screenshots/GIFs

  1. composer install — should resolve cleanly with updated lock file.
  2. npm run up — container should build using Dockerfile-php84 (PHP 8.4).
  3. npm run build — should complete without errors, including i18n extraction and translation download.

Checklist

  • unit tests
  • changelog.txt entry added
  • readme.txt entry added

@bartech bartech self-assigned this Feb 25, 2026
When downloading too fast translation files from wordpress.org we hit 429 error code. Now download script:
- use translation meta endpoint to fetch information about translations
- checks if files timestamp match local files and skip download if it does.
- has workaround as translation meta doesn't return all locales, so for those files we check if they exist in local env and are valid .mo, .po files; download only if missing or broken
- implements delay between downloads to prevent 429
- add 30 sec wait time and retry if we hit 429
@bartech bartech changed the title Chore/dev env cleanup Chore: Build pipeline fix and dev env cleanup Feb 25, 2026
@bartech
Copy link
Copy Markdown
Collaborator Author

bartech commented Feb 25, 2026

The build on the local is now getting all translation files and it's fast as it skips most if not all files. But on GH Action we hit the 429 wall still. Assume it's because rate limit is for the the IP and many GH actions across repositories could hit the wordpress.org API causing 429.

Fetched translation metadata for 11 locales.
Downloaded ar.po
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Downloaded ar.mo
Rate limited, retrying in 30000ms...
Downloaded es_MX.po
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Failed es_MX.mo: HTTP 429 downloading https://translate.wordpress.org/projects/wp-plugins/woocommerce-services/stable/es-mx/default/export-translations/?format=mo
Rate limited, retrying in 30000ms...
Downloaded es_VE.po
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Downloaded es_VE.mo
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Failed es_ES.po: HTTP 429 downloading https://translate.wordpress.org/projects/wp-plugins/woocommerce-services/stable/es/default/export-translations/?format=po
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Failed es_ES.mo: HTTP 429 downloading https://translate.wordpress.org/projects/wp-plugins/woocommerce-services/stable/es/default/export-translations/?format=mo
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Failed fr_CA.po: HTTP 429 downloading https://translate.wordpress.org/projects/wp-plugins/woocommerce-services/stable/fr-ca/default/export-translations/?format=po
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Failed fr_CA.mo: HTTP 429 downloading https://translate.wordpress.org/projects/wp-plugins/woocommerce-services/stable/fr-ca/default/export-translations/?format=mo
Rate limited, retrying in 30000ms...
Downloaded ja.po
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Downloaded ja.mo
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Downloaded nl_NL.po
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Failed nl_NL.mo: HTTP 429 downloading https://translate.wordpress.org/projects/wp-plugins/woocommerce-services/stable/nl/default/export-translations/?format=mo
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Failed ru.po: HTTP 429 downloading https://translate.wordpress.org/projects/wp-plugins/woocommerce-services/stable/ru/default/export-translations/?format=po
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Failed ru.mo: HTTP 429 downloading https://translate.wordpress.org/projects/wp-plugins/woocommerce-services/stable/ru/default/export-translations/?format=mo
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Failed pt_BR.po: HTTP 429 downloading https://translate.wordpress.org/projects/wp-plugins/woocommerce-services/stable/pt-br/default/export-translations/?format=po
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Failed pt_BR.mo: HTTP 429 downloading https://translate.wordpress.org/projects/wp-plugins/woocommerce-services/stable/pt-br/default/export-translations/?format=mo
Rate limited, retrying in 30000ms...
Downloaded ro_RO.po
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...
Failed ro_RO.mo: HTTP 429 downloading https://translate.wordpress.org/projects/wp-plugins/woocommerce-services/stable/ro/default/export-translations/?format=mo
Rate limited, retrying in 30000ms...
Rate limited, retrying in 30000ms...

My idea is to fetch those files locally and add them to project git repo. First download script will read from translation meta API to check translation files timestamps, will compare them with project files and only download/update those that changed. GH Action will use repo files so will only fetch them from API when outdated saving on build time.

@bartech
Copy link
Copy Markdown
Collaborator Author

bartech commented Feb 25, 2026

The change to commit translation files works. Form 28 min build after fixing translation download (many retires with 30s backoff) to below average 2 min.
image

@bartech
Copy link
Copy Markdown
Collaborator Author

bartech commented Feb 25, 2026

WordPress Translation Files — What to Ship

Background

Research into whether plugin authors need to ship .mo, .po, and strings.php files,
or whether WordPress handles translation delivery automatically.

Plugin URL: https://pl.wordpress.org/plugins/woocommerce-services/


For Plugins on wordpress.org

You do NOT need to ship .mo/.po files.

WordPress automatically downloads language packs from translate.wordpress.org once
translations reach 90% completion for a locale. The process is silent and automatic:

  1. User sets site language → WordPress checks api.wordpress.org for available packs
  2. WordPress downloads compiled .mo to wp-content/languages/plugins/
  3. Packs update automatically, independent of plugin updates

What you do need to ship:

  • .pot file — so translators on translate.wordpress.org know what strings to translate

Requirements for language packs to work:

  • The plugin Text Domain must exactly match the wordpress.org plugin slug
  • Translations must reach 90%+ completion on translate.wordpress.org

For WooCommerce.com Paid Extensions

WooCommerce deprecated their own language pack CDN in WooCommerce 2.4.8 (2015)
and migrated to the same translate.wordpress.org system. Paid extensions hosted on
WooCommerce.com follow the same pattern — no separate CDN or custom delivery system.


File Types Reference

File Purpose Ship to users?
.pot Source template for translators Yes — needed for translate.wordpress.org
.po Human-readable per-locale translations No — source only, not needed at runtime
.mo Compiled binary WordPress loads at runtime No — WordPress downloads via language packs
strings.php Build artifact used by i18n tooling to extract strings into .pot No — build artifact only

Bottom Line for This Repo

Since this plugin is on wordpress.org:

  • The build pipeline only strictly needs to generate/update the .pot file
  • .mo/.po files do not need to be shipped — WordPress handles delivery automatically
  • strings.php is a build artifact and is not shipped to users
  • Any .mo/.po files currently in the repo are likely legacy from before language packs were adopted
    — they can be removed from the distributed plugin zip

References

The *.js and *.php globs in lint-staged.config.js matched any staged
file, including wp-calypso/**. lint-staged appends matched paths to
the command, causing ESLint to auto-fix wp-calypso JS files and phpcbf
to scan the entire project (wp-calypso included) on every commit.

- lint-staged.config.js: convert *.js entry to a function returning a
  plain string so lint-staged no longer appends staged paths — ESLint
  runs scoped to tasks/ and client/ only. Convert *.php entry to a
  function that filters out wp-calypso/ files before forwarding to
  phpcbf.
- composer.json: add wp-calypso to --ignore in all seven phpcs/phpcbf
  scripts (check-all, check-all:fix, check-php, check-php:fix,
  check-security, phpcs, phpcbf) as defence-in-depth for standalone
  runs.
@bartech
Copy link
Copy Markdown
Collaborator Author

bartech commented Feb 25, 2026

i18n Build Pipeline — Findings & Recommendations

Current Build Pipeline (end-to-end)

The full translation build runs as the prerelease npm script:

npm run i18n && node tasks/i18n-download && npm run i18njson

Step 1 — String extraction (npm run i18n)

  • Runs a webpack build with NODE_ENV=i18n
  • tasks/i18n.js processes the output and generates:
    • i18n/strings.php — PHP array of all translatable strings (used by Poedit / GlotPress)
    • i18n/languages/woocommerce-services.pot — POT template for translators on translate.wordpress.org

Step 2 — Download translations (node tasks/i18n-download.js)

  • Fetches locale metadata (last-updated timestamps) from api.wordpress.org/translations/plugins/1.0/
  • For each of 11 supported locales, downloads both .po and .mo from translate.wordpress.org
    into a translations/ working directory
  • Validates files (MO magic bytes, PO HTML-error scan) and skips up-to-date ones
  • Copies both .po and .mo to i18n/languages/ for the downstream step

Step 3 — Convert to JSON (npm run i18njson)

  • tasks/i18njson.js reads every .po file from i18n/languages/
  • Converts each via po2json to JED-format JSON
  • Writes woocommerce-services-{LOCALE}.json alongside the source .po in i18n/languages/

Why .json Files MUST Be Kept

The plugin does not use wp_set_script_translations(). It has a custom JS translation
loader wired through PHP:

  1. PHP (woocommerce-services.php:1602) — get_i18n_json() reads
    i18n/languages/woocommerce-services-{LOCALE}.json from disk at request time.

  2. PHP (woocommerce-services.php:1699) — The raw JSON string is passed to JavaScript
    via wp_localize_script('wc_connect_admin', 'i18nLocale', ['json' => …, 'localeSlug' => …]).

  3. JS (client/lib/calypso-boot/index.js) — On page load, window.i18nLocale.json
    is parsed and fed to i18n-calypso's i18n.setLocale(), activating all translations.

If the .json file for the current locale is absent, get_i18n_json() returns '{}'
and every translated string in the JS UI silently falls back to English.

WordPress's automatic language pack system does not cover these files — it only
delivers .mo files (for PHP strings) and wp_set_script_translations() JSON (a
different format and naming scheme). There is no automatic delivery mechanism for
this plugin's custom JED-format JSON files.

Conclusion: .json files in i18n/languages/ must continue to be shipped with the plugin.


Can We Skip Shipping .mo Files?

Yes.

The plugin is hosted on wordpress.org. WordPress automatically downloads compiled .mo
language packs from translate.wordpress.org when:

  • The site language is set to a supported locale
  • The locale has ≥90% translation coverage on translate.wordpress.org

load_plugin_textdomain() (woocommerce-services.php:658) checks
wp-content/languages/plugins/ first — where WordPress stores downloaded language
packs — before falling back to the plugin's own i18n/languages/ directory.

The .mo files currently bundled in i18n/languages/ are therefore redundant for
production installs
and can be removed from the distributed plugin.


Can We Skip Shipping .po Files?

Yes.

.po files are human-readable translation sources. WordPress does not load them at
runtime. They exist in i18n/languages/ solely as intermediate input for
tasks/i18njson.js (Step 3 above).

If the build pipeline generates .json files directly (without copying .po into
i18n/languages/), the .po files can remain only in the translations/ working
directory and never enter the plugin's distributable files.


Plan: Stop Shipping .mo / .po — Implemented Changes

Goal

After the build, i18n/languages/ should contain only .json and .pot files
no .mo, no .po.

Changes Made

1. tasks/i18n-download.js — Skip .mo downloads

Removed 'mo' from the formats array so only .po files are fetched:

// Before
for ( const format of [ 'po', 'mo' ] ) {

// After
for ( const format of [ 'po' ] ) {

2. tasks/i18n-download.js — Remove the .po/.mo copy loop

The copy loop that pushed files into i18n/languages/ has been removed.
Downloaded .po files now stay only in translations/ (the build cache directory).

3. tasks/i18njson.js — Read .po from translations/ instead of i18n/languages/

Changed the glob path so i18njson.js reads source .po files directly from
translations/ and writes the resulting .json into i18n/languages/ as before:

// Before
const poFilePaths = globby.sync( [ 'i18n/languages/*.po' ] );

// After
const poFilePaths = globby.sync( [ 'translations/*.po' ] );

tasks/i18njson.js is retained and unchanged in purpose — it remains the dedicated
.po.json conversion step, now sourcing from the correct directory.

The prerelease script in package.json requires no changes — the sequence
node tasks/i18n-download && npm run i18njson still works correctly with the
updated paths.

Cleanup (still needed)

Remove existing .po and .mo files from i18n/languages/ and delete from git:

git rm i18n/languages/*.po i18n/languages/*.mo

The .json files remain tracked in git (they are the only runtime artifact).

Summary of Changes

File Change
tasks/i18n-download.js Skip .mo download; remove .po/.mo copy loop
tasks/i18njson.js Read .po from translations/ instead of i18n/languages/
i18n/languages/*.po Delete from git
i18n/languages/*.mo Delete from git

strings.php — Build Artifact, Not Needed in Plugin Bundle

What it is

i18n/strings.php is generated by tasks/i18n.js (line 11) during the npm run i18n
build step. It is a PHP array of all translatable strings extracted from the built JS
files, named $i18nStrings, used by Poedit and GlotPress to build the .pot template.

It is not loaded at runtime. The only reference in PHP is a stale @var comment at
woocommerce-services.php:1700$i18nStrings is never required or included
anywhere in the plugin. The comment is a leftover from an older approach.

Why it ends up in the plugin zip

tasks/release.js copies the entire i18n/ directory to the release folder (line 16
of dirsToCopy). Since strings.php is regenerated on every build by npm run i18n,
it is present on disk when release.js runs — even though it has been removed from git
tracking. This means it currently ships inside the plugin zip despite serving no
runtime purpose.

Plan: Exclude it from the release

Add a single cleanup line to tasks/release.js after the directory copy (line 48) and
before archive.finalize():

// Remove build artifacts not needed in the release
rm( '-f', targetFolder + '/i18n/strings.php' );

This is the minimal, safe change — it does not alter the directory structure in the zip
or affect any other file, and it requires no changes to tasks/i18n.js.

Summary

File Status
i18n/strings.php Generated at build time; removed from git; exclude from zip
tasks/release.js Add rm( '-f', targetFolder + '/i18n/strings.php' ) before archiving
woocommerce-services.php:1700 Stale @var comment referencing $i18nStrings — can be removed

Replace the directory-scoped `eslint --fix tasks/ client/` command with a
function that passes only the staged file paths directly to eslint, preventing
the pre-commit hook from reformatting every JS file in the project on each
commit. Also excludes wp-calypso/ files, matching the existing PHP handler.
Copy link
Copy Markdown
Collaborator

@iyut iyut left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the obvious that probably need to be changed.

Comment thread composer.json Outdated
"qit:woo-api": [
"npm run build && composer install && ./vendor/bin/qit run:woo-api woocommerce-services --zip=woocommerce-services.zip"
],
"tests": [
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any reason we change it to tests? Because it's stated in AGENTS.md as a test under Common Commands. If we want to change it to tests then we will need to update the AGENTS.md as well.

RUN pecl install xdebug \
&& echo 'xdebug.mode=debug' >> $PHP_INI_DIR/php.ini \
&& echo 'xdebug.start_with_request=trigger' >> $PHP_INI_DIR/php.ini \
&& echo 'xdebug.remote_timeout=3600' >> $PHP_INI_DIR/php.ini \
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
&& echo 'xdebug.remote_timeout=3600' >> $PHP_INI_DIR/php.ini \
&& echo 'xdebug.connect_timeout_ms=3600' >> $PHP_INI_DIR/php.ini \

XDebug 3 rename that settings.

&& echo 'xdebug.var_display_max_data=512' >> $PHP_INI_DIR/php.ini \
&& echo 'xdebug.var_display_max_children=-1' >> $PHP_INI_DIR/php.ini \
&& echo 'xdebug.var_display_max_depth=128' >> $PHP_INI_DIR/php.ini \
&& echo 'xdebug.remote_cookie_expire_time = 3600' >> $PHP_INI_DIR/php.ini \
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
&& echo 'xdebug.remote_cookie_expire_time = 3600' >> $PHP_INI_DIR/php.ini \

This has been removed on the XDebug 3

Comment thread readme.txt Outdated

== Changelog ==

= 3.5.1 - 2026-02-23 =
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
= 3.5.1 - 2026-02-23 =
= 3.5.1 - 2026-xx-xx =

Comment thread changelog.txt Outdated
@@ -1,7 +1,10 @@
*** WooCommerce Tax Changelog ***

= 3.5.1 - 2026-02-23 =
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
= 3.5.1 - 2026-02-23 =
= 3.5.1 - 2026-xx-xx =

@bartech bartech requested a review from iyut March 5, 2026 09:24
Copy link
Copy Markdown
Collaborator

@iyut iyut left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just having conversation with Bart and listen to his explanation about the translations folder and his reasoning. I'll approve this since this is the efficient way to fix the translation issue.

@bartech bartech merged commit a0d1e35 into trunk Mar 6, 2026
9 checks passed
@bartech bartech deleted the chore/dev-env-cleanup branch March 6, 2026 09:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants