diff --git a/app/(main)/layout.tsx b/app/(main)/layout.tsx index 0a8ffb2..afa5cd2 100644 --- a/app/(main)/layout.tsx +++ b/app/(main)/layout.tsx @@ -4,6 +4,7 @@ import { MainNav } from "@/components/layout/main-nav"; import { UserNav } from "@/components/layout/user-nav"; import { ThemeToggle } from "@/components/layout/theme-toggle"; import {useTranslations} from 'next-intl'; +import { LanguageToggle } from "@/components/layout/lang-toggle"; function MainLayout({ children }: { children: React.ReactNode }) { const t = useTranslations('home'); @@ -15,6 +16,7 @@ function MainLayout({ children }: { children: React.ReactNode }) {
+
diff --git a/app/(main)/submissions/page.tsx b/app/(main)/submissions/page.tsx index 3252203..f08e081 100644 --- a/app/(main)/submissions/page.tsx +++ b/app/(main)/submissions/page.tsx @@ -16,11 +16,13 @@ import { Progress } from "@/components/ui/progress"; import { Button } from '@/components/ui/button'; import { useToast } from '@/hooks/use-toast'; import { Separator } from '@/components/ui/separator'; +import { useTranslations } from 'next-intl'; const fetcher = (url: string) => api.get(url).then(res => res.data.data); // Component for the list of submissions function MySubmissionsList() { + const t = useTranslations('submissions'); const { data: submissions, error, isLoading } = useSWR('/submissions', fetcher, { refreshInterval: 5000 }); @@ -28,8 +30,8 @@ function MySubmissionsList() { if (isLoading) return ( - My Submissions - A list of all your submissions. + {t('list.title')} + {t('list.description')}
@@ -38,23 +40,23 @@ function MySubmissionsList() { ); - if (error) return
Failed to load submissions.
; + if (error) return
{t('list.loadFail')}
; return ( - My Submissions - A list of all your submissions. + {t('list.title')} + {t('list.description')} - ID - Problem ID - Status - Score - Submitted At + {t('list.table.id')} + {t('list.table.problemId')} + {t('list.table.status')} + {t('list.table.score')} + {t('list.table.submittedAt')} @@ -78,7 +80,7 @@ function MySubmissionsList() { )) ) : ( - No submissions yet. + {t('list.none')} )} @@ -89,20 +91,23 @@ function MySubmissionsList() { } function QueuePosition({ submissionId, cluster }: { submissionId: string, cluster: string }) { + const t = useTranslations('submissions'); const { data } = useSWR<{ position: number }>(`/submissions/${submissionId}/queue_position`, fetcher, { refreshInterval: 3000 }); if (data === undefined) return null; return (
- Queue Position - #{data.position + 1} in {cluster} queue + {t('details.queue.position')} + {t('details.queue.info', { position: data.position + 1, cluster })}
); } +// Component for submission details function SubmissionDetails({ submissionId }: { submissionId: string }) { + const t = useTranslations('submissions'); const { toast } = useToast(); const { data: submission, error, isLoading, mutate } = useSWR(`/submissions/${submissionId}`, fetcher, { refreshInterval: (data) => (data?.status === 'Queued' || data?.status === 'Running' ? 2000 : 0), @@ -110,21 +115,21 @@ function SubmissionDetails({ submissionId }: { submissionId: string }) { const { data: problem } = useSWR(submission ? `/problems/${submission.problem_id}` : null, fetcher); if (isLoading) return ; - if (error) return
Failed to load submission.
; - if (!submission) return
Submission not found.
; + if (error) return
{t('details.loadFail')}
; + if (!submission) return
{t('details.notFound')}
; const totalSteps = problem?.workflow.length ?? 0; const progress = totalSteps > 0 ? ((submission.current_step + 1) / totalSteps) * 100 : 0; const canBeInterrupted = submission.status === 'Queued' || submission.status === 'Running'; const handleInterrupt = async () => { - if (!confirm('Are you sure you want to interrupt this submission? This action cannot be undone.')) return; + if (!confirm(t('details.interrupt.confirm'))) return; try { await api.post(`/submissions/${submissionId}/interrupt`); - toast({ title: 'Success', description: 'Submission interruption request sent.' }); + toast({ title: t('details.interrupt.successTitle'), description: t('details.interrupt.successDescription') }); mutate(); } catch (err: any) { - toast({ variant: 'destructive', title: 'Error', description: err.response?.data?.message || 'Failed to interrupt submission.' }); + toast({ variant: 'destructive', title: t('details.interrupt.failTitle'), description: err.response?.data?.message || t('details.interrupt.failDefault') }); } } @@ -133,8 +138,8 @@ function SubmissionDetails({ submissionId }: { submissionId: string }) {
- Live Log - Real-time output from the judge. Select a step to view its log. + {t('details.log.title')} + {t('details.log.description')} {problem && submission ? : } @@ -146,10 +151,10 @@ function SubmissionDetails({ submissionId }: { submissionId: string }) {
- Submission Info + {t('details.info.title')} {canBeInterrupted && ( )}
@@ -157,52 +162,57 @@ function SubmissionDetails({ submissionId }: { submissionId: string }) { {/* --- Submission Details Section --- */}
- Status + {t('details.info.status')}
{submission.status === 'Queued' && } {(submission.status === 'Running') && totalSteps > 0 && (
-

Step {submission.current_step + 1} of {totalSteps}: {problem?.workflow[submission.current_step]?.name}

+

{t('details.info.stepProgress', { + current: submission.current_step + 1, + total: totalSteps, + name: problem?.workflow[submission.current_step]?.name ?? '' + })}

)}
- Score + {t('details.info.score')} {submission.score}
- Submitted + {t('details.info.submitted')} {formatDistanceToNow(new Date(submission.CreatedAt), { addSuffix: true })}
- Problem + {t('details.info.problem')} {submission.problem_id}
- User + {t('details.info.user')} {submission.user.nickname}
- Cluster + {t('details.info.cluster')} {submission.cluster}
- Node + {t('details.info.node')} {submission.node || 'N/A'}
+ {/* --- Judge Info Section (conditionally rendered) --- */} {submission.info && Object.keys(submission.info).length > 0 && ( <>
-

Judge Info

+

{t('details.judgeInfo.title')}

                                         {JSON.stringify(submission.info, null, 2)}
                                     
-

This is the raw JSON output from the final step of the judging process.

+

{t('details.judgeInfo.description')}

)} @@ -215,6 +225,7 @@ function SubmissionDetails({ submissionId }: { submissionId: string }) { function SubmissionDetailsSkeleton() { + const t = useTranslations('submissions'); return (
@@ -233,7 +244,7 @@ function SubmissionDetailsSkeleton() {
- + {t('details.info.title')} {[...Array(6)].map((_, i) => (
@@ -266,4 +277,4 @@ export default function MySubmissionsPage() { ); -} +} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 9f12e41..43f6e33 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -6,7 +6,8 @@ import { SWRProvider } from "@/providers/swr-provider"; import { AuthProvider } from "@/providers/auth-provider"; import { Toaster } from "@/components/ui/toaster"; import { ThemeProvider } from "@/providers/theme-provider"; -import {NextIntlClientProvider} from 'next-intl'; +// import {NextIntlClientProvider} from 'next-intl'; +import { ClientIntlProvider } from "@/providers/i18n-provider"; const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }); @@ -28,7 +29,7 @@ export default function RootLayout({ inter.variable )} > - + - + ); diff --git a/components/layout/lang-toggle.tsx b/components/layout/lang-toggle.tsx new file mode 100644 index 0000000..0be65ea --- /dev/null +++ b/components/layout/lang-toggle.tsx @@ -0,0 +1,43 @@ +// FILE: components/layout/language-switcher.tsx +"use client"; + +import { useClientLocale } from '@/providers/i18n-provider'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Languages } from 'lucide-react'; + +const LOCALE_MAP: Record = { + 'zh': '简体中文', + 'en': 'English', +}; + +export function LanguageToggle() { + const { locale, switchLocale } = useClientLocale(); + + return ( + + + + + + {Object.entries(LOCALE_MAP).map(([code, name]) => ( + switchLocale(code)} + className={locale === code ? "font-bold text-primary" : ""} + > + {name} + + ))} + + + ); +} \ No newline at end of file diff --git a/i18n/request.ts b/i18n/request.ts index fc77866..4ad3279 100644 --- a/i18n/request.ts +++ b/i18n/request.ts @@ -1,11 +1,9 @@ import {getRequestConfig} from 'next-intl/server'; export default getRequestConfig(async () => { - // Static for now, we'll change this later - const locale = 'zh'; - + const locale = 'en'; return { locale, - messages: (await import(`../messages/${locale}.json`)).default + messages: {} as any }; }); \ No newline at end of file diff --git a/messages/zh.json b/messages/zh.json deleted file mode 100644 index 461f873..0000000 --- a/messages/zh.json +++ /dev/null @@ -1,203 +0,0 @@ -{ - "home": { - "power_by": "由 ZJUSCT/CSOJZJUSCT/CSOJ-WebUI 强力驱动", - "contests": "全部比赛", - "submissions": "提交记录", - "profile": "个人资料", - "theme": { - "toggleTheme": "切换主题", - "toggleToDark": "切换到深色模式", - "toggleToLight": "切换到浅色模式", - "toggleToSystem": "切换到跟随系统" - }, - "user": { - "profile": "个人资料", - "mySubmissions": "我的提交", - "logout": "退出登录" - } - }, - "contests": { - "invalidDuration": "竞赛持续时间无效。", - "starts": "开始", - "ends": "结束", - "to": "至", - "viewDetails": "查看详情", - "registered": "已报名", - "checking": "正在检查...", - "register": "报名", - "loading": "加载中...", - "registerForContest": "报名参加竞赛", - "status": { - "upcoming": "即将开始", - "ongoing": "进行中", - "finished": "已结束", - "ended": "已结束", - "live": "实时" - }, - "registration": { - "successTitle": "成功", - "successDescription": "您已成功报名参加竞赛。", - "failTitle": "报名失败", - "unexpectedError": "发生意外错误。" - }, - "list": { - "loadFail": "加载竞赛列表失败。", - "noContests": "暂无可用竞赛。" - }, - "detail": { - "loadFail": "加载竞赛详情失败。", - "notFound": "未找到竞赛。" - }, - "description": { - "title": "竞赛描述" - }, - "problems": { - "title": "题目", - "instruction": "选择一个题目查看详情并提交您的解决方案。", - "none": "此竞赛未激活或暂无题目。" - }, - "problemCard": { - "id": "题目 ID", - "view": "查看题目" - }, - "trend": { - "title": "分数趋势", - "description": "顶尖用户分数随时间的变化趋势。", - "loadFail": "加载分数趋势数据失败。", - "none": "暂无分数趋势数据。" - }, - "leaderboard": { - "title": "排行榜", - "rank": "排名", - "user": "用户", - "totalScore": "总分", - "loadFail": "加载排行榜数据失败。", - "none": "暂无分数记录。", - "contestDetailsFail": "无法加载竞赛详情以显示排行榜头部信息。" - }, - "tabs": { - "problems": "题目", - "leaderboard": "排行榜" - }, - "announcements": { - "title": "公告", - "loadFail": "加载公告失败", - "none": "暂无公告" - } - }, - "ProblemDetails": { - "noProblem": { - "title": "未选择题目", - "description": "请选择一个题目以查看其详细信息。" - }, - "details": { - "loadFail": "加载题目失败。您可能暂无权访问此题目。", - "notFound": "未找到题目。", - "id": "题目 ID" - }, - "submitForm": { - "title": "提交答案" - }, - "submissions": { - "title": "您的提交记录", - "none": "您尚未提交过此题目的任何解决方案。", - "id": "提交 ID", - "status": "状态", - "score": "分数", - "date": "日期" - } - }, - "Profile": { - "avatar": { - "title": "头像", - "description": "更新您的个人资料图片。", - "change": "更换头像", - "uploading": "正在上传...", - "uploadSuccess": "头像更新成功!", - "uploadFailTitle": "上传失败", - "uploadFailDescription": "无法上传头像。" - }, - "form": { - "title": "个人信息", - "description": "更新您的账户详细信息。用户名无法更改。", - "username": "用户名", - "nickname": "昵称", - "nicknamePlaceholder": "您的显示名称", - "signature": "个性签名", - "signaturePlaceholder": "一段简短的个人简介", - "saving": "正在保存...", - "saveChanges": "保存更改", - "updateSuccess": "个人资料更新成功!", - "updateFailTitle": "更新失败", - "updateFailDescription": "无法更新个人资料。", - "nicknameRequired": "昵称是必填项" - }, - "token": { - "title": "认证令牌 (Token)", - "description": "这是您当前的会话令牌。请妥善保管,切勿泄露。", - "label": "您的令牌", - "copySr": "复制令牌", - "copySuccessTitle": "令牌已复制!", - "copySuccessDescription": "认证令牌已复制到您的剪贴板。", - "expiresAt": "过期时间", - "timeRemaining": "有效时间", - "expired": "已过期" - }, - "logout": "退出登录" - }, - "auth": { - "login": { - "title": "登录到 CSOJ", - "loadingDescription": "正在检查可用的登录方式...", - "descriptionLocal": "输入您的凭据,或使用其他登录方式。", - "descriptionExternal": "请使用可用的登录方式。", - "form": { - "username": "用户名", - "password": "密码", - "usernameRequired": "用户名是必填项", - "passwordRequired": "密码是必填项", - "loginButton": "登录", - "loggingIn": "正在登录..." - }, - "separatorText": "或使用以下方式继续", - "gitlabButton": "使用 GitLab 登录", - "noAccount": "还没有账号?", - "registerLink": "注册", - "toast": { - "successTitle": "登录成功!", - "failTitle": "登录失败", - "failDefault": "登录失败" - } - }, - "register": { - "title": "创建账户", - "loadingDescription": "正在检查注册可用性...", - "description": "输入您的详细信息以创建一个新的 CSOJ 账户。", - "disabled": { - "title": "注册已禁用", - "description": "通过用户名和密码注册账户的功能已禁用。", - "instruction": "请返回登录页面并使用其他方式。", - "backToLogin": "返回登录" - }, - "form": { - "username": "用户名", - "nickname": "昵称", - "password": "密码", - "nicknamePlaceholder": "您的显示名称", - "usernameMinLength": "用户名至少需要 3 个字符", - "nicknameRequired": "昵称是必填项", - "passwordMinLength": "密码至少需要 6 个字符", - "registerButton": "注册", - "creatingAccount": "正在创建账户..." - }, - "alreadyHaveAccount": "已有账户?", - "loginLink": "登录", - "toast": { - "successTitle": "注册成功!", - "successDescription": "您现在可以使用您的新账户登录。", - "failTitle": "注册失败", - "failDefault": "注册失败" - } - } - } -} \ No newline at end of file diff --git a/providers/i18n-provider.tsx b/providers/i18n-provider.tsx new file mode 100644 index 0000000..3c53512 --- /dev/null +++ b/providers/i18n-provider.tsx @@ -0,0 +1,132 @@ +"use client"; + +import React, { useState, useEffect, useCallback } from 'react'; +import { NextIntlClientProvider } from 'next-intl'; +import { Loader2 } from 'lucide-react'; + +const AVAILABLE_LOCALES = ['en', 'zh']; +const DEFAULT_LOCALE = 'zh'; +const LOCALE_STORAGE_KEY = 'csoj_locale'; + +interface ClientIntlProviderProps { + children: React.ReactNode; +} + +interface LocaleContextType { + switchLocale: (newLocale: string) => void; + locale: string | null; +} + +const LocaleContext = React.createContext({ + switchLocale: () => { + console.warn("switchLocale called outside ClientIntlProvider."); + }, + locale: null +}); + +export const useClientLocale = () => { + const context = React.useContext(LocaleContext); + if (!context) { + throw new Error('useClientLocale must be used within ClientIntlProvider'); + } + return context; +}; + +export const ClientIntlProvider: React.FC = ({ children }) => { + const [locale, setLocale] = useState(null); + const [messages, setMessages] = useState | null>(null); + const [isLoading, setIsLoading] = useState(true); + + // Use a ref to track if a language load is in progress, preventing jittering from concurrent requests. + const loadingRef = React.useRef(false); + + const loadMessages = useCallback(async (targetLocale: string, updateStorage: boolean = true) => { + // Avoid concurrent calls + if (loadingRef.current) return false; + + loadingRef.current = true; + setIsLoading(true); + + try { + // Check if the target language is available + const finalLocale = AVAILABLE_LOCALES.includes(targetLocale) ? targetLocale : DEFAULT_LOCALE; + + const response = await fetch(`/messages/${finalLocale}.json`); + if (!response.ok) { + throw new Error(`Failed to load messages for locale: ${finalLocale}`); + } + const newMessages = await response.json(); + + // Update state upon success + setMessages(newMessages); + setLocale(finalLocale); + + // Update local storage if requested + if (updateStorage && typeof window !== 'undefined') { + localStorage.setItem(LOCALE_STORAGE_KEY, finalLocale); + } + return true; + + } catch (error) { + console.error(`Failed to load locale: ${targetLocale}.`, error); + // On failure, only set default locale/messages if no locale has been loaded yet (initial load failure) + if (!locale) { + setMessages({}); + setLocale(DEFAULT_LOCALE); + } + return false; + } finally { + setIsLoading(false); + loadingRef.current = false; + } + }, [locale]); + + + // Load initial locale on mount + useEffect(() => { + let initialLocale = DEFAULT_LOCALE; + if (typeof window !== 'undefined') { + const savedLocale = localStorage.getItem(LOCALE_STORAGE_KEY); + if (savedLocale && AVAILABLE_LOCALES.includes(savedLocale)) { + initialLocale = savedLocale; + } + } + + // Asynchronously load the initial language + loadMessages(initialLocale); + + }, [loadMessages]); + + // Handle locale switching + const switchLocale = async (newLocale: string) => { + if (!AVAILABLE_LOCALES.includes(newLocale) || newLocale === locale || loadingRef.current) { + return; + } + + // Attempt to load the new language pack + const success = await loadMessages(newLocale, true); // Update storage on successful load + + if (!success) { + console.warn(`Could not load ${newLocale}. Sticking to current locale.`); + } + }; + + // Show full-screen loader if initial load is pending + if (!locale || !messages) { + if (isLoading) { + return ( +
+ +
+ ); + } + } + + return ( + + + {children} + + + ); +}; \ No newline at end of file diff --git a/messages/en.json b/public/messages/en.json similarity index 80% rename from messages/en.json rename to public/messages/en.json index cdb103e..f27541e 100644 --- a/messages/en.json +++ b/public/messages/en.json @@ -199,5 +199,55 @@ "failDefault": "Registration failed" } } + }, + "submissions": { + "list": { + "title": "My Submissions", + "description": "A list of all your submissions.", + "loadFail": "Failed to load submissions.", + "none": "No submissions yet.", + "table": { + "id": "ID", + "problemId": "Problem ID", + "status": "Status", + "score": "Score", + "submittedAt": "Submitted At" + } + }, + "details": { + "loadFail": "Failed to load submission.", + "notFound": "Submission not found.", + "log": { + "title": "Live Log", + "description": "Real-time output from the judge. Select a step to view its log." + }, + "info": { + "title": "Submission Info", + "status": "Status", + "score": "Score", + "submitted": "Submitted", + "problem": "Problem", + "user": "User", + "cluster": "Cluster", + "node": "Node", + "stepProgress": "Step {{current}} of {{total}}: {{name}}" + }, + "judgeInfo": { + "title": "Judge Info", + "description": "This is the raw JSON output from the final step of the judging process." + }, + "queue": { + "position": "Queue Position", + "info": "#{{position}} in {{cluster}} queue" + }, + "interrupt": { + "button": "Interrupt", + "confirm": "Are you sure you want to interrupt this submission? This action cannot be undone.", + "successTitle": "Success", + "successDescription": "Submission interruption request sent.", + "failTitle": "Error", + "failDefault": "Failed to interrupt submission." + } + } } } diff --git a/public/messages/zh.json b/public/messages/zh.json new file mode 100644 index 0000000..57c0843 --- /dev/null +++ b/public/messages/zh.json @@ -0,0 +1,253 @@ +{ + "home": { + "power_by": "基于 ZJUSCT/CSOJZJUSCT/CSOJ-WebUI 构建", + "contests": "竞赛列表", + "submissions": "提交记录", + "profile": "个人中心", + "theme": { + "toggleTheme": "切换主题", + "toggleToDark": "切换至深色模式", + "toggleToLight": "切换至浅色模式", + "toggleToSystem": "跟随系统主题" + }, + "user": { + "profile": "个人资料", + "mySubmissions": "我的提交", + "logout": "退出登录" + } + }, + "contests": { + "invalidDuration": "竞赛时长设置无效", + "starts": "开始时间", + "ends": "结束时间", + "to": "至", + "viewDetails": "查看详情", + "registered": "已报名", + "checking": "校验中...", + "register": "立即报名", + "loading": "加载中...", + "registerForContest": "竞赛报名", + "status": { + "upcoming": "即将开始", + "ongoing": "进行中", + "finished": "已结束", + "ended": "已结束", + "live": "实时赛况" + }, + "registration": { + "successTitle": "报名成功", + "successDescription": "您已成功报名参加本次竞赛", + "failTitle": "报名失败", + "unexpectedError": "发生未知错误,请稍后重试" + }, + "list": { + "loadFail": "加载竞赛列表失败", + "noContests": "暂无可用竞赛" + }, + "detail": { + "loadFail": "加载竞赛详情失败", + "notFound": "未找到指定竞赛" + }, + "description": { + "title": "竞赛说明" + }, + "problems": { + "title": "题目列表", + "instruction": "请选择题目查看详情并提交解答", + "none": "当前竞赛未激活或暂无题目" + }, + "problemCard": { + "id": "题目编号", + "view": "查看题目" + }, + "trend": { + "title": "分数趋势", + "description": "顶尖选手得分随时间变化趋势图", + "loadFail": "加载趋势数据失败", + "none": "暂无趋势数据" + }, + "leaderboard": { + "title": "排行榜", + "rank": "排名", + "user": "用户名", + "totalScore": "总分", + "loadFail": "加载排行榜数据失败", + "none": "暂无得分记录", + "contestDetailsFail": "无法加载竞赛详情,无法显示排行榜头部信息" + }, + "tabs": { + "problems": "题目列表", + "leaderboard": "排行榜" + }, + "announcements": { + "title": "竞赛公告", + "loadFail": "加载公告失败", + "none": "暂无公告" + } + }, + "ProblemDetails": { + "noProblem": { + "title": "未选择题目", + "description": "请先选择要查看的题目" + }, + "details": { + "loadFail": "加载题目失败,您可能暂无访问权限", + "notFound": "未找到指定题目", + "id": "题目编号" + }, + "submitForm": { + "title": "提交解答" + }, + "submissions": { + "title": "提交历史", + "none": "您尚未提交过本题的解答", + "id": "提交编号", + "status": "状态", + "score": "得分", + "date": "提交时间" + } + }, + "Profile": { + "avatar": { + "title": "头像设置", + "description": "更新您的个人资料图片", + "change": "更换头像", + "uploading": "上传中...", + "uploadSuccess": "头像更新成功!", + "uploadFailTitle": "上传失败", + "uploadFailDescription": "头像上传失败,请重试" + }, + "form": { + "title": "个人信息", + "description": "更新您的账户信息(用户名不可修改)", + "username": "用户名", + "nickname": "昵称", + "nicknamePlaceholder": "请输入显示名称", + "signature": "个性签名", + "signaturePlaceholder": "请输入个人简介", + "saving": "保存中...", + "saveChanges": "保存修改", + "updateSuccess": "个人资料更新成功!", + "updateFailTitle": "更新失败", + "updateFailDescription": "个人资料更新失败,请重试", + "nicknameRequired": "昵称为必填项" + }, + "token": { + "title": "身份验证令牌", + "description": "此为当前会话令牌,请妥善保管切勿泄露", + "label": "您的令牌", + "copySr": "复制令牌", + "copySuccessTitle": "令牌已复制", + "copySuccessDescription": "身份验证令牌已复制到剪贴板", + "expiresAt": "过期时间", + "timeRemaining": "剩余有效期", + "expired": "已过期" + }, + "logout": "退出登录" + }, + "auth": { + "login": { + "title": "登录 CSOJ", + "loadingDescription": "正在检测可用登录方式...", + "descriptionLocal": "请输入账号密码,或使用其他登录方式", + "descriptionExternal": "请选择以下登录方式", + "form": { + "username": "用户名", + "password": "密码", + "usernameRequired": "用户名不能为空", + "passwordRequired": "密码不能为空", + "loginButton": "登录", + "loggingIn": "登录中..." + }, + "separatorText": "或使用以下方式登录", + "gitlabButton": "GitLab 登录", + "noAccount": "还没有账号?", + "registerLink": "立即注册", + "toast": { + "successTitle": "登录成功!", + "failTitle": "登录失败", + "failDefault": "登录失败,请检查凭证" + } + }, + "register": { + "title": "注册账户", + "loadingDescription": "正在检测注册可用性...", + "description": "请输入详细信息创建新的 CSOJ 账户", + "disabled": { + "title": "注册功能已关闭", + "description": "当前暂不支持用户名密码方式注册", + "instruction": "请返回登录页使用其他登录方式", + "backToLogin": "返回登录" + }, + "form": { + "username": "用户名", + "nickname": "昵称", + "password": "密码", + "nicknamePlaceholder": "请输入显示名称", + "usernameMinLength": "用户名至少需要3个字符", + "nicknameRequired": "昵称为必填项", + "passwordMinLength": "密码至少需要6个字符", + "registerButton": "注册", + "creatingAccount": "创建账户中..." + }, + "alreadyHaveAccount": "已有账户?", + "loginLink": "立即登录", + "toast": { + "successTitle": "注册成功!", + "successDescription": "现在可以使用新账户登录", + "failTitle": "注册失败", + "failDefault": "注册失败,请重试" + } + } + }, + "submissions": { + "list": { + "title": "我的提交记录", + "description": "您所有的提交记录列表", + "loadFail": "加载提交记录失败", + "none": "暂无提交记录", + "table": { + "id": "编号", + "problemId": "题目编号", + "status": "状态", + "score": "得分", + "submittedAt": "提交时间" + } + }, + "details": { + "loadFail": "加载提交详情失败", + "notFound": "未找到指定提交记录", + "log": { + "title": "实时日志", + "description": "测评机实时输出,请选择步骤查看详细日志" + }, + "info": { + "title": "提交信息", + "status": "状态", + "score": "得分", + "submitted": "提交时间", + "problem": "题目", + "user": "用户", + "cluster": "测评集群", + "node": "节点", + "stepProgress": "第 {{current}} 步 / 共 {{total}} 步:{{name}}" + }, + "judgeInfo": { + "title": "测评信息", + "description": "此为测评过程最终步骤的原始 JSON 输出" + }, + "queue": { + "position": "队列位置", + "info": "在 {{cluster}} 队列中排第 {{position}} 位" + }, + "interrupt": { + "button": "中断提交", + "confirm": "确定要中断此提交吗?此操作不可撤销", + "successTitle": "操作成功", + "successDescription": "已发送中断提交请求", + "failTitle": "操作失败", + "failDefault": "中断提交失败,请重试" + } + } + } +} \ No newline at end of file