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 } );
+		}
+	);
+} );