From dcf4a577d1213373097cf151b903e47ec61cef9a Mon Sep 17 00:00:00 2001 From: Raashish Aggarwal <94279692+raashish1601@users.noreply.github.com> Date: Sat, 28 Mar 2026 21:37:55 +0530 Subject: [PATCH] fix(query-devtools): guard invalid default locales --- .changeset/olive-carpets-rhyme.md | 7 ++++ packages/query-devtools/src/Devtools.tsx | 9 +++-- .../src/__tests__/locale.test.ts | 32 +++++++++++++++ packages/query-devtools/src/locale.ts | 40 +++++++++++++++++++ 4 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 .changeset/olive-carpets-rhyme.md create mode 100644 packages/query-devtools/src/__tests__/locale.test.ts create mode 100644 packages/query-devtools/src/locale.ts diff --git a/.changeset/olive-carpets-rhyme.md b/.changeset/olive-carpets-rhyme.md new file mode 100644 index 00000000000..1be51f5f2ec --- /dev/null +++ b/.changeset/olive-carpets-rhyme.md @@ -0,0 +1,7 @@ +--- +'@tanstack/query-devtools': patch +--- + +Guard devtools mutation timestamp formatting against invalid browser locale +values by canonicalizing navigator locales before calling +`Date.prototype.toLocaleString()`. diff --git a/packages/query-devtools/src/Devtools.tsx b/packages/query-devtools/src/Devtools.tsx index ed8eed4534e..f8ebf34e90d 100644 --- a/packages/query-devtools/src/Devtools.tsx +++ b/packages/query-devtools/src/Devtools.tsx @@ -18,6 +18,7 @@ import { createResizeObserver } from '@solid-primitives/resize-observer' import { DropdownMenu, RadioGroup } from '@kobalte/core' import { Portal } from 'solid-js/web' import { tokens } from './theme' +import { formatDateTime } from './locale' import { convertRemToPixels, displayValue, @@ -778,7 +779,7 @@ export const ContentView: Component = (props) => { item.options.mutationKey ? JSON.stringify(item.options.mutationKey) + ' - ' : '' - }${new Date(item.state.submittedAt).toLocaleString()}` + }${formatDateTime(item.state.submittedAt)}` return rankItem(value, props.localStore.mutationFilter || '') .passed }) @@ -1580,9 +1581,9 @@ const MutationRow: Component<{ mutation: Mutation }> = (props) => { styles().selectedQueryRow, 'tsqd-query-row', )} - aria-label={`Mutation submitted at ${new Date( + aria-label={`Mutation submitted at ${formatDateTime( props.mutation.state.submittedAt, - ).toLocaleString()}`} + )}`} >
= (props) => { {JSON.stringify(props.mutation.options.mutationKey)} -{' '} - {new Date(props.mutation.state.submittedAt).toLocaleString()} + {formatDateTime(props.mutation.state.submittedAt)} diff --git a/packages/query-devtools/src/__tests__/locale.test.ts b/packages/query-devtools/src/__tests__/locale.test.ts new file mode 100644 index 00000000000..be435f96d74 --- /dev/null +++ b/packages/query-devtools/src/__tests__/locale.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest' +import { + formatDateTime, + getNavigatorLocales, + resolveDateTimeLocale, +} from '../locale' + +describe('locale helpers', () => { + it('uses the first valid locale from navigator languages', () => { + expect(resolveDateTimeLocale(['undefined', 'en-GB', 'fr-FR'])).toBe('en-GB') + }) + + it('falls back to en-US when no provided locale is valid', () => { + expect(resolveDateTimeLocale(['undefined', 'en_US', null])).toBe('en-US') + }) + + it( + 'formats dates without throwing when navigator.language is invalid', + { timeout: 20_000 }, + () => { + const submittedAt = new Date('2026-03-28T12:34:56.000Z') + const locales = getNavigatorLocales({ + language: 'undefined', + languages: ['undefined'], + }) + + expect(formatDateTime(submittedAt, locales)).toBe( + submittedAt.toLocaleString('en-US'), + ) + }, + ) +}) diff --git a/packages/query-devtools/src/locale.ts b/packages/query-devtools/src/locale.ts new file mode 100644 index 00000000000..5c2024290f3 --- /dev/null +++ b/packages/query-devtools/src/locale.ts @@ -0,0 +1,40 @@ +const FALLBACK_LOCALE = 'en-US' + +export function resolveDateTimeLocale( + preferredLocales: ReadonlyArray, +): string { + for (const preferredLocale of preferredLocales) { + if (typeof preferredLocale !== 'string' || preferredLocale.trim() === '') { + continue + } + + try { + return Intl.getCanonicalLocales(preferredLocale)[0]! + } catch { + continue + } + } + + return FALLBACK_LOCALE +} + +export function getNavigatorLocales( + navigatorLike: + | Pick + | undefined = globalThis.navigator, +): Array { + const navigatorLanguages = Array.isArray(navigatorLike?.languages) + ? navigatorLike.languages + : [] + + return [...navigatorLanguages, navigatorLike?.language] +} + +export function formatDateTime( + value: string | number | Date, + preferredLocales: ReadonlyArray = getNavigatorLocales(), +): string { + const date = value instanceof Date ? value : new Date(value) + + return date.toLocaleString(resolveDateTimeLocale(preferredLocales)) +}