Skip to content

Commit 026336e

Browse files
authored
Merge pull request #10 from ZJUSCT/i18n
i18n: add full i18n support with language toggle
2 parents 8a70359 + c821d2e commit 026336e

File tree

9 files changed

+531
-244
lines changed

9 files changed

+531
-244
lines changed

app/(main)/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { MainNav } from "@/components/layout/main-nav";
44
import { UserNav } from "@/components/layout/user-nav";
55
import { ThemeToggle } from "@/components/layout/theme-toggle";
66
import {useTranslations} from 'next-intl';
7+
import { LanguageToggle } from "@/components/layout/lang-toggle";
78

89
function MainLayout({ children }: { children: React.ReactNode }) {
910
const t = useTranslations('home');
@@ -15,6 +16,7 @@ function MainLayout({ children }: { children: React.ReactNode }) {
1516
<div className="flex w-full items-center gap-4 md:ml-auto md:gap-2 lg:gap-4">
1617
<div className="ml-auto flex-1 sm:flex-initial">
1718
</div>
19+
<LanguageToggle />
1820
<ThemeToggle />
1921
<UserNav />
2022
</div>

app/(main)/submissions/page.tsx

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,22 @@ import { Progress } from "@/components/ui/progress";
1616
import { Button } from '@/components/ui/button';
1717
import { useToast } from '@/hooks/use-toast';
1818
import { Separator } from '@/components/ui/separator';
19+
import { useTranslations } from 'next-intl';
1920

2021
const fetcher = (url: string) => api.get(url).then(res => res.data.data);
2122

2223
// Component for the list of submissions
2324
function MySubmissionsList() {
25+
const t = useTranslations('submissions');
2426
const { data: submissions, error, isLoading } = useSWR<Submission[]>('/submissions', fetcher, {
2527
refreshInterval: 5000
2628
});
2729

2830
if (isLoading) return (
2931
<Card>
3032
<CardHeader>
31-
<CardTitle>My Submissions</CardTitle>
32-
<CardDescription>A list of all your submissions.</CardDescription>
33+
<CardTitle>{t('list.title')}</CardTitle>
34+
<CardDescription>{t('list.description')}</CardDescription>
3335
</CardHeader>
3436
<CardContent>
3537
<div className="space-y-2">
@@ -38,23 +40,23 @@ function MySubmissionsList() {
3840
</CardContent>
3941
</Card>
4042
);
41-
if (error) return <div>Failed to load submissions.</div>;
43+
if (error) return <div>{t('list.loadFail')}</div>;
4244

4345
return (
4446
<Card>
4547
<CardHeader>
46-
<CardTitle>My Submissions</CardTitle>
47-
<CardDescription>A list of all your submissions.</CardDescription>
48+
<CardTitle>{t('list.title')}</CardTitle>
49+
<CardDescription>{t('list.description')}</CardDescription>
4850
</CardHeader>
4951
<CardContent>
5052
<Table>
5153
<TableHeader>
5254
<TableRow>
53-
<TableHead>ID</TableHead>
54-
<TableHead>Problem ID</TableHead>
55-
<TableHead>Status</TableHead>
56-
<TableHead>Score</TableHead>
57-
<TableHead>Submitted At</TableHead>
55+
<TableHead>{t('list.table.id')}</TableHead>
56+
<TableHead>{t('list.table.problemId')}</TableHead>
57+
<TableHead>{t('list.table.status')}</TableHead>
58+
<TableHead>{t('list.table.score')}</TableHead>
59+
<TableHead>{t('list.table.submittedAt')}</TableHead>
5860
</TableRow>
5961
</TableHeader>
6062
<TableBody>
@@ -78,7 +80,7 @@ function MySubmissionsList() {
7880
))
7981
) : (
8082
<TableRow>
81-
<TableCell colSpan={5} className="text-center">No submissions yet.</TableCell>
83+
<TableCell colSpan={5} className="text-center">{t('list.none')}</TableCell>
8284
</TableRow>
8385
)}
8486
</TableBody>
@@ -89,42 +91,45 @@ function MySubmissionsList() {
8991
}
9092

9193
function QueuePosition({ submissionId, cluster }: { submissionId: string, cluster: string }) {
94+
const t = useTranslations('submissions');
9295
const { data } = useSWR<{ position: number }>(`/submissions/${submissionId}/queue_position`, fetcher, { refreshInterval: 3000 });
9396

9497
if (data === undefined) return null;
9598

9699
return (
97100
<div className="flex items-center justify-between text-sm text-blue-500">
98-
<span className="text-muted-foreground flex items-center gap-2"><Loader2 className="h-4 w-4 animate-spin" />Queue Position</span>
99-
<span>#{data.position + 1} in {cluster} queue</span>
101+
<span className="text-muted-foreground flex items-center gap-2"><Loader2 className="h-4 w-4 animate-spin" />{t('details.queue.position')}</span>
102+
<span>{t('details.queue.info', { position: data.position + 1, cluster })}</span>
100103
</div>
101104
);
102105
}
103106

104107

108+
// Component for submission details
105109
function SubmissionDetails({ submissionId }: { submissionId: string }) {
110+
const t = useTranslations('submissions');
106111
const { toast } = useToast();
107112
const { data: submission, error, isLoading, mutate } = useSWR<Submission>(`/submissions/${submissionId}`, fetcher, {
108113
refreshInterval: (data) => (data?.status === 'Queued' || data?.status === 'Running' ? 2000 : 0),
109114
});
110115
const { data: problem } = useSWR<Problem>(submission ? `/problems/${submission.problem_id}` : null, fetcher);
111116

112117
if (isLoading) return <SubmissionDetailsSkeleton />;
113-
if (error) return <div>Failed to load submission.</div>;
114-
if (!submission) return <div>Submission not found.</div>;
118+
if (error) return <div>{t('details.loadFail')}</div>;
119+
if (!submission) return <div>{t('details.notFound')}</div>;
115120

116121
const totalSteps = problem?.workflow.length ?? 0;
117122
const progress = totalSteps > 0 ? ((submission.current_step + 1) / totalSteps) * 100 : 0;
118123
const canBeInterrupted = submission.status === 'Queued' || submission.status === 'Running';
119124

120125
const handleInterrupt = async () => {
121-
if (!confirm('Are you sure you want to interrupt this submission? This action cannot be undone.')) return;
126+
if (!confirm(t('details.interrupt.confirm'))) return;
122127
try {
123128
await api.post(`/submissions/${submissionId}/interrupt`);
124-
toast({ title: 'Success', description: 'Submission interruption request sent.' });
129+
toast({ title: t('details.interrupt.successTitle'), description: t('details.interrupt.successDescription') });
125130
mutate();
126131
} catch (err: any) {
127-
toast({ variant: 'destructive', title: 'Error', description: err.response?.data?.message || 'Failed to interrupt submission.' });
132+
toast({ variant: 'destructive', title: t('details.interrupt.failTitle'), description: err.response?.data?.message || t('details.interrupt.failDefault') });
128133
}
129134
}
130135

@@ -133,8 +138,8 @@ function SubmissionDetails({ submissionId }: { submissionId: string }) {
133138
<div className="lg:col-span-2">
134139
<Card>
135140
<CardHeader>
136-
<CardTitle>Live Log</CardTitle>
137-
<CardDescription>Real-time output from the judge. Select a step to view its log.</CardDescription>
141+
<CardTitle>{t('details.log.title')}</CardTitle>
142+
<CardDescription>{t('details.log.description')}</CardDescription>
138143
</CardHeader>
139144
<CardContent>
140145
{problem && submission ? <SubmissionLogViewer submission={submission} problem={problem} onStatusUpdate={mutate} /> : <Skeleton className="h-[60vh] w-full" />}
@@ -146,63 +151,68 @@ function SubmissionDetails({ submissionId }: { submissionId: string }) {
146151
<Card>
147152
<CardHeader>
148153
<div className="flex items-center justify-between">
149-
<CardTitle>Submission Info</CardTitle>
154+
<CardTitle>{t('details.info.title')}</CardTitle>
150155
{canBeInterrupted && (
151156
<Button variant="destructive" size="sm" onClick={handleInterrupt}>
152-
<XCircle /> Interrupt
157+
<XCircle className="h-4 w-4 mr-1" /> {t('details.interrupt.button')}
153158
</Button>
154159
)}
155160
</div>
156161
</CardHeader>
157162
<CardContent className="space-y-4 text-sm">
158163
{/* --- Submission Details Section --- */}
159164
<div className="flex items-center justify-between">
160-
<span className="text-muted-foreground flex items-center gap-2"><Hash className="h-4 w-4"/>Status</span>
165+
<span className="text-muted-foreground flex items-center gap-2"><Hash className="h-4 w-4"/>{t('details.info.status')}</span>
161166
<SubmissionStatusBadge status={submission.status} />
162167
</div>
163168
{submission.status === 'Queued' && <QueuePosition submissionId={submission.id} cluster={submission.cluster} />}
164169
{(submission.status === 'Running') && totalSteps > 0 && (
165170
<div>
166171
<Progress value={progress} className="w-full" />
167-
<p className="text-xs text-muted-foreground mt-1">Step {submission.current_step + 1} of {totalSteps}: {problem?.workflow[submission.current_step]?.name}</p>
172+
<p className="text-xs text-muted-foreground mt-1">{t('details.info.stepProgress', {
173+
current: submission.current_step + 1,
174+
total: totalSteps,
175+
name: problem?.workflow[submission.current_step]?.name ?? ''
176+
})}</p>
168177
</div>
169178
)}
170179
<div className="flex items-center justify-between">
171-
<span className="text-muted-foreground flex items-center gap-2"><Tag className="h-4 w-4"/>Score</span>
180+
<span className="text-muted-foreground flex items-center gap-2"><Tag className="h-4 w-4"/>{t('details.info.score')}</span>
172181
<span className="font-mono text-lg">{submission.score}</span>
173182
</div>
174183
<div className="flex items-center justify-between">
175-
<span className="text-muted-foreground flex items-center gap-2"><Clock className="h-4 w-4"/>Submitted</span>
184+
<span className="text-muted-foreground flex items-center gap-2"><Clock className="h-4 w-4"/>{t('details.info.submitted')}</span>
176185
<span>{formatDistanceToNow(new Date(submission.CreatedAt), { addSuffix: true })}</span>
177186
</div>
178187
<div className="flex items-center justify-between">
179-
<span className="text-muted-foreground flex items-center gap-2"><Code className="h-4 w-4"/>Problem</span>
188+
<span className="text-muted-foreground flex items-center gap-2"><Code className="h-4 w-4"/>{t('details.info.problem')}</span>
180189
<Link href={`/problems?id=${submission.problem_id}`} className="text-primary hover:underline">
181190
{submission.problem_id}
182191
</Link>
183192
</div>
184193
<div className="flex items-center justify-between">
185-
<span className="text-muted-foreground flex items-center gap-2"><User className="h-4 w-4"/>User</span>
194+
<span className="text-muted-foreground flex items-center gap-2"><User className="h-4 w-4"/>{t('details.info.user')}</span>
186195
<span>{submission.user.nickname}</span>
187196
</div>
188197
<div className="flex items-center justify-between">
189-
<span className="text-muted-foreground flex items-center gap-2"><Layers className="h-4 w-4"/>Cluster</span>
198+
<span className="text-muted-foreground flex items-center gap-2"><Layers className="h-4 w-4"/>{t('details.info.cluster')}</span>
190199
<span>{submission.cluster}</span>
191200
</div>
192201
<div className="flex items-center justify-between">
193-
<span className="text-muted-foreground flex items-center gap-2"><Server className="h-4 w-4"/>Node</span>
202+
<span className="text-muted-foreground flex items-center gap-2"><Server className="h-4 w-4"/>{t('details.info.node')}</span>
194203
<span>{submission.node || 'N/A'}</span>
195204
</div>
196205

206+
{/* --- Judge Info Section (conditionally rendered) --- */}
197207
{submission.info && Object.keys(submission.info).length > 0 && (
198208
<>
199209
<Separator className="my-4" />
200210
<div className="space-y-2">
201-
<h3 className="font-semibold tracking-tight">Judge Info</h3>
211+
<h3 className="font-semibold tracking-tight">{t('details.judgeInfo.title')}</h3>
202212
<pre className="p-4 bg-muted rounded-md text-xs overflow-auto">
203213
{JSON.stringify(submission.info, null, 2)}
204214
</pre>
205-
<p className="text-xs text-muted-foreground">This is the raw JSON output from the final step of the judging process.</p>
215+
<p className="text-xs text-muted-foreground">{t('details.judgeInfo.description')}</p>
206216
</div>
207217
</>
208218
)}
@@ -215,6 +225,7 @@ function SubmissionDetails({ submissionId }: { submissionId: string }) {
215225

216226

217227
function SubmissionDetailsSkeleton() {
228+
const t = useTranslations('submissions');
218229
return (
219230
<div className="grid gap-6 lg:grid-cols-3">
220231
<div className="lg:col-span-2 space-y-6">
@@ -233,7 +244,7 @@ function SubmissionDetailsSkeleton() {
233244
</div>
234245
<div className="space-y-6">
235246
<Card>
236-
<CardHeader><Skeleton className="h-6 w-3/4" /></CardHeader>
247+
<CardHeader><CardTitle>{t('details.info.title')}</CardTitle></CardHeader>
237248
<CardContent className="space-y-4">
238249
{[...Array(6)].map((_, i) => (
239250
<div key={i} className="flex justify-between">
@@ -266,4 +277,4 @@ export default function MySubmissionsPage() {
266277
<SubmissionsPageContent />
267278
</Suspense>
268279
);
269-
}
280+
}

app/layout.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import { SWRProvider } from "@/providers/swr-provider";
66
import { AuthProvider } from "@/providers/auth-provider";
77
import { Toaster } from "@/components/ui/toaster";
88
import { ThemeProvider } from "@/providers/theme-provider";
9-
import {NextIntlClientProvider} from 'next-intl';
9+
// import {NextIntlClientProvider} from 'next-intl';
10+
import { ClientIntlProvider } from "@/providers/i18n-provider";
1011

1112
const inter = Inter({ subsets: ["latin"], variable: "--font-sans" });
1213

@@ -28,7 +29,7 @@ export default function RootLayout({
2829
inter.variable
2930
)}
3031
>
31-
<NextIntlClientProvider>
32+
<ClientIntlProvider>
3233
<ThemeProvider
3334
attribute="class"
3435
defaultTheme="system"
@@ -39,7 +40,7 @@ export default function RootLayout({
3940
</AuthProvider>
4041
<Toaster />
4142
</ThemeProvider>
42-
</NextIntlClientProvider>
43+
</ClientIntlProvider>
4344
</body>
4445
</html>
4546
);

components/layout/lang-toggle.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// FILE: components/layout/language-switcher.tsx
2+
"use client";
3+
4+
import { useClientLocale } from '@/providers/i18n-provider';
5+
import { Button } from '@/components/ui/button';
6+
import {
7+
DropdownMenu,
8+
DropdownMenuContent,
9+
DropdownMenuItem,
10+
DropdownMenuTrigger,
11+
} from '@/components/ui/dropdown-menu';
12+
import { Languages } from 'lucide-react';
13+
14+
const LOCALE_MAP: Record<string, string> = {
15+
'zh': '简体中文',
16+
'en': 'English',
17+
};
18+
19+
export function LanguageToggle() {
20+
const { locale, switchLocale } = useClientLocale();
21+
22+
return (
23+
<DropdownMenu>
24+
<DropdownMenuTrigger asChild>
25+
<Button variant="outline" size="icon" title="Change Language">
26+
<Languages className="h-[1.2rem] w-[1.2rem]" />
27+
<span className="sr-only">Change Language</span>
28+
</Button>
29+
</DropdownMenuTrigger>
30+
<DropdownMenuContent className="w-24" align="end">
31+
{Object.entries(LOCALE_MAP).map(([code, name]) => (
32+
<DropdownMenuItem
33+
key={code}
34+
onClick={() => switchLocale(code)}
35+
className={locale === code ? "font-bold text-primary" : ""}
36+
>
37+
{name}
38+
</DropdownMenuItem>
39+
))}
40+
</DropdownMenuContent>
41+
</DropdownMenu>
42+
);
43+
}

i18n/request.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import {getRequestConfig} from 'next-intl/server';
22

33
export default getRequestConfig(async () => {
4-
// Static for now, we'll change this later
5-
const locale = 'zh';
6-
4+
const locale = 'en';
75
return {
86
locale,
9-
messages: (await import(`../messages/${locale}.json`)).default
7+
messages: {} as any
108
};
119
});

0 commit comments

Comments
 (0)