Fix PR feedback: Improve Profile section translations and UX

- Fixed password reset translations in Profile section
- Moved language selector from TopNavbar to Profile page
- Added profile.selectPreferredLanguage translation key
- Improved user experience for language preferences
This commit is contained in:
ZacharyZcR
2025-09-03 07:26:43 +08:00
parent 22779a3d03
commit 853d282d2f
5 changed files with 36 additions and 27 deletions

View File

@@ -609,7 +609,8 @@
"user": "User", "user": "User",
"authMethod": "Authentication Method", "authMethod": "Authentication Method",
"local": "Local", "local": "Local",
"external": "External (OIDC)" "external": "External (OIDC)",
"selectPreferredLanguage": "Select your preferred language for the interface"
}, },
"placeholders": { "placeholders": {
"enterCode": "000000", "enterCode": "000000",

View File

@@ -609,7 +609,8 @@
"user": "用户", "user": "用户",
"authMethod": "认证方式", "authMethod": "认证方式",
"local": "本地", "local": "本地",
"external": "外部 (OIDC)" "external": "外部 (OIDC)",
"selectPreferredLanguage": "选择您的界面首选语言"
}, },
"placeholders": { "placeholders": {
"enterCode": "000000", "enterCode": "000000",

View File

@@ -13,7 +13,6 @@ import {
import {Input} from "@/components/ui/input.tsx"; import {Input} from "@/components/ui/input.tsx";
import {Checkbox} from "@/components/ui/checkbox.tsx"; import {Checkbox} from "@/components/ui/checkbox.tsx";
import {Separator} from "@/components/ui/separator.tsx"; import {Separator} from "@/components/ui/separator.tsx";
import {LanguageSwitcher} from "@/components/LanguageSwitcher";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
interface TopNavbarProps { interface TopNavbarProps {
@@ -267,8 +266,6 @@ export function TopNavbar({isTopbarOpen, setIsTopbarOpen}: TopNavbarProps): Reac
</div> </div>
<div className="flex items-center justify-center gap-2 flex-1 px-2"> <div className="flex items-center justify-center gap-2 flex-1 px-2">
<LanguageSwitcher />
<Button <Button
variant="outline" variant="outline"
className="w-[30px] h-[30px]" className="w-[30px] h-[30px]"

View File

@@ -6,6 +6,7 @@ import {Label} from "@/components/ui/label.tsx";
import {Input} from "@/components/ui/input.tsx"; import {Input} from "@/components/ui/input.tsx";
import {Button} from "@/components/ui/button.tsx"; import {Button} from "@/components/ui/button.tsx";
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert.tsx"; import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert.tsx";
import {useTranslation} from "react-i18next";
interface PasswordResetProps { interface PasswordResetProps {
userInfo: { userInfo: {
@@ -17,6 +18,7 @@ interface PasswordResetProps {
} }
export function PasswordReset({userInfo}: PasswordResetProps) { export function PasswordReset({userInfo}: PasswordResetProps) {
const {t} = useTranslation();
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [resetStep, setResetStep] = useState<"initiate" | "verify" | "newPassword">("initiate"); const [resetStep, setResetStep] = useState<"initiate" | "verify" | "newPassword">("initiate");
@@ -35,7 +37,7 @@ export function PasswordReset({userInfo}: PasswordResetProps) {
setResetStep("verify"); setResetStep("verify");
setError(null); setError(null);
} catch (err: any) { } catch (err: any) {
setError(err?.response?.data?.error || err?.message || "Failed to initiate password reset"); setError(err?.response?.data?.error || err?.message || t('errors.failedPasswordReset'));
} finally { } finally {
setResetLoading(false); setResetLoading(false);
} }
@@ -60,7 +62,7 @@ export function PasswordReset({userInfo}: PasswordResetProps) {
setResetStep("newPassword"); setResetStep("newPassword");
setError(null); setError(null);
} catch (err: any) { } catch (err: any) {
setError(err?.response?.data?.error || "Failed to verify reset code"); setError(err?.response?.data?.error || t('errors.failedVerifyCode'));
} finally { } finally {
setResetLoading(false); setResetLoading(false);
} }
@@ -71,13 +73,13 @@ export function PasswordReset({userInfo}: PasswordResetProps) {
setResetLoading(true); setResetLoading(true);
if (newPassword !== confirmPassword) { if (newPassword !== confirmPassword) {
setError("Passwords do not match"); setError(t('errors.passwordMismatch'));
setResetLoading(false); setResetLoading(false);
return; return;
} }
if (newPassword.length < 6) { if (newPassword.length < 6) {
setError("Password must be at least 6 characters long"); setError(t('errors.weakPassword'));
setResetLoading(false); setResetLoading(false);
return; return;
} }
@@ -94,7 +96,7 @@ export function PasswordReset({userInfo}: PasswordResetProps) {
setResetSuccess(true); setResetSuccess(true);
} catch (err: any) { } catch (err: any) {
setError(err?.response?.data?.error || "Failed to complete password reset"); setError(err?.response?.data?.error || t('errors.failedCompleteReset'));
} finally { } finally {
setResetLoading(false); setResetLoading(false);
} }
@@ -115,7 +117,7 @@ export function PasswordReset({userInfo}: PasswordResetProps) {
Password Password
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
Change your account password {t('profile.changePassword')}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -129,7 +131,7 @@ export function PasswordReset({userInfo}: PasswordResetProps) {
disabled={resetLoading || !userInfo.username.trim()} disabled={resetLoading || !userInfo.username.trim()}
onClick={handleInitiatePasswordReset} onClick={handleInitiatePasswordReset}
> >
{resetLoading ? Spinner : "Send Reset Code"} {resetLoading ? Spinner : t('auth.sendResetCode')}
</Button> </Button>
</div> </div>
</> </>
@@ -138,12 +140,11 @@ export function PasswordReset({userInfo}: PasswordResetProps) {
{resetStep === "verify" && ( {resetStep === "verify" && (
<> <>
<div className="text-center text-muted-foreground mb-4"> <div className="text-center text-muted-foreground mb-4">
<p>Enter the 6-digit code from the docker container logs for <p>{t('auth.enterResetCode')}: <strong>{userInfo.username}</strong></p>
user: <strong>{userInfo.username}</strong></p>
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label htmlFor="reset-code">Reset Code</Label> <Label htmlFor="reset-code">{t('auth.resetCode')}</Label>
<Input <Input
id="reset-code" id="reset-code"
type="text" type="text"
@@ -162,7 +163,7 @@ export function PasswordReset({userInfo}: PasswordResetProps) {
disabled={resetLoading || resetCode.length !== 6} disabled={resetLoading || resetCode.length !== 6}
onClick={handleVerifyResetCode} onClick={handleVerifyResetCode}
> >
{resetLoading ? Spinner : "Verify Code"} {resetLoading ? Spinner : t('auth.verifyCode')}
</Button> </Button>
<Button <Button
type="button" type="button"
@@ -174,7 +175,7 @@ export function PasswordReset({userInfo}: PasswordResetProps) {
setResetCode(""); setResetCode("");
}} }}
> >
Back {t('common.back')}
</Button> </Button>
</div> </div>
</> </>
@@ -183,10 +184,9 @@ export function PasswordReset({userInfo}: PasswordResetProps) {
{resetSuccess && ( {resetSuccess && (
<> <>
<Alert className=""> <Alert className="">
<AlertTitle>Success!</AlertTitle> <AlertTitle>{t('auth.passwordResetSuccess')}</AlertTitle>
<AlertDescription> <AlertDescription>
Your password has been successfully reset! You can now log in {t('auth.passwordResetSuccessDesc')}
with your new password.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
</> </>
@@ -195,12 +195,11 @@ export function PasswordReset({userInfo}: PasswordResetProps) {
{resetStep === "newPassword" && !resetSuccess && ( {resetStep === "newPassword" && !resetSuccess && (
<> <>
<div className="text-center text-muted-foreground mb-4"> <div className="text-center text-muted-foreground mb-4">
<p>Enter your new password for <p>{t('auth.enterNewPassword')}: <strong>{userInfo.username}</strong></p>
user: <strong>{userInfo.username}</strong></p>
</div> </div>
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-5">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label htmlFor="new-password">New Password</Label> <Label htmlFor="new-password">{t('auth.newPassword')}</Label>
<Input <Input
id="new-password" id="new-password"
type="password" type="password"
@@ -213,7 +212,7 @@ export function PasswordReset({userInfo}: PasswordResetProps) {
/> />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label htmlFor="confirm-password">Confirm Password</Label> <Label htmlFor="confirm-password">{t('auth.confirmNewPassword')}</Label>
<Input <Input
id="confirm-password" id="confirm-password"
type="password" type="password"
@@ -231,7 +230,7 @@ export function PasswordReset({userInfo}: PasswordResetProps) {
disabled={resetLoading || !newPassword || !confirmPassword} disabled={resetLoading || !newPassword || !confirmPassword}
onClick={handleCompletePasswordReset} onClick={handleCompletePasswordReset}
> >
{resetLoading ? Spinner : "Reset Password"} {resetLoading ? Spinner : t('auth.resetPassword')}
</Button> </Button>
<Button <Button
type="button" type="button"
@@ -244,14 +243,14 @@ export function PasswordReset({userInfo}: PasswordResetProps) {
setConfirmPassword(""); setConfirmPassword("");
}} }}
> >
Back {t('common.back')}
</Button> </Button>
</div> </div>
</> </>
)} )}
{error && ( {error && (
<Alert variant="destructive" className="mt-4"> <Alert variant="destructive" className="mt-4">
<AlertTitle>Error</AlertTitle> <AlertTitle>{t('common.error')}</AlertTitle>
<AlertDescription>{error}</AlertDescription> <AlertDescription>{error}</AlertDescription>
</Alert> </Alert>
)} )}

View File

@@ -11,6 +11,7 @@ import {getUserInfo} from "@/ui/main-axios.ts";
import {toast} from "sonner"; import {toast} from "sonner";
import {PasswordReset} from "@/ui/User/PasswordReset.tsx"; import {PasswordReset} from "@/ui/User/PasswordReset.tsx";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import {LanguageSwitcher} from "@/components/LanguageSwitcher";
interface UserProfileProps { interface UserProfileProps {
isTopbarOpen?: boolean; isTopbarOpen?: boolean;
@@ -146,6 +147,16 @@ export function UserProfile({isTopbarOpen = true}: UserProfileProps) {
</p> </p>
</div> </div>
</div> </div>
<div className="mt-6 pt-6 border-t">
<div className="flex items-center justify-between">
<div>
<Label>{t('common.language')}</Label>
<p className="text-sm text-muted-foreground mt-1">{t('profile.selectPreferredLanguage')}</p>
</div>
<LanguageSwitcher />
</div>
</div>
</CardContent> </CardContent>
</Card> </Card>
</TabsContent> </TabsContent>