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

@@ -1,6 +1,7 @@
<script lang="ts">
import type { PresenceHour } from "#lib/api/types.js";
import { formatSpan } from "#lib/format/presence.js";
import { formatSpan, weekdayLabels } from "#lib/format/presence.js";
import { t } from "#lib/i18n/locale.svelte.js";
interface Props {
week: PresenceHour[];
@@ -8,28 +9,14 @@
const { week }: Props = $props();
const TEXT = {
cell: (day: string, cell: PresenceHour) => {
const slot = `${day}, ${cell.hour}:00`;
if (cell.hours === 0) {
return slot;
}
if (cell.online_minutes === null) {
return `${slot}, нет данных`;
}
return `${slot}, в среднем ${formatSpan(cell.online_minutes * 60)} в сети`;
},
days: ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"],
empty: "нет данных",
max: (minutes: number) => `до ${formatSpan(minutes * 60)}`,
};
const HOURS_IN_DAY = 24;
const SECONDS_IN_MINUTE = 60;
const HOUR_TICKS = [0, 6, 12, 18];
const MIN_SHADE = 0.18;
const weekdays = $derived(weekdayLabels());
const rows = $derived(
TEXT.days.map((_, weekday) =>
weekdays.map((_, weekday) =>
week
.filter((cell) => cell.weekday === weekday)
.sort((a, b) => a.hour - b.hour)
@@ -44,6 +31,20 @@
return cell.hours > 0 && cell.online_minutes === null;
}
function title(day: string, cell: PresenceHour): string {
const slot = `${day}, ${cell.hour}:00`;
if (cell.hours === 0) {
return slot;
}
if (cell.online_minutes === null) {
return t("presence.cellNoData", { slot });
}
return t("presence.cellAverage", {
slot,
span: formatSpan(cell.online_minutes * SECONDS_IN_MINUTE),
});
}
function shade(minutes: number | null): number {
if (minutes === null || minutes === 0 || peak === 0) {
return 0;
@@ -55,7 +56,7 @@
<div class="heatmap">
{#each rows as cells, weekday (weekday)}
<div class="row">
<span class="day">{TEXT.days[weekday]}</span>
<span class="day">{weekdays[weekday]}</span>
<div class="cells">
{#each cells as cell (cell.hour)}
<span
@@ -67,7 +68,7 @@
zero: cell.online_minutes === 0,
},
]}
title={TEXT.cell(TEXT.days[weekday] ?? "", cell)}
title={title(weekdays[weekday] ?? "", cell)}
style:--shade="{shade(cell.online_minutes) * 100}%"
></span>
{/each}
@@ -87,13 +88,13 @@
{#if peak > 0}
<span class="key">
<span class="scale"></span>
{TEXT.max(peak)}
{t("presence.max", { span: formatSpan(peak * SECONDS_IN_MINUTE) })}
</span>
{/if}
{#if hasGaps}
<span class="key">
<span class="swatch empty"></span>
{TEXT.empty}
{t("presence.noData")}
</span>
{/if}
</div>
@@ -115,7 +116,7 @@
.day {
flex-shrink: 0;
width: 1.375rem;
width: 1.75rem;
font-size: 0.75rem;
color: var(--color-text-secondary);
}
@@ -173,7 +174,7 @@
gap: 0.25rem 0.75rem;
align-items: center;
margin: 0.375rem 0 0 1.875rem;
margin: 0.375rem 0 0 2.25rem;
font-size: 0.75rem;
font-variant-numeric: tabular-nums;