mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-25 15:56:19 -05:00
Fix: "maybe we can avoid some added translations to front.json by using native web i18n apis instead"
This commit is contained in:
parent
e08ffcf125
commit
8c51e60480
|
|
@ -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)})`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
23
types/intl-duration-format.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user