Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions front_end/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
},
Comment thread
ncarazon marked this conversation as resolved.
],
},
],
},
},
{
files: ["src/utils/formatters/date.ts"],
rules: {
"no-restricted-imports": "off",
},
},
{
Expand Down
6 changes: 2 additions & 4 deletions front_end/src/components/consumer_post_card/upcoming_cp.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -17,9 +17,7 @@ const UpcomingCP: FC<Props> = ({ cpRevealsOn, className }) => {
<div className={cn("w-full text-center", className)}>
<span className="block">{t("cpRevealed")}</span>
<RelativeTime datetime={cpRevealsOn} lang={locale} className="leading-6">
{intlFormatDistance(cpRevealsOn, new Date(), {
locale,
})}
{formatIntlDistance(locale, cpRevealsOn, new Date())}
</RelativeTime>
</div>
);
Expand Down
11 changes: 6 additions & 5 deletions front_end/src/components/cp_reveal_time/cp_reveal_time.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -27,7 +28,8 @@ const CPRevealTime: FC<Props> = ({
<span className={className}>
{t("hiddenUntil")}
<br />
{intlFormat(
{formatIntlDate(
locale,
revealDate,
hoursUntilReveal < 24
? {
Expand All @@ -38,8 +40,7 @@ const CPRevealTime: FC<Props> = ({
year: "numeric",
month: "long",
day: "numeric",
},
{ locale }
}
)}
</span>
);
Expand All @@ -49,7 +50,7 @@ const CPRevealTime: FC<Props> = ({
<span className={className}>
{t("cpRevealed")}{" "}
<RelativeTime datetime={cpRevealTime} lang={locale}>
{intlFormatDistance(cpRevealTime, new Date(), { locale })}
{formatIntlDistance(locale, cpRevealTime, new Date())}
</RelativeTime>
</span>
);
Expand Down
7 changes: 5 additions & 2 deletions front_end/src/components/ui/relative_time.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@ 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<HTMLElement>,
Partial<Omit<RelativeTimeElement, keyof HTMLElement>> {
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
<relative-time suppressHydrationWarning {...props}>
<relative-time suppressHydrationWarning lang={normalizedLang} {...props}>
{children}
{/* @ts-expect-error relative-time-element lacks TS compatibility with React 19, tracked here: https://github.com/github/relative-time-element/issues/304 */}
</relative-time>
Expand Down
48 changes: 45 additions & 3 deletions front_end/src/utils/formatters/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Duration,
intlFormat,
intlFormatDistance,
type IntlFormatDistanceOptions,
} from "date-fns";
import { es, cs, pt, zhTW, zhCN, enUS } from "date-fns/locale";

Expand Down Expand Up @@ -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) }
);
}

Expand All @@ -106,7 +107,7 @@ export function formatDatetime(locale: string, date: Date) {
hour: "numeric",
minute: "numeric",
},
{ locale }
{ locale: normalizeIntlLocale(locale) }
);
}

Expand All @@ -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",
});
Expand Down Expand Up @@ -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<IntlFormatDistanceOptions, "locale">
): string {
return intlFormatDistance(date, baseDate, {
...options,
locale: normalizeIntlLocale(locale),
});
}

export const getDateFnsLocale = (locale: string) => {
switch (locale) {
case "es":
Expand Down
Loading