diff --git a/app/client/components/backup-summary-card.tsx b/app/client/components/backup-summary-card.tsx index 00aa40f4..3c10d0af 100644 --- a/app/client/components/backup-summary-card.tsx +++ b/app/client/components/backup-summary-card.tsx @@ -1,5 +1,6 @@ import { Card, CardContent } from "~/client/components/ui/card"; import { ByteSize } from "~/client/components/bytes-size"; +import { useRootLoaderData } from "~/client/hooks/use-root-loader-data"; import type { ResticSnapshotSummaryDto } from "@zerobyte/core/restic"; import { formatDuration } from "~/utils/utils"; @@ -7,8 +8,6 @@ type Props = { summary?: ResticSnapshotSummaryDto | null; }; -const formatCount = (value: number) => value.toLocaleString(); - const getDurationLabel = (start: string, end: string) => { const startMs = new Date(start).getTime(); const endMs = new Date(end).getTime(); @@ -17,8 +16,11 @@ const getDurationLabel = (start: string, end: string) => { }; export const BackupSummaryCard = ({ summary }: Props) => { + const { locale } = useRootLoaderData(); + if (!summary) return null; + const formatCount = (value: number) => value.toLocaleString(locale); const durationLabel = getDurationLabel(summary.backup_start, summary.backup_end); const topStats = [ diff --git a/app/client/components/bytes-size.tsx b/app/client/components/bytes-size.tsx index 0ffa5fad..08278c36 100644 --- a/app/client/components/bytes-size.tsx +++ b/app/client/components/bytes-size.tsx @@ -1,5 +1,5 @@ import type React from "react"; -import { formatBytes } from "~/utils/format-bytes"; +import { useFormatBytes } from "~/client/hooks/use-format-bytes"; type ByteSizeProps = { bytes: number; @@ -14,6 +14,7 @@ type ByteSizeProps = { }; export function ByteSize(props: ByteSizeProps) { + const formatBytes = useFormatBytes(); const { bytes, base = 1000, diff --git a/app/client/components/restore-progress.tsx b/app/client/components/restore-progress.tsx index 11c0eaf0..249a911e 100644 --- a/app/client/components/restore-progress.tsx +++ b/app/client/components/restore-progress.tsx @@ -1,9 +1,10 @@ import { useEffect, useState } from "react"; import { ByteSize } from "~/client/components/bytes-size"; +import { useFormatBytes } from "~/client/hooks/use-format-bytes"; +import { useRootLoaderData } from "~/client/hooks/use-root-loader-data"; import { Card } from "~/client/components/ui/card"; import { Progress } from "~/client/components/ui/progress"; import { type RestoreProgressEvent, useServerEvents } from "~/client/hooks/use-server-events"; -import { formatBytes } from "~/utils/format-bytes"; import { formatDuration } from "~/utils/utils"; type Props = { @@ -12,6 +13,8 @@ type Props = { }; export const RestoreProgress = ({ repositoryId, snapshotId }: Props) => { + const formatBytes = useFormatBytes(); + const { locale } = useRootLoaderData(); const { addEventListener } = useServerEvents(); const [progress, setProgress] = useState(null); @@ -71,9 +74,9 @@ export const RestoreProgress = ({ repositoryId, snapshotId }: Props) => {

Files

- {progress.files_restored.toLocaleString()} + {progress.files_restored.toLocaleString(locale)}  /  - {progress.total_files.toLocaleString()} + {progress.total_files.toLocaleString(locale)}

diff --git a/app/client/components/ui/chart.tsx b/app/client/components/ui/chart.tsx index 15014093..b23ceccb 100644 --- a/app/client/components/ui/chart.tsx +++ b/app/client/components/ui/chart.tsx @@ -3,6 +3,7 @@ import * as React from "react"; import * as RechartsPrimitive from "recharts"; +import { useRootLoaderData } from "~/client/hooks/use-root-loader-data"; import { cn } from "~/client/lib/utils"; // Format: { THEME_NAME: CSS_SELECTOR } @@ -116,6 +117,7 @@ function ChartTooltipContent({ labelKey?: string; }) { const { config } = useChart(); + const { locale } = useRootLoaderData(); const tooltipLabel = React.useMemo(() => { if (hideLabel || !payload?.length) { @@ -205,7 +207,7 @@ function ChartTooltipContent({
{item.value && ( - {item.value.toLocaleString()} + {item.value.toLocaleString(locale)} )} diff --git a/app/client/components/ui/sidebar.tsx b/app/client/components/ui/sidebar.tsx index 8de08d06..84db8e74 100644 --- a/app/client/components/ui/sidebar.tsx +++ b/app/client/components/ui/sidebar.tsx @@ -565,10 +565,16 @@ function SidebarMenuSkeleton({ }: React.ComponentProps<"div"> & { showIcon?: boolean; }) { - // Random width between 50 to 90%. + const skeletonId = React.useId(); const width = React.useMemo(() => { - return `${Math.floor(Math.random() * 40) + 50}%`; - }, []); + let hash = 0; + + for (const char of skeletonId) { + hash = (hash * 31 + char.charCodeAt(0)) % 40; + } + + return `${hash + 50}%`; + }, [skeletonId]); return (
{ + const { locale } = useRootLoaderData(); + + return (bytes: number, options?: FormatBytesOptions) => + formatBytes(bytes, { + ...options, + locale: options?.locale ?? locale, + }); +}; diff --git a/app/client/hooks/use-root-loader-data.ts b/app/client/hooks/use-root-loader-data.ts new file mode 100644 index 00000000..eb48b7a8 --- /dev/null +++ b/app/client/hooks/use-root-loader-data.ts @@ -0,0 +1,3 @@ +import { Route as RootRoute } from "~/routes/__root"; + +export const useRootLoaderData = () => RootRoute.useLoaderData(); diff --git a/app/client/lib/datetime.ts b/app/client/lib/datetime.ts index 0fb340df..8c212390 100644 --- a/app/client/lib/datetime.ts +++ b/app/client/lib/datetime.ts @@ -1,6 +1,6 @@ import { formatDistanceToNow, isValid } from "date-fns"; import { useEffect, useMemo, useState } from "react"; -import { Route as RootRoute } from "~/routes/__root"; +import { useRootLoaderData } from "~/client/hooks/use-root-loader-data"; export type DateInput = Date | string | number | null | undefined; @@ -201,7 +201,7 @@ export function formatTimeAgo(date: DateInput, now = Date.now()): string { } export function useTimeFormat() { - const { locale, timeZone, dateFormat, timeFormat, now } = RootRoute.useLoaderData(); + const { locale, timeZone, dateFormat, timeFormat, now } = useRootLoaderData(); const [currentNow, setCurrentNow] = useState(now); useEffect(() => { diff --git a/app/client/modules/backups/components/backup-progress-card.tsx b/app/client/modules/backups/components/backup-progress-card.tsx index 579433f1..2a11a2df 100644 --- a/app/client/modules/backups/components/backup-progress-card.tsx +++ b/app/client/modules/backups/components/backup-progress-card.tsx @@ -1,11 +1,12 @@ import { useQuery } from "@tanstack/react-query"; import { ByteSize } from "~/client/components/bytes-size"; +import { useFormatBytes } from "~/client/hooks/use-format-bytes"; +import { useRootLoaderData } from "~/client/hooks/use-root-loader-data"; import { Card } from "~/client/components/ui/card"; import { Progress } from "~/client/components/ui/progress"; import { getBackupProgressOptions } from "~/client/api-client/@tanstack/react-query.gen"; import type { GetBackupProgressResponse } from "~/client/api-client/types.gen"; import { formatDuration } from "~/utils/utils"; -import { formatBytes } from "~/utils/format-bytes"; type Props = { scheduleShortId: string; @@ -13,6 +14,8 @@ type Props = { }; export const BackupProgressCard = ({ scheduleShortId, initialProgress }: Props) => { + const formatBytes = useFormatBytes(); + const { locale } = useRootLoaderData(); const { data: progress } = useQuery({ ...getBackupProgressOptions({ path: { shortId: scheduleShortId } }), initialData: initialProgress, @@ -52,7 +55,7 @@ export const BackupProgressCard = ({ scheduleShortId, initialProgress }: Props)

{progress ? ( <> - {files_done.toLocaleString()} / {total_files.toLocaleString()} + {files_done.toLocaleString(locale)} / {total_files.toLocaleString(locale)} ) : ( "—" diff --git a/app/client/modules/repositories/components/compression-stats-chart.tsx b/app/client/modules/repositories/components/compression-stats-chart.tsx index 4cf8612b..4ada5e42 100644 --- a/app/client/modules/repositories/components/compression-stats-chart.tsx +++ b/app/client/modules/repositories/components/compression-stats-chart.tsx @@ -6,6 +6,7 @@ import { } from "~/client/api-client/@tanstack/react-query.gen"; import type { GetRepositoryStatsResponse } from "~/client/api-client/types.gen"; import { ByteSize } from "~/client/components/bytes-size"; +import { useRootLoaderData } from "~/client/hooks/use-root-loader-data"; import { Button } from "~/client/components/ui/button"; import { Card, CardContent, CardTitle } from "~/client/components/ui/card"; import { parseError } from "~/client/lib/errors"; @@ -26,6 +27,7 @@ const toSafeNumber = (value: number | undefined) => { }; export function CompressionStatsChart({ repositoryShortId, initialStats }: Props) { + const { locale } = useRootLoaderData(); const refreshStats = useMutation({ ...refreshRepositoryStatsMutation(), onSuccess: () => { @@ -133,7 +135,9 @@ export function CompressionStatsChart({ repositoryShortId, initialStats }: Props

Snapshots
- {snapshotsCount.toLocaleString()} + + {snapshotsCount.toLocaleString(locale)} + {compressionProgressPercent.toFixed(1)}% compressed diff --git a/app/client/modules/settings/routes/settings.tsx b/app/client/modules/settings/routes/settings.tsx index 15f44021..fb30ef9e 100644 --- a/app/client/modules/settings/routes/settings.tsx +++ b/app/client/modules/settings/routes/settings.tsx @@ -19,6 +19,7 @@ import { Input } from "~/client/components/ui/input"; import { Label } from "~/client/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs"; +import { useRootLoaderData } from "~/client/hooks/use-root-loader-data"; import { authClient } from "~/client/lib/auth-client"; import { DATE_FORMATS, @@ -29,7 +30,6 @@ import { } from "~/client/lib/datetime"; import { logger } from "~/client/lib/logger"; import { type AppContext } from "~/context"; -import { Route as RootRoute } from "~/routes/__root"; import { TwoFactorSection } from "../components/two-factor-section"; import { useNavigate, useSearch } from "@tanstack/react-router"; import { SsoSettingsSection } from "~/client/modules/sso/components/sso-settings-section"; @@ -50,7 +50,7 @@ export function SettingsPage({ appContext, initialMembers, initialSsoSettings, i const [downloadDialogOpen, setDownloadDialogOpen] = useState(false); const [downloadPassword, setDownloadPassword] = useState(""); const [isChangingPassword, setIsChangingPassword] = useState(false); - const { locale, dateFormat, timeFormat } = RootRoute.useLoaderData(); + const { locale, dateFormat, timeFormat } = useRootLoaderData(); const { tab } = useSearch({ from: "/(dashboard)/settings/" }); const activeTab = tab || "account"; diff --git a/app/test/setup-client.ts b/app/test/setup-client.ts index 258a72c5..c32d4e4b 100644 --- a/app/test/setup-client.ts +++ b/app/test/setup-client.ts @@ -1,9 +1,20 @@ import "./setup.ts"; import { GlobalRegistrator } from "@happy-dom/global-registrator"; -import { afterAll, afterEach, beforeAll } from "bun:test"; +import { afterAll, afterEach, beforeAll, mock } from "bun:test"; import { client } from "~/client/api-client/client.gen"; import { server } from "~/test/msw/server"; +void mock.module("~/client/hooks/use-root-loader-data", () => ({ + useRootLoaderData: () => ({ + theme: "dark", + locale: "en-US", + timeZone: "UTC", + dateFormat: "MM/DD/YYYY", + timeFormat: "12h", + now: Date.now(), + }), +})); + GlobalRegistrator.register({ url: "http://localhost:3000" }); client.setConfig({