diff --git a/front_end/eslint.config.mjs b/front_end/eslint.config.mjs index 8452af75cc..e1888eed0c 100644 --- a/front_end/eslint.config.mjs +++ b/front_end/eslint.config.mjs @@ -64,6 +64,25 @@ const eslintConfig = [ }, }, ], + "no-restricted-imports": [ + "error", + { + paths: [ + { + name: "date-fns", + importNames: ["intlFormat", "intlFormatDistance"], + message: + "Use formatIntlDate / formatIntlDistance from @/utils/formatters/date so the 'original' locale (Untranslated mode) is normalized for Intl APIs.", + }, + ], + }, + ], + }, + }, + { + files: ["src/utils/formatters/date.ts"], + rules: { + "no-restricted-imports": "off", }, }, { diff --git a/front_end/src/components/consumer_post_card/upcoming_cp.tsx b/front_end/src/components/consumer_post_card/upcoming_cp.tsx index 6d4e5d8b75..2a98900f03 100644 --- a/front_end/src/components/consumer_post_card/upcoming_cp.tsx +++ b/front_end/src/components/consumer_post_card/upcoming_cp.tsx @@ -1,9 +1,9 @@ -import { intlFormatDistance } from "date-fns"; import { useLocale, useTranslations } from "next-intl"; import { FC } from "react"; import RelativeTime from "@/components/ui/relative_time"; import cn from "@/utils/core/cn"; +import { formatIntlDistance } from "@/utils/formatters/date"; type Props = { cpRevealsOn: string; @@ -17,9 +17,7 @@ const UpcomingCP: FC = ({ cpRevealsOn, className }) => {
{t("cpRevealed")} - {intlFormatDistance(cpRevealsOn, new Date(), { - locale, - })} + {formatIntlDistance(locale, cpRevealsOn, new Date())}
); diff --git a/front_end/src/components/cp_reveal_time/cp_reveal_time.tsx b/front_end/src/components/cp_reveal_time/cp_reveal_time.tsx index ff1a4d0c35..01e21d2b91 100644 --- a/front_end/src/components/cp_reveal_time/cp_reveal_time.tsx +++ b/front_end/src/components/cp_reveal_time/cp_reveal_time.tsx @@ -1,8 +1,9 @@ -import { differenceInHours, intlFormat, intlFormatDistance } from "date-fns"; +import { differenceInHours } from "date-fns"; import { useLocale, useTranslations } from "next-intl"; import React, { FC } from "react"; import RelativeTime from "@/components/ui/relative_time"; +import { formatIntlDate, formatIntlDistance } from "@/utils/formatters/date"; type Props = { cpRevealTime: string; @@ -27,7 +28,8 @@ const CPRevealTime: FC = ({ {t("hiddenUntil")}
- {intlFormat( + {formatIntlDate( + locale, revealDate, hoursUntilReveal < 24 ? { @@ -38,8 +40,7 @@ const CPRevealTime: FC = ({ year: "numeric", month: "long", day: "numeric", - }, - { locale } + } )}
); @@ -49,7 +50,7 @@ const CPRevealTime: FC = ({ {t("cpRevealed")}{" "} - {intlFormatDistance(cpRevealTime, new Date(), { locale })} + {formatIntlDistance(locale, cpRevealTime, new Date())} ); diff --git a/front_end/src/components/ui/relative_time.tsx b/front_end/src/components/ui/relative_time.tsx index 8ac3c8e4a6..ae0cce51ca 100644 --- a/front_end/src/components/ui/relative_time.tsx +++ b/front_end/src/components/ui/relative_time.tsx @@ -2,6 +2,8 @@ import type { RelativeTimeElement } from "@github/relative-time-element"; import { HTMLAttributes, ReactNode } from "react"; import "@github/relative-time-element"; +import { normalizeIntlLocale } from "@/utils/formatters/date"; + // Extend the native RelativeTimeElement properties for React interface RelativeTimeProps extends HTMLAttributes, @@ -9,10 +11,11 @@ interface RelativeTimeProps children?: ReactNode; } -const RelativeTime = ({ children, ...props }: RelativeTimeProps) => { +const RelativeTime = ({ children, lang, ...props }: RelativeTimeProps) => { + const normalizedLang = lang ? normalizeIntlLocale(lang) : lang; return ( // @ts-expect-error relative-time-element lacks TS compatibility with React 19, tracked here: https://github.com/github/relative-time-element/issues/304 - + {children} {/* @ts-expect-error relative-time-element lacks TS compatibility with React 19, tracked here: https://github.com/github/relative-time-element/issues/304 */} diff --git a/front_end/src/utils/formatters/date.ts b/front_end/src/utils/formatters/date.ts index 46f7d09ce7..61e4a629fb 100644 --- a/front_end/src/utils/formatters/date.ts +++ b/front_end/src/utils/formatters/date.ts @@ -3,6 +3,7 @@ import { Duration, intlFormat, intlFormatDistance, + type IntlFormatDistanceOptions, } from "date-fns"; import { es, cs, pt, zhTW, zhCN, enUS } from "date-fns/locale"; @@ -92,7 +93,7 @@ export function formatDate(locale: string, date: Date) { return intlFormat( new Date(date), { year: "numeric", month: "short", day: "numeric" }, - { locale } + { locale: normalizeIntlLocale(locale) } ); } @@ -106,7 +107,7 @@ export function formatDatetime(locale: string, date: Date) { hour: "numeric", minute: "numeric", }, - { locale } + { locale: normalizeIntlLocale(locale) } ); } @@ -133,7 +134,7 @@ export function formatRelativeDate( let dateStr: string = ""; if (Math.abs(delta) < relCutoff) { dateStr = intlFormatDistance(date, now, { - locale, + locale: normalizeIntlLocale(locale), numeric: "always", style: short ? "short" : "long", }); @@ -189,6 +190,47 @@ export function formatDurationToShortStr(duration: Duration): string { return str; } +/** + * Normalizes the app locale for use with the browser's Intl API. + * The "original" locale (Untranslated mode) is not a valid BCP 47 tag, + * so it falls back to the OS language. This maps it to "en" instead. + */ +export function normalizeIntlLocale(locale: string): string { + return locale === "original" ? "en" : locale; +} + +/** + * Wrapper around date-fns `intlFormat` that normalizes the locale. + * Prefer this over calling `intlFormat` directly so Untranslated mode + * (`"original"` locale) is handled consistently. + */ +export function formatIntlDate( + locale: string, + date: Date | number | string, + formatOptions?: Intl.DateTimeFormatOptions +): string { + return intlFormat(date, formatOptions ?? {}, { + locale: normalizeIntlLocale(locale), + }); +} + +/** + * Wrapper around date-fns `intlFormatDistance` that normalizes the locale. + * Prefer this over calling `intlFormatDistance` directly so Untranslated mode + * (`"original"` locale) is handled consistently. + */ +export function formatIntlDistance( + locale: string, + date: Date | number | string, + baseDate: Date | number | string, + options?: Omit +): string { + return intlFormatDistance(date, baseDate, { + ...options, + locale: normalizeIntlLocale(locale), + }); +} + export const getDateFnsLocale = (locale: string) => { switch (locale) { case "es":