mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-01 10:56:02 +00:00
feat(frontend): internationalize login page (en-US, zh-CN) (#3677)
The login page used hardcoded English strings. Add a `login` section to the i18n Translations interface and both locale files, then wire the login page to `useI18n()` so all titles, labels, placeholders, buttons, SSO hints, and error messages resolve from the active locale.
This commit is contained in:
parent
e7b88a97ed
commit
5654a082f5
@ -10,6 +10,7 @@ import { FlickeringGrid } from "@/components/ui/flickering-grid";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { useAuth } from "@/core/auth/AuthProvider";
|
import { useAuth } from "@/core/auth/AuthProvider";
|
||||||
import { parseAuthError } from "@/core/auth/types";
|
import { parseAuthError } from "@/core/auth/types";
|
||||||
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate next parameter
|
* Validate next parameter
|
||||||
@ -49,6 +50,7 @@ export default function LoginPage() {
|
|||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const { isAuthenticated } = useAuth();
|
const { isAuthenticated } = useAuth();
|
||||||
const { theme, resolvedTheme } = useTheme();
|
const { theme, resolvedTheme } = useTheme();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
@ -59,16 +61,11 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
// Extract error from query params (e.g., ?error=sso_failed)
|
// Extract error from query params (e.g., ?error=sso_failed)
|
||||||
const errorParam = searchParams.get("error");
|
const errorParam = searchParams.get("error");
|
||||||
const errorMessages: Record<string, string> = {
|
|
||||||
sso_failed: "SSO login failed. Please try again or use email login.",
|
|
||||||
sso_cancelled: "SSO login was cancelled.",
|
|
||||||
sso_account_exists:
|
|
||||||
"An account with this email already exists. Please sign in with your password or contact your administrator.",
|
|
||||||
sso_not_allowed:
|
|
||||||
"SSO login is not allowed for your account. Contact your administrator.",
|
|
||||||
};
|
|
||||||
const [error, setError] = useState(
|
const [error, setError] = useState(
|
||||||
errorParam ? (errorMessages[errorParam] ?? "Authentication failed.") : "",
|
errorParam
|
||||||
|
? (t.login.errors[errorParam as keyof typeof t.login.errors] ??
|
||||||
|
t.login.authFailed)
|
||||||
|
: "",
|
||||||
);
|
);
|
||||||
// Soft hint shown after a failed login when SSO is configured: an SSO-only
|
// Soft hint shown after a failed login when SSO is configured: an SSO-only
|
||||||
// account has no local password, so the backend returns a generic
|
// account has no local password, so the backend returns a generic
|
||||||
@ -164,7 +161,7 @@ export default function LoginPage() {
|
|||||||
// Both login and register set a cookie — redirect to workspace
|
// Both login and register set a cookie — redirect to workspace
|
||||||
router.push(redirectPath);
|
router.push(redirectPath);
|
||||||
} catch {
|
} catch {
|
||||||
setError("Network error. Please try again.");
|
setError(t.login.networkError);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -186,34 +183,34 @@ export default function LoginPage() {
|
|||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-foreground font-serif text-3xl">DeerFlow</h1>
|
<h1 className="text-foreground font-serif text-3xl">DeerFlow</h1>
|
||||||
<p className="text-muted-foreground mt-2">
|
<p className="text-muted-foreground mt-2">
|
||||||
{isLogin ? "Sign in to your account" : "Create a new account"}
|
{isLogin ? t.login.signInTitle : t.login.createAccountTitle}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-2">
|
<form onSubmit={handleSubmit} className="space-y-2">
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<label htmlFor="email" className="text-sm font-medium">
|
<label htmlFor="email" className="text-sm font-medium">
|
||||||
Email
|
{t.login.email}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
placeholder="you@example.com"
|
placeholder={t.login.emailPlaceholder}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<label htmlFor="password" className="text-sm font-medium">
|
<label htmlFor="password" className="text-sm font-medium">
|
||||||
Password
|
{t.login.password}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder="•••••••"
|
placeholder={t.login.passwordPlaceholder}
|
||||||
required
|
required
|
||||||
minLength={isLogin ? 6 : 8}
|
minLength={isLogin ? 6 : 8}
|
||||||
/>
|
/>
|
||||||
@ -223,10 +220,10 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
<Button type="submit" className="w-full" disabled={loading}>
|
<Button type="submit" className="w-full" disabled={loading}>
|
||||||
{loading
|
{loading
|
||||||
? "Please wait..."
|
? t.login.pleaseWait
|
||||||
: isLogin
|
: isLogin
|
||||||
? "Sign In"
|
? t.login.signIn
|
||||||
: "Create Account"}
|
: t.login.createAccount}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -239,15 +236,14 @@ export default function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="relative flex justify-center text-xs uppercase">
|
<div className="relative flex justify-center text-xs uppercase">
|
||||||
<span className="bg-background text-muted-foreground px-2">
|
<span className="bg-background text-muted-foreground px-2">
|
||||||
Or continue with
|
{t.login.orContinueWith}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showSsoHint && (
|
{showSsoHint && (
|
||||||
<p className="text-muted-foreground text-center text-sm">
|
<p className="text-muted-foreground text-center text-sm">
|
||||||
If your account uses single sign-on, sign in with the option
|
{t.login.ssoHint}
|
||||||
below instead.
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{ssoProviders.map((provider) => (
|
{ssoProviders.map((provider) => (
|
||||||
@ -261,7 +257,7 @@ export default function LoginPage() {
|
|||||||
window.location.href = `/api/v1/auth/oauth/${provider.id}?next=${encodeURIComponent(redirectPath)}`;
|
window.location.href = `/api/v1/auth/oauth/${provider.id}?next=${encodeURIComponent(redirectPath)}`;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Continue with {provider.display_name}
|
{t.login.continueWith(provider.display_name)}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -277,15 +273,13 @@ export default function LoginPage() {
|
|||||||
}}
|
}}
|
||||||
className="text-blue-500 hover:underline"
|
className="text-blue-500 hover:underline"
|
||||||
>
|
>
|
||||||
{isLogin
|
{isLogin ? t.login.noAccountSignUp : t.login.haveAccountSignIn}
|
||||||
? "Don't have an account? Sign up"
|
|
||||||
: "Already have an account? Sign in"}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-muted-foreground text-center text-xs">
|
<div className="text-muted-foreground text-center text-xs">
|
||||||
<Link href="/" className="hover:underline">
|
<Link href="/" className="hover:underline">
|
||||||
← Back to home
|
{t.login.backToHome}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -555,4 +555,32 @@ export const enUS: Translations = {
|
|||||||
emptyDescription: "Credits and acknowledgements will show here.",
|
emptyDescription: "Credits and acknowledgements will show here.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
login: {
|
||||||
|
signInTitle: "Sign in to your account",
|
||||||
|
createAccountTitle: "Create a new account",
|
||||||
|
email: "Email",
|
||||||
|
emailPlaceholder: "you@example.com",
|
||||||
|
password: "Password",
|
||||||
|
passwordPlaceholder: "•••••••",
|
||||||
|
pleaseWait: "Please wait...",
|
||||||
|
signIn: "Sign In",
|
||||||
|
createAccount: "Create Account",
|
||||||
|
orContinueWith: "Or continue with",
|
||||||
|
ssoHint:
|
||||||
|
"If your account uses single sign-on, sign in with the option below instead.",
|
||||||
|
continueWith: (provider: string) => `Continue with ${provider}`,
|
||||||
|
noAccountSignUp: "Don't have an account? Sign up",
|
||||||
|
haveAccountSignIn: "Already have an account? Sign in",
|
||||||
|
backToHome: "← Back to home",
|
||||||
|
networkError: "Network error. Please try again.",
|
||||||
|
authFailed: "Authentication failed.",
|
||||||
|
errors: {
|
||||||
|
sso_failed: "SSO login failed. Please try again or use email login.",
|
||||||
|
sso_cancelled: "SSO login was cancelled.",
|
||||||
|
sso_account_exists:
|
||||||
|
"An account with this email already exists. Please sign in with your password or contact your administrator.",
|
||||||
|
sso_not_allowed:
|
||||||
|
"SSO login is not allowed for your account. Contact your administrator.",
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -459,4 +459,31 @@ export interface Translations {
|
|||||||
emptyDescription: string;
|
emptyDescription: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Login / Auth
|
||||||
|
login: {
|
||||||
|
signInTitle: string;
|
||||||
|
createAccountTitle: string;
|
||||||
|
email: string;
|
||||||
|
emailPlaceholder: string;
|
||||||
|
password: string;
|
||||||
|
passwordPlaceholder: string;
|
||||||
|
pleaseWait: string;
|
||||||
|
signIn: string;
|
||||||
|
createAccount: string;
|
||||||
|
orContinueWith: string;
|
||||||
|
ssoHint: string;
|
||||||
|
continueWith: (provider: string) => string;
|
||||||
|
noAccountSignUp: string;
|
||||||
|
haveAccountSignIn: string;
|
||||||
|
backToHome: string;
|
||||||
|
networkError: string;
|
||||||
|
authFailed: string;
|
||||||
|
errors: {
|
||||||
|
sso_failed: string;
|
||||||
|
sso_cancelled: string;
|
||||||
|
sso_account_exists: string;
|
||||||
|
sso_not_allowed: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -534,4 +534,30 @@ export const zhCN: Translations = {
|
|||||||
emptyDescription: "相关的致谢信息会展示在这里。",
|
emptyDescription: "相关的致谢信息会展示在这里。",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
login: {
|
||||||
|
signInTitle: "登录你的账号",
|
||||||
|
createAccountTitle: "创建新账号",
|
||||||
|
email: "邮箱",
|
||||||
|
emailPlaceholder: "you@example.com",
|
||||||
|
password: "密码",
|
||||||
|
passwordPlaceholder: "•••••••",
|
||||||
|
pleaseWait: "请稍候...",
|
||||||
|
signIn: "登录",
|
||||||
|
createAccount: "创建账号",
|
||||||
|
orContinueWith: "或使用以下方式登录",
|
||||||
|
ssoHint: "如果你的账号使用单点登录(SSO),请改用下方的选项登录。",
|
||||||
|
continueWith: (provider: string) => `使用 ${provider} 登录`,
|
||||||
|
noAccountSignUp: "还没有账号?立即注册",
|
||||||
|
haveAccountSignIn: "已有账号?立即登录",
|
||||||
|
backToHome: "← 返回首页",
|
||||||
|
networkError: "网络错误,请重试。",
|
||||||
|
authFailed: "身份验证失败。",
|
||||||
|
errors: {
|
||||||
|
sso_failed: "SSO 登录失败,请重试或使用邮箱登录。",
|
||||||
|
sso_cancelled: "SSO 登录已取消。",
|
||||||
|
sso_account_exists:
|
||||||
|
"该邮箱对应的账号已存在。请使用密码登录或联系管理员。",
|
||||||
|
sso_not_allowed: "你的账号不允许使用 SSO 登录。请联系管理员。",
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user