From 0a7c3dd8e99a6f3ed62389f2d1e201984423cbbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E6=99=A8=E6=BD=87?= <3220101835@zju.edu.cn> Date: Tue, 14 Oct 2025 11:29:08 +0800 Subject: [PATCH 1/3] i18n: add translation for submissions --- app/(main)/submissions/page.tsx | 82 ++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/app/(main)/submissions/page.tsx b/app/(main)/submissions/page.tsx index e4ac086..c8f46fa 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,21 +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 --- +// 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), @@ -111,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') }); } } @@ -134,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 ? : } @@ -143,15 +147,14 @@ function SubmissionDetails({ submissionId }: { submissionId: string }) {
- {/* --- [修改] 右侧合并为一个卡片 --- */}
- Submission Info + {t('details.info.title')} {canBeInterrupted && ( )}
@@ -159,53 +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) --- */} + {/* --- 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')}

)} @@ -218,6 +225,7 @@ function SubmissionDetails({ submissionId }: { submissionId: string }) { function SubmissionDetailsSkeleton() { + const t = useTranslations('submissions'); return (
@@ -236,7 +244,7 @@ function SubmissionDetailsSkeleton() {
- + {t('details.info.title')} {[...Array(6)].map((_, i) => (
@@ -269,4 +277,4 @@ export default function MySubmissionsPage() { ); -} +} \ No newline at end of file From 223c4f83ffca1012ee5033411a54e9c63a4a7dff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E6=99=A8=E6=BD=87?= <3220101835@zju.edu.cn> Date: Tue, 14 Oct 2025 11:29:32 +0800 Subject: [PATCH 2/3] feat: add language toggle --- app/(main)/layout.tsx | 2 + app/layout.tsx | 7 +- components/layout/lang-toggle.tsx | 46 +++++++++ i18n/request.ts | 6 +- providers/i18n-provider.tsx | 132 ++++++++++++++++++++++++++ {messages => public/messages}/en.json | 50 ++++++++++ {messages => public/messages}/zh.json | 50 ++++++++++ 7 files changed, 286 insertions(+), 7 deletions(-) create mode 100644 components/layout/lang-toggle.tsx create mode 100644 providers/i18n-provider.tsx rename {messages => public/messages}/en.json (80%) rename {messages => public/messages}/zh.json (80%) diff --git a/app/(main)/layout.tsx b/app/(main)/layout.tsx index 0a8ffb2..cc5da22 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'); @@ -16,6 +17,7 @@ function MainLayout({ children }: { children: React.ReactNode }) {
+
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..1282724 --- /dev/null +++ b/components/layout/lang-toggle.tsx @@ -0,0 +1,46 @@ +// 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 = { + 'en': 'English', + 'zh': '中文', +}; + +export function LanguageToggle() { + // 使用导出的 Hook + const { locale, switchLocale } = useClientLocale(); + + const currentLabel = locale ? LOCALE_MAP[locale] : 'Lang'; + + 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/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/messages/zh.json b/public/messages/zh.json similarity index 80% rename from messages/zh.json rename to public/messages/zh.json index 461f873..7dd8f03 100644 --- a/messages/zh.json +++ b/public/messages/zh.json @@ -199,5 +199,55 @@ "failDefault": "注册失败" } } + }, + "submissions": { + "list": { + "title": "我的提交记录", + "description": "您所有提交的列表。", + "loadFail": "加载提交记录失败。", + "none": "暂无提交记录。", + "table": { + "id": "ID", + "problemId": "题目 ID", + "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 From 30c6304ee308118c07da34a205d400503d184cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E6=99=A8=E6=BD=87?= <3220101835@zju.edu.cn> Date: Tue, 14 Oct 2025 11:40:43 +0800 Subject: [PATCH 3/3] feat: optimize zh-cn translation and language toggle layout --- app/(main)/layout.tsx | 2 +- components/layout/lang-toggle.tsx | 5 +- public/messages/zh.json | 214 +++++++++++++++--------------- 3 files changed, 109 insertions(+), 112 deletions(-) diff --git a/app/(main)/layout.tsx b/app/(main)/layout.tsx index cc5da22..afa5cd2 100644 --- a/app/(main)/layout.tsx +++ b/app/(main)/layout.tsx @@ -16,8 +16,8 @@ function MainLayout({ children }: { children: React.ReactNode }) {
- +
diff --git a/components/layout/lang-toggle.tsx b/components/layout/lang-toggle.tsx index 1282724..0be65ea 100644 --- a/components/layout/lang-toggle.tsx +++ b/components/layout/lang-toggle.tsx @@ -12,16 +12,13 @@ import { import { Languages } from 'lucide-react'; const LOCALE_MAP: Record = { + 'zh': '简体中文', 'en': 'English', - 'zh': '中文', }; export function LanguageToggle() { - // 使用导出的 Hook const { locale, switchLocale } = useClientLocale(); - const currentLabel = locale ? LOCALE_MAP[locale] : 'Lang'; - return ( diff --git a/public/messages/zh.json b/public/messages/zh.json index 7dd8f03..57c0843 100644 --- a/public/messages/zh.json +++ b/public/messages/zh.json @@ -1,14 +1,14 @@ { "home": { - "power_by": "由 ZJUSCT/CSOJZJUSCT/CSOJ-WebUI 强力驱动", - "contests": "全部比赛", + "power_by": "基于 ZJUSCT/CSOJZJUSCT/CSOJ-WebUI 构建", + "contests": "竞赛列表", "submissions": "提交记录", - "profile": "个人资料", + "profile": "个人中心", "theme": { "toggleTheme": "切换主题", - "toggleToDark": "切换到深色模式", - "toggleToLight": "切换到浅色模式", - "toggleToSystem": "切换到跟随系统" + "toggleToDark": "切换至深色模式", + "toggleToLight": "切换至浅色模式", + "toggleToSystem": "跟随系统主题" }, "user": { "profile": "个人资料", @@ -17,70 +17,70 @@ } }, "contests": { - "invalidDuration": "竞赛持续时间无效。", - "starts": "开始", - "ends": "结束", + "invalidDuration": "竞赛时长设置无效", + "starts": "开始时间", + "ends": "结束时间", "to": "至", "viewDetails": "查看详情", "registered": "已报名", - "checking": "正在检查...", - "register": "报名", + "checking": "校验中...", + "register": "立即报名", "loading": "加载中...", - "registerForContest": "报名参加竞赛", + "registerForContest": "竞赛报名", "status": { "upcoming": "即将开始", "ongoing": "进行中", "finished": "已结束", "ended": "已结束", - "live": "实时" + "live": "实时赛况" }, "registration": { - "successTitle": "成功", - "successDescription": "您已成功报名参加竞赛。", + "successTitle": "报名成功", + "successDescription": "您已成功报名参加本次竞赛", "failTitle": "报名失败", - "unexpectedError": "发生意外错误。" + "unexpectedError": "发生未知错误,请稍后重试" }, "list": { - "loadFail": "加载竞赛列表失败。", - "noContests": "暂无可用竞赛。" + "loadFail": "加载竞赛列表失败", + "noContests": "暂无可用竞赛" }, "detail": { - "loadFail": "加载竞赛详情失败。", - "notFound": "未找到竞赛。" + "loadFail": "加载竞赛详情失败", + "notFound": "未找到指定竞赛" }, "description": { - "title": "竞赛描述" + "title": "竞赛说明" }, "problems": { - "title": "题目", - "instruction": "选择一个题目查看详情并提交您的解决方案。", - "none": "此竞赛未激活或暂无题目。" + "title": "题目列表", + "instruction": "请选择题目查看详情并提交解答", + "none": "当前竞赛未激活或暂无题目" }, "problemCard": { - "id": "题目 ID", + "id": "题目编号", "view": "查看题目" }, "trend": { "title": "分数趋势", - "description": "顶尖用户分数随时间的变化趋势。", - "loadFail": "加载分数趋势数据失败。", - "none": "暂无分数趋势数据。" + "description": "顶尖选手得分随时间变化趋势图", + "loadFail": "加载趋势数据失败", + "none": "暂无趋势数据" }, "leaderboard": { "title": "排行榜", "rank": "排名", - "user": "用户", + "user": "用户名", "totalScore": "总分", - "loadFail": "加载排行榜数据失败。", - "none": "暂无分数记录。", - "contestDetailsFail": "无法加载竞赛详情以显示排行榜头部信息。" + "loadFail": "加载排行榜数据失败", + "none": "暂无得分记录", + "contestDetailsFail": "无法加载竞赛详情,无法显示排行榜头部信息" }, "tabs": { - "problems": "题目", + "problems": "题目列表", "leaderboard": "排行榜" }, "announcements": { - "title": "公告", + "title": "竞赛公告", "loadFail": "加载公告失败", "none": "暂无公告" } @@ -88,165 +88,165 @@ "ProblemDetails": { "noProblem": { "title": "未选择题目", - "description": "请选择一个题目以查看其详细信息。" + "description": "请先选择要查看的题目" }, "details": { - "loadFail": "加载题目失败。您可能暂无权访问此题目。", - "notFound": "未找到题目。", - "id": "题目 ID" + "loadFail": "加载题目失败,您可能暂无访问权限", + "notFound": "未找到指定题目", + "id": "题目编号" }, "submitForm": { - "title": "提交答案" + "title": "提交解答" }, "submissions": { - "title": "您的提交记录", - "none": "您尚未提交过此题目的任何解决方案。", - "id": "提交 ID", + "title": "提交历史", + "none": "您尚未提交过本题的解答", + "id": "提交编号", "status": "状态", - "score": "分数", - "date": "日期" + "score": "得分", + "date": "提交时间" } }, "Profile": { "avatar": { - "title": "头像", - "description": "更新您的个人资料图片。", + "title": "头像设置", + "description": "更新您的个人资料图片", "change": "更换头像", - "uploading": "正在上传...", + "uploading": "上传中...", "uploadSuccess": "头像更新成功!", "uploadFailTitle": "上传失败", - "uploadFailDescription": "无法上传头像。" + "uploadFailDescription": "头像上传失败,请重试" }, "form": { "title": "个人信息", - "description": "更新您的账户详细信息。用户名无法更改。", + "description": "更新您的账户信息(用户名不可修改)", "username": "用户名", "nickname": "昵称", - "nicknamePlaceholder": "您的显示名称", + "nicknamePlaceholder": "请输入显示名称", "signature": "个性签名", - "signaturePlaceholder": "一段简短的个人简介", - "saving": "正在保存...", - "saveChanges": "保存更改", + "signaturePlaceholder": "请输入个人简介", + "saving": "保存中...", + "saveChanges": "保存修改", "updateSuccess": "个人资料更新成功!", "updateFailTitle": "更新失败", - "updateFailDescription": "无法更新个人资料。", - "nicknameRequired": "昵称是必填项" + "updateFailDescription": "个人资料更新失败,请重试", + "nicknameRequired": "昵称为必填项" }, "token": { - "title": "认证令牌 (Token)", - "description": "这是您当前的会话令牌。请妥善保管,切勿泄露。", + "title": "身份验证令牌", + "description": "此为当前会话令牌,请妥善保管切勿泄露", "label": "您的令牌", "copySr": "复制令牌", - "copySuccessTitle": "令牌已复制!", - "copySuccessDescription": "认证令牌已复制到您的剪贴板。", + "copySuccessTitle": "令牌已复制", + "copySuccessDescription": "身份验证令牌已复制到剪贴板", "expiresAt": "过期时间", - "timeRemaining": "有效时间", + "timeRemaining": "剩余有效期", "expired": "已过期" }, "logout": "退出登录" }, "auth": { "login": { - "title": "登录到 CSOJ", - "loadingDescription": "正在检查可用的登录方式...", - "descriptionLocal": "输入您的凭据,或使用其他登录方式。", - "descriptionExternal": "请使用可用的登录方式。", + "title": "登录 CSOJ", + "loadingDescription": "正在检测可用登录方式...", + "descriptionLocal": "请输入账号密码,或使用其他登录方式", + "descriptionExternal": "请选择以下登录方式", "form": { "username": "用户名", "password": "密码", - "usernameRequired": "用户名是必填项", - "passwordRequired": "密码是必填项", + "usernameRequired": "用户名不能为空", + "passwordRequired": "密码不能为空", "loginButton": "登录", - "loggingIn": "正在登录..." + "loggingIn": "登录中..." }, - "separatorText": "或使用以下方式继续", - "gitlabButton": "使用 GitLab 登录", + "separatorText": "或使用以下方式登录", + "gitlabButton": "GitLab 登录", "noAccount": "还没有账号?", - "registerLink": "注册", + "registerLink": "立即注册", "toast": { "successTitle": "登录成功!", "failTitle": "登录失败", - "failDefault": "登录失败" + "failDefault": "登录失败,请检查凭证" } }, "register": { - "title": "创建账户", - "loadingDescription": "正在检查注册可用性...", - "description": "输入您的详细信息以创建一个新的 CSOJ 账户。", + "title": "注册账户", + "loadingDescription": "正在检测注册可用性...", + "description": "请输入详细信息创建新的 CSOJ 账户", "disabled": { - "title": "注册已禁用", - "description": "通过用户名和密码注册账户的功能已禁用。", - "instruction": "请返回登录页面并使用其他方式。", + "title": "注册功能已关闭", + "description": "当前暂不支持用户名密码方式注册", + "instruction": "请返回登录页使用其他登录方式", "backToLogin": "返回登录" }, "form": { "username": "用户名", "nickname": "昵称", "password": "密码", - "nicknamePlaceholder": "您的显示名称", - "usernameMinLength": "用户名至少需要 3 个字符", - "nicknameRequired": "昵称是必填项", - "passwordMinLength": "密码至少需要 6 个字符", + "nicknamePlaceholder": "请输入显示名称", + "usernameMinLength": "用户名至少需要3个字符", + "nicknameRequired": "昵称为必填项", + "passwordMinLength": "密码至少需要6个字符", "registerButton": "注册", - "creatingAccount": "正在创建账户..." + "creatingAccount": "创建账户中..." }, "alreadyHaveAccount": "已有账户?", - "loginLink": "登录", + "loginLink": "立即登录", "toast": { "successTitle": "注册成功!", - "successDescription": "您现在可以使用您的新账户登录。", + "successDescription": "现在可以使用新账户登录", "failTitle": "注册失败", - "failDefault": "注册失败" + "failDefault": "注册失败,请重试" } } }, "submissions": { "list": { "title": "我的提交记录", - "description": "您所有提交的列表。", - "loadFail": "加载提交记录失败。", - "none": "暂无提交记录。", + "description": "您所有的提交记录列表", + "loadFail": "加载提交记录失败", + "none": "暂无提交记录", "table": { - "id": "ID", - "problemId": "题目 ID", + "id": "编号", + "problemId": "题目编号", "status": "状态", - "score": "分数", + "score": "得分", "submittedAt": "提交时间" } }, "details": { - "loadFail": "加载提交详情失败。", - "notFound": "未找到提交记录。", + "loadFail": "加载提交详情失败", + "notFound": "未找到指定提交记录", "log": { "title": "实时日志", - "description": "来自判题机的实时输出。选择一个步骤以查看其日志。" + "description": "测评机实时输出,请选择步骤查看详细日志" }, "info": { "title": "提交信息", "status": "状态", - "score": "分数", - "submitted": "提交于", + "score": "得分", + "submitted": "提交时间", "problem": "题目", "user": "用户", - "cluster": "集群", + "cluster": "测评集群", "node": "节点", - "stepProgress": "第 {{current}} / {{total}} 步: {{name}}" + "stepProgress": "第 {{current}} 步 / 共 {{total}} 步:{{name}}" }, "judgeInfo": { - "title": "判题信息", - "description": "这是判题过程最终步骤的原始 JSON 输出。" + "title": "测评信息", + "description": "此为测评过程最终步骤的原始 JSON 输出" }, "queue": { "position": "队列位置", - "info": "在 {{cluster}} 队列中排在 #{{position}} 位" + "info": "在 {{cluster}} 队列中排第 {{position}} 位" }, "interrupt": { - "button": "中断", - "confirm": "您确定要中断此提交吗?此操作无法撤销。", - "successTitle": "成功", - "successDescription": "已发送中断提交的请求。", - "failTitle": "错误", - "failDefault": "中断提交失败。" + "button": "中断提交", + "confirm": "确定要中断此提交吗?此操作不可撤销", + "successTitle": "操作成功", + "successDescription": "已发送中断提交请求", + "failTitle": "操作失败", + "failDefault": "中断提交失败,请重试" } } }