Skip to content

wp-build dashboards: translate JavaScript strings (load catalogs + stamp text domains) - #50762

Merged
dhasilva merged 24 commits into
trunkfrom
add/wp-build-dashboards-js-i18n
Jul 27, 2026
Merged

dhasilva merged 24 commits into
trunkfrom
add/wp-build-dashboards-js-i18n

Conversation

@dhasilva

@dhasilva dhasilva commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Fixes # N/A

Proposed changes

JavaScript strings on the modernized wp-build (esbuild) dashboards shipped mostly untranslated: the pipeline externalizes @wordpress/i18n to window.wp.i18n but never loads the translation catalog, and it drops the package text domain from the built __() / _x() / _n() calls. Only ~26% of strings (the ones that happened to keep the right domain) translated — the DataViews / @wordpress/ui chrome (table headers, filters, pagination, search) stayed English regardless of site locale.

This fixes both root causes for seven dashboards (activity-log, publicize/Social, videopress, backup, newsletter, forms, premium-analytics), generalizing the approach shipped for Activity Log in #50714.

  • New in packages/wp-build-polyfills:
    • stamp-textdomains — a post-build CLI that runs @automattic/babel-plugin-replace-textdomain over the esbuild output, stamping the package text domain onto every gettext call in build/{routes,scripts,modules,widgets} (both .js and .min.js). Idempotent; unit-tested.
    • loadI18nCatalogs( domain, bundles ) — a shared init-module helper that downloads and installs the JS catalogs via wp.jpI18nLoader before routes render.
    • strip-unminified-prod now replaces a string-bearing unminified .js with a generated i18n reference stub (only its gettext calls + translator comments) instead of deleting it. GlotPress ignores *.min.js (see anomiex's comment), so deleting the .js would leave production builds with nothing to extract strings from and no catalogs would ever be generated at release. Same path → make-pot references and WordPress' md5( path )-keyed catalog names keep matching; never loaded at runtime (the generated PHP loaders stay pinned to .min.js). String-less .js, .css, and sourcemaps are still deleted.
  • Per dashboard: a boot init sub-package that calls the helper, wpPlugin.pages in object form, the stamp step chained after build:wp-build, a wp-jp-i18n-loader enqueue on the dashboard page, and a direct jetpack_<name>_register_script_modules() call (these load wp-build at admin_menu, after wp_default_scripts has fired). Forms is the exception — it loads wp-build at plugins-load time, so the generated registration hook lands in time and no direct call is needed (verified the init module reaches the page's import map).
  • Added the missing alias_textdomains_from_file() call to the standalone Premium Analytics plugin (the social/videopress/backup/jetpack plugins already had it) so the loader computes the right catalog URLs there.

Related product discussion/links

  • Reference implementation / template: Activity Log: migrate the dashboard to wp-build #50714 (Activity Log dashboard JS i18n).
  • Stalled upstream @wordpress/build effort (it has no build-time domain-stamping hook — verified in lib/build.mjs): Slack #jetpack-monorepo thread p1774956551.856089 (jsnajdr). This PR's stamp-textdomains tool could feed that.
  • Follow-up, same gap but out of scope here: seo, podcast, scan, and jetpack-mu-wpcom also build with wp-build and need the same treatment.
  • WP 7.0's wp_set_script_module_translations() (wordpress-develop#11543) may eventually replace both halves of this once Jetpack's WP floor reaches 7.0 (noted in code comments).

Does this pull request change what data or activity we track or use?

No.

Testing instructions

In the Jetpack Docker environment:

  • Build the affected packages: jetpack build packages/{wp-build-polyfills,activity-log,publicize,videopress,backup,newsletter,forms,premium-analytics}.
    • ⚠️ Use full builds when verifying i18n: wp-build --watch rebuilds route bundles without the stamp-textdomains step, so watch output is unstamped and will look untranslated on a non-en_US site — that's expected, not a regression.
  • Generate fake translations for the plugin that serves the dashboard, e.g. tools/docker/bin/fakepot.sh <monorepo>/projects/plugins/jetpack (and also .../projects/plugins/backup if the standalone Backup plugin is active — its dashboard resolves catalogs under the jetpack-backup domain).
  • Set Settings → General → Site Language to "English (Pig Latin)".
  • Load each dashboard and confirm the previously-English chrome (DataViews column headers, filters, pagination, search; @wordpress/ui buttons/labels) now renders pig-latin:
    • Activity Log — admin.php?page=jetpack-activity-log
    • Social — admin.php?page=jetpack-social
    • VideoPress — admin.php?page=jetpack-videopress
    • Backup — admin.php?page=jetpack-backup (modernized dashboard is behind rsm_jetpack_ui_modernization_backup)
    • Newsletter — admin.php?page=jetpack-newsletter
    • Forms — admin.php?page=jetpack-forms-responses-wp-admin
  • Regression: confirm the block editor still loads normally (the polyfill changes are page-scoped).
  • Reset the site language when done.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack or WordPress.com Site Helper), and enable the add/wp-build-dashboards-js-i18n branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack add/wp-build-dashboards-js-i18n
bin/jetpack-downloader test jetpack-mu-wpcom-plugin add/wp-build-dashboards-js-i18n

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions

github-actions Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!


Premium Analytics plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.

@jp-launch-control

jp-launch-control Bot commented Jul 22, 2026 •

Copy link
Copy Markdown

Code Coverage Summary

Coverage changed in 13 files. Only the first 5 are listed here.

File Coverage Δ% Δ Uncovered
projects/packages/composer-plugin/src/class-plugin.php 0/66 (0.00%) 0.00% 8 💔
projects/packages/backup/src/class-jetpack-backup.php 77/438 (17.58%) -0.16% 4 💔
projects/packages/newsletter/src/class-subscribers-announcement.php 89/113 (78.76%) -2.72% 4 💔
projects/packages/premium-analytics/src/class-analytics.php 39/80 (48.75%) -2.57% 4 💔
projects/packages/videopress/src/class-admin-ui.php 34/274 (12.41%) -0.18% 4 💔

9 files are newly checked for coverage. Only the first 5 are listed here.

File Coverage
projects/packages/backup/packages/init/src/index.ts 0/1 (0.00%) 💔
projects/packages/forms/packages/init/src/index.ts 0/1 (0.00%) 💔
projects/packages/newsletter/packages/init/src/index.ts 0/1 (0.00%) 💔
projects/packages/videopress/packages/init/src/index.ts 0/1 (0.00%) 💔
projects/js-packages/babel-plugin-replace-textdomain/src/resolve-gettext-callee.js 74/84 (88.10%) 💚

Full summary · PHP report · JS report

If appropriate, add one of these labels to override the failing coverage check: Covered by non-unit tests Use to ignore the Code coverage requirement check when E2Es or other non-unit tests cover the code Coverage tests to be added later Use to ignore the Code coverage requirement check when tests will be added in a follow-up PR I don't care about code coverage for this PR Use this label to ignore the check for insufficient code coveage.

@LiamSarsfield LiamSarsfield left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for addressing this 🚀

  1. Standalone Premium Analytics will fatal on load. Its composer.json has no plugin slug (wp-plugin-slug / beta-plugin-slug), so the composer plugin drops jetpack_vendor/i18n-map.php at build, then the new bootstrap does a bare require on that path through alias_textdomains_from_file() and dies with "Failed opening required." Every other plugin that calls it (backup, social, starter-plugin) sets a slug so the map generates, so adding "beta-plugin-slug": "jetpack-premium-analytics" (or wp-plugin-slug, whichever channel it ships on) should sort it. I checked, and the built plugin has no map where the others do.

  2. PA misses catalogs for its non-content bundles. The init list only loads routes/*/content.js, but the lazy-loaded widget renders carry strings too (e.g. Published %s in latest-post), and they're keyed on their own bundle paths, so they'll render in English. Probably want to derive the list from the emitted assets instead of hand-maintaining it.

  3. Catalog load can wedge first render. Every dashboard awaits the downloads before boot, and downloadI18n has no fetch timeout, so a stalled endpoint blocks first paint instead of falling back to English. A bounded race fixes it.

Rest looked solid.

@LiamSarsfield LiamSarsfield left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The manifest and the bounded wait both look right. That covers what I raised last time.

Four more things that appear to have substance:

  1. The self-alias guards undo the Premium Analytics fix from earlier in this same PR. alias_textdomain() returns at the $from === $to check before it populates $domain_map, and generateManifest() drops the package from the map outright. But that record is also the only thing feeding domainPaths, which is the prefix downloadI18n hashes. So standalone PA hashes build/routes/.../content.js rather than jetpack_vendor/automattic/jetpack-premium-analytics/build/..., every catalog 404s, and the loader silences exactly that error class. The guards are right, the recursion is real. It's that the map entry is carrying two contracts at once. Keeping the (ver, path) record and skipping only the filter registration should do it, though the new AssetsTest case would need updating with it. Renaming the package domain to jetpack-premium-analytics-pkg, like backup/videopress/search, avoids the collision entirely.

  2. Stamping doesn't change the asset version. wp-build hashes the emitted JS into 'version' in *.asset.php during the esbuild pass, and stamp-textdomains rewrites the file afterwards without recomputing it. On a bundle where the stamp is the only change, the version comes out identical to the previous release, so browsers keep serving the cached unstamped copy. Either stamp before the hash is taken, or rewrite the sibling asset file.

  3. The stamper matches any member call named __, _x, _n or _nx. In babel-plugin-replace-textdomain, a MemberExpression whose property is in functions skips the import-provenance check; only bare identifiers get resolved back to @wordpress/i18n. So cache.__( 'key' ) comes out as cache.__( 'key', 'jetpack-forms' ). Nothing in the tree collides today, but this is the first time it's been pointed at whole esbuild output including bundled deps, so the exposure is a lot wider than the webpack usage it was written for.

  4. startsWith( 'HTTP request failed:' ) swallows 500s and 403s, not just the missing-catalog 404 it's meant for. That's what would keep 1 and 2 invisible in the wild. Worth reading the status and staying quiet only on 404.

Minor: PA's build-production is the only one of the seven without a clean step. And the lockfile diff is about 5k lines of tarball: metadata with no actual dependency change, so probably worth regenerating before it starts conflicting with everything else in flight.

@dhasilva
dhasilva marked this pull request as ready for review July 27, 2026 19:13
@dhasilva
dhasilva requested a review from a team July 27, 2026 19:13
@dhasilva
dhasilva requested review from a team as code owners July 27, 2026 19:13
dhasilva and others added 6 commits July 27, 2026 18:06
…rom elsewhere

A member call is matched by its property name alone, so `cache.__( key )` in a
bundled dependency reads as gettext. That was tolerable while the plugin only
ran over hand-written source; pointing it at whole esbuild output — bundled
dependencies included — widens the exposure a lot, in both the stamp pass and
the i18n stub the .pot is extracted from.

New opt-in `requireI18nSource` traces a callee back through its binding and
skips the call when it resolves to something other than `@wordpress/i18n`.
It rejects what is proven foreign rather than accepting only what is proven
i18n, so an unrecognised bundler shape keeps stamping instead of silently
shipping a bundle in English. The trace follows esbuild's `__toESM` /
`__commonJS` chain by looking for the module name or a `wp.i18n` read, both of
which survive minification; the wp-build fixture now carries a decoy call that
must come through unstamped and unextracted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ewrites

wp-build hashes each emitted bundle into `'version'` in its sibling
`*.asset.php` during the esbuild pass, and the stamp pass rewrites the bundle
afterwards. On a release where the text-domain stamp is a bundle's only change,
the version comes out identical to the previous one and browsers keep serving
the cached, unstamped copy — the strings stay English until something else in
that bundle happens to change.

Recompute the version from the stamped bytes using wp-build's own scheme
(sha256, first 20 hex chars).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`startsWith( 'HTTP request failed:' )` swallowed every HTTP failure, not just
the missing-catalog 404 it was meant for. A 403 from a hardened languages
directory or a 500 from the origin rendered the UI in English with nothing in
the console — the same invisible failure the rest of this helper's diagnostics
exist to prevent. Read the status back out of the message and keep quiet only
on 404.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`build-production` was the only one of the seven dashboards that never cleaned
build/ — it inlined the build steps instead of going through `build`, which is
where the other packages get their clean. Route it through `build` like they
do, and add the `clean` script it lacked.

The lockfile also carried ~2.5k added `tarball:` metadata lines with no
dependency change behind them; regenerated with `--resolution-only` so only the
patch hash and the new workspace `init` packages remain.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`downloadI18n()` prepends a domain's prefix to every bundle path looked up
under it, so a domain can only carry one package's path. That was implicit
while every entry was a package domain distinct from its plugin's; now that a
plugin's own domain can hold one, a plugin that both bundled such a package and
loaded its own bundles under that domain would send its own catalogs looking
under the package's path. Nothing does that today, and the fix if anything ever
needs to is the existing `-pkg` textdomain convention.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follows #50850, which moved the package to `jetpack-premium-analytics-pkg` so
it no longer collides with the plugin slug. The build-time stamp reads the
domain from composer.json and picked the new one up on its own; this is the
runtime half, where the domain is passed explicitly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dhasilva
dhasilva force-pushed the add/wp-build-dashboards-js-i18n branch from 0af5eee to a7a40ae Compare July 27, 2026 21:12
@dhasilva
dhasilva merged commit 398ef1b into trunk Jul 27, 2026
131 of 134 checks passed
@dhasilva
dhasilva deleted the add/wp-build-dashboards-js-i18n branch July 27, 2026 22:45
@github-actions github-actions Bot removed the [Status] Needs Review This PR is ready for review. label Jul 27, 2026
@chihsuan

Copy link
Copy Markdown
Member

👋 @dhasilva Heads-up on something we ran into testing Premium Analytics dashboard on a non-English locale. loadI18nCatalogs() fires ~100 catalog requests in one burst at boot, and my local site sat blank for ~48s before rendering. WOOA7S-1822

The blocking split is fine (~16 awaited). It's the ~84 widget catalogs: unawaited, but still all requested at boot for widgets that may never render. Wondering about loading each at its own dynamic import instead — did you already look at that?

louwie17 added a commit that referenced this pull request Aug 6, 2026
…t-domain stamping) (#51086)

* Build debug code frames only when debug output is enabled

`buildCodeFrameError()` renders an excerpt of the surrounding source, so its
cost scales with the size of the file. Passing it straight to `debug()` reads
as free when debug output is off, but JavaScript evaluates call arguments
eagerly, so the frame was built for every matched call either way.

That was cheap while this plugin ran over hand-written source with domains
already present. Since #50762 it also runs over the esbuild bundles that
`stamp-textdomains` post-processes, where the domain is absent by construction
— so the missing-domain branch, the one that builds a frame, became the hot
path on multi-megabyte files.

Measured over premium-analytics' 342 built bundles: 211.8s -> 6.2s (34x), with
byte-identical output for every file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDM1RKuvDmbfyMZspnghaa

* Shorten debug code frame comment

* Add hyphens before @PARAM descriptions

Fixes the two jsdoc/require-hyphen-before-param-description warnings that
failed the lint check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDM1RKuvDmbfyMZspnghaa

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* would be applied to the plugin's catalogs too, and they would all 404. No
* plugin does both today. One that needs to should give the package a
* distinct textdomain, the way `jetpack-backup-pkg` and
* `jetpack-videopress-pkg` do.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Did something actually run into all this, or is it that someone's AI didn't realize "doing that is already broken, plus .github/files/lint-project-structure.sh will fail CI if a plugin and a package share textdomains in the first place" and no one questioned it?

// A self-alias would make filter_gettext() re-translate into the same
// domain, recursing infinitely on any untranslated string (a package
// textdomain can collide with its containing plugin's slug).
if ( $from === $to ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Seems like a legitimate bug, but more likely it should just throw so people actually fix it.

That should also allow for reverting most of the other complication added in here.

// untranslated string in it. The path is still needed — it is
// the prefix WordPress hashes to name the package's JavaScript
// translation files — so it goes in `paths`, which registers
// only that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same thing here too, did anything actually run into this or is it AI lacking context and getting confused?

Again, just throwing so people actually fix the error would probably make more sense than adding a bunch of complexity to work around a case CI already rejects.

anomiex added a commit that referenced this pull request Aug 18, 2026
PR #50762 caused a significant rise in build times, which was fixed
by #51086. Let's turn the build times back down after the fix.

This effectively reverts #51029. As for #51059, the progression is
25 before → 40 during the rise → 30 now, to leave some leeway since
full builds are up to ~18 minutes now.
CGastrell added a commit that referenced this pull request Sep 22, 2026
Every item confirmed against trunk before it was written in.

Corrections:
- The restore does not protect `pagenow`. `admin-header.php` prints it from
  `$current_screen->id` before it fires `admin_enqueue_scripts`, so only the
  old `current_screen` alias ever reached it. Moved to the old-pattern line.
- Seven ports shipped without the restore, not five. #52471 also fixed
  Podcast and SEO.
- The JITM container belongs in the page shell, not in a tab panel.
  `Tabs.Panel` defaults to `keepMounted={ false }`, so Search's container
  leaves the DOM on a tab switch. Cite Social and Boost instead.
- An empty `apiRoot` breaks `@automattic/jetpack-api`, not every API call.
  `@wordpress/api-fetch` has its own root.
- #50762 fixed seven dashboards. SEO, Podcast and Scan still have no
  `packages/init`, no `build:stamp-textdomains` and no i18n loader, so §6
  now says to copy Backup or Search rather than the SEO file it cites.
- §8 required a visible error and then called a blank page expected. Split
  into two rules: a brand-new route shows an error, a retired port renders
  nothing and that is accepted. §9 step 7 matches.
- Drop the port count and list, which was already stale. Name the flag
  mechanism: a `Feature_Flags` flag, which is where ports should land even
  though the shipped ones still use bare filters.

Additions, each one something a port needs and the checklist omitted: the
polyfills package in both manifests; the `.gitattributes` excludes and the
`build/**` include; importing `route.scss` from `stage.tsx`, since
`@wordpress/build` records `hasStyle` and never reads it; a shared mixin for
multi-route layout; the Safe Mode banner, hidden by the same template rule;
and moving the legacy entry's side effects into `stage.tsx`.

Linear links replaced with bare issue IDs — external contributors cannot
open them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CGastrell added a commit that referenced this pull request Sep 22, 2026
* Add the wp-build porting checklist

Nine dashboards have shipped through wp-build so far, and the same
traps kept recurring: the screen-ID alias shipping without its
restore (five ports), JITM views logged for cards nobody could see,
wp_localize_script silently doing nothing against an ES module page,
and a build/ fallback that disagreed between call sites.

Fold the checklist that already existed piecemeal across four Linear
comments on JETPACK-2575, plus JETPACK-2573's verification steps and
JETPACK-2687/2689's screen-ID findings, into one document in docs/ so
the next port can follow it without rediscovering any of this.

Fixes JETPACK-2575

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Checklist: unwrap, fix the dashboard count and two section cross-references

* Checklist: correct the boot-bump attribution and two fix-vs-regression citations

* Checklist: fix the §8 attribution and link the doc from monorepo.md

`is_wp_build_dashboard_active()` lives in `Jetpack_Backup`, not in Search.
Search had it while #52416 was current; #52506 deleted it with the legacy
bundle, which §8 already cites. Attribute the shared-predicate pattern to
Backup, and describe Search's post-retirement gates as they are now.

Drop the duplicate symbol+line citation in the second bullet, and add a
pointer from `docs/monorepo.md` § Building so the checklist is reachable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Checklist: apply the review

Every item confirmed against trunk before it was written in.

Corrections:
- The restore does not protect `pagenow`. `admin-header.php` prints it from
  `$current_screen->id` before it fires `admin_enqueue_scripts`, so only the
  old `current_screen` alias ever reached it. Moved to the old-pattern line.
- Seven ports shipped without the restore, not five. #52471 also fixed
  Podcast and SEO.
- The JITM container belongs in the page shell, not in a tab panel.
  `Tabs.Panel` defaults to `keepMounted={ false }`, so Search's container
  leaves the DOM on a tab switch. Cite Social and Boost instead.
- An empty `apiRoot` breaks `@automattic/jetpack-api`, not every API call.
  `@wordpress/api-fetch` has its own root.
- #50762 fixed seven dashboards. SEO, Podcast and Scan still have no
  `packages/init`, no `build:stamp-textdomains` and no i18n loader, so §6
  now says to copy Backup or Search rather than the SEO file it cites.
- §8 required a visible error and then called a blank page expected. Split
  into two rules: a brand-new route shows an error, a retired port renders
  nothing and that is accepted. §9 step 7 matches.
- Drop the port count and list, which was already stale. Name the flag
  mechanism: a `Feature_Flags` flag, which is where ports should land even
  though the shipped ones still use bare filters.

Additions, each one something a port needs and the checklist omitted: the
polyfills package in both manifests; the `.gitattributes` excludes and the
`build/**` include; importing `route.scss` from `stage.tsx`, since
`@wordpress/build` records `hasStyle` and never reads it; a shared mixin for
multi-route layout; the Safe Mode banner, hidden by the same template rule;
and moving the legacy entry's side effects into `stage.tsx`.

Linear links replaced with bare issue IDs — external contributors cannot
open them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
dlind1 pushed a commit that referenced this pull request Oct 12, 2026
The wp-build Podcast dashboard shipped with its strings in English on
non-English sites: nothing downloads its JS translation catalog at boot.
This applies the same approach #50762 used for the other wp-build dashboards.

- Add a boot `init` package that calls loadI18nCatalogs( 'jetpack-podcast' ).
- Register it on the dashboard page and stamp the text domain / write the i18n
  manifest after wp-build.
- Enqueue wp-jp-i18n-loader on the Podcast page.
- Register the init script module directly when `wp_default_scripts` has
  already fired. The Podcast build is loaded on admin_menu, after that hook, so
  the generated registrar never ran and the init module was missing from the
  import map ("Failed to resolve module specifier").

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
dlind1 pushed a commit that referenced this pull request Oct 13, 2026
The wp-build Podcast dashboard shipped with its strings in English on
non-English sites: nothing downloads its JS translation catalog at boot.
This applies the same approach #50762 used for the other wp-build dashboards.

- Add a boot `init` package that calls loadI18nCatalogs( 'jetpack-podcast' ).
- Register it on the dashboard page and stamp the text domain / write the i18n
  manifest after wp-build.
- Enqueue wp-jp-i18n-loader on the Podcast page.
- Register the init script module directly when `wp_default_scripts` has
  already fired. The Podcast build is loaded on admin_menu, after that hook, so
  the generated registrar never ran and the init module was missing from the
  import map ("Failed to resolve module specifier").

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
dlind1 pushed a commit that referenced this pull request Oct 13, 2026
The wp-build Podcast dashboard shipped with its strings in English on
non-English sites: nothing downloads its JS translation catalog at boot.
This applies the same approach #50762 used for the other wp-build dashboards.

- Add a boot `init` package that calls loadI18nCatalogs( 'jetpack-podcast' ).
- Register it on the dashboard page and stamp the text domain / write the i18n
  manifest after wp-build.
- Enqueue wp-jp-i18n-loader on the Podcast page.
- Register the init script module directly when `wp_default_scripts` has
  already fired. The Podcast build is loaded on admin_menu, after that hook, so
  the generated registrar never ran and the init module was missing from the
  import map ("Failed to resolve module specifier").

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
dlind1 added a commit that referenced this pull request Oct 13, 2026
* Podcast: load the dashboard's JavaScript translations

The wp-build Podcast dashboard shipped with its strings in English on
non-English sites: nothing downloads its JS translation catalog at boot.
This applies the same approach #50762 used for the other wp-build dashboards.

- Add a boot `init` package that calls loadI18nCatalogs( 'jetpack-podcast' ).
- Register it on the dashboard page and stamp the text domain / write the i18n
  manifest after wp-build.
- Enqueue wp-jp-i18n-loader on the Podcast page.
- Register the init script module directly when `wp_default_scripts` has
  already fired. The Podcast build is loaded on admin_menu, after that hook, so
  the generated registrar never ran and the init module was missing from the
  import map ("Failed to resolve module specifier").

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Podcast: suppress the Phan undeclared-function error for the generated registrar

jetpack_podcast_register_script_modules() is defined by the generated
build/modules.php, which Phan doesn't analyze. The call is already guarded by
function_exists().

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Podcast: cover the translation loader changes with tests

Moves the late script-module registration into a small method that takes the
registrar as a parameter, so it can be tested without the generated build code
(and no longer needs a Phan suppression). Adds tests for the loader enqueue and
the registration.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Add changelog entry for plugins/jetpack

* Refactor to follow the pattern from #50762 for consistency

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Daniel Lind <daniel.lind@automattic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment