feat(frontend): analytics panel and online statuses speak russian and english with real plurals
This commit is contained in:
1 parent
543e6eaa50
commit
cff493f5dd
9 files changed
+286
-190
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import type { PresenceSample } from "#lib/api/types.js";
|
||||
import { formatDay, formatTime } from "#lib/format/datetime.js";
|
||||
import { plural } from "#lib/format/shares.js";
|
||||
import { locale, t } from "#lib/i18n/locale.svelte.js";
|
||||
import type { MessageKey } from "#lib/i18n/messages.js";
|
||||
|
||||
const SECONDS_IN_MINUTE = 60;
|
||||
const MINUTES_IN_HOUR = 60;
|
||||
@@ -8,32 +9,14 @@ const SECONDS_IN_HOUR = 3600;
|
||||
const MS_IN_MINUTE = 60_000;
|
||||
const SECONDS_IN_DAY = 86_400;
|
||||
const MS_IN_DAY = 86_400_000;
|
||||
const DAYS_IN_WEEK = 7;
|
||||
const MONDAY = Date.UTC(2024, 0, 1);
|
||||
|
||||
const TEXT = {
|
||||
ago: (minutes: number) =>
|
||||
`был(а) ${minutes} ${plural(minutes, "минуту", "минуты", "минут")} назад`,
|
||||
days: (days: number) => `${days} ${plural(days, "день", "дня", "дней")}`,
|
||||
hours: (hours: number) => `${hours} ч`,
|
||||
hoursMinutes: (hours: number, minutes: number) => `${hours} ч ${minutes} мин`,
|
||||
justNow: "был(а) только что",
|
||||
lastMonth: "был(а) в этом месяце",
|
||||
lastWeek: "был(а) на этой неделе",
|
||||
longAgo: "был(а) давно",
|
||||
minutes: (minutes: number) => `${minutes} мин`,
|
||||
offline: "не в сети",
|
||||
on: (day: string, time: string) => `был(а) ${day} в ${time}`,
|
||||
online: "в сети",
|
||||
recently: "был(а) недавно",
|
||||
seconds: (seconds: number) => `${seconds} с`,
|
||||
today: (time: string) => `был(а) сегодня в ${time}`,
|
||||
yesterday: (time: string) => `был(а) вчера в ${time}`,
|
||||
};
|
||||
|
||||
const FUZZY: Record<string, string> = {
|
||||
last_month: TEXT.lastMonth,
|
||||
last_week: TEXT.lastWeek,
|
||||
long_time_ago: TEXT.longAgo,
|
||||
recently: TEXT.recently,
|
||||
const FUZZY: Record<string, MessageKey> = {
|
||||
last_month: "presence.lastMonth",
|
||||
last_week: "presence.lastWeek",
|
||||
long_time_ago: "presence.longAgo",
|
||||
recently: "presence.recently",
|
||||
};
|
||||
|
||||
export function isOnline(
|
||||
@@ -50,20 +33,20 @@ function lastSeen(iso: string, now: number): string {
|
||||
const date = new Date(iso);
|
||||
const minutes = Math.floor((now - date.getTime()) / MS_IN_MINUTE);
|
||||
if (minutes < 1) {
|
||||
return TEXT.justNow;
|
||||
return t("presence.justNow");
|
||||
}
|
||||
if (minutes < MINUTES_IN_HOUR) {
|
||||
return TEXT.ago(minutes);
|
||||
return t("presence.ago", { count: minutes });
|
||||
}
|
||||
const time = formatTime(iso);
|
||||
const today = new Date(now);
|
||||
if (date.toDateString() === today.toDateString()) {
|
||||
return TEXT.today(time);
|
||||
return t("presence.todayAt", { time });
|
||||
}
|
||||
if (date.toDateString() === new Date(now - MS_IN_DAY).toDateString()) {
|
||||
return TEXT.yesterday(time);
|
||||
return t("presence.yesterdayAt", { time });
|
||||
}
|
||||
return TEXT.on(formatDay(iso), time);
|
||||
return t("presence.on", { day: formatDay(iso), time });
|
||||
}
|
||||
|
||||
export function formatPresence(
|
||||
@@ -72,29 +55,31 @@ export function formatPresence(
|
||||
): string {
|
||||
if (sample.status === "online") {
|
||||
return isOnline(sample, now)
|
||||
? TEXT.online
|
||||
? t("presence.online")
|
||||
: lastSeen(sample.next_offline_date ?? sample.ts, now);
|
||||
}
|
||||
const fuzzy = FUZZY[sample.status];
|
||||
if (fuzzy) {
|
||||
return fuzzy;
|
||||
return t(fuzzy);
|
||||
}
|
||||
return sample.last_online_date
|
||||
? lastSeen(sample.last_online_date, now)
|
||||
: TEXT.offline;
|
||||
: t("presence.offline");
|
||||
}
|
||||
|
||||
export function formatSpan(seconds: number): string {
|
||||
if (seconds < SECONDS_IN_MINUTE) {
|
||||
return TEXT.seconds(Math.round(seconds));
|
||||
return t("presence.seconds", { seconds: Math.round(seconds) });
|
||||
}
|
||||
const minutes = Math.round(seconds / SECONDS_IN_MINUTE);
|
||||
if (minutes < MINUTES_IN_HOUR) {
|
||||
return TEXT.minutes(minutes);
|
||||
return t("presence.minutes", { minutes });
|
||||
}
|
||||
const hours = Math.floor(minutes / MINUTES_IN_HOUR);
|
||||
const rest = minutes % MINUTES_IN_HOUR;
|
||||
return rest === 0 ? TEXT.hours(hours) : TEXT.hoursMinutes(hours, rest);
|
||||
return rest === 0
|
||||
? t("presence.hours", { hours })
|
||||
: t("presence.hoursMinutes", { hours, minutes: rest });
|
||||
}
|
||||
|
||||
export function formatClock(secondsOfDay: number): string {
|
||||
@@ -111,7 +96,20 @@ export function formatLongSpan(seconds: number): string {
|
||||
}
|
||||
const days = Math.floor(seconds / SECONDS_IN_DAY);
|
||||
const hours = Math.round((seconds % SECONDS_IN_DAY) / SECONDS_IN_HOUR);
|
||||
return hours === 0
|
||||
? TEXT.days(days)
|
||||
: `${TEXT.days(days)} ${TEXT.hours(hours)}`;
|
||||
const text = t("presence.days", { count: days });
|
||||
return hours === 0 ? text : `${text} ${t("presence.hours", { hours })}`;
|
||||
}
|
||||
|
||||
function capitalize(text: string): string {
|
||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||
}
|
||||
|
||||
export function weekdayLabels(): string[] {
|
||||
const format = new Intl.DateTimeFormat(locale.tag, {
|
||||
timeZone: "UTC",
|
||||
weekday: "short",
|
||||
});
|
||||
return Array.from({ length: DAYS_IN_WEEK }, (_, day) =>
|
||||
capitalize(format.format(MONDAY + day * MS_IN_DAY))
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user