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:
Khalid Abdi
2026-06-07 21:57:00 +03:00
parent 288d14758f
commit e0de20b551
20 changed files with 837 additions and 319 deletions
+16 -12
View File
@@ -3,12 +3,14 @@
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { Suspense, useState } from "react";
import { useTranslation } from "react-i18next";
import { AuthShell, FormAlert } from "@/components/auth/auth-ui";
import { Button } from "@/components/ui/button";
import { authClient } from "@/lib/auth-client";
function AcceptInviteInner() {
const { t } = useTranslation();
const router = useRouter();
const params = useSearchParams();
const invitationId = params.get("id") ?? "";
@@ -24,7 +26,7 @@ function AcceptInviteInner() {
invitationId,
});
if (err || !data) {
setError(err?.message ?? "This invitation is invalid or has expired.");
setError(err?.message ?? t("auth.acceptInvite.error"));
setAccepting(false);
return;
}
@@ -37,8 +39,8 @@ function AcceptInviteInner() {
if (!invitationId) {
return (
<AuthShell title="Invitation not found">
<FormAlert>This invitation link is missing or invalid.</FormAlert>
<AuthShell title={t("auth.acceptInvite.notFoundTitle")}>
<FormAlert>{t("auth.acceptInvite.notFoundBody")}</FormAlert>
</AuthShell>
);
}
@@ -48,8 +50,8 @@ function AcceptInviteInner() {
const back = `/accept-invite?id=${encodeURIComponent(invitationId)}`;
return (
<AuthShell
subtitle="Sign in with the email this invitation was sent to, then return to this link to accept."
title="You've been invited"
subtitle={t("auth.acceptInvite.invitedSubtitle")}
title={t("auth.acceptInvite.invitedTitle")}
>
<div className="flex flex-col gap-3">
<Button
@@ -57,7 +59,7 @@ function AcceptInviteInner() {
onClick={() => router.push("/login")}
type="button"
>
Sign in
{t("auth.acceptInvite.signIn")}
</Button>
<Button
className="w-full"
@@ -65,12 +67,12 @@ function AcceptInviteInner() {
type="button"
variant="outline"
>
Create an account
{t("auth.acceptInvite.createAccount")}
</Button>
<p className="text-center text-xs text-muted-foreground">
After signing in, reopen{" "}
{t("auth.acceptInvite.reopenPrefix")}{" "}
<Link className="hover:underline" href={back}>
this invitation link
{t("auth.acceptInvite.reopenLink")}
</Link>
.
</p>
@@ -81,8 +83,8 @@ function AcceptInviteInner() {
return (
<AuthShell
subtitle="Join the clinic you were invited to on temetro."
title="Accept your invitation"
subtitle={t("auth.acceptInvite.acceptSubtitle")}
title={t("auth.acceptInvite.acceptTitle")}
>
<div className="flex flex-col gap-4">
{error && <FormAlert>{error}</FormAlert>}
@@ -92,7 +94,9 @@ function AcceptInviteInner() {
onClick={accept}
type="button"
>
{accepting ? "Joining…" : "Accept invitation"}
{accepting
? t("auth.acceptInvite.accepting")
: t("auth.acceptInvite.accept")}
</Button>
</div>
</AuthShell>
+17 -11
View File
@@ -2,6 +2,7 @@
import Link from "next/link";
import { type FormEvent, useState } from "react";
import { useTranslation } from "react-i18next";
import { AuthShell, Field, FormAlert } from "@/components/auth/auth-ui";
import { Button } from "@/components/ui/button";
@@ -10,6 +11,7 @@ import { authClient } from "@/lib/auth-client";
import { notify } from "@/lib/toast";
export default function ForgotPasswordPage() {
const { t } = useTranslation();
const [email, setEmail] = useState("");
const [sent, setSent] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -28,12 +30,15 @@ export default function ForgotPasswordPage() {
setSubmitting(false);
if (err) {
const message = err.message ?? "Could not send the reset email.";
const message = err.message ?? t("auth.forgotPassword.error");
setError(message);
notify.error("Couldn't send reset link", message);
notify.error(t("auth.forgotPassword.failedToastTitle"), message);
return;
}
notify.success("Reset link sent", "Check your inbox for the link.");
notify.success(
t("auth.forgotPassword.sentToastTitle"),
t("auth.forgotPassword.sentToastBody"),
);
setSent(true);
};
@@ -41,33 +46,34 @@ export default function ForgotPasswordPage() {
<AuthShell
footer={
<Link className="text-foreground hover:underline" href="/login">
Back to sign in
{t("common.backToSignIn")}
</Link>
}
subtitle="We'll email you a link to reset your password"
title="Reset your password"
subtitle={t("auth.forgotPassword.subtitle")}
title={t("auth.forgotPassword.title")}
>
{sent ? (
<FormAlert tone="success">
If an account exists for {email}, a reset link is on its way. Check
your inbox.
{t("auth.forgotPassword.sent", { email })}
</FormAlert>
) : (
<form className="flex flex-col gap-4" onSubmit={onSubmit}>
{error && <FormAlert>{error}</FormAlert>}
<Field htmlFor="email" label="Email">
<Field htmlFor="email" label={t("auth.forgotPassword.emailLabel")}>
<Input
autoComplete="email"
id="email"
onChange={(e) => setEmail(e.target.value)}
placeholder="you@clinic.org"
placeholder={t("auth.forgotPassword.emailPlaceholder")}
required
type="email"
value={email}
/>
</Field>
<Button className="mt-1 w-full" disabled={submitting} type="submit">
{submitting ? "Sending…" : "Send reset link"}
{submitting
? t("auth.forgotPassword.submitting")
: t("auth.forgotPassword.submit")}
</Button>
</form>
)}
+4 -2
View File
@@ -2,12 +2,14 @@
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { AuthShell } from "@/components/auth/auth-ui";
import { CreateClinicForm } from "@/components/clinic/create-clinic-form";
import { authClient } from "@/lib/auth-client";
export default function OnboardingPage() {
const { t } = useTranslation();
const router = useRouter();
const { data: session, isPending } = authClient.useSession();
@@ -20,8 +22,8 @@ export default function OnboardingPage() {
return (
<AuthShell
subtitle="Create your clinic to start organizing patient records"
title="Set up your clinic"
subtitle={t("auth.onboarding.subtitle")}
title={t("auth.onboarding.title")}
>
<CreateClinicForm onCreated={() => router.push("/")} />
</AuthShell>
+24 -16
View File
@@ -3,6 +3,7 @@
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { type FormEvent, Suspense, useState } from "react";
import { useTranslation } from "react-i18next";
import { AuthShell, Field, FormAlert } from "@/components/auth/auth-ui";
import { Button } from "@/components/ui/button";
@@ -13,6 +14,7 @@ import { notify } from "@/lib/toast";
const MIN_PASSWORD = 12;
function ResetPasswordInner() {
const { t } = useTranslation();
const router = useRouter();
const params = useSearchParams();
const token = params.get("token") ?? "";
@@ -28,15 +30,15 @@ function ResetPasswordInner() {
setError(null);
if (!token) {
setError("This reset link is invalid or has expired.");
setError(t("auth.resetPassword.invalidToken"));
return;
}
if (password.length < MIN_PASSWORD) {
setError(`Password must be at least ${MIN_PASSWORD} characters.`);
setError(t("auth.resetPassword.tooShort", { count: MIN_PASSWORD }));
return;
}
if (password !== confirm) {
setError("Passwords do not match.");
setError(t("auth.resetPassword.mismatch"));
return;
}
@@ -47,12 +49,15 @@ function ResetPasswordInner() {
});
setSubmitting(false);
if (err) {
const message = err.message ?? "Could not reset your password.";
const message = err.message ?? t("auth.resetPassword.error");
setError(message);
notify.error("Couldn't reset password", message);
notify.error(t("auth.resetPassword.failedToastTitle"), message);
return;
}
notify.success("Password updated", "Redirecting you to sign in…");
notify.success(
t("auth.resetPassword.successToastTitle"),
t("auth.resetPassword.successToastBody"),
);
setDone(true);
setTimeout(() => router.push("/login"), 1500);
};
@@ -61,23 +66,21 @@ function ResetPasswordInner() {
<AuthShell
footer={
<Link className="text-foreground hover:underline" href="/login">
Back to sign in
{t("common.backToSignIn")}
</Link>
}
subtitle="Choose a new password for your account"
title="Set a new password"
subtitle={t("auth.resetPassword.subtitle")}
title={t("auth.resetPassword.title")}
>
{done ? (
<FormAlert tone="success">
Your password has been reset. Redirecting you to sign in
</FormAlert>
<FormAlert tone="success">{t("auth.resetPassword.done")}</FormAlert>
) : (
<form className="flex flex-col gap-4" onSubmit={onSubmit}>
{error && <FormAlert>{error}</FormAlert>}
<Field
hint={`At least ${MIN_PASSWORD} characters.`}
hint={t("auth.resetPassword.passwordHint", { count: MIN_PASSWORD })}
htmlFor="password"
label="New password"
label={t("auth.resetPassword.passwordLabel")}
>
<Input
autoComplete="new-password"
@@ -89,7 +92,10 @@ function ResetPasswordInner() {
value={password}
/>
</Field>
<Field htmlFor="confirm" label="Confirm new password">
<Field
htmlFor="confirm"
label={t("auth.resetPassword.confirmLabel")}
>
<Input
autoComplete="new-password"
id="confirm"
@@ -101,7 +107,9 @@ function ResetPasswordInner() {
/>
</Field>
<Button className="mt-1 w-full" disabled={submitting} type="submit">
{submitting ? "Saving…" : "Reset password"}
{submitting
? t("auth.resetPassword.submitting")
: t("auth.resetPassword.submit")}
</Button>
</form>
)}
+11 -15
View File
@@ -3,12 +3,14 @@
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { AuthShell, FormAlert } from "@/components/auth/auth-ui";
import { Button } from "@/components/ui/button";
import { authClient } from "@/lib/auth-client";
function VerifyEmailInner() {
const { t } = useTranslation();
const router = useRouter();
const params = useSearchParams();
const email = params.get("email") ?? "";
@@ -34,31 +36,25 @@ function VerifyEmailInner() {
});
setSending(false);
if (err) {
setError(err.message ?? "Could not resend the verification email.");
setError(err.message ?? t("auth.verifyEmail.resendError"));
return;
}
setNotice("Verification email sent. Check your inbox.");
setNotice(t("auth.verifyEmail.resent"));
};
return (
<AuthShell
footer={
<Link className="text-foreground hover:underline" href="/login">
Back to sign in
{t("common.backToSignIn")}
</Link>
}
subtitle={
email ? (
<>
We sent a verification link to{" "}
<span className="text-foreground">{email}</span>. Open it to activate
your account.
</>
) : (
"Open the verification link we emailed you to activate your account."
)
email
? t("auth.verifyEmail.subtitle", { email })
: t("auth.verifyEmail.subtitleNoEmail")
}
title="Check your inbox"
title={t("auth.verifyEmail.title")}
>
<div className="flex flex-col gap-4">
{notice && <FormAlert tone="success">{notice}</FormAlert>}
@@ -68,7 +64,7 @@ function VerifyEmailInner() {
onClick={() => router.push("/")}
type="button"
>
I&apos;ve verified continue
{t("auth.verifyEmail.continue")}
</Button>
{email && (
<Button
@@ -78,7 +74,7 @@ function VerifyEmailInner() {
type="button"
variant="outline"
>
{sending ? "Sending" : "Resend email"}
{sending ? t("auth.verifyEmail.resending") : t("auth.verifyEmail.resend")}
</Button>
)}
</div>