diff --git a/src/generate/dayjs.ts b/src/generate/dayjs.ts index d8fba6780..a4fa37804 100644 --- a/src/generate/dayjs.ts +++ b/src/generate/dayjs.ts @@ -165,7 +165,8 @@ const generateConfig: GenerateConfig = { getShortMonths: (locale) => dayjs().locale(parseLocale(locale)).localeData().monthsShort(), format: (locale, date, format) => getUDayjs(date).locale(parseLocale(locale)).format(format), parse: (locale, text, formats) => { - const localeStr = parseLocale(locale); + // Preserve Day.js fallback when the requested locale data is not registered. + const localeStr = dayjs().locale(parseLocale(locale)).locale(); for (let i = 0; i < formats.length; i += 1) { const format = formats[i]; const formatText = text; @@ -183,7 +184,7 @@ const generateConfig: GenerateConfig = { parseNoMatchNotice(); return null; } - const date = dayjs(formatText, format, true).locale(localeStr); + const date = dayjs(formatText, format, localeStr, true); if (date.isValid()) { return date; } diff --git a/tests/generate.spec.tsx b/tests/generate.spec.tsx index 84ff9a6cc..f0a7aed60 100644 --- a/tests/generate.spec.tsx +++ b/tests/generate.spec.tsx @@ -1,4 +1,5 @@ import MockDate from 'mockdate'; +import dayjs from 'dayjs'; import dateFnsGenerateConfig from '../src/generate/dateFns'; import dayjsGenerateConfig from '../src/generate/dayjs'; import luxonGenerateConfig from '../src/generate/luxon'; @@ -7,6 +8,7 @@ import { getMoment } from './util/commonUtil'; import 'dayjs/locale/zh-cn'; import 'dayjs/locale/ko'; +import 'dayjs/locale/fr'; import type { GenerateConfig } from '../src/generate'; describe('Picker.Generate', () => { @@ -289,6 +291,68 @@ describe('Generate:moment', () => { }); describe('Generate:dayjs', () => { + describe('locale-aware parsing', () => { + let originalLocale: string; + + beforeEach(() => { + originalLocale = dayjs.locale(); + dayjs.locale('en'); + }); + + afterEach(() => { + dayjs.locale(originalLocale); + }); + + it.each([ + ['fr_FR', '25 août 2026', 'D MMM YYYY', '2026-08-25'], + ['fr_FR', '25 septembre 2026', 'D MMMM YYYY', '2026-09-25'], + ['fr_BE', '25 août 2026', 'D MMM YYYY', '2026-08-25'], + ])('parses %s text %s without changing the global locale', (locale, text, format, expected) => { + const date = dayjsGenerateConfig.locale.parse(locale, text, [format]); + + expect(date?.format('YYYY-MM-DD')).toBe(expected); + expect(date?.locale()).toBe('fr'); + expect(dayjs.locale()).toBe('en'); + }); + + it('tries alternate formats with the requested locale', () => { + const date = dayjsGenerateConfig.locale.parse('fr_FR', '25 août 2026', [ + 'YYYY-MM-DD', + 'D MMM YYYY', + ]); + + expect(date?.format('YYYY-MM-DD')).toBe('2026-08-25'); + expect(dayjs.locale()).toBe('en'); + }); + + it('parses English text when the global locale is French', () => { + dayjs.locale('fr'); + const date = dayjsGenerateConfig.locale.parse('en_US', '25 Aug 2026', ['D MMM YYYY']); + + expect(date?.format('YYYY-MM-DD')).toBe('2026-08-25'); + expect(date?.locale()).toBe('en'); + expect(dayjs.locale()).toBe('fr'); + }); + + it('keeps the global-locale fallback when locale data is not registered', () => { + const date = dayjsGenerateConfig.locale.parse('unregistered_LOCALE', '25 Aug 2026', [ + 'D MMM YYYY', + ]); + + expect(date?.format('YYYY-MM-DD')).toBe('2026-08-25'); + expect(date?.locale()).toBe('en'); + expect(dayjs.locale()).toBe('en'); + }); + + it.each(['31 février 2026', '25 août 2026 extra'])( + 'keeps strict validation for localized input %s', + (text) => { + expect(dayjsGenerateConfig.locale.parse('fr_FR', text, ['D MMMM YYYY'])).toBeNull(); + expect(dayjs.locale()).toBe('en'); + }, + ); + }); + it('getFixedDate', () => { const timea = dayjsGenerateConfig.getFixedDate('2019-2-08'); const timeb = dayjsGenerateConfig.getFixedDate('2019-02-08'); diff --git a/tests/picker.spec.tsx b/tests/picker.spec.tsx index 4354a897b..82ef2f3de 100644 --- a/tests/picker.spec.tsx +++ b/tests/picker.spec.tsx @@ -2,6 +2,7 @@ import { act, createEvent, fireEvent, render } from '@testing-library/react'; import type { Dayjs } from 'dayjs'; import dayjs from 'dayjs'; +import 'dayjs/locale/fr'; import moment from 'moment'; import 'moment/locale/zh-cn'; import { KeyCode, resetWarned, spyElementPrototypes } from '@rc-component/util'; @@ -10,6 +11,7 @@ import Picker, { PickerPanel, type PickerRef } from '../src'; import type { PanelMode, PickerMode } from '../src/interface'; import momentGenerateConfig from '../src/generate/moment'; import enUS from '../src/locale/en_US'; +import frFR from '../src/locale/fr_FR'; import zhCN from '../src/locale/zh_CN'; import { // MomentPicker, @@ -66,6 +68,36 @@ describe('Picker.Basic', () => { fireEvent.click(document.querySelectorAll('ul')[colIndex].querySelectorAll('li')[rowIndex]); } + it('commits edited localized month text while the global locale stays English', () => { + const originalLocale = dayjs.locale(); + dayjs.locale('en'); + + try { + const onChange = jest.fn(); + const { container } = render( + , + ); + const input = container.querySelector('input'); + expect(input.value).toBe('24 août 2026'); + + fireEvent.change(input, { target: { value: '25 août 2026' } }); + keyDown(KeyCode.ENTER); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange.mock.calls[0][0].format('YYYY-MM-DD')).toBe('2026-08-25'); + expect(onChange.mock.calls[0][1]).toBe('25 août 2026'); + expect(input).not.toHaveAttribute('aria-invalid', 'true'); + expect(dayjs.locale()).toBe('en'); + } finally { + dayjs.locale(originalLocale); + } + }); + describe('mode', () => { const modeList: { mode: PanelMode; className: string }[] = [ {