Skip to content

wordpress/date: Recover WP timezone after third-party reload#75831

Merged
jsnajdr merged 1 commit intoWordPress:trunkfrom
epeicher:epeicher/fix-wp-timezone-loss
Mar 26, 2026
Merged

wordpress/date: Recover WP timezone after third-party reload#75831
jsnajdr merged 1 commit intoWordPress:trunkfrom
epeicher:epeicher/fix-wp-timezone-loss

Conversation

@epeicher
Copy link
Copy Markdown
Contributor

@epeicher epeicher commented Feb 23, 2026

What?

Makes the @wordpress/date package resilient to third-party plugins that reload moment-timezone and destroy the custom 'WP' timezone zone.

Why?

On sites with non-UTC timezones, post times in the block editor can be offset by 2x the site's UTC difference (e.g., a post published at 10 AM on a UTC+4 site shows as 2 AM). This occurs because plugins like WooCommerce load their own copy of moment-timezone, which reinitializes the internal zone storage and destroys Gutenberg's custom 'WP' timezone zone. When the zone is lost, moment.tz(date, 'WP') falls back incorrectly, causing the UTC offset to be applied twice.

How?

The fix adds resilience to moment-timezone reloads by:

  1. Caching the packed WP timezone zone string after first creation
  2. Adding an ensureWPTimezone() guard that checks if the 'WP' zone still exists before using it
  3. Re-adding the zone from cache when needed (which only requires momentLib.tz.add(), not the utils that get destroyed)
  4. Calling the guard in isInTheFuture(), getDate(), and humanTimeDiff() before accessing WP_ZONE

The cached approach avoids calling pack() on recovery since the utils are also destroyed by third-party reloads.

Testing Instructions

  1. Install a plugin that loads moment-timezone after wp-date (e.g., enqueue moment-timezone-with-data-10-year-range.min.js from cdnjs with wp-date as a dependency), for example, the following testing plugin
    telex-timezone-converter.zip
  2. Set the site timezone to something other than UTC (e.g., UTC+4)
  3. Create/edit a post in the block editor
  4. If you installed the timezone-converter plugin, add the Timezone Date Converter block to the pot
  5. Publish the post and observe the publish time is correct (not offset by 2x the UTC difference)

Testing Instructions for Keyboard

N/A — no UI changes.

Fixes issue where plugins (e.g. WooCommerce) that load their own copy of moment-timezone destroy the custom 'WP' zone and cause post times to be offset by 2x the UTC difference.

The fix adds ensureWPTimezone() guards to isInTheFuture(), getDate(), and humanTimeDiff(). The custom WP timezone zone is now cached in packed form on first creation, so it can be re-added without requiring moment-timezone-utils after a third-party reload. Includes tests that verify recovery after the zone is destroyed.
@github-actions
Copy link
Copy Markdown

github-actions bot commented Feb 23, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: epeicher <epeicher@git.wordpress.org>
Co-authored-by: jsnajdr <jsnajdr@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions github-actions bot added the [Package] Date /packages/date label Feb 23, 2026
@epeicher
Copy link
Copy Markdown
Contributor Author

Hey @jsnajdr @aduth, would love your eyes on this one when you get a chance! You've both worked on moment-timezone and @wordpress/date recently, so you'd be great reviewers. Feel free to ping someone else if you think they'd be a better fit.

TL;DR: Adds resilience against third-party plugins that reload moment-timezone and destroy the custom 'WP' timezone zone. Thanks! 🙏

@jsnajdr
Copy link
Copy Markdown
Member

jsnajdr commented Mar 18, 2026

I'd like to better understand the mechanism how WooCommerce overwrites the moment library. Core loads the moment.js file (from /wp-includes/js/dist/vendor) as a script which assigns to the window.moment global. The imports of the moment library, like these in wp-date, are externalized to reference this window.moment.

How does WooCommerce load the second copy of moment? Does it ship its own moment.js, loaded as a different script (with different WP handle), but assigning to the same window.moment global?

If WooCommerce merely bundled moment as part of another script, then moment.js would be treated as a CommonJS module and the assignment to window.moment wouldn't happen. It uses the UMD convention.

@epeicher
Copy link
Copy Markdown
Contributor Author

Thanks @jsnajdr for your reply! Please let me provide some more context, I was not able to reproduce the issue using WooCommerce so I created a plugin that mimics the incorrect behaviour. This is a snippet of the plugin, you can find it here telex-timezone-converter.zip

function telex_timezone_converter_simulate_moment_overwrite() {
	// Log state BEFORE the CDN script loads
	wp_add_inline_script(
		'wp-date',
		'console.log("[TZ Debug] BEFORE CDN - WP zone:", window.moment.tz.zone("WP"));
		 console.log("[TZ Debug] BEFORE CDN - tz.names count:", window.moment.tz.names().length);',
		'after'
	);

	wp_enqueue_script(
		'moment-timezone-override',
		'https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.46/moment-timezone-with-data-10-year-range.min.js',
		array( 'wp-date' ),
		null,
		false
	);

	// Log state AFTER the CDN script loads
	wp_add_inline_script(
		'moment-timezone-override',
		'console.log("[TZ Debug] AFTER CDN - WP zone:", window.moment.tz.zone("WP"));
		 console.log("[TZ Debug] AFTER CDN - tz.names count:", window.moment.tz.names().length);',
		'after'
	);
}
add_action( 'admin_enqueue_scripts', 'telex_timezone_converter_simulate_moment_overwrite' );

When I load the block, the timezone is not correctly applied.

But after giving this some more thought, if the issue isn't reproducible with popular plugins or doesn't represent a standard use case, it might make sense to close this PR. What do you think?

@jsnajdr
Copy link
Copy Markdown
Member

jsnajdr commented Mar 23, 2026

if the issue isn't reproducible with popular plugins or doesn't represent a standard use case, it might make sense to close this PR.

The telex reproduction case makes sense: if any plugin registers its own copy of moment with wp_enqueue_script, even if the handle of the script is different, it will overwrite the window.moment global.

It would be nice to know where did the issue come from. What motivated you to fix it, did it happen to you personally on some site.

If someone overwrites moment with their own copy, the date formatting can break in other, more subtle ways. Maybe the moment version number is different than we expect. Or the set of supported timezones is different. This PR fixes only one of possible symptoms.

I quite like the PR, but would like to see a stronger case why it needs to be done. Such a change will last for many years, and folks in the future will be asking what is this special code doing and whether it's still needed.

@epeicher
Copy link
Copy Markdown
Contributor Author

It would be nice to know where did the issue come from. What motivated you to fix it, did it happen to you personally on some site.

This originates from a Calypso issue: Automattic/wp-calypso#71529. The latest occurrences are from more than a year ago, so they may already have been resolved.

I quite like the PR, but would like to see a stronger case why it needs to be done. Such a change will last for many years, and folks in the future will be asking what is this special code doing and whether it's still needed.

That’s very reasonable, and it makes total sense. I think the change is self-explanatory, but it may also be considered unnecessary, so I understand that if we don’t have a strong case, this can be closed. I will update the issue with these comments, and it can be reopened if further occurrences appear and can be reproduced without a plugin created just for that purpose

@jsnajdr
Copy link
Copy Markdown
Member

jsnajdr commented Mar 26, 2026

This originates from a Calypso issue: Automattic/wp-calypso#71529. The latest occurrences are from more than a year ago, so they may already have been resolved.

There are indeed many reports there, but most seem to have been resolved after a moment-timezone upgrade in #47879.

Still, this PR is pretty good, let's merge it 🙂

@jsnajdr jsnajdr merged commit fff5f25 into WordPress:trunk Mar 26, 2026
39 of 40 checks passed
@github-actions github-actions bot added this to the Gutenberg 22.9 milestone Mar 26, 2026
@epeicher epeicher deleted the epeicher/fix-wp-timezone-loss branch March 26, 2026 15:07
adamsilverstein pushed a commit that referenced this pull request Mar 26, 2026
…ad (#75831)

Fixes issue where plugins (e.g. WooCommerce) that load their own copy of moment-timezone destroy the custom 'WP' zone and cause post times to be offset by 2x the UTC difference.

The fix adds ensureWPTimezone() guards to isInTheFuture(), getDate(), and humanTimeDiff(). The custom WP timezone zone is now cached in packed form on first creation, so it can be re-added without requiring moment-timezone-utils after a third-party reload. Includes tests that verify recovery after the zone is destroyed.

Co-authored-by: epeicher <epeicher@git.wordpress.org>
Co-authored-by: jsnajdr <jsnajdr@git.wordpress.org>
adamsilverstein pushed a commit that referenced this pull request Mar 31, 2026
…ad (#75831)

Fixes issue where plugins (e.g. WooCommerce) that load their own copy of moment-timezone destroy the custom 'WP' zone and cause post times to be offset by 2x the UTC difference.

The fix adds ensureWPTimezone() guards to isInTheFuture(), getDate(), and humanTimeDiff(). The custom WP timezone zone is now cached in packed form on first creation, so it can be re-added without requiring moment-timezone-utils after a third-party reload. Includes tests that verify recovery after the zone is destroyed.

Co-authored-by: epeicher <epeicher@git.wordpress.org>
Co-authored-by: jsnajdr <jsnajdr@git.wordpress.org>
@oandregal oandregal changed the title fix(date): Recover WP timezone after third-party reload wordpress/date: Recover WP timezone after third-party reload Apr 1, 2026
@oandregal oandregal added the [Type] Bug An existing feature does not function as intended label Apr 1, 2026
adamsilverstein pushed a commit that referenced this pull request Apr 7, 2026
…ad (#75831)

Fixes issue where plugins (e.g. WooCommerce) that load their own copy of moment-timezone destroy the custom 'WP' zone and cause post times to be offset by 2x the UTC difference.

The fix adds ensureWPTimezone() guards to isInTheFuture(), getDate(), and humanTimeDiff(). The custom WP timezone zone is now cached in packed form on first creation, so it can be re-added without requiring moment-timezone-utils after a third-party reload. Includes tests that verify recovery after the zone is destroyed.

Co-authored-by: epeicher <epeicher@git.wordpress.org>
Co-authored-by: jsnajdr <jsnajdr@git.wordpress.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] Date /packages/date [Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants