I18n auth (#23)

* New translation keys in en-US locale

* New translation keys in de-DE locale

* New translation keys in fr-FR locale

* New translation keys in it-IT locale

* New translation keys in pl-PL locale

* New translation keys in pt-PT locale

* New translation keys in tr-TR locale

* Add translation keys in app/auth

* Fix build

---------

Co-authored-by: Lokowitz <marvinlokowitz@gmail.com>
This commit is contained in:
vlalx
2025-05-17 19:11:56 +03:00
committed by GitHub
parent d2d84be99a
commit b8ed5ac1c5
23 changed files with 727 additions and 115 deletions

View File

@@ -311,7 +311,7 @@ export default function CreateShareLinkForm({
<CommandInput placeholder={t('resourceSearch')} />
<CommandList>
<CommandEmpty>
{t('resourceNotFound')}
{t('resourcesNotFound')}
</CommandEmpty>
<CommandGroup>
{resources.map(

View File

@@ -57,9 +57,11 @@ const createSiteFormSchema = z.object({
.string()
.min(2, {
message: "Name must be at least 2 characters."
message: "Name must be at least 2 characters."
})
.max(30, {
message: "Name must not be longer than 30 characters."
message: "Name must not be longer than 30 characters."
}),
method: z.enum(["wireguard", "newt", "local"])
});
@@ -273,6 +275,8 @@ PersistentKeepalive = 5`
const newtConfigDockerRun = `docker run -it fosrl/newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${env.app.dashboardUrl}`;
const t = useTranslations();
return loadingPage ? (
<LoaderPlaceholder height="300px" />
) : (

View File

@@ -16,6 +16,7 @@ import {
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Loader2, CheckCircle2, AlertCircle } from "lucide-react";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { useTranslations } from "next-intl";
type ValidateOidcTokenParams = {
orgId: string;
@@ -36,11 +37,13 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
const { licenseStatus, isLicenseViolation } = useLicenseStatusContext();
const t = useTranslations();
useEffect(() => {
async function validate() {
setLoading(true);
console.log("Validating OIDC token", {
console.log(t('idpOidcTokenValidating'), {
code: props.code,
expectedState: props.expectedState,
stateCookie: props.stateCookie
@@ -59,7 +62,7 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
storedState: props.stateCookie
});
console.log("Validate OIDC token response", res.data);
console.log(t('idpOidcTokenResponse'), res.data);
const redirectUrl = res.data.data.redirectUrl;
@@ -76,7 +79,7 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
router.push(res.data.data.redirectUrl);
}
} catch (e) {
setError(formatAxiosError(e, "Error validating OIDC token"));
setError(formatAxiosError(e, t('idpErrorOidcTokenValidating')));
} finally {
setLoading(false);
}
@@ -89,20 +92,20 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
<div className="flex items-center justify-center min-h-screen">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Connecting to {props.idp.name}</CardTitle>
<CardDescription>Validating your identity</CardDescription>
<CardTitle>{t('idpConnectingTo', {name: props.idp.name})}</CardTitle>
<CardDescription>{t('idpConnectingToDescription')}</CardDescription>
</CardHeader>
<CardContent className="flex flex-col items-center space-y-4">
{loading && (
<div className="flex items-center space-x-2">
<Loader2 className="h-5 w-5 animate-spin" />
<span>Connecting...</span>
<span>{t('idpConnectingToProcess')}</span>
</div>
)}
{!loading && !error && (
<div className="flex items-center space-x-2 text-green-600">
<CheckCircle2 className="h-5 w-5" />
<span>Connected</span>
<span>{t('idpConnectingToFinished')}</span>
</div>
)}
{error && (
@@ -110,9 +113,7 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
<AlertCircle className="h-5 w-5" />
<AlertDescription className="flex flex-col space-y-2">
<span>
There was a problem connecting to{" "}
{props.idp.name}. Please contact your
administrator.
{t('idpErrorConnectingTo', {name: props.idp.name})}
</span>
<span className="text-xs">{error}</span>
</AlertDescription>

View File

@@ -3,6 +3,7 @@ import ValidateOidcToken from "./ValidateOidcToken";
import { idp } from "@server/db/schemas";
import db from "@server/db";
import { eq } from "drizzle-orm";
import { useTranslations } from "next-intl";
export default async function Page(props: {
params: Promise<{ orgId: string; idpId: string }>;
@@ -23,8 +24,10 @@ export default async function Page(props: {
.from(idp)
.where(eq(idp.idpId, parseInt(params.idpId!)));
const t = useTranslations();
if (!idpRes) {
return <div>IdP not found</div>;
return <div>{t('idpErrorNotFound')}</div>;
}
return (

View File

@@ -8,6 +8,7 @@ import { AxiosResponse } from "axios";
import { ExternalLink } from "lucide-react";
import { Metadata } from "next";
import { cache } from "react";
import { useTranslations } from "next-intl";
export const metadata: Metadata = {
title: `Auth - Pangolin`,
@@ -21,6 +22,7 @@ type AuthLayoutProps = {
export default async function AuthLayout({ children }: AuthLayoutProps) {
const getUser = cache(verifySession);
const user = await getUser();
const t = useTranslations();
const licenseStatusRes = await cache(
async () =>

View File

@@ -14,6 +14,7 @@ import { useRouter } from "next/navigation";
import { useEffect } from "react";
import Image from "next/image";
import { cleanRedirect } from "@app/lib/cleanRedirect";
import { useTranslations } from "next-intl";
type DashboardLoginFormProps = {
redirect?: string;
@@ -38,23 +39,25 @@ export default function DashboardLoginForm({
// logout();
// });
const t = useTranslations();
return (
<Card className="w-full max-w-md">
<CardHeader>
<div className="flex flex-row items-center justify-center">
<Image
src={`/logo/pangolin_orange.svg`}
alt="Pangolin Logo"
alt={t('pangolinLogoAlt')}
width="100"
height="100"
/>
</div>
<div className="text-center space-y-1">
<h1 className="text-2xl font-bold mt-1">
Welcome to Pangolin
{t('welcome')}
</h1>
<p className="text-sm text-muted-foreground">
Log in to get started
{t('loginStart')}
</p>
</div>
</CardHeader>

View File

@@ -9,6 +9,7 @@ import { cleanRedirect } from "@app/lib/cleanRedirect";
import db from "@server/db";
import { idp } from "@server/db/schemas";
import { LoginFormIDP } from "@app/components/LoginForm";
import { useTranslations } from "next-intl";
export const dynamic = "force-dynamic";
@@ -40,6 +41,8 @@ export default async function Page(props: {
name: idp.name
})) as LoginFormIDP[];
const t = useTranslations();
return (
<>
{isInvite && (
@@ -47,11 +50,10 @@ export default async function Page(props: {
<div className="flex flex-col items-center">
<Mail className="w-12 h-12 mb-4 text-primary" />
<h2 className="text-2xl font-bold mb-2 text-center">
Looks like you've been invited!
{t('inviteAlready')}
</h2>
<p className="text-center">
To accept the invite, you must log in or create an
account.
{t('inviteAlreadyDescription')}
</p>
</div>
</div>
@@ -70,7 +72,7 @@ export default async function Page(props: {
}
className="underline"
>
Sign up
{t('signup')}
</Link>
</p>
)}

View File

@@ -44,6 +44,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
import { passwordSchema } from "@server/auth/passwordSchema";
import { cleanRedirect } from "@app/lib/cleanRedirect";
import { useTranslations } from "next-intl";
const requestSchema = z.object({
email: z.string().email()
@@ -122,6 +123,8 @@ export default function ResetPasswordForm({
}
});
const t = useTranslations();
async function onRequest(data: z.infer<typeof requestSchema>) {
const { email } = data;
@@ -200,9 +203,9 @@ export default function ResetPasswordForm({
<div>
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Reset Password</CardTitle>
<CardTitle>{t('passwordReset')}</CardTitle>
<CardDescription>
Follow the steps to reset your password
{t('passwordResetDescription')}
</CardDescription>
</CardHeader>
<CardContent>
@@ -221,14 +224,13 @@ export default function ResetPasswordForm({
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormLabel>{t('email')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
<FormDescription>
We'll send a password reset
code to this email address.
{t('passwordResetSent')}
</FormDescription>
</FormItem>
)}
@@ -249,7 +251,7 @@ export default function ResetPasswordForm({
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormLabel>{t('email')}</FormLabel>
<FormControl>
<Input
{...field}
@@ -268,7 +270,7 @@ export default function ResetPasswordForm({
render={({ field }) => (
<FormItem>
<FormLabel>
Reset Code
{t('passwordResetCode')}
</FormLabel>
<FormControl>
<Input
@@ -278,8 +280,7 @@ export default function ResetPasswordForm({
</FormControl>
<FormMessage />
<FormDescription>
Check your email for the
reset code.
{t('passwordResetCodeDescription')}
</FormDescription>
</FormItem>
)}
@@ -292,7 +293,7 @@ export default function ResetPasswordForm({
render={({ field }) => (
<FormItem>
<FormLabel>
New Password
{t('passwordNew')}
</FormLabel>
<FormControl>
<Input
@@ -310,7 +311,7 @@ export default function ResetPasswordForm({
render={({ field }) => (
<FormItem>
<FormLabel>
Confirm New Password
{t('passwordNewConfirm')}
</FormLabel>
<FormControl>
<Input
@@ -339,7 +340,7 @@ export default function ResetPasswordForm({
render={({ field }) => (
<FormItem>
<FormLabel>
Authenticator Code
{t('pincodeAuth')}
</FormLabel>
<FormControl>
<div className="flex justify-center">
@@ -407,8 +408,8 @@ export default function ResetPasswordForm({
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
{state === "reset"
? "Reset Password"
: "Submit Code"}
? t('passwordReset')
: t('pincodeSubmit2')}
</Button>
)}
@@ -422,7 +423,7 @@ export default function ResetPasswordForm({
{isSubmitting && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
Request Reset
{t('passwordResetSubmit')}
</Button>
)}
@@ -436,7 +437,7 @@ export default function ResetPasswordForm({
mfaForm.reset();
}}
>
Back to Password
{t('passwordBack')}
</Button>
)}
@@ -450,7 +451,7 @@ export default function ResetPasswordForm({
form.reset();
}}
>
Back to Email
{t('backToEmail')}
</Button>
)}
</div>

View File

@@ -4,6 +4,7 @@ import { cache } from "react";
import ResetPasswordForm from "./ResetPasswordForm";
import Link from "next/link";
import { cleanRedirect } from "@app/lib/cleanRedirect";
import { useTranslations } from "next-intl";
export const dynamic = "force-dynamic";
@@ -27,6 +28,8 @@ export default async function Page(props: {
redirectUrl = cleanRedirect(searchParams.redirect);
}
const t = useTranslations();
return (
<>
<ResetPasswordForm
@@ -44,7 +47,7 @@ export default async function Page(props: {
}
className="underline"
>
Go back to log in
{t('loginBack')}
</Link>
</p>
</>

View File

@@ -13,6 +13,7 @@ import { AuthWithAccessTokenResponse } from "@server/routers/resource";
import { AxiosResponse } from "axios";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
type AccessTokenProps = {
token: string;
@@ -29,6 +30,8 @@ export default function AccessToken({
const { env } = useEnvContext();
const api = createApiClient({ env });
const t = useTranslations();
function appendRequestToken(url: string, token: string) {
const fullUrl = new URL(url);
fullUrl.searchParams.append(
@@ -76,7 +79,7 @@ export default function AccessToken({
);
}
} catch (e) {
console.error("Error checking access token", e);
console.error(t('accessTokenError'), e);
} finally {
setLoading(false);
}
@@ -115,9 +118,9 @@ export default function AccessToken({
function renderTitle() {
if (isValid) {
return "Access Granted";
return t('accessGranted');
} else {
return "Access URL Invalid";
return t('accessUrlInvalid');
}
}
@@ -125,18 +128,16 @@ export default function AccessToken({
if (isValid) {
return (
<div>
You have been granted access to this resource. Redirecting
you...
{t('accessGrantedDescription')}
</div>
);
} else {
return (
<div>
This shared access URL is invalid. Please contact the
resource owner for a new URL.
{t('accessUrlInvalidDescription')}
<div className="text-center mt-4">
<Button>
<Link href="/">Go Home</Link>
<Link href="/">{t('goHome')}</Link>
</Button>
</div>
</div>

View File

@@ -9,21 +9,23 @@ import {
CardTitle,
} from "@app/components/ui/card";
import Link from "next/link";
import { useTranslations } from "next-intl";
export default function ResourceAccessDenied() {
const t = useTranslations();
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="text-center text-2xl font-bold">
Access Denied
{t('accessDenied')}
</CardTitle>
</CardHeader>
<CardContent>
You're not allowed to access this resource. If this is a mistake,
please contact the administrator.
{t('accessDeniedDescription')}
<div className="text-center mt-4">
<Button>
<Link href="/">Go Home</Link>
<Link href="/">{t('goHome')}</Link>
</Button>
</div>
</CardContent>

View File

@@ -44,6 +44,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast";
import Link from "next/link";
import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext";
import { useTranslations } from "next-intl";
const pinSchema = z.object({
pin: z
@@ -170,6 +171,8 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
return fullUrl.toString();
}
const t = useTranslations();
const onWhitelistSubmit = (values: any) => {
setLoadingLogin(true);
api.post<AxiosResponse<AuthWithWhitelistResponse>>(
@@ -183,8 +186,8 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
setOtpState("otp_sent");
submitOtpForm.setValue("email", values.email);
toast({
title: "OTP Sent",
description: "An OTP has been sent to your email"
title: t('otpEmailSent'),
description: t('otpEmailSentDescription')
});
return;
}
@@ -200,7 +203,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
.catch((e) => {
console.error(e);
setWhitelistError(
formatAxiosError(e, "Failed to authenticate with email")
formatAxiosError(e, t('otpEmailErrorAuthenticate'))
);
})
.then(() => setLoadingLogin(false));
@@ -225,7 +228,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
.catch((e) => {
console.error(e);
setPincodeError(
formatAxiosError(e, "Failed to authenticate with pincode")
formatAxiosError(e, t('pincodeErrorAuthenticate'))
);
})
.then(() => setLoadingLogin(false));
@@ -253,7 +256,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
.catch((e) => {
console.error(e);
setPasswordError(
formatAxiosError(e, "Failed to authenticate with password")
formatAxiosError(e, t('passwordErrorAuthenticate'))
);
})
.finally(() => setLoadingLogin(false));
@@ -280,7 +283,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
<div>
<div className="text-center mb-2">
<span className="text-sm text-muted-foreground">
Powered by{" "}
{t('poweredBy')}{" "}
<Link
href="https://github.com/fosrl/pangolin"
target="_blank"
@@ -293,11 +296,11 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
</div>
<Card>
<CardHeader>
<CardTitle>Authentication Required</CardTitle>
<CardTitle>{t('authenticationRequired')}</CardTitle>
<CardDescription>
{numMethods > 1
? `Choose your preferred method to access ${props.resource.name}`
: `You must authenticate to access ${props.resource.name}`}
? t('authenticationMethodChoose', {name: props.resource.name})
: t('authenticationRequest', {name: props.resource.name})}
</CardDescription>
</CardHeader>
<CardContent>
@@ -327,19 +330,19 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
{props.methods.password && (
<TabsTrigger value="password">
<Key className="w-4 h-4 mr-1" />{" "}
Password
{t('password')}
</TabsTrigger>
)}
{props.methods.sso && (
<TabsTrigger value="sso">
<User className="w-4 h-4 mr-1" />{" "}
User
{t('user')}
</TabsTrigger>
)}
{props.methods.whitelist && (
<TabsTrigger value="whitelist">
<AtSign className="w-4 h-4 mr-1" />{" "}
Email
{t('email')}
</TabsTrigger>
)}
</TabsList>
@@ -362,7 +365,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
render={({ field }) => (
<FormItem>
<FormLabel>
6-digit PIN Code
{t('pincodeInput')}
</FormLabel>
<FormControl>
<div className="flex justify-center">
@@ -431,7 +434,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
disabled={loadingLogin}
>
<LockIcon className="w-4 h-4 mr-2" />
Log in with PIN
{t('pincodeSubmit')}
</Button>
</form>
</Form>
@@ -457,7 +460,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
render={({ field }) => (
<FormItem>
<FormLabel>
Password
{t('password')}
</FormLabel>
<FormControl>
<Input
@@ -485,7 +488,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
disabled={loadingLogin}
>
<LockIcon className="w-4 h-4 mr-2" />
Log In with Password
{t('passwordSubmit')}
</Button>
</form>
</Form>
@@ -526,7 +529,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
render={({ field }) => (
<FormItem>
<FormLabel>
Email
{t('email')}
</FormLabel>
<FormControl>
<Input
@@ -535,10 +538,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
/>
</FormControl>
<FormDescription>
A one-time
code will be
sent to this
email.
{t('otpEmailDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -560,7 +560,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
disabled={loadingLogin}
>
<Send className="w-4 h-4 mr-2" />
Send One-time Code
{t('otpEmailSend')}
</Button>
</form>
</Form>
@@ -582,9 +582,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
render={({ field }) => (
<FormItem>
<FormLabel>
One-Time
Password
(OTP)
{t('otpEmail')}
</FormLabel>
<FormControl>
<Input
@@ -612,7 +610,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
disabled={loadingLogin}
>
<LockIcon className="w-4 h-4 mr-2" />
Submit OTP
{t('otpEmailSubmit')}
</Button>
<Button
@@ -624,7 +622,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
submitOtpForm.reset();
}}
>
Back to Email
{t('backToEmail')}
</Button>
</form>
</Form>
@@ -637,9 +635,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
{supporterStatus?.visible && (
<div className="text-center mt-2">
<span className="text-sm text-muted-foreground opacity-50">
Server is running without a supporter key.
<br />
Consider supporting the project!
{t('noSupportKey')}
</span>
</div>
)}

View File

@@ -7,20 +7,23 @@ import {
CardTitle,
} from "@app/components/ui/card";
import Link from "next/link";
import { useTranslations } from "next-intl";
export default async function ResourceNotFound() {
const t = useTranslations();
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="text-center text-2xl font-bold">
Resource Not Found
{t('resourceNotFound')}
</CardTitle>
</CardHeader>
<CardContent>
The resource you're trying to access does not exist.
{t('resourceNotFoundDescription')}
<div className="text-center mt-4">
<Button>
<Link href="/">Go Home</Link>
<Link href="/">{t('goHome')}</Link>
</Button>
</div>
</CardContent>

View File

@@ -71,6 +71,8 @@ export default function SignupForm({
}
});
const t = useTranslations();
async function onSubmit(values: z.infer<typeof formSchema>) {
const { email, password } = values;
@@ -113,15 +115,13 @@ export default function SignupForm({
setLoading(false);
}
const t = useTranslations();
return (
<Card className="w-full max-w-md">
<CardHeader>
<div className="flex flex-row items-center justify-center">
<Image
src={`/logo/pangolin_orange.svg`}
alt="Pangolin Logo"
alt={t('pangolinLogoAlt')}
width="100"
height="100"
/>

View File

@@ -6,6 +6,7 @@ import { Mail } from "lucide-react";
import Link from "next/link";
import { redirect } from "next/navigation";
import { cache } from "react";
import { useTranslations } from "next-intl";
export const dynamic = "force-dynamic";
@@ -20,6 +21,8 @@ export default async function Page(props: {
const isInvite = searchParams?.redirect?.includes("/invite");
const t = useTranslations();
if (env.flags.disableSignupWithoutInvite && !isInvite) {
redirect("/");
}
@@ -54,11 +57,10 @@ export default async function Page(props: {
<div className="flex flex-col items-center">
<Mail className="w-12 h-12 mb-4 text-primary" />
<h2 className="text-2xl font-bold mb-2 text-center">
Looks like you've been invited!
{t('inviteAlready')}
</h2>
<p className="text-center">
To accept the invite, you must log in or create an
account.
{t('inviteAlreadyDescription')}
</p>
</div>
</div>
@@ -71,7 +73,7 @@ export default async function Page(props: {
/>
<p className="text-center text-muted-foreground mt-4">
Already have an account?{" "}
{t('signupQuestion')}{" "}
<Link
href={
!redirectUrl
@@ -80,7 +82,7 @@ export default async function Page(props: {
}
className="underline"
>
Log in
{t('login')}
</Link>
</p>
</>

View File

@@ -37,6 +37,7 @@ import { formatAxiosError } from "@app/lib/api";;
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { cleanRedirect } from "@app/lib/cleanRedirect";
import { useTranslations } from "next-intl";
const FormSchema = z.object({
email: z.string().email({ message: "Invalid email address" }),
@@ -71,6 +72,8 @@ export default function VerifyEmailForm({
},
});
const t = useTranslations();
async function onSubmit(data: z.infer<typeof FormSchema>) {
setIsSubmitting(true);
@@ -114,8 +117,7 @@ export default function VerifyEmailForm({
toast({
variant: "default",
title: "Verification code resent",
description:
"We've resent a verification code to your email address. Please check your inbox.",
description: "We've resent a verification code to your email address. Please check your inbox.",
});
}
@@ -126,9 +128,9 @@ export default function VerifyEmailForm({
<div>
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Verify Email</CardTitle>
<CardTitle>{t('emailVerify')}</CardTitle>
<CardDescription>
Enter the verification code sent to your email address.
{t('emailVerifyDescription')}
</CardDescription>
</CardHeader>
<CardContent>
@@ -142,7 +144,7 @@ export default function VerifyEmailForm({
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormLabel>{t('email')}</FormLabel>
<FormControl>
<Input
{...field}
@@ -159,7 +161,7 @@ export default function VerifyEmailForm({
name="pin"
render={({ field }) => (
<FormItem>
<FormLabel>Verification Code</FormLabel>
<FormLabel>{t('verificationCode')}</FormLabel>
<FormControl>
<div className="flex justify-center">
<InputOTP
@@ -197,8 +199,7 @@ export default function VerifyEmailForm({
</FormControl>
<FormMessage />
<FormDescription>
We sent a verification code to your
email address.
{t('verificationCodeEmailSent')}
</FormDescription>
</FormItem>
)}
@@ -226,7 +227,7 @@ export default function VerifyEmailForm({
{isSubmitting && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
Submit
{t('emailVerifySubmit')}
</Button>
</form>
</Form>
@@ -241,8 +242,8 @@ export default function VerifyEmailForm({
disabled={isResending}
>
{isResending
? "Resending..."
: "Didn't receive a code? Click here to resend"}
? t('emailVerifyResendProgress')
: t('emailVerifyResend')}
</Button>
</div>
</div>