diff --git a/wp-admin/includes/class-wp-community-events.php b/wp-admin/includes/class-wp-community-events.php
index 37c71be412..6f4a101067 100644
--- a/wp-admin/includes/class-wp-community-events.php
+++ b/wp-admin/includes/class-wp-community-events.php
@@ -77,6 +77,8 @@ class WP_Community_Events {
* mitigates possible privacy concerns.
*
* @since 4.8.0
+ * @since 5.6.0 Response no longer contains formatted date field. They're added
+ * in `wp.communityEvents.populateDynamicEventFields()` now.
*
* @param string $location_search Optional. City name to help determine the location.
* e.g., "Seattle". Default empty string.
@@ -165,7 +167,6 @@ class WP_Community_Events {
$this->cache_events( $response_body, $expiration );
$response_body['events'] = $this->trim_events( $response_body['events'] );
- $response_body = $this->format_event_data_time( $response_body );
return $response_body;
}
@@ -344,6 +345,8 @@ class WP_Community_Events {
* Gets cached events.
*
* @since 4.8.0
+ * @since 5.6.0 Response no longer contains formatted date field. They're added
+ * in `wp.communityEvents.populateDynamicEventFields()` now.
*
* @return array|false An array containing `location` and `events` items
* on success, false on failure.
@@ -355,7 +358,7 @@ class WP_Community_Events {
$cached_response['events'] = $this->trim_events( $cached_response['events'] );
}
- return $this->format_event_data_time( $cached_response );
+ return $cached_response;
}
/**
@@ -372,6 +375,12 @@ class WP_Community_Events {
* @return array The response with dates and times formatted.
*/
protected function format_event_data_time( $response_body ) {
+ _deprecated_function(
+ __METHOD__,
+ '5.6.0',
+ 'This is no longer used by Core, and only kept for backwards-compatibility.'
+ );
+
if ( isset( $response_body['events'] ) ) {
foreach ( $response_body['events'] as $key => $event ) {
$timestamp = strtotime( $event['date'] );
diff --git a/wp-admin/includes/dashboard.php b/wp-admin/includes/dashboard.php
index b36ef18dce..971a60d77b 100644
--- a/wp-admin/includes/dashboard.php
+++ b/wp-admin/includes/dashboard.php
@@ -1389,9 +1389,11 @@ function wp_print_community_events_templates() {
- {{ event.formatted_date }}
+ {{ event.user_formatted_date }}
<# if ( 'meetup' === event.type ) { #>
- {{ event.formatted_time }}
+
+ {{ event.user_formatted_time }} {{ event.timeZoneAbbreviation }}
+
<# } #>
diff --git a/wp-admin/js/dashboard.js b/wp-admin/js/dashboard.js
index 581746bb45..87a8493128 100644
--- a/wp-admin/js/dashboard.js
+++ b/wp-admin/js/dashboard.js
@@ -266,6 +266,11 @@ jQuery( function( $ ) {
'use strict';
var communityEventsData = window.communityEventsData || {},
+ dateI18n = wp.date.dateI18n,
+ format = wp.date.format,
+ sprintf = wp.i18n.sprintf,
+ __ = wp.i18n.__,
+ _x = wp.i18n._x,
app;
/**
@@ -441,6 +446,7 @@ jQuery( function( $ ) {
.fail( function() {
app.renderEventsTemplate({
'location' : false,
+ 'events' : [],
'error' : true
}, initiatedBy );
});
@@ -465,6 +471,11 @@ jQuery( function( $ ) {
$locationMessage = $( '#community-events-location-message' ),
$results = $( '.community-events-results' );
+ templateParams.events = app.populateDynamicEventFields(
+ templateParams.events,
+ communityEventsData.time_format
+ );
+
/*
* Hide all toggleable elements by default, to keep the logic simple.
* Otherwise, each block below would have to turn hide everything that
@@ -576,6 +587,195 @@ jQuery( function( $ ) {
} else {
app.toggleLocationForm( 'show' );
}
+ },
+
+ /**
+ * Populate event fields that have to be calculated on the fly.
+ *
+ * These can't be stored in the database, because they're dependent on
+ * the user's current time zone, locale, etc.
+ *
+ * @since 5.6.0
+ *
+ * @param {Array} rawEvents The events that should have dynamic fields added to them.
+ * @param {string} timeFormat A time format acceptable by `wp.date.dateI18n()`.
+ *
+ * @returns {Array}
+ */
+ populateDynamicEventFields: function( rawEvents, timeFormat ) {
+ // Clone the parameter to avoid mutating it, so that this can remain a pure function.
+ var populatedEvents = JSON.parse( JSON.stringify( rawEvents ) );
+
+ $.each( populatedEvents, function( index, event ) {
+ var timeZone = app.getTimeZone( event.start_unix_timestamp * 1000 );
+
+ event.user_formatted_date = app.getFormattedDate(
+ event.start_unix_timestamp * 1000,
+ event.end_unix_timestamp * 1000,
+ timeZone
+ );
+
+ event.user_formatted_time = dateI18n(
+ timeFormat,
+ event.start_unix_timestamp * 1000,
+ timeZone
+ );
+
+ event.timeZoneAbbreviation = app.getTimeZoneAbbreviation( event.start_unix_timestamp * 1000 );
+ } );
+
+ return populatedEvents;
+ },
+
+ /**
+ * Returns the user's local/browser time zone, in a form suitable for `wp.date.i18n()`.
+ *
+ * @since 5.6.0
+ *
+ * @param startTimestamp
+ *
+ * @returns {string|number}
+ */
+ getTimeZone: function( startTimestamp ) {
+ /*
+ * Prefer a name like `Europe/Helsinki`, since that automatically tracks daylight savings. This
+ * doesn't need to take `startTimestamp` into account for that reason.
+ */
+ var timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+
+ /*
+ * Fall back to an offset for IE11, which declares the property but doesn't assign a value.
+ */
+ if ( 'undefined' === typeof timeZone ) {
+ /*
+ * It's important to use the _event_ time, not the _current_
+ * time, so that daylight savings time is accounted for.
+ */
+ timeZone = app.getFlippedTimeZoneOffset( startTimestamp );
+ }
+
+ return timeZone;
+ },
+
+ /**
+ * Get intuitive time zone offset.
+ *
+ * `Data.prototype.getTimezoneOffset()` returns a positive value for time zones
+ * that are _behind_ UTC, and a _negative_ value for ones that are ahead.
+ *
+ * See https://stackoverflow.com/questions/21102435/why-does-javascript-date-gettimezoneoffset-consider-0500-as-a-positive-off.
+ *
+ * @since 5.6.0
+ *
+ * @param {number} startTimestamp
+ *
+ * @returns {number}
+ */
+ getFlippedTimeZoneOffset: function( startTimestamp ) {
+ return new Date( startTimestamp ).getTimezoneOffset() * -1;
+ },
+
+ /**
+ * Get a short time zone name, like `PST`.
+ *
+ * @since 5.6.0
+ *
+ * @param {number} startTimestamp
+ *
+ * @returns {string}
+ */
+ getTimeZoneAbbreviation: function( startTimestamp ) {
+ var timeZoneAbbreviation,
+ eventDateTime = new Date( startTimestamp );
+
+ /*
+ * Leaving the `locales` argument undefined is important, so that the browser
+ * displays the abbreviation that's most appropriate for the current locale. For
+ * some that will be `UTC{+|-}{n}`, and for others it will be a code like `PST`.
+ *
+ * This doesn't need to take `startTimestamp` into account, because a name like
+ * `America/Chicago` automatically tracks daylight savings.
+ */
+ var shortTimeStringParts = eventDateTime.toLocaleTimeString( undefined, { timeZoneName : 'short' } ).split( ' ' );
+
+ if ( 3 === shortTimeStringParts.length ) {
+ timeZoneAbbreviation = shortTimeStringParts[2];
+ }
+
+ if ( 'undefined' === typeof timeZoneAbbreviation ) {
+ /*
+ * It's important to use the _event_ time, not the _current_
+ * time, so that daylight savings time is accounted for.
+ */
+ var timeZoneOffset = app.getFlippedTimeZoneOffset( startTimestamp ),
+ sign = -1 === Math.sign( timeZoneOffset ) ? '' : '+';
+
+ // translators: Used as part of a string like `GMT+5` in the Events Widget.
+ timeZoneAbbreviation = _x( 'GMT', 'Events widget offset prefix' ) + sign + ( timeZoneOffset / 60 );
+ }
+
+ return timeZoneAbbreviation;
+ },
+
+ /**
+ * Format a start/end date in the user's local time zone and locale.
+ *
+ * @since 5.6.0
+ *
+ * @param {int} startDate The Unix timestamp in milliseconds when the the event starts.
+ * @param {int} endDate The Unix timestamp in milliseconds when the the event ends.
+ * @param {string} timeZone A time zone string or offset which is parsable by `wp.date.i18n()`.
+ *
+ * @returns {string}
+ */
+ getFormattedDate: function( startDate, endDate, timeZone ) {
+ var formattedDate;
+
+ /*
+ * The `date_format` option is not used because it's important
+ * in this context to keep the day of the week in the displayed date,
+ * so that users can tell at a glance if the event is on a day they
+ * are available, without having to open the link.
+ *
+ * The case of crossing a year boundary is intentionally not handled.
+ * It's so rare in practice that it's not worth the complexity
+ * tradeoff. The _ending_ year should be passed to
+ * `multiple_month_event`, though, just in case.
+ */
+ /* translators: Date format for upcoming events on the dashboard. Include the day of the week. See https://www.php.net/manual/datetime.format.php */
+ var singleDayEvent = __( 'l, M j, Y' ),
+ /* translators: Date string for upcoming events. 1: Month, 2: Starting day, 3: Ending day, 4: Year. */
+ multipleDayEvent = __( '%1$s %2$dā%3$d, %4$d' ),
+ /* translators: Date string for upcoming events. 1: Starting month, 2: Starting day, 3: Ending month, 4: Ending day, 5: Ending year. */
+ multipleMonthEvent = __( '%1$s %2$d ā %3$s %4$d, %5$d' );
+
+ // Detect single-day events.
+ if ( ! endDate || format( 'Y-m-d', startDate ) === format( 'Y-m-d', endDate ) ) {
+ formattedDate = dateI18n( singleDayEvent, startDate, timeZone );
+
+ // Multiple day events.
+ } else if ( format( 'Y-m', startDate ) === format( 'Y-m', endDate ) ) {
+ formattedDate = sprintf(
+ multipleDayEvent,
+ dateI18n( _x( 'F', 'upcoming events month format' ), startDate, timeZone ),
+ dateI18n( _x( 'j', 'upcoming events day format' ), startDate, timeZone ),
+ dateI18n( _x( 'j', 'upcoming events day format' ), endDate, timeZone ),
+ dateI18n( _x( 'Y', 'upcoming events year format' ), endDate, timeZone )
+ );
+
+ // Multi-day events that cross a month boundary.
+ } else {
+ formattedDate = sprintf(
+ multipleMonthEvent,
+ dateI18n( _x( 'F', 'upcoming events month format' ), startDate, timeZone ),
+ dateI18n( _x( 'j', 'upcoming events day format' ), startDate, timeZone ),
+ dateI18n( _x( 'F', 'upcoming events month format' ), endDate, timeZone ),
+ dateI18n( _x( 'j', 'upcoming events day format' ), endDate, timeZone ),
+ dateI18n( _x( 'Y', 'upcoming events year format' ), endDate, timeZone )
+ );
+ }
+
+ return formattedDate;
}
};
diff --git a/wp-admin/js/dashboard.min.js b/wp-admin/js/dashboard.min.js
index c5cd843934..a51b6218a5 100644
--- a/wp-admin/js/dashboard.min.js
+++ b/wp-admin/js/dashboard.min.js
@@ -1,2 +1,2 @@
/*! This file is auto-generated */
-window.wp=window.wp||{},jQuery(document).ready(function(c){var t,n=c("#welcome-panel"),e=c("#wp_welcome_panel-hide");t=function(e){c.post(ajaxurl,{action:"update-welcome-panel",visible:e,welcomepanelnonce:c("#welcomepanelnonce").val()})},n.hasClass("hidden")&&e.prop("checked")&&n.removeClass("hidden"),c(".welcome-panel-close, .welcome-panel-dismiss a",n).click(function(e){e.preventDefault(),n.addClass("hidden"),t(0),c("#wp_welcome_panel-hide").prop("checked",!1)}),e.click(function(){n.toggleClass("hidden",!this.checked),t(this.checked?1:0)}),window.ajaxWidgets=["dashboard_primary"],window.ajaxPopulateWidgets=function(e){function t(e,t){var n,o=c("#"+t+" div.inside:visible").find(".widget-loading");o.length&&(n=o.parent(),setTimeout(function(){n.load(ajaxurl+"?action=dashboard-widgets&widget="+t+"&pagenow="+pagenow,"",function(){n.hide().slideDown("normal",function(){c(this).css("display","")})})},500*e))}e?(e=e.toString(),-1!==c.inArray(e,ajaxWidgets)&&t(0,e)):c.each(ajaxWidgets,t)},ajaxPopulateWidgets(),postboxes.add_postbox_toggles(pagenow,{pbshow:ajaxPopulateWidgets}),window.quickPressLoad=function(){var t,e=c("#quickpost-action");c('#quick-press .submit input[type="submit"], #quick-press .submit input[type="reset"]').prop("disabled",!1),t=c("#quick-press").submit(function(e){e.preventDefault(),c("#dashboard_quick_press #publishing-action .spinner").show(),c('#quick-press .submit input[type="submit"], #quick-press .submit input[type="reset"]').prop("disabled",!0),c.post(t.attr("action"),t.serializeArray(),function(e){c("#dashboard_quick_press .inside").html(e),c("#quick-press").removeClass("initial-form"),quickPressLoad(),function(){var e=c(".drafts ul li").first();e.css("background","#fffbe5"),setTimeout(function(){e.css("background","none")},1e3)}(),c("#title").focus()})}),c("#publish").click(function(){e.val("post-quickpress-publish")}),c("#quick-press").on("click focusin",function(){wpActiveEditor="content"}),function(){if(document.documentMode&&document.documentMode<9)return;c("body").append('');var o=c(".quick-draft-textarea-clone"),i=c("#content"),a=i.height(),s=c(window).height()-100;o.css({"font-family":i.css("font-family"),"font-size":i.css("font-size"),"line-height":i.css("line-height"),"padding-bottom":i.css("paddingBottom"),"padding-left":i.css("paddingLeft"),"padding-right":i.css("paddingRight"),"padding-top":i.css("paddingTop"),"white-space":"pre-wrap","word-wrap":"break-word",display:"none"}),i.on("focus input propertychange",function(){var e=c(this),t=e.val()+" ",n=o.css("width",e.css("width")).text(t).outerHeight()+2;i.css("overflow-y","auto"),n===a||s<=n&&s<=a||(a=s');var i=c(".quick-draft-textarea-clone"),o=c("#content"),a=o.height(),s=c(window).height()-100;i.css({"font-family":o.css("font-family"),"font-size":o.css("font-size"),"line-height":o.css("line-height"),"padding-bottom":o.css("paddingBottom"),"padding-left":o.css("paddingLeft"),"padding-right":o.css("paddingRight"),"padding-top":o.css("paddingTop"),"white-space":"pre-wrap","word-wrap":"break-word",display:"none"}),o.on("focus input propertychange",function(){var e=c(this),t=e.val()+" ",n=i.css("width",e.css("width")).text(t).outerHeight()+2;o.css("overflow-y","auto"),n===a||s<=n&&s<=a||(a=sadd( 'wp-color-picker', "/wp-admin/js/color-picker$suffix.js", array( 'iris' ), false, 1 );
$scripts->set_translations( 'wp-color-picker' );
- $scripts->add( 'dashboard', "/wp-admin/js/dashboard$suffix.js", array( 'jquery', 'admin-comments', 'postbox', 'wp-util', 'wp-a11y' ), false, 1 );
+ $scripts->add( 'dashboard', "/wp-admin/js/dashboard$suffix.js", array( 'jquery', 'admin-comments', 'postbox', 'wp-util', 'wp-a11y', 'wp-date' ), false, 1 );
+ $scripts->set_translations( 'dashboard' );
$scripts->add( 'list-revisions', "/wp-includes/js/wp-list-revisions$suffix.js" );
@@ -1755,6 +1756,7 @@ function wp_localize_community_events() {
array(
'nonce' => wp_create_nonce( 'community_events' ),
'cache' => $events_client->get_cached_events(),
+ 'time_format' => get_option( 'time_format' ),
'l10n' => array(
'enter_closest_city' => __( 'Enter your closest city to find nearby events.' ),
diff --git a/wp-includes/version.php b/wp-includes/version.php
index bac267d646..3ca4847fa9 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -13,7 +13,7 @@
*
* @global string $wp_version
*/
-$wp_version = '5.6-alpha-49145';
+$wp_version = '5.6-alpha-49146';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.