fix: align widget accessibility and locales

This commit is contained in:
Thomas Camlong authored and ajnart committed 2026-08-13 19:54:42 +02:00
1 parent d880c4a281
commit 6c8dbc182f
11 files changed
+46 -46

No files matched your search

@@ -14,7 +14,7 @@ import { clientApi } from "@homarr/api/client";
import { useIntegrationsWithInteractAccess } from "@homarr/auth/client";
import { useTimeAgo } from "@homarr/common";
import type { AnchorNotePermission } from "@homarr/integrations";
import { useScopedI18n } from "@homarr/translation/client";
import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client";
import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
@@ -180,6 +180,7 @@ const AnchorNoteWidgetContent = ({
isAdvanced,
}: AnchorNoteWidgetContentProps) => {
const t = useScopedI18n("widget.anchorNote");
const locale = useCurrentIntlLocale();
const { data: note, refetch } = clientApi.widget.anchorNotes.getNote.useQuery({
integrationId,
noteId,
@@ -327,7 +328,7 @@ const AnchorNoteWidgetContent = ({
</Badge>
)}
<Text size="xs" c="dimmed">
{t("createdAt", { date: new Date(note.createdAt).toLocaleDateString() })}
{t("createdAt", { date: new Date(note.createdAt).toLocaleDateString(locale) })}
</Text>
</Group>
)}
@@ -2,19 +2,7 @@
import { useMemo } from "react";
import type { BoxProps } from "@mantine/core";
import {
Avatar,
AvatarGroup,
Badge,
Card,
Flex,
Group,
SimpleGrid,
Stack,
Text,
Tooltip,
TooltipFloating,
} from "@mantine/core";
import { Avatar, AvatarGroup, Badge, Card, Flex, Group, SimpleGrid, Stack, Text, Tooltip } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
import { IconBarrierBlock, IconPercentage, IconSearch, IconWorldWww } from "@tabler/icons-react";
@@ -177,10 +165,11 @@ const StatCard = ({ item, data, usePiHoleColors, t }: StatCardProps) => {
const value = item.value(data, width <= 64 ? "sm" : "md");
return (
<TooltipFloating label={tooltip} disabled={!tooltip} w={250} multiline>
<Tooltip label={tooltip} disabled={!tooltip} w={250} multiline events={{ hover: true, focus: true, touch: true }}>
<Card
ref={ref}
component="section"
tabIndex={tooltip ? 0 : undefined}
aria-label={`${label}: ${value}`}
className="summary-card"
p="sm"
@@ -223,7 +212,7 @@ const StatCard = ({ item, data, usePiHoleColors, t }: StatCardProps) => {
</Flex>
</Flex>
</Card>
</TooltipFloating>
</Tooltip>
);
};
@@ -14,7 +14,7 @@ import {
} from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { useCurrentIntlLocale, useI18n, useScopedI18n } from "@homarr/translation/client";
import { WidgetEmptyState } from "../../common/empty-state";
import type { WidgetComponentProps } from "../../definition";
@@ -139,6 +139,7 @@ function Carousel({
setPaused,
}: CarouselProps) {
const t = useScopedI18n("widget.immich-albumCarousel");
const locale = useCurrentIntlLocale();
const reduceMotion = useReducedMotion();
useEffect(() => {
@@ -214,7 +215,7 @@ function Carousel({
)}
<Group gap="xs">
<IconCalendar size={16} />
<Text size="xs">{new Date(currentAsset.fileModifiedAt).toLocaleDateString()}</Text>
<Text size="xs">{new Date(currentAsset.fileModifiedAt).toLocaleDateString(locale)}</Text>
</Group>
<Text size="xs" c="dimmed">
{safeCurrentIndex + 1} / {assets.length}
@@ -20,6 +20,7 @@ vi.mock("@homarr/api/client", () => ({
}));
vi.mock("@homarr/translation/client", () => ({
useCurrentIntlLocale: () => "en",
useI18n: () => (key: string) => key,
useScopedI18n: () => (key: string) => key,
}));
@@ -6,7 +6,7 @@ import { IconDatabase, IconPhoto, IconUsers, IconVideo } from "@tabler/icons-rea
import { clientApi } from "@homarr/api/client";
import { formatBytes } from "@homarr/common";
import { useI18n } from "@homarr/translation/client";
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
import { WidgetEmptyState } from "../../common/empty-state";
import type { WidgetComponentProps } from "../../definition";
@@ -24,6 +24,7 @@ export default function ImmichServerStatsWidget({
widgetStateRef,
}: WidgetComponentProps<"immich-serverStats">) {
const t = useI18n();
const locale = useCurrentIntlLocale();
const input = { integrationId: integrationIds[0] ?? "" };
const albumsInput = { ...input, limit: MAX_ADVANCED_ALBUMS };
const isAdvanced = displayMode === "advanced";
@@ -50,7 +51,7 @@ export default function ImmichServerStatsWidget({
<StatItem
icon={<IconUsers size={20} />}
label={t("widget.immich-serverStats.users")}
value={stats.userCount}
value={stats.userCount.toLocaleString(locale)}
dense={statsLayout.dense}
/>
)}
@@ -58,7 +59,7 @@ export default function ImmichServerStatsWidget({
<StatItem
icon={<IconPhoto size={20} />}
label={t("widget.immich-serverStats.photos")}
value={stats.photoCount}
value={stats.photoCount.toLocaleString(locale)}
dense={statsLayout.dense}
/>
)}
@@ -66,7 +67,7 @@ export default function ImmichServerStatsWidget({
<StatItem
icon={<IconVideo size={20} />}
label={t("widget.immich-serverStats.videos")}
value={stats.videoCount}
value={stats.videoCount.toLocaleString(locale)}
dense={statsLayout.dense}
/>
)}
@@ -103,7 +104,7 @@ export default function ImmichServerStatsWidget({
{album.albumName}
</Text>
<Text size="xs" c="dimmed">
{album.assetCount.toLocaleString()}
{album.assetCount.toLocaleString(locale)}
</Text>
</Group>
<Progress value={(album.assetCount / Math.max(maxAssets, 1)) * 100} size="sm" />
@@ -12,7 +12,7 @@ import {
SimpleGrid,
Stack,
Text,
TooltipFloating,
Tooltip,
UnstyledButton,
} from "@mantine/core";
import { IconBook, IconCalendar, IconClock, IconStarFilled } from "@tabler/icons-react";
@@ -88,10 +88,11 @@ const Item = ({ item, options, isAdvanced, width, height }: ItemProps) => {
const showSide = isAdvanced || (!isTiny && (options.showType || options.showSource));
return (
<TooltipFloating
<Tooltip
label={item.description}
w={300}
multiline
events={{ hover: true, focus: true, touch: true }}
disabled={
isAdvanced ||
item.description === undefined ||
@@ -221,7 +222,7 @@ const Item = ({ item, options, isAdvanced, width, height }: ItemProps) => {
)}
</Group>
</UnstyledButton>
</TooltipFloating>
</Tooltip>
);
};
@@ -19,7 +19,7 @@ import { IconAutomation, IconCheck } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useIntegrationsWithInteractAccess } from "@homarr/auth/client";
import { useRegisterSpotlightContextActions } from "@homarr/spotlight";
import { useI18n } from "@homarr/translation/client";
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../../definition";
@@ -61,6 +61,7 @@ export default function SmartHomeTriggerAutomationWidget({
}, [canInteract, integrationId, isEditMode, isPending, mutateAsync, options.automationId]);
const t = useI18n();
const locale = useCurrentIntlLocale();
useRegisterSpotlightContextActions(
`smartHome-automation-${options.automationId}`,
[
@@ -150,7 +151,7 @@ export default function SmartHomeTriggerAutomationWidget({
<Text ta="center" size="xs" c="dimmed">
{lastExecutedAt
? t("widget.smartHome-executeAutomation.advanced.lastExecuted", {
time: lastExecutedAt.toLocaleTimeString(),
time: lastExecutedAt.toLocaleTimeString(locale),
})
: t("widget.smartHome-executeAutomation.advanced.automationId", {
id: options.automationId,
+6 -4
View File
@@ -5,7 +5,7 @@ import { Flex, Stack, Text, Title, useMantineTheme } from "@mantine/core";
import { IconMinus, IconTrendingDown, IconTrendingUp } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useScopedI18n } from "@homarr/translation/client";
import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client";
import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
@@ -73,6 +73,8 @@ export function getStockSummary(priceHistory: number[], previousClose: number):
export default function StockPriceWidget({ options, width, height, displayMode }: WidgetComponentProps<"stockPrice">) {
const t = useScopedI18n("widget.stockPrice");
const locale = useCurrentIntlLocale();
const numberFormatter = new Intl.NumberFormat(locale);
const theme = useMantineTheme();
const { data: result } = clientApi.widget.stockPrice.getPriceHistory.useQuery(options);
@@ -120,15 +122,15 @@ export default function StockPriceWidget({ options, width, height, displayMode }
</Stack>
<Title pos="absolute" bottom={10} right={10} order={layout.priceOrder} fw={700}>
{new Intl.NumberFormat().format(round(summary.currentPrice))}
{numberFormatter.format(round(summary.currentPrice))}
</Title>
{layout.showChange && (
<Text pos="absolute" top={10} right={10} size="xl" fw={700}>
{new Intl.NumberFormat().format(stockValuesChange)}
{numberFormatter.format(stockValuesChange)}
{stockValuesChangePercentage === null
? null
: ` (${stockValuesChange > 0 ? "+" : ""}${new Intl.NumberFormat().format(stockValuesChangePercentage)}%)`}
: ` (${stockValuesChange > 0 ? "+" : ""}${numberFormatter.format(stockValuesChangePercentage)}%)`}
</Text>
)}
@@ -1,10 +1,11 @@
import { Avatar, Badge, Group, Paper, Stack, Text } from "@mantine/core";
import type { TracearrHistorySession } from "@homarr/integrations/types";
import { useScopedI18n } from "@homarr/translation/client";
import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client";
export function RecentActivityList({ sessions }: { sessions: TracearrHistorySession[] }) {
const t = useScopedI18n("widget.tracearr");
const locale = useCurrentIntlLocale();
return (
<Stack gap={4}>
@@ -42,7 +43,7 @@ export function RecentActivityList({ sessions }: { sessions: TracearrHistorySess
{session.watched ? t("recentActivity.watched") : t("recentActivity.partial")}
</Badge>
<Text size="xs" c="dimmed" lineClamp={1}>
{new Date(session.startedAt).toLocaleDateString()}
{new Date(session.startedAt).toLocaleDateString(locale)}
</Text>
</Stack>
</Group>
@@ -2,10 +2,11 @@ import { Avatar, Badge, Group, Paper, Stack, Text } from "@mantine/core";
import { IconAlertTriangle } from "@tabler/icons-react";
import type { TracearrViolation } from "@homarr/integrations/types";
import { useScopedI18n } from "@homarr/translation/client";
import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client";
export function ViolationsList({ violations }: { violations: TracearrViolation[] }) {
const t = useScopedI18n("widget.tracearr");
const locale = useCurrentIntlLocale();
return (
<Stack gap={4}>
@@ -53,7 +54,7 @@ export function ViolationsList({ violations }: { violations: TracearrViolation[]
{violation.severity}
</Badge>
<Text size="xs" c="dimmed" lineClamp={1}>
{new Date(violation.createdAt).toLocaleDateString()}
{new Date(violation.createdAt).toLocaleDateString(locale)}
</Text>
</Stack>
</Group>
+10 -9
View File
@@ -6,7 +6,7 @@ import { Box, Group, ScrollArea, SimpleGrid, Stack, Text, useMantineColorScheme
import { clientApi } from "@homarr/api/client";
import { formatDuration } from "@homarr/common";
import type { UmamiVisitorStats } from "@homarr/integrations/types";
import { useScopedI18n } from "@homarr/translation/client";
import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client";
import { UmamiEventsContent } from "./umami-events-content";
import { UmamiTopPagesContent, UmamiTopReferrersContent } from "./umami-top-list";
@@ -44,6 +44,7 @@ export function UmamiContent({
}: UmamiContentProps) {
const t = useScopedI18n("widget.umami");
const tCommon = useScopedI18n("common");
const locale = useCurrentIntlLocale();
const { colorScheme } = useMantineColorScheme();
const tickColor = colorScheme === "dark" ? "#c1c2c5" : "#495057";
@@ -217,8 +218,8 @@ export function UmamiContent({
<Group gap={4} align="baseline">
<Text size="lg" fw={700} lh={1}>
{viewMode === "events"
? (multiEventTotal ?? 0).toLocaleString()
: (visitorStats.eventCount ?? 0).toLocaleString()}
? (multiEventTotal ?? 0).toLocaleString(locale)
: (visitorStats.eventCount ?? 0).toLocaleString(locale)}
</Text>
<Text size="xs" c="dimmed">
{t("event.label")}
@@ -229,7 +230,7 @@ export function UmamiContent({
</Text>
<Group gap={4} align="baseline">
<Text size="lg" fw={700} lh={1}>
{visitorStats.totalVisitors.toLocaleString()}
{visitorStats.totalVisitors.toLocaleString(locale)}
</Text>
<Text size="xs" c="dimmed">
{t("visitors")}
@@ -239,7 +240,7 @@ export function UmamiContent({
) : (
<Group gap={4} align="baseline">
<Text size="xl" fw={700} lh={1}>
{visitorStats.totalVisitors.toLocaleString()}
{visitorStats.totalVisitors.toLocaleString(locale)}
</Text>
<Text size="xs" c="dimmed">
{t("visitors")}
@@ -250,18 +251,18 @@ export function UmamiContent({
{showSecondaryStats && (
<Group wrap="wrap" style={{ columnGap: 12, rowGap: 4 }}>
<Text size="xs" c="green">
● {activeVisitors.toLocaleString()} {t("active")}
● {activeVisitors.toLocaleString(locale)} {t("active")}
</Text>
<Text size="xs" c="dimmed">
{visitorStats.totalPageviews.toLocaleString()} {t("pageviews")}
{visitorStats.totalPageviews.toLocaleString(locale)} {t("pageviews")}
</Text>
<Text size="xs" c="dimmed">
{visitorStats.totalVisits.toLocaleString()} {t("visits")}
{visitorStats.totalVisits.toLocaleString(locale)} {t("visits")}
</Text>
{showDetailedStats && (
<>
<Text size="xs" c="dimmed">
{visitorStats.bounceRate}% {t("bounceRate")}
{visitorStats.bounceRate.toLocaleString(locale)}% {t("bounceRate")}
</Text>
<Text size="xs" c="dimmed">
{formatDuration(visitorStats.avgDuration * 1000)} {t("avgDuration")}