mirror of
https://github.com/temetro/temetro.git
synced 2026-08-19 14:46:29 +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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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'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>
|
||||
|
||||
Reference in New Issue
Block a user