feat(frontend): storage, trash, policy, stories, shares, jobs, bots, media, login and notes speak russian and english
This commit is contained in:
1 parent
e5a5edcaae
commit
4a5c92f0e9
51 files changed
+1504
-588
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { t } from "#lib/i18n/locale.svelte.js";
|
||||
import { locale, t } from "#lib/i18n/locale.svelte.js";
|
||||
import type { MessageKey } from "#lib/i18n/messages.js";
|
||||
|
||||
const MEDIA_KIND_KEYS: Record<string, MessageKey> = {
|
||||
@@ -27,8 +27,9 @@ export function mediaKindLabel(kind: string | null): string | null {
|
||||
return t(MEDIA_KIND_KEYS[kind] ?? "media.kind.other");
|
||||
}
|
||||
|
||||
const BYTE_UNITS = ["B", "KB", "MB", "GB"];
|
||||
const BYTE_UNITS = ["B", "KB", "MB", "GB"] as const;
|
||||
const BYTE_STEP = 1024;
|
||||
const PRECISE_BELOW = 10;
|
||||
|
||||
export function formatBytes(bytes: number | null): string {
|
||||
if (bytes === null || bytes <= 0) {
|
||||
@@ -40,7 +41,10 @@ export function formatBytes(bytes: number | null): string {
|
||||
value /= BYTE_STEP;
|
||||
unit += 1;
|
||||
}
|
||||
const rounded =
|
||||
value >= 10 || unit === 0 ? Math.round(value) : value.toFixed(1);
|
||||
return `${rounded} ${BYTE_UNITS[unit]}`;
|
||||
const digits = value >= PRECISE_BELOW || unit === 0 ? 0 : 1;
|
||||
const rounded = value.toLocaleString(locale.tag, {
|
||||
maximumFractionDigits: digits,
|
||||
minimumFractionDigits: digits,
|
||||
});
|
||||
return `${rounded} ${t(`media.unit.${BYTE_UNITS[unit] ?? "B"}`)}`;
|
||||
}
|
||||
@@ -1,20 +1,9 @@
|
||||
import type { CaptureToggles, PolicyScopeType } from "#lib/api/types.js";
|
||||
import { plural } from "#lib/format/shares.js";
|
||||
import { t } from "#lib/i18n/locale.svelte.js";
|
||||
|
||||
export type ToggleKey = keyof CaptureToggles;
|
||||
|
||||
export const TOGGLE_LABELS: Record<ToggleKey, string> = {
|
||||
backfill: "Бэкфилл",
|
||||
media: "Медиа",
|
||||
messages: "Сообщения",
|
||||
presence: "Присутствие",
|
||||
profile_history: "История профиля",
|
||||
reactions: "Реакции",
|
||||
self_destruct_media: "Исчезающие",
|
||||
stories: "Сторис",
|
||||
stt: "Расшифровка",
|
||||
track_edits_deletes: "Правки, удаления",
|
||||
};
|
||||
type DefaultScope = "channel" | "dm" | "group";
|
||||
|
||||
export const TOGGLE_KEYS: ToggleKey[] = [
|
||||
"messages",
|
||||
@@ -29,16 +18,10 @@ export const TOGGLE_KEYS: ToggleKey[] = [
|
||||
"backfill",
|
||||
];
|
||||
|
||||
export const DEFAULT_LABELS: Partial<Record<PolicyScopeType, string>> = {
|
||||
default_channel: "Каналы",
|
||||
default_dm: "Личные чаты",
|
||||
default_group: "Группы",
|
||||
};
|
||||
|
||||
const PARENT_LABELS: Partial<Record<PolicyScopeType, string>> = {
|
||||
default_channel: "у каналов",
|
||||
default_dm: "у личных чатов",
|
||||
default_group: "у групп",
|
||||
const DEFAULT_SCOPES: Partial<Record<PolicyScopeType, DefaultScope>> = {
|
||||
default_channel: "channel",
|
||||
default_dm: "dm",
|
||||
default_group: "group",
|
||||
};
|
||||
|
||||
const SHOWN_CHIPS = 3;
|
||||
@@ -50,6 +33,19 @@ export interface Chip {
|
||||
sign: "plus" | "minus";
|
||||
}
|
||||
|
||||
export function toggleLabel(key: ToggleKey): string {
|
||||
return t(`policy.toggle.${key}`);
|
||||
}
|
||||
|
||||
function exceptLabel(key: ToggleKey): string {
|
||||
return t(`policy.except.${key}`);
|
||||
}
|
||||
|
||||
export function defaultLabel(scopeType: PolicyScopeType): string {
|
||||
const scope = DEFAULT_SCOPES[scopeType];
|
||||
return scope ? t(`policy.default.${scope}`) : "";
|
||||
}
|
||||
|
||||
export function pickToggles(source: CaptureToggles): CaptureToggles {
|
||||
return {
|
||||
backfill: source.backfill,
|
||||
@@ -77,37 +73,31 @@ export function sameToggles(a: CaptureToggles, b: CaptureToggles): boolean {
|
||||
return TOGGLE_KEYS.every((key) => a[key] === b[key]);
|
||||
}
|
||||
|
||||
const EXCEPT_LABELS: Record<ToggleKey, string> = {
|
||||
backfill: "бэкфилла",
|
||||
media: "медиа",
|
||||
messages: "сообщений",
|
||||
presence: "присутствия",
|
||||
profile_history: "истории профиля",
|
||||
reactions: "реакций",
|
||||
self_destruct_media: "исчезающих",
|
||||
stories: "сторис",
|
||||
stt: "расшифровки",
|
||||
track_edits_deletes: "правок и удалений",
|
||||
};
|
||||
function exceptList(keys: ToggleKey[]): string {
|
||||
const [first, second] = keys.map(exceptLabel);
|
||||
return second === undefined
|
||||
? (first ?? "")
|
||||
: t("policy.summary.pair", { a: first ?? "", b: second });
|
||||
}
|
||||
|
||||
export function summary(toggles: CaptureToggles): string {
|
||||
const on = TOGGLE_KEYS.filter((key) => toggles[key]);
|
||||
const off = TOGGLE_KEYS.filter((key) => !toggles[key]);
|
||||
if (off.length === 0) {
|
||||
return "Всё";
|
||||
return t("policy.summary.all");
|
||||
}
|
||||
if (on.length === 0) {
|
||||
return "Ничего";
|
||||
return t("policy.summary.none");
|
||||
}
|
||||
if (on.length === 1 && on[0] === "messages") {
|
||||
return "Только сообщения";
|
||||
return t("policy.summary.messagesOnly");
|
||||
}
|
||||
if (off.length <= SHORT_LIST) {
|
||||
return `Всё, кроме ${off.map((key) => EXCEPT_LABELS[key]).join(" и ")}`;
|
||||
return t("policy.summary.except", { list: exceptList(off) });
|
||||
}
|
||||
const names = on.slice(0, SHORT_LIST).map((key) => TOGGLE_LABELS[key]);
|
||||
const rest = on.length - names.length;
|
||||
return rest > 0 ? `${names.join(", ")} и ещё ${rest}` : names.join(", ");
|
||||
const names = on.slice(0, SHORT_LIST).map(toggleLabel).join(", ");
|
||||
const rest = on.length - SHORT_LIST;
|
||||
return rest > 0 ? t("policy.summary.more", { count: rest, names }) : names;
|
||||
}
|
||||
|
||||
export function parentLabel(
|
||||
@@ -115,17 +105,20 @@ export function parentLabel(
|
||||
title: string | null
|
||||
): string {
|
||||
if (scopeType === "folder") {
|
||||
return `у папки «${title ?? "без названия"}»`;
|
||||
return t("policy.parent.folder", {
|
||||
title: title ?? t("policy.parent.untitled"),
|
||||
});
|
||||
}
|
||||
return PARENT_LABELS[scopeType] ?? "по умолчанию";
|
||||
const scope = DEFAULT_SCOPES[scopeType];
|
||||
return scope ? t(`policy.parent.${scope}`) : t("policy.parent.default");
|
||||
}
|
||||
|
||||
function labelList(keys: ToggleKey[]): string {
|
||||
return keys.map((key) => TOGGLE_LABELS[key]).join(", ");
|
||||
return keys.map(toggleLabel).join(", ");
|
||||
}
|
||||
|
||||
function named(keys: ToggleKey[], sign: Chip["sign"]): Chip[] {
|
||||
return keys.map((key) => ({ label: TOGGLE_LABELS[key], sign }));
|
||||
return keys.map((key) => ({ label: toggleLabel(key), sign }));
|
||||
}
|
||||
|
||||
function counted(keys: ToggleKey[], sign: Chip["sign"]): Chip[] {
|
||||
@@ -135,21 +128,29 @@ function counted(keys: ToggleKey[], sign: Chip["sign"]): Chip[] {
|
||||
if (keys.length <= SHOWN_CHIPS) {
|
||||
return named(keys, sign);
|
||||
}
|
||||
const word = plural(keys.length, "настройка", "настройки", "настроек");
|
||||
return [{ hint: labelList(keys), label: `${keys.length} ${word}`, sign }];
|
||||
return [
|
||||
{
|
||||
hint: labelList(keys),
|
||||
label: t("policy.chip.settings", { count: keys.length }),
|
||||
sign,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function contentChips(rule: CaptureToggles): Chip[] {
|
||||
const on = TOGGLE_KEYS.filter((key) => rule[key]);
|
||||
const off = TOGGLE_KEYS.filter((key) => !rule[key]);
|
||||
if (off.length === 0) {
|
||||
return [{ label: "Всё включено", sign: "plus" }];
|
||||
return [{ label: t("policy.chip.allOn"), sign: "plus" }];
|
||||
}
|
||||
if (on.length === 0) {
|
||||
return [{ label: "Всё выключено", sign: "minus" }];
|
||||
return [{ label: t("policy.chip.allOff"), sign: "minus" }];
|
||||
}
|
||||
if (off.length <= SHOWN_CHIPS) {
|
||||
return [{ label: "Всё", sign: "plus" }, ...named(off, "minus")];
|
||||
return [
|
||||
{ label: t("policy.summary.all"), sign: "plus" },
|
||||
...named(off, "minus"),
|
||||
];
|
||||
}
|
||||
return [...counted(on, "plus"), ...counted(off, "minus")];
|
||||
}
|
||||
@@ -158,10 +159,10 @@ export function diffChips(rule: CaptureToggles, base: CaptureToggles): Chip[] {
|
||||
const plus = TOGGLE_KEYS.filter((key) => rule[key] && !base[key]);
|
||||
const minus = TOGGLE_KEYS.filter((key) => !rule[key] && base[key]);
|
||||
if (plus.length > SHOWN_CHIPS && TOGGLE_KEYS.every((key) => rule[key])) {
|
||||
return [{ label: "Всё включено", sign: "plus" }];
|
||||
return [{ label: t("policy.chip.allOn"), sign: "plus" }];
|
||||
}
|
||||
if (minus.length > SHOWN_CHIPS && TOGGLE_KEYS.every((key) => !rule[key])) {
|
||||
return [{ label: "Всё выключено", sign: "minus" }];
|
||||
return [{ label: t("policy.chip.allOff"), sign: "minus" }];
|
||||
}
|
||||
return [...counted(plus, "plus"), ...counted(minus, "minus")];
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { FileShare } from "#lib/api/types.js";
|
||||
import { formatFull } from "#lib/format/datetime.js";
|
||||
import { t } from "#lib/i18n/locale.svelte.js";
|
||||
|
||||
const TEEN_START = 11;
|
||||
const TEEN_END = 14;
|
||||
@@ -26,28 +27,26 @@ export function plural(
|
||||
}
|
||||
|
||||
export function formatDownloads(share: FileShare): string {
|
||||
const word = plural(
|
||||
share.download_count,
|
||||
"скачивание",
|
||||
"скачивания",
|
||||
"скачиваний"
|
||||
);
|
||||
if (share.max_downloads === null) {
|
||||
return `${share.download_count} ${word}`;
|
||||
return t("shares.downloads", { count: share.download_count });
|
||||
}
|
||||
return `${share.download_count} из ${share.max_downloads}`;
|
||||
return t("shares.downloadsOf", {
|
||||
count: share.download_count,
|
||||
max: share.max_downloads,
|
||||
});
|
||||
}
|
||||
|
||||
export function formatExpiry(share: FileShare): string {
|
||||
if (share.revoked_at) {
|
||||
return "отозвана";
|
||||
return t("shares.revoked");
|
||||
}
|
||||
if (!share.expires_at) {
|
||||
return "бессрочно";
|
||||
return t("shares.forever");
|
||||
}
|
||||
const expires = Date.parse(share.expires_at);
|
||||
const prefix = expires <= Date.now() ? "истекла" : "до";
|
||||
return `${prefix} ${formatFull(share.expires_at)}`;
|
||||
const date = formatFull(share.expires_at);
|
||||
return Date.parse(share.expires_at) <= Date.now()
|
||||
? t("shares.expired", { date })
|
||||
: t("shares.until", { date });
|
||||
}
|
||||
|
||||
export function formatShareLimits(share: FileShare): string {
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
import type { StorageGroupName } from "#lib/api/types.js";
|
||||
import { plural } from "#lib/format/shares.js";
|
||||
import { locale, t } from "#lib/i18n/locale.svelte.js";
|
||||
|
||||
const DAY_MS = 86_400_000;
|
||||
const EXTENSION_LIMIT = 4;
|
||||
|
||||
export const GROUP_LABELS: Record<StorageGroupName, string> = {
|
||||
audio: "Аудио",
|
||||
file: "Файлы",
|
||||
other: "Фото и прочее",
|
||||
video: "Видео",
|
||||
voice: "Кружки и голосовые",
|
||||
};
|
||||
|
||||
const KIND_GROUPS: Record<string, StorageGroupName> = {
|
||||
audio: "audio",
|
||||
document: "file",
|
||||
@@ -20,16 +12,20 @@ const KIND_GROUPS: Record<string, StorageGroupName> = {
|
||||
voice: "voice",
|
||||
};
|
||||
|
||||
export function groupLabel(group: StorageGroupName): string {
|
||||
return t(`storage.group.${group}`);
|
||||
}
|
||||
|
||||
export function groupOf(kind: string): StorageGroupName {
|
||||
return KIND_GROUPS[kind] ?? "other";
|
||||
}
|
||||
|
||||
export function filesLabel(count: number): string {
|
||||
return `${count.toLocaleString("ru-RU")} ${plural(count, "файл", "файла", "файлов")}`;
|
||||
return t("storage.files", { count, n: count.toLocaleString(locale.tag) });
|
||||
}
|
||||
|
||||
export function messagesLabel(count: number): string {
|
||||
return `в ${count} ${plural(count, "сообщении", "сообщениях", "сообщениях")}`;
|
||||
return t("storage.inMessages", { count });
|
||||
}
|
||||
|
||||
export function fileExtension(fileName: string): string {
|
||||
@@ -42,10 +38,10 @@ export function fileExtension(fileName: string): string {
|
||||
export function purgeLabel(iso: string): string {
|
||||
const days = Math.ceil((new Date(iso).getTime() - Date.now()) / DAY_MS);
|
||||
if (days <= 0) {
|
||||
return "сегодня";
|
||||
return t("storage.purge.today");
|
||||
}
|
||||
if (days === 1) {
|
||||
return "завтра";
|
||||
return t("storage.purge.tomorrow");
|
||||
}
|
||||
return `через ${days} дн.`;
|
||||
return t("storage.purge.days", { count: days });
|
||||
}
|
||||
Reference in new issue
Block a user