Commit 47d71704541 for woocommerce
commit 47d71704541b1000640ccb1a211a655c632d1e1c
Author: louwie17 <lourensschep@gmail.com>
Date: Tue Oct 13 12:05:15 2026 +0200
Validate the translated date format in the Analytics date filters and historical data import (#69745)
* Validate the translated short date format in every Analytics date input
Add getShortDateFormat() to @woocommerce/date. It pads single D/M tokens
so strict parsing accepts zero-padded input, and falls back to MM/DD/YYYY
when the translation of 'MM/DD/YYYY' isn't a Moment.js format (cs_CZ
'MM/DD/RRRR', et 'KK/PP/AAAA', fr 'JJ/MM/AAAA', ...).
Use it in the date range picker, the advanced date filter, the historical
data import, the import store's default date and moment's "L" format.
In cs_CZ the historical import read '09/25/2025' as this year's date.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ty8GSHdBSk2jMVnHvBvqC
* Validate the short date format with the current Moment locale
The check date was created once at module load, so after a locale change
it formatted month names in the old locale while parsing used the new one,
and valid formats like "D MMM YYYY" fell back to MM/DD/YYYY.
Found by Codex review.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ty8GSHdBSk2jMVnHvBvqC
* Reject localized tokens in the translated short date format
A translation such as "L" round-trips through the locale's own "L"
format, so loadLocaleData() would define "L" as itself and dates would
render as the letter. Also cover formats missing a date part.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ty8GSHdBSk2jMVnHvBvqC
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
diff --git a/packages/js/components/changelog/fix-analytics-short-date-format-fallback b/packages/js/components/changelog/fix-analytics-short-date-format-fallback
new file mode 100644
index 00000000000..5f31501cdb3
--- /dev/null
+++ b/packages/js/components/changelog/fix-analytics-short-date-format-fallback
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+DateRangeFilterPicker and the advanced date filter: accept zero-padded dates in D/M locales and ignore invalid date format translations.
diff --git a/packages/js/components/src/advanced-filters/date-filter.tsx b/packages/js/components/src/advanced-filters/date-filter.tsx
index 719dbb0e8d9..a47232330f9 100644
--- a/packages/js/components/src/advanced-filters/date-filter.tsx
+++ b/packages/js/components/src/advanced-filters/date-filter.tsx
@@ -6,7 +6,7 @@ import { SelectControl } from '@wordpress/components';
import { find, partial } from 'lodash';
import clsx from 'clsx';
import { __, _x } from '@wordpress/i18n';
-import { isoDateFormat, toMoment } from '@woocommerce/date';
+import { getShortDateFormat, isoDateFormat, toMoment } from '@woocommerce/date';
import moment from 'moment';
import type { Moment } from 'moment';
import type { ReactNode } from 'react';
@@ -22,7 +22,6 @@ import {
import type { FilterComponentProps, FilterConfig, FilterRule } from './types';
const dateStringFormat = __( 'MMM D, YYYY', 'woocommerce' );
-const dateFormat = __( 'MM/DD/YYYY', 'woocommerce' );
export type DateFilterProps = FilterComponentProps;
@@ -54,6 +53,7 @@ class DateFilter extends Component< DateFilterProps, DateFilterState > {
: [ null, filter.value ];
const after = isoAfter ? toMoment( isoDateFormat, isoAfter ) : null;
const before = isoBefore ? toMoment( isoDateFormat, isoBefore ) : null;
+ const dateFormat = getShortDateFormat();
this.state = {
before,
@@ -208,7 +208,7 @@ class DateFilter extends Component< DateFilterProps, DateFilterState > {
return (
<DatePicker
date={ date }
- dateFormat={ dateFormat }
+ dateFormat={ getShortDateFormat() }
error={ error }
isInvalidDate={ this.isFutureDate }
onUpdate={ onUpdate }
diff --git a/packages/js/components/src/advanced-filters/test/date-filter.test.tsx b/packages/js/components/src/advanced-filters/test/date-filter.test.tsx
new file mode 100644
index 00000000000..61943652c30
--- /dev/null
+++ b/packages/js/components/src/advanced-filters/test/date-filter.test.tsx
@@ -0,0 +1,84 @@
+/**
+ * @jest-environment jsdom
+ */
+/**
+ * External dependencies
+ */
+import { fireEvent, render, screen } from '@testing-library/react';
+import { createElement } from '@wordpress/element';
+import { resetLocaleData, setLocaleData } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import DateFilter from '../date-filter';
+import type { FilterConfig } from '../types';
+
+const config: FilterConfig = {
+ labels: {
+ add: 'Date',
+ remove: 'Remove date filter',
+ rule: 'Select a date filter match',
+ title: '<title>Date</title> <rule/> <filter/>',
+ filter: 'Select a transaction date',
+ },
+ rules: [
+ { value: 'before', label: 'Before' },
+ { value: 'after', label: 'After' },
+ ],
+ input: { component: 'Date' },
+};
+
+describe( 'DateFilter', () => {
+ afterEach( () => {
+ resetLocaleData();
+ } );
+
+ it.each( [
+ [ 'untranslated', undefined, 'MM/DD/YYYY', '09/25/2024', '10/04/2024' ],
+ [ 'day-first', 'DD/MM/YYYY', 'DD/MM/YYYY', '25/09/2024', '04/10/2024' ],
+ [ 'dotted', 'DD.MM.YYYY', 'DD.MM.YYYY', '25.09.2024', '04.10.2024' ],
+ [ 'hr', 'D/M/YYYY', 'DD/MM/YYYY', '25/09/2024', '04/10/2024' ],
+ [ 'fr_FR', 'JJ/MM/AAAA', 'MM/DD/YYYY', '09/25/2024', '10/04/2024' ],
+ [ 'cs_CZ', 'MM/DD/RRRR', 'MM/DD/YYYY', '09/25/2024', '10/04/2024' ],
+ [ 'lowercase', 'dd/mm/aaaa', 'MM/DD/YYYY', '09/25/2024', '10/04/2024' ],
+ ] )(
+ 'shows and parses typed dates for the %s translation',
+ ( _, translatedFormat, format, shownText, typedText ) => {
+ if ( translatedFormat ) {
+ setLocaleData(
+ { 'MM/DD/YYYY': [ translatedFormat ] },
+ 'woocommerce'
+ );
+ }
+ const onFilterChange = jest.fn();
+ render(
+ <DateFilter
+ config={ config }
+ filter={ {
+ key: 'date',
+ rule: 'before',
+ value: '2024-09-25',
+ } }
+ onFilterChange={ onFilterChange }
+ />
+ );
+
+ const input = screen.getByRole( 'textbox', {
+ name: 'Choose a date',
+ } );
+ expect( input ).toHaveValue( shownText );
+ expect( input ).toHaveAttribute(
+ 'placeholder',
+ format.toLowerCase()
+ );
+
+ fireEvent.change( input, { target: { value: typedText } } );
+ expect( screen.queryByText( 'Invalid date' ) ).toBeNull();
+ expect( onFilterChange ).toHaveBeenCalledWith( {
+ property: 'value',
+ value: '2024-10-04',
+ } );
+ }
+ );
+} );
diff --git a/packages/js/components/src/date-range-filter-picker/index.js b/packages/js/components/src/date-range-filter-picker/index.js
index b3cc53dd782..cbdfa5d8533 100644
--- a/packages/js/components/src/date-range-filter-picker/index.js
+++ b/packages/js/components/src/date-range-filter-picker/index.js
@@ -7,7 +7,7 @@ import { Dropdown } from '@wordpress/components';
import PropTypes from 'prop-types';
import { withViewportMatch } from '@wordpress/viewport';
import clsx from 'clsx';
-import moment from 'moment';
+import { getShortDateFormat } from '@woocommerce/date';
/**
* Internal dependencies
@@ -15,22 +15,6 @@ import moment from 'moment';
import DatePickerContent from './content';
import DropdownButton from '../dropdown-button';
-const defaultShortDateFormat = 'MM/DD/YYYY';
-const formatCheckDate = moment( '2001-02-03', 'YYYY-MM-DD' );
-
-/**
- * Format shown as the input hint and used to parse typed dates, so both always agree.
- * Some translations aren't Moment.js formats (e.g. "MM/DD/RRRR"), so fall back unless the translation round-trips a date.
- */
-const getShortDateFormat = () => {
- /* translators: Moment.js date format for typed dates. Keep the DD, MM and YYYY tokens. */
- const format = __( 'MM/DD/YYYY', 'woocommerce' );
- const parsed = moment( formatCheckDate.format( format ), format, true );
- return parsed.isSame( formatCheckDate, 'day' )
- ? format
- : defaultShortDateFormat;
-};
-
/**
* Select a range of dates or single dates.
*/
diff --git a/packages/js/components/src/date-range-filter-picker/test/index.js b/packages/js/components/src/date-range-filter-picker/test/index.js
index 390f6d3a93d..fb5473c108c 100644
--- a/packages/js/components/src/date-range-filter-picker/test/index.js
+++ b/packages/js/components/src/date-range-filter-picker/test/index.js
@@ -46,6 +46,8 @@ describe( 'DateRangeFilterPicker', () => {
[ 'untranslated', undefined, 'MM/DD/YYYY', '09/25/2024', '10/04/2024' ],
[ 'day-first', 'DD/MM/YYYY', 'DD/MM/YYYY', '25/09/2024', '04/10/2024' ],
[ 'dotted', 'DD.MM.YYYY', 'DD.MM.YYYY', '25.09.2024', '04.10.2024' ],
+ // Croatian uses single tokens, which strict parsing rejects padded input for.
+ [ 'hr', 'D/M/YYYY', 'DD/MM/YYYY', '25/09/2024', '04/10/2024' ],
// Translations from real language packs that aren't Moment.js formats.
// The French one is why #61028 stopped parsing with the translation.
[ 'fr_FR', 'JJ/MM/AAAA', 'MM/DD/YYYY', '09/25/2024', '10/04/2024' ],
diff --git a/packages/js/data/changelog/fix-analytics-short-date-format-fallback b/packages/js/data/changelog/fix-analytics-short-date-format-fallback
new file mode 100644
index 00000000000..d4855318c57
--- /dev/null
+++ b/packages/js/data/changelog/fix-analytics-short-date-format-fallback
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Import store: format the default import date with the validated short date format.
diff --git a/packages/js/data/src/import/reducer.ts b/packages/js/data/src/import/reducer.ts
index 19138d0ac56..bee15f53b5f 100644
--- a/packages/js/data/src/import/reducer.ts
+++ b/packages/js/data/src/import/reducer.ts
@@ -1,8 +1,8 @@
/**
* External dependencies
*/
-import { __ } from '@wordpress/i18n';
import moment from 'moment';
+import { getShortDateFormat } from '@woocommerce/date';
import type { Reducer } from 'redux';
/**
@@ -20,7 +20,7 @@ const reducer: Reducer< ImportState, Action > = (
errors: {},
lastImportStartTimestamp: 0,
period: {
- date: moment().format( __( 'MM/DD/YYYY', 'woocommerce' ) ),
+ date: moment().format( getShortDateFormat() ),
label: 'all',
},
skipPrevious: true,
diff --git a/packages/js/data/src/import/test/reducer.ts b/packages/js/data/src/import/test/reducer.ts
index 815dad26463..407e555d81e 100644
--- a/packages/js/data/src/import/test/reducer.ts
+++ b/packages/js/data/src/import/test/reducer.ts
@@ -2,6 +2,7 @@
* External dependencies
*/
import moment from 'moment';
+import { resetLocaleData, setLocaleData } from '@wordpress/i18n';
/**
* Internal dependencies
@@ -30,6 +31,27 @@ describe( 'import reducer', () => {
expect( state ).not.toBe( defaultState );
} );
+ it.each( [
+ [ 'DD/MM/YYYY', 'DD/MM/YYYY' ],
+ [ 'D/M/YYYY', 'DD/MM/YYYY' ],
+ [ 'MM/DD/RRRR', 'MM/DD/YYYY' ],
+ ] )(
+ 'formats the default import date with the %s translation as %s',
+ ( translation, format ) => {
+ // A single-digit day and month, so "D/M/YYYY" and "DD/MM/YYYY" differ.
+ jest.useFakeTimers().setSystemTime(
+ new Date( '2026-03-05T12:00:00' )
+ );
+ setLocaleData( { 'MM/DD/YYYY': [ translation ] }, 'woocommerce' );
+ // @ts-expect-error reducer action should not be empty but it is
+ const state = reducer( undefined, {} );
+ resetLocaleData();
+
+ expect( state.period.date ).toBe( moment().format( format ) );
+ jest.useRealTimers();
+ }
+ );
+
it( 'should handle SET_IMPORT_STATUS', () => {
const query = Date.now();
const state = reducer( defaultState, {
diff --git a/packages/js/date/changelog/fix-analytics-short-date-format-fallback b/packages/js/date/changelog/fix-analytics-short-date-format-fallback
new file mode 100644
index 00000000000..b52e190d5e0
--- /dev/null
+++ b/packages/js/date/changelog/fix-analytics-short-date-format-fallback
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add getShortDateFormat() for the translated date input format, falling back to MM/DD/YYYY when the translation is not a valid Moment.js format.
diff --git a/packages/js/date/src/index.ts b/packages/js/date/src/index.ts
index 0c4581aad1a..af9716d363b 100644
--- a/packages/js/date/src/index.ts
+++ b/packages/js/date/src/index.ts
@@ -1249,6 +1249,48 @@ export function getDateFormatsForInterval(
}
}
+const defaultShortDateFormat = 'MM/DD/YYYY';
+
+/**
+ * Pads single "D" and "M" tokens to "DD" and "MM", leaving literals, "Do",
+ * "DDD" and the month name tokens alone. Strict parsing rejects a
+ * leading zero for "D" and "M", but the hint and formatted dates both pad.
+ *
+ * @param {string} format Moment.js date format.
+ * @return {string} Format with single day and month tokens padded.
+ */
+function padShortDateTokens( format: string ) {
+ return format.replace( /\[[^[]*\]|\\?(?:D+o?|M+o?)/g, ( token ) => {
+ if ( token === 'D' ) {
+ return 'DD';
+ }
+ return token === 'M' ? 'MM' : token;
+ } );
+}
+
+/**
+ * Returns the translated Moment.js format for typing a date, like "DD/MM/YYYY".
+ * Some translations aren't Moment.js formats (e.g. "MM/DD/RRRR"), so fall back
+ * to "MM/DD/YYYY" unless the translation formats and parses a date back to itself.
+ *
+ * @return {string} Moment.js date format for date inputs.
+ */
+export function getShortDateFormat() {
+ const format = padShortDateTokens(
+ /* translators: Moment.js date format for typed dates. Keep the DD, MM and YYYY tokens. */
+ __( 'MM/DD/YYYY', 'woocommerce' )
+ );
+ // Localized tokens ("L", "l", "LT", ...) round-trip, but loadLocaleData()
+ // would then define "L" in terms of itself.
+ if ( /[Ll]/.test( format.replace( /\[[^[]*\]|\\./g, '' ) ) ) {
+ return defaultShortDateFormat;
+ }
+ // Created per call so month names format and parse in the current locale.
+ const checkDate = moment( '2001-02-03', isoDateFormat );
+ const parsed = moment( checkDate.format( format ), format, true );
+ return parsed.isSame( checkDate, 'day' ) ? format : defaultShortDateFormat;
+}
+
/**
* Gutenberg's moment instance is loaded with i18n values, which are
* PHP date formats, ie 'LLL: "F j, Y g:i a"'. Override those with translations
@@ -1269,7 +1311,7 @@ export function loadLocaleData( {
if ( moment.locale() !== 'en' ) {
moment.updateLocale( userLocale, {
longDateFormat: {
- L: __( 'MM/DD/YYYY', 'woocommerce' ),
+ L: getShortDateFormat(),
LL: __( 'MMMM D, YYYY', 'woocommerce' ),
LLL: __( 'D MMMM YYYY LT', 'woocommerce' ),
LLLL: __( 'dddd, D MMMM YYYY LT', 'woocommerce' ),
diff --git a/packages/js/date/src/test/index.ts b/packages/js/date/src/test/index.ts
index 5c1cabdcca0..86f50c67377 100644
--- a/packages/js/date/src/test/index.ts
+++ b/packages/js/date/src/test/index.ts
@@ -39,6 +39,7 @@ import {
dayTicksThreshold,
isLeapYear,
containsLeapYear,
+ getShortDateFormat,
} from '..';
declare global {
interface Window {
@@ -1446,6 +1447,78 @@ describe( 'getRangeLabel', () => {
} );
} );
+describe( 'getShortDateFormat', () => {
+ const translateShortDateFormat = ( translation: string ) =>
+ ( __ as jest.Mock ).mockImplementation( ( text: string ) =>
+ text === 'MM/DD/YYYY' ? translation : text
+ );
+
+ afterEach( () => {
+ ( __ as jest.Mock ).mockImplementation( ( text: string ) => text );
+ } );
+
+ it.each( [
+ [ 'MM/DD/YYYY', 'MM/DD/YYYY' ],
+ [ 'DD/MM/YYYY', 'DD/MM/YYYY' ],
+ [ 'DD.MM.YYYY', 'DD.MM.YYYY' ],
+ [ 'YYYY-MM-DD', 'YYYY-MM-DD' ],
+ // Strict parsing rejects "09" for "M", so single tokens are padded.
+ [ 'D/M/YYYY', 'DD/MM/YYYY' ],
+ [ 'D. M. YYYY', 'DD. MM. YYYY' ],
+ [ '[D] D/M/YYYY', '[D] DD/MM/YYYY' ],
+ [ 'D/M/YYYY \\D', 'DD/MM/YYYY \\D' ],
+ [ 'Do MMM YYYY', 'Do MMM YYYY' ],
+ // Translations from real language packs that aren't Moment.js formats.
+ [ 'MM/DD/RRRR', 'MM/DD/YYYY' ],
+ [ 'JJ/MM/AAAA', 'MM/DD/YYYY' ],
+ [ 'KK/PP/AAAA', 'MM/DD/YYYY' ],
+ [ 'MM/DD/AAAA', 'MM/DD/YYYY' ],
+ [ 'MM/DD/VVVV', 'MM/DD/YYYY' ],
+ [ 'dd/mm/aaaa', 'MM/DD/YYYY' ],
+ [ 'મમ/દદ/વવવવ', 'MM/DD/YYYY' ],
+ [ 'DD/MM', 'MM/DD/YYYY' ],
+ [ 'DD/YYYY', 'MM/DD/YYYY' ],
+ [ 'MM/YYYY', 'MM/DD/YYYY' ],
+ // Localized tokens would make loadLocaleData() define "L" as itself.
+ [ 'L', 'MM/DD/YYYY' ],
+ [ 'l', 'MM/DD/YYYY' ],
+ [ 'LL', 'MM/DD/YYYY' ],
+ [ 'DD/MM/YYYY LT', 'MM/DD/YYYY' ],
+ [ 'DD/MM/YYYY [L]', 'DD/MM/YYYY [L]' ],
+ ] )( 'returns %s as %s', ( translation, expected ) => {
+ translateShortDateFormat( translation );
+ expect( getShortDateFormat() ).toBe( expected );
+ } );
+
+ it( 'validates month names with the current locale', () => {
+ const originalLocale = moment.locale();
+ moment.defineLocale( 'xx-short-date', {
+ monthsShort:
+ 'sij_velj_ožu_tra_svi_lip_srp_kol_ruj_lis_stu_pro'.split( '_' ),
+ } );
+ translateShortDateFormat( 'D MMM YYYY' );
+
+ const format = getShortDateFormat();
+ moment.locale( originalLocale );
+ moment.updateLocale( 'xx-short-date', null );
+
+ expect( format ).toBe( 'DD MMM YYYY' );
+ } );
+
+ it( 'parses padded and formats padded dates for a single-token translation', () => {
+ translateShortDateFormat( 'D/M/YYYY' );
+ const format = getShortDateFormat();
+
+ expect(
+ toMoment( format, '25/09/2026' )?.format( isoDateFormat )
+ ).toBe( '2026-09-25' );
+ expect(
+ toMoment( format, '05/10/2026' )?.format( isoDateFormat )
+ ).toBe( '2026-10-05' );
+ expect( moment( '2026-10-05' ).format( format ) ).toBe( '05/10/2026' );
+ } );
+} );
+
describe( 'loadLocaleData', () => {
const originalLocale = {
siteLocale: 'en_US',
@@ -1476,6 +1549,20 @@ describe( 'loadLocaleData', () => {
} );
expect( moment.localeData().weekdaysMin() ).toEqual( weekdaysShort );
} );
+
+ it( 'falls back to MM/DD/YYYY for "L" when the translation is not a Moment.js format', () => {
+ moment.locale( 'cs_CZ', {} );
+ ( __ as jest.Mock ).mockImplementation( ( text: string ) =>
+ text === 'MM/DD/YYYY' ? 'MM/DD/RRRR' : text
+ );
+
+ loadLocaleData( { userLocale: 'cs_CZ' } );
+
+ ( __ as jest.Mock ).mockImplementation( ( text: string ) => text );
+ expect( moment.localeData().longDateFormat( 'L' ) ).toBe(
+ 'MM/DD/YYYY'
+ );
+ } );
} );
describe( 'getCurrentDates', () => {
diff --git a/plugins/woocommerce/changelog/fix-analytics-short-date-format-fallback b/plugins/woocommerce/changelog/fix-analytics-short-date-format-fallback
new file mode 100644
index 00000000000..b2d90779e22
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-analytics-short-date-format-fallback
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Analytics: typed dates in the historical data import and advanced date filters now work in Czech, Croatian and other locales.
diff --git a/plugins/woocommerce/client/admin/client/analytics/settings/historical-data/index.js b/plugins/woocommerce/client/admin/client/analytics/settings/historical-data/index.js
index 5539e9980cc..462bc877243 100644
--- a/plugins/woocommerce/client/admin/client/analytics/settings/historical-data/index.js
+++ b/plugins/woocommerce/client/admin/client/analytics/settings/historical-data/index.js
@@ -1,7 +1,6 @@
/**
* External dependencies
*/
-import { __ } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import {
@@ -12,6 +11,7 @@ import {
} from '@woocommerce/data';
import { withDispatch, withSelect } from '@wordpress/data';
import { withSpokenMessages } from '@wordpress/components';
+import { getShortDateFormat } from '@woocommerce/date';
/**
* Internal dependencies
@@ -23,7 +23,7 @@ class HistoricalData extends Component {
constructor() {
super( ...arguments );
- this.dateFormat = __( 'MM/DD/YYYY', 'woocommerce' );
+ this.dateFormat = getShortDateFormat();
this.intervalId = -1;
this.lastImportStopTimestamp = 0;
this.cacheNeedsClearing = true;
diff --git a/plugins/woocommerce/client/admin/client/analytics/settings/historical-data/test/index.js b/plugins/woocommerce/client/admin/client/analytics/settings/historical-data/test/index.js
new file mode 100644
index 00000000000..52e16bd73a8
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/analytics/settings/historical-data/test/index.js
@@ -0,0 +1,69 @@
+/**
+ * External dependencies
+ */
+import { render } from '@testing-library/react';
+import { resetLocaleData, setLocaleData } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import HistoricalData from '../index';
+import HistoricalDataLayout from '../layout';
+import { formatParams } from '../utils';
+
+jest.mock( '@wordpress/data', () => ( {
+ ...jest.requireActual( '@wordpress/data' ),
+ withSelect: () => ( Component ) => ( props ) => (
+ <Component
+ { ...props }
+ selectedPeriod={ { label: 'all', date: '' } }
+ skipChecked
+ />
+ ),
+ withDispatch: () => ( Component ) => Component,
+} ) );
+jest.mock( '../layout', () => jest.fn( () => null ) );
+
+describe( 'HistoricalData', () => {
+ beforeEach( () => {
+ jest.useFakeTimers().setSystemTime( new Date( '2026-10-13T12:00:00' ) );
+ } );
+
+ afterEach( () => {
+ jest.useRealTimers();
+ resetLocaleData();
+ HistoricalDataLayout.mockClear();
+ } );
+
+ it.each( [
+ [ 'untranslated', undefined, 'MM/DD/YYYY', '09/25/2025' ],
+ [ 'day-first', 'DD/MM/YYYY', 'DD/MM/YYYY', '25/09/2025' ],
+ [ 'dotted', 'DD.MM.YYYY', 'DD.MM.YYYY', '25.09.2025' ],
+ [ 'hr', 'D/M/YYYY', 'DD/MM/YYYY', '25/09/2025' ],
+ [ 'cs_CZ', 'MM/DD/RRRR', 'MM/DD/YYYY', '09/25/2025' ],
+ [ 'fr_FR', 'JJ/MM/AAAA', 'MM/DD/YYYY', '09/25/2025' ],
+ [ 'lowercase', 'dd/mm/aaaa', 'MM/DD/YYYY', '09/25/2025' ],
+ ] )(
+ 'imports from the typed "Beginning on" date for the %s translation',
+ ( _, translatedFormat, format, typedDate ) => {
+ if ( translatedFormat ) {
+ setLocaleData(
+ { 'MM/DD/YYYY': [ translatedFormat ] },
+ 'woocommerce'
+ );
+ }
+ render( <HistoricalData /> );
+
+ const { dateFormat } = HistoricalDataLayout.mock.calls[ 0 ][ 0 ];
+ expect( dateFormat ).toBe( format );
+ // 2025-09-25 is 383 days before the mocked 2026-10-13.
+ expect(
+ formatParams(
+ dateFormat,
+ { label: 'custom', date: typedDate },
+ false
+ )
+ ).toEqual( { days: 383 } );
+ }
+ );
+} );