mirror of
https://github.com/temetro/temetro.git
synced 2026-08-21 23:47:14 +00:00
i18n: convert feature pages, dialogs, auth sub-pages and nav to t()
Move all hardcoded UI strings in the appointments, prescriptions, tasks, messages, activity, analysis and patients views (plus their dialogs and detail sheets), the remaining auth pages (verify-email, forgot/reset password, accept-invite, onboarding), the clinic form and the sidebar user menu into i18next keys in en/translation.json. Clinical option values (appointment types, frequencies) stay canonical. English-only; no new locale yet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { CalendarDays, Search } from "lucide-react";
|
||||
import { type FormEvent, type ReactNode, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { TODAY } from "@/components/appointments/appointments-view";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -69,6 +70,7 @@ export function AddAppointmentDialog({
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onAdd: (appt: NewAppointment) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [patients, setPatients] = useState<Patient[]>([]);
|
||||
const [query, setQuery] = useState("");
|
||||
const [selected, setSelected] = useState<Patient | null>(null);
|
||||
@@ -118,7 +120,10 @@ export function AddAppointmentDialog({
|
||||
const submit = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (!selected) {
|
||||
notify.error("Pick a patient", "Search and select a patient first.");
|
||||
notify.error(
|
||||
t("appointments.dialog.pickPatientTitle"),
|
||||
t("appointments.dialog.pickPatientBody"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
onAdd({
|
||||
@@ -130,7 +135,10 @@ export function AddAppointmentDialog({
|
||||
type,
|
||||
provider: provider.trim() || selected.pcp,
|
||||
});
|
||||
notify.success("Appointment added", `${selected.name} at ${time}`);
|
||||
notify.success(
|
||||
t("appointments.dialog.addedTitle"),
|
||||
`${selected.name} · ${time}`,
|
||||
);
|
||||
reset();
|
||||
onOpenChange(false);
|
||||
};
|
||||
@@ -145,15 +153,15 @@ export function AddAppointmentDialog({
|
||||
>
|
||||
<DialogPopup className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>New appointment</DialogTitle>
|
||||
<DialogTitle>{t("appointments.dialog.title")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Search for a patient by name or file number, then set the slot.
|
||||
{t("appointments.dialog.description")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form className="contents" onSubmit={submit}>
|
||||
<DialogPanel className="flex flex-col gap-4">
|
||||
<Field label="Patient">
|
||||
<Field label={t("appointments.dialog.patient")}>
|
||||
{selected ? (
|
||||
<div className="flex items-center justify-between gap-2 rounded-2xl border bg-input/30 px-3 py-2">
|
||||
<div className="flex min-w-0 flex-col">
|
||||
@@ -161,7 +169,9 @@ export function AddAppointmentDialog({
|
||||
{selected.name}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
File #{selected.fileNumber}
|
||||
{t("appointments.dialog.fileNumber", {
|
||||
number: selected.fileNumber,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
@@ -173,7 +183,7 @@ export function AddAppointmentDialog({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
Change
|
||||
{t("appointments.dialog.change")}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -184,7 +194,7 @@ export function AddAppointmentDialog({
|
||||
autoFocus
|
||||
className="pl-9"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Search name or file number"
|
||||
placeholder={t("appointments.dialog.searchPlaceholder")}
|
||||
value={query}
|
||||
/>
|
||||
</div>
|
||||
@@ -192,7 +202,7 @@ export function AddAppointmentDialog({
|
||||
<div className="max-h-56 overflow-y-auto rounded-2xl border bg-popover p-1">
|
||||
{matches.length === 0 ? (
|
||||
<p className="px-2 py-2 text-muted-foreground text-sm">
|
||||
No patients found.
|
||||
{t("appointments.dialog.noPatients")}
|
||||
</p>
|
||||
) : (
|
||||
matches.map((p) => (
|
||||
@@ -222,7 +232,9 @@ export function AddAppointmentDialog({
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="text-muted-foreground text-xs">Date</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{t("appointments.dialog.date")}
|
||||
</span>
|
||||
<Popover onOpenChange={setDateOpen} open={dateOpen}>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
@@ -254,7 +266,7 @@ export function AddAppointmentDialog({
|
||||
</PopoverPopup>
|
||||
</Popover>
|
||||
</div>
|
||||
<Field label="Time">
|
||||
<Field label={t("appointments.dialog.time")}>
|
||||
<Input
|
||||
onChange={(event) => setTime(event.target.value)}
|
||||
type="time"
|
||||
@@ -264,23 +276,23 @@ export function AddAppointmentDialog({
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label="Type">
|
||||
<Field label={t("appointments.dialog.type")}>
|
||||
<select
|
||||
className={controlClass}
|
||||
onChange={(event) => setType(event.target.value)}
|
||||
value={type}
|
||||
>
|
||||
{TYPES.map((t) => (
|
||||
<option key={t} value={t}>
|
||||
{t}
|
||||
{TYPES.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="Provider">
|
||||
<Field label={t("appointments.dialog.provider")}>
|
||||
<Input
|
||||
onChange={(event) => setProvider(event.target.value)}
|
||||
placeholder="e.g. Dr. Okafor"
|
||||
placeholder={t("appointments.dialog.providerPlaceholder")}
|
||||
value={provider}
|
||||
/>
|
||||
</Field>
|
||||
@@ -289,10 +301,10 @@ export function AddAppointmentDialog({
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose render={<Button type="button" variant="outline" />}>
|
||||
Cancel
|
||||
{t("appointments.dialog.cancel")}
|
||||
</DialogClose>
|
||||
<Button disabled={!selected} type="submit">
|
||||
Add appointment
|
||||
{t("appointments.dialog.submit")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { type ReactNode, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {
|
||||
AddAppointmentDialog,
|
||||
@@ -60,13 +61,6 @@ const statusVariant: Record<
|
||||
cancelled: "destructive",
|
||||
};
|
||||
|
||||
const statusLabel: Record<ApptStatus, string> = {
|
||||
"checked-in": "Checked in",
|
||||
confirmed: "Confirmed",
|
||||
completed: "Completed",
|
||||
cancelled: "Cancelled",
|
||||
};
|
||||
|
||||
// "2026-06-05" -> "Wednesday, June 5"
|
||||
function formatDayKey(key: string): string {
|
||||
return new Date(`${key}T00:00:00`).toLocaleDateString("en-US", {
|
||||
@@ -105,6 +99,7 @@ function Kpi({
|
||||
}
|
||||
|
||||
function ApptRow({ appt }: { appt: Appointment }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex items-center gap-3 px-4 py-3">
|
||||
<span className="w-12 shrink-0 font-medium text-foreground text-sm tabular-nums">
|
||||
@@ -121,7 +116,9 @@ function ApptRow({ appt }: { appt: Appointment }) {
|
||||
{appt.type} · {appt.provider}
|
||||
</span>
|
||||
</div>
|
||||
<Badge variant={statusVariant[appt.status]}>{statusLabel[appt.status]}</Badge>
|
||||
<Badge variant={statusVariant[appt.status]}>
|
||||
{t(`appointments.status.${appt.status}`)}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -159,6 +156,7 @@ function Section({
|
||||
}
|
||||
|
||||
export function AppointmentsView() {
|
||||
const { t } = useTranslation();
|
||||
const [addOpen, setAddOpen] = useState(false);
|
||||
const [calendarOpen, setCalendarOpen] = useState(false);
|
||||
const [appointments, setAppointments] = useState<Appointment[]>([]);
|
||||
@@ -192,7 +190,10 @@ export function AppointmentsView() {
|
||||
});
|
||||
setAppointments((prev) => [...prev, created]);
|
||||
} catch {
|
||||
notify.error("Couldn't add appointment", "Please try again.");
|
||||
notify.error(
|
||||
t("appointments.addFailedTitle"),
|
||||
t("appointments.addFailedBody"),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -201,20 +202,20 @@ export function AppointmentsView() {
|
||||
const today = appointments.filter((a) => a.date === TODAY);
|
||||
const week = appointments.filter((a) => a.date >= start && a.date <= end);
|
||||
return [
|
||||
{ label: "Today", value: String(today.length), icon: CalendarClock },
|
||||
{ label: "This week", value: String(week.length), icon: Users },
|
||||
{ label: t("appointments.kpi.today"), value: String(today.length), icon: CalendarClock },
|
||||
{ label: t("appointments.kpi.thisWeek"), value: String(week.length), icon: Users },
|
||||
{
|
||||
label: "Checked in",
|
||||
label: t("appointments.kpi.checkedIn"),
|
||||
value: String(today.filter((a) => a.status === "checked-in").length),
|
||||
icon: Stethoscope,
|
||||
},
|
||||
{
|
||||
label: "Completed",
|
||||
label: t("appointments.kpi.completed"),
|
||||
value: String(today.filter((a) => a.status === "completed").length),
|
||||
icon: Clock,
|
||||
},
|
||||
];
|
||||
}, [appointments]);
|
||||
}, [appointments, t]);
|
||||
|
||||
const todayItems = useMemo(
|
||||
() => appointments.filter((a) => a.date === TODAY).sort(byTime),
|
||||
@@ -256,10 +257,10 @@ export function AppointmentsView() {
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<h1 className="font-semibold text-2xl tracking-tight">
|
||||
Appointments & Schedule
|
||||
{t("appointments.title")}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Today's clinic schedule and what's coming up.
|
||||
{t("appointments.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -268,7 +269,7 @@ export function AppointmentsView() {
|
||||
<Input
|
||||
className="w-full pl-9 sm:w-64"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Search patient, type, provider"
|
||||
placeholder={t("appointments.searchPlaceholder")}
|
||||
value={query}
|
||||
/>
|
||||
</div>
|
||||
@@ -279,7 +280,7 @@ export function AppointmentsView() {
|
||||
variant="outline"
|
||||
>
|
||||
<CalendarDays className="size-4" />
|
||||
Calendar
|
||||
{t("appointments.calendar")}
|
||||
</Button>
|
||||
<Button
|
||||
className="rounded-3xl"
|
||||
@@ -287,7 +288,7 @@ export function AppointmentsView() {
|
||||
type="button"
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
Add
|
||||
{t("appointments.add")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -301,7 +302,7 @@ export function AppointmentsView() {
|
||||
))
|
||||
) : (
|
||||
<p className="rounded-2xl border border-dashed bg-card/20 px-4 py-8 text-center text-muted-foreground text-sm">
|
||||
No matching appointments.
|
||||
{t("appointments.noMatches")}
|
||||
</p>
|
||||
)
|
||||
) : (
|
||||
@@ -312,12 +313,12 @@ export function AppointmentsView() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Section description={formatDayKey(TODAY)} title="Today">
|
||||
<Section description={formatDayKey(TODAY)} title={t("appointments.today")}>
|
||||
{todayItems.length > 0 ? (
|
||||
<ScheduleList items={todayItems} />
|
||||
) : (
|
||||
<p className="rounded-2xl border border-dashed bg-card/20 px-4 py-8 text-center text-muted-foreground text-sm">
|
||||
Nothing scheduled today.
|
||||
{t("appointments.nothingToday")}
|
||||
</p>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {
|
||||
type Appointment,
|
||||
@@ -57,6 +58,7 @@ export function CalendarDialog({
|
||||
onOpenChange: (open: boolean) => void;
|
||||
appointments: Appointment[];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
// First-of-month for the displayed month; defaults to TODAY's month.
|
||||
const [viewMonth, setViewMonth] = useState<Date>(() => {
|
||||
const t = parseKey(TODAY);
|
||||
@@ -120,7 +122,7 @@ export function CalendarDialog({
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
Today
|
||||
{t("appointments.calendarDialog.today")}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Previous month"
|
||||
@@ -214,16 +216,16 @@ export function CalendarDialog({
|
||||
{formatDayKey(selectedKey)}
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{selectedItems.length === 1
|
||||
? "1 appointment"
|
||||
: `${selectedItems.length} appointments`}
|
||||
{t("appointments.calendarDialog.appointmentCount", {
|
||||
count: selectedItems.length,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
{selectedItems.length > 0 ? (
|
||||
<ScheduleList items={selectedItems} />
|
||||
) : (
|
||||
<div className="rounded-2xl border border-dashed bg-card/20 px-4 py-8 text-center text-muted-foreground text-sm">
|
||||
No appointments on this day.
|
||||
{t("appointments.calendarDialog.none")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user