mirror of
https://github.com/temetro/temetro.git
synced 2026-08-25 18:07:44 +00:00
d237504af9
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>
87 lines
2.7 KiB
TypeScript
87 lines
2.7 KiB
TypeScript
"use client";
|
|
|
|
import { CalendarClock } from "lucide-react";
|
|
import type { ReactNode } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
import {
|
|
Dialog,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogPanel,
|
|
DialogPopup,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import type { MessageAttachment } from "@/lib/messages";
|
|
|
|
// The appointment data carried inside a shared-appointment message. It's a
|
|
// point-in-time snapshot, so it survives the appointment later being edited or
|
|
// deleted.
|
|
type AppointmentSnapshot = Extract<
|
|
MessageAttachment,
|
|
{ kind: "appointment" }
|
|
>["appointment"];
|
|
|
|
function Row({ label, value }: { label: string; value: ReactNode }) {
|
|
if (!value) return null;
|
|
return (
|
|
<div className="flex items-baseline justify-between gap-3">
|
|
<dt className="text-muted-foreground text-sm">{label}</dt>
|
|
<dd className="text-end text-foreground text-sm">{value}</dd>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// A read-only view of a shared appointment, opened by clicking the card in the
|
|
// thread. Works the same for the sender and the recipient.
|
|
export function AppointmentDetailDialog({
|
|
appointment,
|
|
open,
|
|
onOpenChange,
|
|
}: {
|
|
appointment: AppointmentSnapshot;
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const a = appointment;
|
|
const statusLabel = a.status
|
|
? t(`appointments.status.${a.status}`, { defaultValue: a.status })
|
|
: null;
|
|
|
|
return (
|
|
<Dialog onOpenChange={onOpenChange} open={open}>
|
|
<DialogPopup className="sm:max-w-sm">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<CalendarClock className="size-4 text-muted-foreground" />
|
|
{t("messages.attach.apptDetailTitle")}
|
|
</DialogTitle>
|
|
<DialogDescription>{a.name}</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogPanel>
|
|
<dl className="flex flex-col gap-2.5">
|
|
<Row
|
|
label={t("messages.attach.apptWhen")}
|
|
value={[a.date, a.time].filter(Boolean).join(" · ")}
|
|
/>
|
|
<Row label={t("messages.attach.apptType")} value={a.type} />
|
|
<Row label={t("messages.attach.apptProvider")} value={a.provider} />
|
|
<Row
|
|
label={t("messages.attach.apptPatient")}
|
|
value={a.fileNumber ? `#${a.fileNumber}` : null}
|
|
/>
|
|
<Row
|
|
label={t("messages.attach.apptStatus")}
|
|
value={
|
|
statusLabel ? <Badge variant="outline">{statusLabel}</Badge> : null
|
|
}
|
|
/>
|
|
</dl>
|
|
</DialogPanel>
|
|
</DialogPopup>
|
|
</Dialog>
|
|
);
|
|
}
|