Files
temetro/frontend/components/sidebar-02/nav-notifications.tsx
T
Khalid Abdi d237504af9 frontend: add Somali, Arabic (RTL) & German languages
Add three UI locales (so/ar/de) with full ~1,660-key translations alongside
en/fr, selectable in Settings → Profile. Arabic gets full right-to-left support:

- config.ts registers the locales and exports a `dirFor` helper; an inline
  <head> script in layout.tsx sets <html dir/lang> before first paint (no RTL
  flash), and i18n-provider keeps them in sync on language change.
- ~160 physical direction utilities converted to logical (ms/me/ps/pe/
  start/end/text-start/text-end); directional chevrons/arrows get rtl:rotate-180;
  chat-bubble align variants fixed to logical.
- IBM Plex Sans Arabic appended to the sans/heading font stacks for
  per-character Arabic fallback.
- Language persists to the backend user_settings and re-applies on sign-in so it
  roams across devices (localStorage stays the offline source of truth).
- New scripts/check-locales.mjs (npm run check-locales) enforces key/placeholder
  parity and Arabic CLDR plural completeness.

Bump to 0.3.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:03:13 +03:00

101 lines
3.1 KiB
TypeScript

"use client";
import { BellIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
Menu,
MenuGroup,
MenuGroupLabel,
MenuItem,
MenuPopup,
MenuSeparator,
MenuTrigger,
} from "@/components/ui/menu";
import { markNotificationRead, notificationHref } from "@/lib/notifications";
import { useNotifications } from "@/lib/use-notifications";
// ISO timestamp -> "just now" / "10m ago" / "3h ago" / "2d ago".
function relativeTime(iso: string): string {
const diff = Date.now() - new Date(iso).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return "just now";
if (mins < 60) return `${mins}m ago`;
const hours = Math.floor(mins / 60);
if (hours < 24) return `${hours}h ago`;
return `${Math.floor(hours / 24)}d ago`;
}
export function NotificationsPopover() {
const { t } = useTranslation();
const router = useRouter();
const { items, unread, markAllRead } = useNotifications();
return (
<Menu
onOpenChange={(open) => {
// Viewing the list clears the unread badge.
if (open && unread > 0) markAllRead();
}}
>
<MenuTrigger
render={
<Button
aria-label={t("nav.notifications")}
className="relative rounded-full"
size="icon"
variant="ghost"
/>
}
>
<BellIcon className="size-5" />
{unread > 0 && (
<span className="-top-0.5 -end-0.5 absolute flex min-w-4 items-center justify-center rounded-full bg-primary px-1 font-medium text-[10px] text-primary-foreground leading-4">
{unread > 9 ? "9+" : unread}
</span>
)}
</MenuTrigger>
<MenuPopup side="right" className="my-6 w-80">
<MenuGroup>
<MenuGroupLabel>{t("nav.notifications")}</MenuGroupLabel>
</MenuGroup>
<MenuSeparator />
{items.length === 0 ? (
<div className="px-3 py-6 text-center text-muted-foreground text-sm">
{t("nav.notificationsEmpty")}
</div>
) : (
items.map((n) => {
const href = notificationHref(n);
return (
<MenuItem
className="flex items-start gap-3"
disabled={!href}
key={n.id}
onClick={() => {
if (!href) return;
if (!n.read) void markNotificationRead(n.id).catch(() => {});
router.push(href);
}}
>
<Avatar className="size-8">
<AvatarFallback>{n.actorInitials ?? "•"}</AvatarFallback>
</Avatar>
<div className="flex min-w-0 flex-col">
<span className="font-medium text-sm">{n.text}</span>
<span className="text-muted-foreground text-xs">
{relativeTime(n.createdAt)}
</span>
</div>
</MenuItem>
);
})
)}
</MenuPopup>
</Menu>
);
}