diff --git a/web/pages/press.tsx b/web/pages/press.tsx index 7fbe7af6..991a5ef3 100644 --- a/web/pages/press.tsx +++ b/web/pages/press.tsx @@ -9,7 +9,7 @@ import { PlayCircleIcon, } from '@heroicons/react/24/outline' import clsx from 'clsx' -import {githubRepo, supportEmail} from 'common/constants' +import {defaultLocale, githubRepo, supportEmail} from 'common/constants' import Link from 'next/link' import {ComponentType, ReactNode, SVGProps, useMemo, useState} from 'react' import {SectionLabel} from 'web/components/about/section' @@ -19,6 +19,7 @@ import {SEO} from 'web/components/SEO' import {Reveal} from 'web/components/widgets/reveal' import {eyebrow, Section, surface, surfaceHover} from 'web/components/widgets/surface' import {useAPIGetter} from 'web/hooks/use-api-getter' +import {useIsClient} from 'web/hooks/use-is-client' import {useLocale, useT} from 'web/lib/locale' import {copyToClipboard} from 'web/lib/util/copy' @@ -129,15 +130,32 @@ function useMediumLabels() { return {label, action} } +/** + * The locale to format dates in — the default one until the client has mounted. + * + * This is what makes locale-formatted dates safe on this page, and it is not optional. `useT` loads its + * message bundle in an effect, so every translated string is English on the server *and* on the first + * client render, and only swaps afterwards. `Intl` is synchronous and would not wait: formatting straight + * off the context locale renders "9 février 2026" into a tree the server rendered as "February 9, 2026", + * which is a hydration mismatch — and is why the previous version of this page had its locale-formatted + * date commented out beside a raw ISO string. Deferring by one render puts the date on exactly the same + * schedule as the text around it. + */ +function useDateLocale(): Intl.LocalesArgument { + const {locale} = useLocale() + const isClient = useIsClient() + return isClient ? locale : defaultLocale +} + /** * The date, formatted in the reader's own locale. * - * The previous version printed the raw ISO string, with the locale-formatted line commented out beside - * it. It is pinned to UTC here, which is the thing that makes it safe: these are plain calendar dates - * with no time, so without a fixed zone a reader west of Greenwich gets yesterday, and the server and - * the client can disagree about which day it is at all. + * Pinned to UTC, which is the other half of what makes it safe: these are plain calendar dates with no + * time, so without a fixed zone a reader west of Greenwich gets yesterday. */ -function PressDate({date, locale}: {date: string; locale: Intl.LocalesArgument}) { +function PressDate({date}: {date: string}) { + const locale = useDateLocale() + const formatted = useMemo(() => { try { return new Date(`${date}T00:00:00Z`).toLocaleDateString(locale, { @@ -162,7 +180,6 @@ function PressDate({date, locale}: {date: string; locale: Intl.LocalesArgument}) function ItemMeta({item, className}: {item: PressItem; className?: string}) { const t = useT() const {label} = useMediumLabels() - const {locale} = useLocale() const Icon = MEDIUM_ICON[item.medium] return ( @@ -174,7 +191,7 @@ function ItemMeta({item, className}: {item: PressItem; className?: string}) { · - + · @@ -338,7 +355,7 @@ function PressRow({item}: {item: PressItem}) { */ function CoverageBand({items}: {items: PressItem[]}) { const t = useT() - const {locale} = useLocale() + const locale = useDateLocale() const outlets = new Set(items.map((i) => i.outlet)).size const since = useMemo(() => {