Fix: "maybe we can avoid some added translations to front.json by using native web i18n apis instead"

This commit is contained in:
Kalle 2026-03-14 15:35:23 +02:00
parent e08ffcf125
commit 8c51e60480
19 changed files with 85 additions and 119 deletions

View File

@ -18,8 +18,6 @@ import styles from "./SplatoonRotations.module.css";
const ROTATION_MODE_FILTERS = ["ALL", "SZ", "TC", "RM", "CB"] as const;
type RotationModeFilter = (typeof ROTATION_MODE_FILTERS)[number];
// xxx: maybe we can avoid some added translations to front.json by using native web i18n apis instead
const ROTATION_TYPE_LABELS: Record<string, string> = {
SERIES: "rotations.series",
OPEN: "rotations.open",
@ -186,6 +184,7 @@ function RotationCard({
now: Date;
}) {
const { t } = useTranslation(["front", "game-misc"]);
const { formatTime, formatDuration, formatRelativeTime } = useTimeFormat();
const remaining = timeRemaining(
now,
databaseTimestampToDate(current?.startTime ?? 0),
@ -218,10 +217,7 @@ function RotationCard({
style={{ width: `${remaining.progress * 100}%` }}
/>
<span className={styles.rotationCardProgressText}>
{t("front:rotations.remaining", {
hours: remaining.hours,
minutes: remaining.minutes,
})}
{formatDuration(remaining.hours, remaining.minutes)}
</span>
</div>
) : null}
@ -236,6 +232,8 @@ function RotationCard({
<NextLabel
startTime={databaseTimestampToDate(next.startTime)}
startsIn={nextStartsIn}
formatTime={formatTime}
formatRelativeTime={formatRelativeTime}
/>
</span>
</div>
@ -261,6 +259,8 @@ function RotationCard({
<NextLabel
startTime={databaseTimestampToDate(shownNext.startTime)}
startsIn={shownNextStartsIn}
formatTime={formatTime}
formatRelativeTime={formatRelativeTime}
compact
/>
) : null}
@ -277,37 +277,30 @@ function RotationCard({
function NextLabel({
startTime,
startsIn,
formatTime,
formatRelativeTime,
compact,
}: {
startTime: Date;
startsIn: { hours: number; minutes: number };
formatTime: (date: Date) => string;
formatRelativeTime: (hours: number, minutes: number) => string;
compact?: boolean;
}) {
const { t } = useTranslation(["front"]);
const { formatTime } = useTimeFormat();
const withinTwoHours = startsIn.hours * 60 + startsIn.minutes <= 120;
if (compact) {
if (withinTwoHours) {
return t("front:rotations.in", {
hours: startsIn.hours,
minutes: startsIn.minutes,
});
return formatRelativeTime(startsIn.hours, startsIn.minutes);
}
return t("front:rotations.at", {
time: formatTime(startTime),
});
return formatTime(startTime);
}
if (withinTwoHours) {
return t("front:rotations.next", {
hours: startsIn.hours,
minutes: startsIn.minutes,
});
return `${t("front:rotations.nextLabel")} (${formatRelativeTime(startsIn.hours, startsIn.minutes)})`;
}
return t("front:rotations.nextAt", {
time: formatTime(startTime),
});
return `${t("front:rotations.nextLabel")} (${formatTime(startTime)})`;
}

View File

@ -31,11 +31,12 @@ function getClockFormatOptions(
}
/**
* Hook for formatting dates and times according to user preferences and locale.
* Hook for formatting dates, times, durations, and relative times
* according to user preferences and locale.
* Respects the user's clock format preference (12h/24h) and current language.
*
* @example
* const { formatDateTime, formatTime, formatDate } = useTimeFormat();
* const { formatDateTime, formatTime, formatDate, formatDuration, formatRelativeTime } = useTimeFormat();
*
* // Format full date and time
* formatDateTime(new Date('2025-01-15T14:30:00'));
@ -52,6 +53,16 @@ function getClockFormatOptions(
* // Custom options
* formatDateTime(new Date(), { dateStyle: 'full', timeStyle: 'short' });
* // => "Wednesday, January 15, 2025 at 2:30 PM"
*
* // Format a duration (hours + minutes)
* formatDuration(1, 30);
* // => "1h 30m" (en) or locale-appropriate narrow format
*
* // Format relative time (picks the largest significant unit)
* formatRelativeTime(2, 15);
* // => "in 2 hr."
* formatRelativeTime(0, 45);
* // => "in 45 min."
*/
export function useTimeFormat() {
const { i18n } = useTranslation();
@ -122,12 +133,31 @@ export function useTimeFormat() {
});
};
const formatDuration = (hours: number, minutes: number) => {
return new Intl.DurationFormat(i18n.language, { style: "narrow" }).format({
hours,
minutes,
});
};
const formatRelativeTime = (hours: number, minutes: number) => {
const rtf = new Intl.RelativeTimeFormat(i18n.language, { style: "short" });
if (hours > 0) {
return rtf.format(hours, "hour");
}
return rtf.format(minutes, "minute");
};
return {
formatDateTime,
formatTime,
formatDate,
formatDateTimeSmartMinutes,
formatDistanceToNow,
formatDuration,
formatRelativeTime,
};
}

View File

@ -36,12 +36,7 @@
"rotations.open": "",
"rotations.x": "",
"rotations.current": "",
"rotations.next": "",
"rotations.nextAt": "",
"rotations.in": "",
"rotations.at": "",
"rotations.nextLabel": "",
"rotations.credit": "",
"rotations.filter.all": "",
"rotations.remaining": ""
"rotations.filter.all": ""
}

View File

@ -36,12 +36,7 @@
"rotations.open": "",
"rotations.x": "",
"rotations.current": "",
"rotations.next": "",
"rotations.nextAt": "",
"rotations.in": "",
"rotations.at": "",
"rotations.nextLabel": "",
"rotations.credit": "",
"rotations.filter.all": "",
"rotations.remaining": ""
"rotations.filter.all": ""
}

View File

@ -36,12 +36,7 @@
"rotations.open": "Anarchy Open",
"rotations.x": "X Battle",
"rotations.current": "Now",
"rotations.next": "Next (in {{hours}}h {{minutes}}m)",
"rotations.nextAt": "Next (at {{time}})",
"rotations.in": "in {{hours}}h {{minutes}}m",
"rotations.at": "at {{time}}",
"rotations.nextLabel": "Next",
"rotations.credit": "Data from splatoon3.ink",
"rotations.filter.all": "All",
"rotations.remaining": "{{hours}}h {{minutes}}m"
"rotations.filter.all": "All"
}

View File

@ -36,12 +36,7 @@
"rotations.open": "",
"rotations.x": "",
"rotations.current": "",
"rotations.next": "",
"rotations.nextAt": "",
"rotations.in": "",
"rotations.at": "",
"rotations.nextLabel": "",
"rotations.credit": "",
"rotations.filter.all": "",
"rotations.remaining": ""
"rotations.filter.all": ""
}

View File

@ -36,12 +36,7 @@
"rotations.open": "",
"rotations.x": "",
"rotations.current": "",
"rotations.next": "",
"rotations.nextAt": "",
"rotations.in": "",
"rotations.at": "",
"rotations.nextLabel": "",
"rotations.credit": "",
"rotations.filter.all": "",
"rotations.remaining": ""
"rotations.filter.all": ""
}

View File

@ -36,12 +36,7 @@
"rotations.open": "",
"rotations.x": "",
"rotations.current": "",
"rotations.next": "",
"rotations.nextAt": "",
"rotations.in": "",
"rotations.at": "",
"rotations.nextLabel": "",
"rotations.credit": "",
"rotations.filter.all": "",
"rotations.remaining": ""
"rotations.filter.all": ""
}

View File

@ -36,12 +36,7 @@
"rotations.open": "",
"rotations.x": "",
"rotations.current": "",
"rotations.next": "",
"rotations.nextAt": "",
"rotations.in": "",
"rotations.at": "",
"rotations.nextLabel": "",
"rotations.credit": "",
"rotations.filter.all": "",
"rotations.remaining": ""
"rotations.filter.all": ""
}

View File

@ -36,12 +36,7 @@
"rotations.open": "",
"rotations.x": "",
"rotations.current": "",
"rotations.next": "",
"rotations.nextAt": "",
"rotations.in": "",
"rotations.at": "",
"rotations.nextLabel": "",
"rotations.credit": "",
"rotations.filter.all": "",
"rotations.remaining": ""
"rotations.filter.all": ""
}

View File

@ -36,12 +36,7 @@
"rotations.open": "",
"rotations.x": "",
"rotations.current": "",
"rotations.next": "",
"rotations.nextAt": "",
"rotations.in": "",
"rotations.at": "",
"rotations.nextLabel": "",
"rotations.credit": "",
"rotations.filter.all": "",
"rotations.remaining": ""
"rotations.filter.all": ""
}

View File

@ -36,12 +36,7 @@
"rotations.open": "",
"rotations.x": "",
"rotations.current": "",
"rotations.next": "",
"rotations.nextAt": "",
"rotations.in": "",
"rotations.at": "",
"rotations.nextLabel": "",
"rotations.credit": "",
"rotations.filter.all": "",
"rotations.remaining": ""
"rotations.filter.all": ""
}

View File

@ -36,12 +36,7 @@
"rotations.open": "",
"rotations.x": "",
"rotations.current": "",
"rotations.next": "",
"rotations.nextAt": "",
"rotations.in": "",
"rotations.at": "",
"rotations.nextLabel": "",
"rotations.credit": "",
"rotations.filter.all": "",
"rotations.remaining": ""
"rotations.filter.all": ""
}

View File

@ -36,12 +36,7 @@
"rotations.open": "",
"rotations.x": "",
"rotations.current": "",
"rotations.next": "",
"rotations.nextAt": "",
"rotations.in": "",
"rotations.at": "",
"rotations.nextLabel": "",
"rotations.credit": "",
"rotations.filter.all": "",
"rotations.remaining": ""
"rotations.filter.all": ""
}

View File

@ -36,12 +36,7 @@
"rotations.open": "",
"rotations.x": "",
"rotations.current": "",
"rotations.next": "",
"rotations.nextAt": "",
"rotations.in": "",
"rotations.at": "",
"rotations.nextLabel": "",
"rotations.credit": "",
"rotations.filter.all": "",
"rotations.remaining": ""
"rotations.filter.all": ""
}

View File

@ -36,12 +36,7 @@
"rotations.open": "",
"rotations.x": "",
"rotations.current": "",
"rotations.next": "",
"rotations.nextAt": "",
"rotations.in": "",
"rotations.at": "",
"rotations.nextLabel": "",
"rotations.credit": "",
"rotations.filter.all": "",
"rotations.remaining": ""
"rotations.filter.all": ""
}

View File

@ -36,12 +36,7 @@
"rotations.open": "",
"rotations.x": "",
"rotations.current": "",
"rotations.next": "",
"rotations.nextAt": "",
"rotations.in": "",
"rotations.at": "",
"rotations.nextLabel": "",
"rotations.credit": "",
"rotations.filter.all": "",
"rotations.remaining": ""
"rotations.filter.all": ""
}

View File

@ -36,12 +36,7 @@
"rotations.open": "",
"rotations.x": "",
"rotations.current": "",
"rotations.next": "",
"rotations.nextAt": "",
"rotations.in": "",
"rotations.at": "",
"rotations.nextLabel": "",
"rotations.credit": "",
"rotations.filter.all": "",
"rotations.remaining": ""
"rotations.filter.all": ""
}

23
types/intl-duration-format.d.ts vendored Normal file
View File

@ -0,0 +1,23 @@
declare namespace Intl {
interface DurationFormatOptions {
style?: "long" | "short" | "narrow" | "digital";
}
interface DurationInput {
years?: number;
months?: number;
weeks?: number;
days?: number;
hours?: number;
minutes?: number;
seconds?: number;
milliseconds?: number;
microseconds?: number;
nanoseconds?: number;
}
class DurationFormat {
constructor(locales?: string | string[], options?: DurationFormatOptions);
format(duration: DurationInput): string;
}
}