diff --git a/packages/date/README.md b/packages/date/README.md
index e45bb664c85519..954ea388ece67d 100644
--- a/packages/date/README.md
+++ b/packages/date/README.md
@@ -18,26 +18,40 @@ _This package assumes that your code will run in an **ES2015+** environment. If
# **date**
-Formats a date (like `date()` in PHP), in the site's timezone.
+Formats a date (like `date()` in PHP).
+
+_Related_
+
+-
+-
_Parameters_
- _dateFormat_ `string`: PHP-style formatting string. See php.net/date.
- _dateValue_ `(Date|string|Moment|null)`: Date object or string, parsable by moment.js.
+- _timezone_ `(string|number|null)`: Timezone to output result in or a UTC offset. Defaults to timezone from site.
_Returns_
-- `string`: Formatted date.
+- `string`: Formatted date in English.
# **dateI18n**
-Formats a date (like `date_i18n()` in PHP).
+Formats a date (like `wp_date()` in PHP), translating it into site's locale.
+
+Backward Compatibility Notice: if `timezone` is set to `true`, the function
+behaves like `gmdateI18n`.
+
+_Related_
+
+-
+-
_Parameters_
- _dateFormat_ `string`: PHP-style formatting string. See php.net/date.
- _dateValue_ `(Date|string|Moment|null)`: Date object or string, parsable by moment.js.
-- _gmt_ `boolean`: True for GMT/UTC, false for site's timezone.
+- _timezone_ `(string|number|boolean|null)`: Timezone to output result in or a UTC offset. Defaults to timezone from site. Notice: `boolean` is effectively deprecated, but still supported for backward compatibility reasons.
_Returns_
@@ -79,6 +93,20 @@ _Parameters_
_Returns_
+- `string`: Formatted date in English.
+
+# **gmdateI18n**
+
+Formats a date (like `wp_date()` in PHP), translating it into site's locale
+and using the UTC timezone.
+
+_Parameters_
+
+- _dateFormat_ `string`: PHP-style formatting string. See php.net/date.
+- _dateValue_ `(Date|string|Moment|null)`: Date object or string, parsable by moment.js.
+
+_Returns_
+
- `string`: Formatted date.
# **isInTheFuture**
diff --git a/packages/date/src/index.js b/packages/date/src/index.js
index 6251e1c82c04bf..25697d8fadd596 100644
--- a/packages/date/src/index.js
+++ b/packages/date/src/index.js
@@ -9,6 +9,10 @@ import 'moment-timezone/moment-timezone-utils';
const WP_ZONE = 'WP';
+// This regular expression tests positive for UTC offsets as described in ISO 8601.
+// See: https://en.wikipedia.org/wiki/ISO_8601#Time_offsets_from_UTC
+const VALID_UTC_OFFSET = /^[+-][0-1][0-9](:?[0-9][0-9])?$/;
+
// Changes made here will likely need to be made in `lib/client-assets.php` as
// well because it uses the `setSettings()` function to change these settings.
let settings = {
@@ -318,10 +322,10 @@ const formatMap = {
/**
* Formats a date. Does not alter the date's timezone.
*
- * @param {string} dateFormat PHP-style formatting string.
- * See php.net/date.
- * @param {(Date|string|Moment|null)} dateValue Date object or string,
- * parsable by moment.js.
+ * @param {string} dateFormat PHP-style formatting string.
+ * See php.net/date.
+ * @param {Date|string|Moment|null} dateValue Date object or string,
+ * parsable by moment.js.
*
* @return {string} Formatted date.
*/
@@ -357,30 +361,35 @@ export function format( dateFormat, dateValue = new Date() ) {
}
/**
- * Formats a date (like `date()` in PHP), in the site's timezone.
+ * Formats a date (like `date()` in PHP).
*
- * @param {string} dateFormat PHP-style formatting string.
- * See php.net/date.
- * @param {(Date|string|Moment|null)} dateValue Date object or string,
- * parsable by moment.js.
+ * @param {string} dateFormat PHP-style formatting string.
+ * See php.net/date.
+ * @param {Date|string|Moment|null} dateValue Date object or string, parsable
+ * by moment.js.
+ * @param {string|number|null} timezone Timezone to output result in or a
+ * UTC offset. Defaults to timezone from
+ * site.
*
- * @return {string} Formatted date.
+ * @see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
+ * @see https://en.wikipedia.org/wiki/ISO_8601#Time_offsets_from_UTC
+ *
+ * @return {string} Formatted date in English.
*/
-export function date( dateFormat, dateValue = new Date() ) {
- const offset = settings.timezone.offset * HOUR_IN_MINUTES;
- const dateMoment = momentLib( dateValue ).utcOffset( offset, true );
+export function date( dateFormat, dateValue = new Date(), timezone ) {
+ const dateMoment = buildMoment( dateValue, timezone );
return format( dateFormat, dateMoment );
}
/**
* Formats a date (like `date()` in PHP), in the UTC timezone.
*
- * @param {string} dateFormat PHP-style formatting string.
- * See php.net/date.
- * @param {(Date|string|Moment|null)} dateValue Date object or string,
- * parsable by moment.js.
+ * @param {string} dateFormat PHP-style formatting string.
+ * See php.net/date.
+ * @param {Date|string|Moment|null} dateValue Date object or string,
+ * parsable by moment.js.
*
- * @return {string} Formatted date.
+ * @return {string} Formatted date in English.
*/
export function gmdate( dateFormat, dateValue = new Date() ) {
const dateMoment = momentLib( dateValue ).utc();
@@ -388,26 +397,54 @@ export function gmdate( dateFormat, dateValue = new Date() ) {
}
/**
- * Formats a date (like `date_i18n()` in PHP).
+ * Formats a date (like `wp_date()` in PHP), translating it into site's locale.
+ *
+ * Backward Compatibility Notice: if `timezone` is set to `true`, the function
+ * behaves like `gmdateI18n`.
+ *
+ * @param {string} dateFormat PHP-style formatting string.
+ * See php.net/date.
+ * @param {Date|string|Moment|null} dateValue Date object or string, parsable by
+ * moment.js.
+ * @param {string|number|boolean|null} timezone Timezone to output result in or a
+ * UTC offset. Defaults to timezone from
+ * site. Notice: `boolean` is effectively
+ * deprecated, but still supported for
+ * backward compatibility reasons.
*
- * @param {string} dateFormat PHP-style formatting string.
- * See php.net/date.
- * @param {(Date|string|Moment|null)} dateValue Date object or string,
- * parsable by moment.js.
- * @param {boolean} gmt True for GMT/UTC, false for
- * site's timezone.
+ * @see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
+ * @see https://en.wikipedia.org/wiki/ISO_8601#Time_offsets_from_UTC
*
* @return {string} Formatted date.
*/
-export function dateI18n( dateFormat, dateValue = new Date(), gmt = false ) {
- // Defaults.
- const offset = gmt ? 0 : settings.timezone.offset * HOUR_IN_MINUTES;
- // Convert to moment object.
- const dateMoment = momentLib( dateValue ).utcOffset( offset, true );
+export function dateI18n( dateFormat, dateValue = new Date(), timezone ) {
+ if ( true === timezone ) {
+ return gmdateI18n( dateFormat, dateValue );
+ }
+
+ if ( false === timezone ) {
+ timezone = undefined;
+ }
- // Set the locale.
+ const dateMoment = buildMoment( dateValue, timezone );
+ dateMoment.locale( settings.l10n.locale );
+ return format( dateFormat, dateMoment );
+}
+
+/**
+ * Formats a date (like `wp_date()` in PHP), translating it into site's locale
+ * and using the UTC timezone.
+ *
+ * @param {string} dateFormat PHP-style formatting string.
+ * See php.net/date.
+ * @param {Date|string|Moment|null} dateValue Date object or string,
+ * parsable by moment.js.
+ *
+ * @return {string} Formatted date.
+ */
+export function gmdateI18n( dateFormat, dateValue = new Date() ) {
+ const dateMoment = momentLib( dateValue ).utc();
dateMoment.locale( settings.l10n.locale );
- // Format and return.
return format( dateFormat, dateMoment );
}
@@ -440,4 +477,51 @@ export function getDate( dateString ) {
return momentLib.tz( dateString, WP_ZONE ).toDate();
}
+/**
+ * Creates a moment instance using the given timezone or, if none is provided, using global settings.
+ *
+ * @param {Date|string|Moment|null} dateValue Date object or string, parsable
+ * by moment.js.
+ * @param {string|number|null} timezone Timezone to output result in or a
+ * UTC offset. Defaults to timezone from
+ * site.
+ *
+ * @see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
+ * @see https://en.wikipedia.org/wiki/ISO_8601#Time_offsets_from_UTC
+ *
+ * @return {Moment} a moment instance.
+ */
+function buildMoment( dateValue, timezone = '' ) {
+ const dateMoment = momentLib( dateValue );
+
+ if ( timezone && ! isUTCOffset( timezone ) ) {
+ return dateMoment.tz( timezone );
+ }
+
+ if ( timezone && isUTCOffset( timezone ) ) {
+ return dateMoment.utcOffset( timezone );
+ }
+
+ if ( settings.timezone.string ) {
+ return dateMoment.tz( settings.timezone.string );
+ }
+
+ return dateMoment.utcOffset( settings.timezone.offset );
+}
+
+/**
+ * Returns whether a certain UTC offset is valid or not.
+ *
+ * @param {number|string} offset a UTC offset.
+ *
+ * @return {boolean} whether a certain UTC offset is valid or not.
+ */
+function isUTCOffset( offset ) {
+ if ( 'number' === typeof offset ) {
+ return true;
+ }
+
+ return VALID_UTC_OFFSET.test( offset );
+}
+
setupWPTimezone();
diff --git a/packages/date/src/test/index.js b/packages/date/src/test/index.js
index 69368b1fb630b5..d9b832e528ce6b 100644
--- a/packages/date/src/test/index.js
+++ b/packages/date/src/test/index.js
@@ -2,10 +2,14 @@
* Internal dependencies
*/
import {
- isInTheFuture,
+ __experimentalGetSettings,
+ date as dateNoI18n,
+ dateI18n,
getDate,
+ gmdate,
+ gmdateI18n,
+ isInTheFuture,
setSettings,
- __experimentalGetSettings,
} from '../';
describe( 'isInTheFuture', () => {
@@ -16,7 +20,7 @@ describe( 'isInTheFuture', () => {
expect( isInTheFuture( date ) ).toBe( true );
} );
- it( 'should return true if the date is in the past', () => {
+ it( 'should return false if the date is in the past', () => {
// Create a Date object 1 minute in the past.
const date = new Date( Number( getDate() ) - 1000 * 60 );
@@ -44,6 +48,433 @@ describe( 'isInTheFuture', () => {
} );
} );
+describe( 'Function date', () => {
+ it( 'should format date in English, ignoring locale settings', () => {
+ const settings = __experimentalGetSettings();
+
+ // Simulate different locale
+ const l10n = settings.l10n;
+ setSettings( {
+ ...settings,
+ l10n: {
+ ...l10n,
+ locale: 'es',
+ months: l10n.months.map( ( month ) => `es_${ month }` ),
+ monthsShort: l10n.monthsShort.map(
+ ( month ) => `es_${ month }`
+ ),
+ weekdays: l10n.weekdays.map( ( weekday ) => `es_${ weekday }` ),
+ weekdaysShort: l10n.weekdaysShort.map(
+ ( weekday ) => `es_${ weekday }`
+ ),
+ },
+ } );
+
+ // Check
+ const formattedDate = dateNoI18n(
+ 'F M l D',
+ '2019-06-18T11:00:00.000Z'
+ );
+ expect( formattedDate ).toBe( 'June Jun Tuesday Tue' );
+
+ // Restore default settings
+ setSettings( settings );
+ } );
+
+ it( 'should format date into a date that uses site’s timezone, if no timezone was provided and there’s a site timezone set', () => {
+ const settings = __experimentalGetSettings();
+
+ // Simulate different timezone
+ setSettings( {
+ ...settings,
+ timezone: { offset: -4, string: 'America/New_York' },
+ } );
+
+ // Check
+ const winterFormattedDate = dateNoI18n(
+ 'Y-m-d H:i',
+ '2019-01-18T11:00:00.000Z'
+ );
+ expect( winterFormattedDate ).toBe( '2019-01-18 06:00' );
+
+ const summerFormattedDate = dateNoI18n(
+ 'Y-m-d H:i',
+ '2019-06-18T11:00:00.000Z'
+ );
+ expect( summerFormattedDate ).toBe( '2019-06-18 07:00' );
+
+ // Restore default settings
+ setSettings( settings );
+ } );
+
+ it( 'should format date into a date that uses site’s UTC offset setting, if no timezone was provided and there isn’t a timezone set in the site', () => {
+ const settings = __experimentalGetSettings();
+
+ // Simulate different timezone
+ setSettings( {
+ ...settings,
+ timezone: { offset: -4, string: '' },
+ } );
+
+ // Check
+ const winterFormattedDate = dateNoI18n(
+ 'Y-m-d H:i',
+ '2019-01-18T11:00:00.000Z'
+ );
+ expect( winterFormattedDate ).toBe( '2019-01-18 07:00' );
+
+ const summerFormattedDate = dateNoI18n(
+ 'Y-m-d H:i',
+ '2019-06-18T11:00:00.000Z'
+ );
+ expect( summerFormattedDate ).toBe( '2019-06-18 07:00' );
+
+ // Restore default settings
+ setSettings( settings );
+ } );
+
+ it( 'should format date into a date that uses the given timezone, if said timezone is valid', () => {
+ const settings = __experimentalGetSettings();
+
+ // Simulate different timezone
+ setSettings( {
+ ...settings,
+ timezone: { offset: -4, string: 'America/New_York' },
+ } );
+
+ // Check
+ const formattedDate = dateNoI18n(
+ 'Y-m-d H:i',
+ '2019-06-18T11:00:00.000Z',
+ 'Asia/Macau'
+ );
+ expect( formattedDate ).toBe( '2019-06-18 19:00' );
+
+ // Restore default settings
+ setSettings( settings );
+ } );
+
+ it( 'should format date into a date that uses the given UTC offset, if given timezone is actually a UTC offset', () => {
+ const settings = __experimentalGetSettings();
+
+ // Simulate different timezone
+ setSettings( {
+ ...settings,
+ timezone: { offset: -4, string: 'America/New_York' },
+ } );
+
+ // Check
+ let formattedDate;
+ formattedDate = dateNoI18n(
+ 'Y-m-d H:i',
+ '2019-06-18T11:00:00.000Z',
+ '+08:00'
+ );
+ expect( formattedDate ).toBe( '2019-06-18 19:00' );
+
+ formattedDate = dateNoI18n(
+ 'Y-m-d H:i',
+ '2019-06-18T11:00:00.000Z',
+ 8
+ );
+ expect( formattedDate ).toBe( '2019-06-18 19:00' );
+
+ formattedDate = dateNoI18n(
+ 'Y-m-d H:i',
+ '2019-06-18T11:00:00.000Z',
+ 480
+ );
+ expect( formattedDate ).toBe( '2019-06-18 19:00' );
+
+ // Restore default settings
+ setSettings( settings );
+ } );
+} );
+
+describe( 'Function gmdate', () => {
+ it( 'should format date in English, ignoring locale settings', () => {
+ const settings = __experimentalGetSettings();
+
+ // Simulate different locale
+ const l10n = settings.l10n;
+ setSettings( {
+ ...settings,
+ l10n: {
+ ...l10n,
+ locale: 'es',
+ months: l10n.months.map( ( month ) => `es_${ month }` ),
+ monthsShort: l10n.monthsShort.map(
+ ( month ) => `es_${ month }`
+ ),
+ weekdays: l10n.weekdays.map( ( weekday ) => `es_${ weekday }` ),
+ weekdaysShort: l10n.weekdaysShort.map(
+ ( weekday ) => `es_${ weekday }`
+ ),
+ },
+ } );
+
+ // Check
+ const formattedDate = gmdate( 'F M l D', '2019-06-18T11:00:00.000Z' );
+ expect( formattedDate ).toBe( 'June Jun Tuesday Tue' );
+
+ // Restore default settings
+ setSettings( settings );
+ } );
+
+ it( 'should format date into a UTC date', () => {
+ const settings = __experimentalGetSettings();
+
+ // Simulate different timezone
+ setSettings( {
+ ...settings,
+ timezone: { offset: -4, string: 'America/New_York' },
+ } );
+
+ // Check
+ const formattedDate = gmdate( 'Y-m-d H:i', '2019-06-18T11:00:00.000Z' );
+ expect( formattedDate ).toBe( '2019-06-18 11:00' );
+
+ // Restore default settings
+ setSettings( settings );
+ } );
+} );
+
+describe( 'Function dateI18n', () => {
+ it( 'should format date using locale settings', () => {
+ const settings = __experimentalGetSettings();
+
+ // Simulate different locale
+ const l10n = settings.l10n;
+ setSettings( {
+ ...settings,
+ l10n: {
+ ...l10n,
+ locale: 'es',
+ months: l10n.months.map( ( month ) => `es_${ month }` ),
+ monthsShort: l10n.monthsShort.map(
+ ( month ) => `es_${ month }`
+ ),
+ weekdays: l10n.weekdays.map( ( weekday ) => `es_${ weekday }` ),
+ weekdaysShort: l10n.weekdaysShort.map(
+ ( weekday ) => `es_${ weekday }`
+ ),
+ },
+ } );
+
+ // Check
+ const formattedDate = dateI18n(
+ 'F M l D',
+ '2019-06-18T11:00:00.000Z',
+ true
+ );
+ expect( formattedDate ).toBe( 'es_June es_Jun es_Tuesday es_Tue' );
+
+ // Restore default settings
+ setSettings( settings );
+ } );
+
+ it( 'should format date into a date that uses site’s timezone, if no timezone was provided and there’s a site timezone set', () => {
+ const settings = __experimentalGetSettings();
+
+ // Simulate different timezone
+ setSettings( {
+ ...settings,
+ timezone: { offset: -4, string: 'America/New_York' },
+ } );
+
+ // Check
+ const winterFormattedDate = dateI18n(
+ 'Y-m-d H:i',
+ '2019-01-18T11:00:00.000Z'
+ );
+ expect( winterFormattedDate ).toBe( '2019-01-18 06:00' );
+
+ const summerFormattedDate = dateI18n(
+ 'Y-m-d H:i',
+ '2019-06-18T11:00:00.000Z'
+ );
+ expect( summerFormattedDate ).toBe( '2019-06-18 07:00' );
+
+ // Restore default settings
+ setSettings( settings );
+ } );
+
+ it( 'should format date into a date that uses site’s UTC offset setting, if no timezone was provided and there isn’t a timezone set in the site', () => {
+ const settings = __experimentalGetSettings();
+
+ // Simulate different timezone
+ setSettings( {
+ ...settings,
+ timezone: { offset: -4, string: '' },
+ } );
+
+ // Check
+ const winterFormattedDate = dateI18n(
+ 'Y-m-d H:i',
+ '2019-01-18T11:00:00.000Z'
+ );
+ expect( winterFormattedDate ).toBe( '2019-01-18 07:00' );
+
+ const summerFormattedDate = dateI18n(
+ 'Y-m-d H:i',
+ '2019-06-18T11:00:00.000Z'
+ );
+ expect( summerFormattedDate ).toBe( '2019-06-18 07:00' );
+
+ // Restore default settings
+ setSettings( settings );
+ } );
+
+ it( 'should format date into a date that uses the given timezone, if said timezone is valid', () => {
+ const settings = __experimentalGetSettings();
+
+ // Simulate different timezone
+ setSettings( {
+ ...settings,
+ timezone: { offset: -4, string: 'America/New_York' },
+ } );
+
+ // Check
+ const formattedDate = dateI18n(
+ 'Y-m-d H:i',
+ '2019-06-18T11:00:00.000Z',
+ 'Asia/Macau'
+ );
+ expect( formattedDate ).toBe( '2019-06-18 19:00' );
+
+ // Restore default settings
+ setSettings( settings );
+ } );
+
+ it( 'should format date into a date that uses the given UTC offset, if given timezone is actually a UTC offset', () => {
+ const settings = __experimentalGetSettings();
+
+ // Simulate different timezone
+ setSettings( {
+ ...settings,
+ timezone: { offset: -4, string: 'America/New_York' },
+ } );
+
+ // Check
+ let formattedDate;
+ formattedDate = dateI18n(
+ 'Y-m-d H:i',
+ '2019-06-18T11:00:00.000Z',
+ '+08:00'
+ );
+ expect( formattedDate ).toBe( '2019-06-18 19:00' );
+
+ formattedDate = dateI18n( 'Y-m-d H:i', '2019-06-18T11:00:00.000Z', 8 );
+ expect( formattedDate ).toBe( '2019-06-18 19:00' );
+
+ formattedDate = dateI18n(
+ 'Y-m-d H:i',
+ '2019-06-18T11:00:00.000Z',
+ 480
+ );
+ expect( formattedDate ).toBe( '2019-06-18 19:00' );
+
+ // Restore default settings
+ setSettings( settings );
+ } );
+
+ it( 'should format date into a UTC date if `gmt` is set to `true`', () => {
+ const settings = __experimentalGetSettings();
+
+ // Simulate different timezone
+ setSettings( {
+ ...settings,
+ timezone: { offset: -4, string: 'America/New_York' },
+ } );
+
+ // Check
+ const formattedDate = dateI18n(
+ 'Y-m-d H:i',
+ '2019-06-18T11:00:00.000Z',
+ true
+ );
+ expect( formattedDate ).toBe( '2019-06-18 11:00' );
+
+ // Restore default settings
+ setSettings( settings );
+ } );
+
+ it( 'should format date into a date that uses site’s timezone if `gmt` is set to `false`', () => {
+ const settings = __experimentalGetSettings();
+
+ // Simulate different timezone
+ setSettings( {
+ ...settings,
+ timezone: { offset: -4, string: 'America/New_York' },
+ } );
+
+ // Check
+ const formattedDate = dateI18n(
+ 'Y-m-d H:i',
+ '2019-06-18T11:00:00.000Z',
+ false
+ );
+ expect( formattedDate ).toBe( '2019-06-18 07:00' );
+
+ // Restore default settings
+ setSettings( settings );
+ } );
+} );
+
+describe( 'Function gmdateI18n', () => {
+ it( 'should format date using locale settings', () => {
+ const settings = __experimentalGetSettings();
+
+ // Simulate different locale
+ const l10n = settings.l10n;
+ setSettings( {
+ ...settings,
+ l10n: {
+ ...l10n,
+ locale: 'es',
+ months: l10n.months.map( ( month ) => `es_${ month }` ),
+ monthsShort: l10n.monthsShort.map(
+ ( month ) => `es_${ month }`
+ ),
+ weekdays: l10n.weekdays.map( ( weekday ) => `es_${ weekday }` ),
+ weekdaysShort: l10n.weekdaysShort.map(
+ ( weekday ) => `es_${ weekday }`
+ ),
+ },
+ } );
+
+ // Check
+ const formattedDate = gmdateI18n(
+ 'F M l D',
+ '2019-06-18T11:00:00.000Z'
+ );
+ expect( formattedDate ).toBe( 'es_June es_Jun es_Tuesday es_Tue' );
+
+ // Restore default settings
+ setSettings( settings );
+ } );
+
+ it( 'should format date into a UTC date', () => {
+ const settings = __experimentalGetSettings();
+
+ // Simulate different timezone
+ setSettings( {
+ ...settings,
+ timezone: { offset: -4, string: 'America/New_York' },
+ } );
+
+ // Check
+ const formattedDate = gmdateI18n(
+ 'Y-m-d H:i',
+ '2019-06-18T11:00:00.000Z'
+ );
+ expect( formattedDate ).toBe( '2019-06-18 11:00' );
+
+ // Restore default settings
+ setSettings( settings );
+ } );
+} );
+
describe( 'Moment.js Localization', () => {
it( 'should change the relative time strings', () => {
const settings = __experimentalGetSettings();