refactor(i18n): replace hardcoded texts with i18n message keys for auth

This commit is contained in:
cfngc4594 2025-04-16 00:40:36 +08:00
parent 0b3086f333
commit 1d0b74136f
10 changed files with 141 additions and 34 deletions

View File

@ -25,6 +25,27 @@
"open": "Open Bot", "open": "Open Bot",
"close": "Close Bot" "close": "Close Bot"
}, },
"CredentialsSignInForm": {
"email": "Email",
"password": "Password",
"signIn": "Sign In",
"signingIn": "Signing In...",
"signInSuccess": "Signed In Successfully",
"signInFailed": "Sign In Failed",
"showPassword": "Show password",
"hidePassword": "Hide password"
},
"CredentialsSignUpForm": {
"email": "Email",
"password": "Password",
"signUp": "Sign Up",
"creatingAccount": "Creating Account...",
"signUpSuccess": "Account Created",
"signUpSuccessDescription": "You can now sign in with your credentials",
"signUpFailed": "Registration Failed",
"showPassword": "Show password",
"hidePassword": "Hide password"
},
"DetailsPage": { "DetailsPage": {
"BackButton": "All Submissions", "BackButton": "All Submissions",
"Time": "Submitted on", "Time": "Submitted on",
@ -38,6 +59,7 @@
"MEDIUM": "MEDIUM", "MEDIUM": "MEDIUM",
"HARD": "HARD" "HARD": "HARD"
}, },
"GithubSignInForm": "Continue with GitHub",
"LanguageSettings": { "LanguageSettings": {
"en": { "en": {
"flag": "🇺🇸", "flag": "🇺🇸",
@ -82,6 +104,30 @@
"Advanced": "Advanced" "Advanced": "Advanced"
} }
}, },
"SignInForm": {
"title": "Sign in to your account",
"description": "Enter your email below to sign in to your account",
"or": "Or",
"noAccount": "Don't have an account?",
"signUp": "Sign up"
},
"signInWithCredentials": {
"userNotFound": "User not found.",
"invalidCredentials": "Invalid credentials.",
"incorrectPassword": "Incorrect password.",
"signInFailedFallback": "Failed to sign in. Please try again."
},
"signUpWithCredentials": {
"userAlreadyExists": "User already exists.",
"registrationFailedFallback": "Registration failed. Please try again."
},
"SignUpForm": {
"title": "Sign up to your account",
"description": "Enter your email below to sign up to your account",
"or": "Or",
"haveAccount": "Already have an account?",
"signIn": "Sign in"
},
"StatusMessage": { "StatusMessage": {
"PD": "Pending", "PD": "Pending",
"QD": "Queued", "QD": "Queued",

View File

@ -25,6 +25,27 @@
"open": "打开AI助手", "open": "打开AI助手",
"close": "关闭AI助手" "close": "关闭AI助手"
}, },
"CredentialsSignInForm": {
"email": "邮箱",
"password": "密码",
"signIn": "登录",
"signingIn": "正在登录...",
"signInSuccess": "登录成功",
"signInFailed": "登录失败",
"showPassword": "显示密码",
"hidePassword": "隐藏密码"
},
"CredentialsSignUpForm": {
"email": "邮箱",
"password": "密码",
"signUp": "注册",
"creatingAccount": "正在创建账户...",
"signUpSuccess": "账户创建成功",
"signUpSuccessDescription": "你现在可以使用凭据登录",
"signUpFailed": "注册失败",
"showPassword": "显示密码",
"hidePassword": "隐藏密码"
},
"DetailsPage": { "DetailsPage": {
"BackButton": "所有提交记录", "BackButton": "所有提交记录",
"Time": "提交于", "Time": "提交于",
@ -38,6 +59,7 @@
"MEDIUM": "中等", "MEDIUM": "中等",
"HARD": "困难" "HARD": "困难"
}, },
"GithubSignInForm": "使用 GitHub 登录",
"LanguageSettings": { "LanguageSettings": {
"en": { "en": {
"flag": "🇺🇸", "flag": "🇺🇸",
@ -82,6 +104,30 @@
"Advanced": "高级设置" "Advanced": "高级设置"
} }
}, },
"SignInForm": {
"title": "登录到你的账户",
"description": "请输入你的邮箱以登录账户",
"or": "或者",
"noAccount": "还没有账户?",
"signUp": "注册"
},
"signInWithCredentials": {
"userNotFound": "未找到用户。",
"invalidCredentials": "凭据无效。",
"incorrectPassword": "密码错误。",
"signInFailedFallback": "登录失败,请重试。"
},
"signUpWithCredentials": {
"userAlreadyExists": "用户已存在。",
"registrationFailedFallback": "注册失败,请重试。"
},
"SignUpForm": {
"title": "注册你的账户",
"description": "请输入你的邮箱以注册账户",
"or": "或者",
"haveAccount": "已经有账户了?",
"signIn": "登录"
},
"StatusMessage": { "StatusMessage": {
"PD": "待处理", "PD": "待处理",
"QD": "排队中", "QD": "排队中",

View File

@ -4,12 +4,15 @@ import bcrypt from "bcrypt";
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { signIn } from "@/lib/auth"; import { signIn } from "@/lib/auth";
import { authSchema } from "@/lib/zod"; import { authSchema } from "@/lib/zod";
import { getTranslations } from "next-intl/server";
import { CredentialsSignInFormValues } from "@/components/credentials-sign-in-form"; import { CredentialsSignInFormValues } from "@/components/credentials-sign-in-form";
import { CredentialsSignUpFormValues } from "@/components/credentials-sign-up-form"; import { CredentialsSignUpFormValues } from "@/components/credentials-sign-up-form";
const saltRounds = 10; const saltRounds = 10;
export async function signInWithCredentials(formData: CredentialsSignInFormValues, redirectTo?: string) { export async function signInWithCredentials(formData: CredentialsSignInFormValues, redirectTo?: string) {
const t = await getTranslations("signInWithCredentials");
try { try {
// Parse credentials using authSchema for validation // Parse credentials using authSchema for validation
const { email, password } = await authSchema.parseAsync(formData); const { email, password } = await authSchema.parseAsync(formData);
@ -19,35 +22,37 @@ export async function signInWithCredentials(formData: CredentialsSignInFormValue
// Check if the user exists // Check if the user exists
if (!user) { if (!user) {
throw new Error("User not found."); throw new Error(t("userNotFound"));
} }
// Check if the user has a password // Check if the user has a password
if (!user.password) { if (!user.password) {
throw new Error("Invalid credentials."); throw new Error(t("invalidCredentials"));
} }
// Check if the password matches // Check if the password matches
const passwordMatch = await bcrypt.compare(password, user.password); const passwordMatch = await bcrypt.compare(password, user.password);
if (!passwordMatch) { if (!passwordMatch) {
throw new Error("Incorrect password."); throw new Error(t("incorrectPassword"));
} }
await signIn("credentials", { ...formData, redirectTo, redirect: !!redirectTo }); await signIn("credentials", { ...formData, redirectTo, redirect: !!redirectTo });
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
return { error: error instanceof Error ? error.message : "Failed to sign in. Please try again." }; return { error: error instanceof Error ? error.message : t("signInFailedFallback") };
} }
} }
export async function signUpWithCredentials(formData: CredentialsSignUpFormValues) { export async function signUpWithCredentials(formData: CredentialsSignUpFormValues) {
const t = await getTranslations("signUpWithCredentials");
try { try {
const validatedData = await authSchema.parseAsync(formData); const validatedData = await authSchema.parseAsync(formData);
// Check if user already exists // Check if user already exists
const existingUser = await prisma.user.findUnique({ where: { email: validatedData.email } }); const existingUser = await prisma.user.findUnique({ where: { email: validatedData.email } });
if (existingUser) { if (existingUser) {
throw new Error("User already exists"); throw new Error(t("userAlreadyExists"));
} }
// Hash password and create user // Hash password and create user
@ -64,7 +69,7 @@ export async function signUpWithCredentials(formData: CredentialsSignUpFormValue
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
return { error: error instanceof Error ? error.message : "Registration failed. Please try again." }; return { error: error instanceof Error ? error.message : t("registrationFailedFallback") };
} }
} }

View File

@ -1,4 +1,3 @@
import { LogOut } from "lucide-react";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -8,6 +7,7 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { LogOutIcon } from "lucide-react";
import { auth, signOut } from "@/lib/auth"; import { auth, signOut } from "@/lib/auth";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
@ -65,7 +65,7 @@ export async function AvatarButton() {
<DropdownMenuGroup> <DropdownMenuGroup>
<SettingsButton /> <SettingsButton />
<DropdownMenuItem onClick={handleLogOut}> <DropdownMenuItem onClick={handleLogOut}>
<LogOut /> <LogOutIcon />
{t("LogOut")} {t("LogOut")}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuGroup> </DropdownMenuGroup>

View File

@ -12,6 +12,7 @@ import {
import { toast } from "sonner"; import { toast } from "sonner";
import { authSchema } from "@/lib/zod"; import { authSchema } from "@/lib/zod";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useTranslations } from "next-intl";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useState, useTransition } from "react"; import { useState, useTransition } from "react";
@ -26,6 +27,7 @@ export function CredentialsSignInForm() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const redirectTo = searchParams.get("redirectTo"); const redirectTo = searchParams.get("redirectTo");
const t = useTranslations("CredentialsSignInForm");
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
@ -44,11 +46,11 @@ export function CredentialsSignInForm() {
const result = await signInWithCredentials(data); const result = await signInWithCredentials(data);
if (result?.error) { if (result?.error) {
toast.error("Sign In Failed", { toast.error(t("signInFailed"), {
description: result.error, description: result.error,
}); });
} else { } else {
toast.success("Signed In Successfully"); toast.success(t("signInSuccess"));
router.push(redirectTo || "/"); router.push(redirectTo || "/");
} }
}); });
@ -62,7 +64,7 @@ export function CredentialsSignInForm() {
name="email" name="email"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Email</FormLabel> <FormLabel>{t("email")}</FormLabel>
<FormControl> <FormControl>
<div className="relative"> <div className="relative">
<Input className="peer pe-9" {...field} /> <Input className="peer pe-9" {...field} />
@ -81,7 +83,7 @@ export function CredentialsSignInForm() {
name="password" name="password"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Password</FormLabel> <FormLabel>{t("password")}</FormLabel>
<FormControl> <FormControl>
<div className="relative"> <div className="relative">
<Input <Input
@ -93,7 +95,7 @@ export function CredentialsSignInForm() {
className="text-muted-foreground/80 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 absolute inset-y-0 end-0 flex h-full w-9 items-center justify-center rounded-e-md transition-[color,box-shadow] outline-none focus:z-10 focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50" className="text-muted-foreground/80 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 absolute inset-y-0 end-0 flex h-full w-9 items-center justify-center rounded-e-md transition-[color,box-shadow] outline-none focus:z-10 focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
type="button" type="button"
onClick={toggleVisibility} onClick={toggleVisibility}
aria-label={isVisible ? "Hide password" : "Show password"} aria-label={isVisible ? t("hidePassword") : t("showPassword")}
aria-pressed={isVisible} aria-pressed={isVisible}
aria-controls="password" aria-controls="password"
> >
@ -111,7 +113,7 @@ export function CredentialsSignInForm() {
/> />
<Button type="submit" disabled={isPending} className="w-full"> <Button type="submit" disabled={isPending} className="w-full">
{isPending ? "Signing In..." : "Sign In"} {isPending ? t("signingIn") : t("signIn")}
</Button> </Button>
</form> </form>
</Form> </Form>

View File

@ -12,6 +12,7 @@ import {
import { toast } from "sonner"; import { toast } from "sonner";
import { authSchema } from "@/lib/zod"; import { authSchema } from "@/lib/zod";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useTranslations } from "next-intl";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useState, useTransition } from "react"; import { useState, useTransition } from "react";
@ -26,6 +27,7 @@ export function CredentialsSignUpForm() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const redirectTo = searchParams.get("redirectTo"); const redirectTo = searchParams.get("redirectTo");
const t = useTranslations("CredentialsSignUpForm");
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
@ -44,12 +46,12 @@ export function CredentialsSignUpForm() {
const result = await signUpWithCredentials(data); const result = await signUpWithCredentials(data);
if (result?.error) { if (result?.error) {
toast.error("Registration Failed", { toast.error(t("signUpFailed"), {
description: result.error, description: result.error,
}); });
} else { } else {
toast.success("Account Created", { toast.success(t("signUpSuccess"), {
description: "You can now sign in with your credentials", description: t("signUpSuccessDescription"),
}); });
router.push(`/sign-in?${redirectTo}`) router.push(`/sign-in?${redirectTo}`)
} }
@ -64,7 +66,7 @@ export function CredentialsSignUpForm() {
name="email" name="email"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Email</FormLabel> <FormLabel>{t("email")}</FormLabel>
<FormControl> <FormControl>
<div className="relative"> <div className="relative">
<Input className="peer pe-9" {...field} /> <Input className="peer pe-9" {...field} />
@ -83,7 +85,7 @@ export function CredentialsSignUpForm() {
name="password" name="password"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Password</FormLabel> <FormLabel>{t("password")}</FormLabel>
<FormControl> <FormControl>
<div className="relative"> <div className="relative">
<Input <Input
@ -95,7 +97,7 @@ export function CredentialsSignUpForm() {
className="text-muted-foreground/80 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 absolute inset-y-0 end-0 flex h-full w-9 items-center justify-center rounded-e-md transition-[color,box-shadow] outline-none focus:z-10 focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50" className="text-muted-foreground/80 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 absolute inset-y-0 end-0 flex h-full w-9 items-center justify-center rounded-e-md transition-[color,box-shadow] outline-none focus:z-10 focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
type="button" type="button"
onClick={toggleVisibility} onClick={toggleVisibility}
aria-label={isVisible ? "Hide password" : "Show password"} aria-label={isVisible ? t("hidePassword") : t("showPassword")}
aria-pressed={isVisible} aria-pressed={isVisible}
aria-controls="password" aria-controls="password"
> >
@ -113,7 +115,7 @@ export function CredentialsSignUpForm() {
/> />
<Button type="submit" disabled={isPending} className="w-full"> <Button type="submit" disabled={isPending} className="w-full">
{isPending ? "Creating Account..." : "Sign Up"} {isPending ? t("creatingAccount") : t("signUp")}
</Button> </Button>
</form> </form>
</Form> </Form>

View File

@ -1,10 +1,12 @@
"use client"; "use client";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { signInWithGithub } from "@/actions/auth"; import { signInWithGithub } from "@/actions/auth";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
export function GithubSignInForm() { export function GithubSignInForm() {
const t = useTranslations();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const redirectTo = searchParams.get("redirectTo"); const redirectTo = searchParams.get("redirectTo");
const signInAction = signInWithGithub.bind(null, redirectTo || "/"); const signInAction = signInWithGithub.bind(null, redirectTo || "/");
@ -18,7 +20,7 @@ export function GithubSignInForm() {
fill="currentColor" fill="currentColor"
/> />
</svg> </svg>
Continue with GitHub {t("GithubSignInForm")}
</Button> </Button>
</form> </form>
); );

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { Settings } from "lucide-react"; import { SettingsIcon } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useSettingsStore } from "@/stores/useSettingsStore"; import { useSettingsStore } from "@/stores/useSettingsStore";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
@ -11,7 +11,7 @@ export function SettingsButton() {
return ( return (
<DropdownMenuItem onClick={() => setDialogOpen(true)}> <DropdownMenuItem onClick={() => setDialogOpen(true)}>
<Settings /> <SettingsIcon />
{t("Settings")} {t("Settings")}
</DropdownMenuItem> </DropdownMenuItem>
); );

View File

@ -1,5 +1,6 @@
"use client"; "use client";
import { useTranslations } from "next-intl";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { GithubSignInForm } from "@/components/github-sign-in-form"; import { GithubSignInForm } from "@/components/github-sign-in-form";
import { CredentialsSignInForm } from "@/components/credentials-sign-in-form"; import { CredentialsSignInForm } from "@/components/credentials-sign-in-form";
@ -7,6 +8,7 @@ import { CredentialsSignInForm } from "@/components/credentials-sign-in-form";
export function SignInForm() { export function SignInForm() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const t = useTranslations("SignInForm");
const handleSignUp = () => { const handleSignUp = () => {
const params = new URLSearchParams(searchParams.toString()); const params = new URLSearchParams(searchParams.toString());
@ -16,25 +18,25 @@ export function SignInForm() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex flex-col items-center gap-2 text-center"> <div className="flex flex-col items-center gap-2 text-center">
<h1 className="text-2xl font-bold">Sign in to your account</h1> <h1 className="text-2xl font-bold">{t("title")}</h1>
<p className="text-balance text-sm text-muted-foreground"> <p className="text-balance text-sm text-muted-foreground">
Enter your email below to sign in to your account {t("description")}
</p> </p>
</div> </div>
<CredentialsSignInForm /> <CredentialsSignInForm />
<div className="relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t after:border-border"> <div className="relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t after:border-border">
<span className="relative z-10 bg-background px-2 text-muted-foreground"> <span className="relative z-10 bg-background px-2 text-muted-foreground">
Or {t("or")}
</span> </span>
</div> </div>
<GithubSignInForm /> <GithubSignInForm />
<div className="text-center text-sm"> <div className="text-center text-sm">
Don&apos;t have an account?{" "} {t("noAccount")}{" "}
<button <button
onClick={handleSignUp} onClick={handleSignUp}
className="underline underline-offset-4" className="underline underline-offset-4"
> >
Sign up {t("signUp")}
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
"use client"; "use client";
import { useTranslations } from "next-intl";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { GithubSignInForm } from "@/components/github-sign-in-form"; import { GithubSignInForm } from "@/components/github-sign-in-form";
import { CredentialsSignUpForm } from "@/components/credentials-sign-up-form"; import { CredentialsSignUpForm } from "@/components/credentials-sign-up-form";
@ -7,6 +8,7 @@ import { CredentialsSignUpForm } from "@/components/credentials-sign-up-form";
export function SignUpForm() { export function SignUpForm() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const t = useTranslations("SignUpForm");
const handleSignIn = () => { const handleSignIn = () => {
const params = new URLSearchParams(searchParams.toString()); const params = new URLSearchParams(searchParams.toString());
@ -16,25 +18,25 @@ export function SignUpForm() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex flex-col items-center gap-2 text-center"> <div className="flex flex-col items-center gap-2 text-center">
<h1 className="text-2xl font-bold">Sign up to your account</h1> <h1 className="text-2xl font-bold">{t("title")}</h1>
<p className="text-balance text-sm text-muted-foreground"> <p className="text-balance text-sm text-muted-foreground">
Enter your email below to sign up to your account {t("description")}
</p> </p>
</div> </div>
<CredentialsSignUpForm /> <CredentialsSignUpForm />
<div className="relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t after:border-border"> <div className="relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t after:border-border">
<span className="relative z-10 bg-background px-2 text-muted-foreground"> <span className="relative z-10 bg-background px-2 text-muted-foreground">
Or {t("or")}
</span> </span>
</div> </div>
<GithubSignInForm /> <GithubSignInForm />
<div className="text-center text-sm"> <div className="text-center text-sm">
Already have an account?{" "} {t("haveAccount")}{" "}
<button <button
onClick={handleSignIn} onClick={handleSignIn}
className="underline underline-offset-4" className="underline underline-offset-4"
> >
Sign in {t("signIn")}
</button> </button>
</div> </div>
</div> </div>