feat(frontend): analytics panel and online statuses speak russian and english with real plurals

This commit is contained in:
hh committed 2026-10-19 05:37:44 +00:00
1 parent 543e6eaa50
commit cff493f5dd
9 files changed
+286 -190

No files matched your search

+38 -40
View File
@@ -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))
);
}