Skip to content

Commit e7151f5

Browse files
committed
i18n: initial translation support
1 parent e4180aa commit e7151f5

File tree

18 files changed

+783
-182
lines changed

18 files changed

+783
-182
lines changed

app/(main)/contests/page.tsx

Lines changed: 56 additions & 46 deletions
Large diffs are not rendered by default.

app/(main)/layout.tsx

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ import withAuth from "@/components/layout/with-auth";
33
import { MainNav } from "@/components/layout/main-nav";
44
import { UserNav } from "@/components/layout/user-nav";
55
import { ThemeToggle } from "@/components/layout/theme-toggle";
6+
import {useTranslations} from 'next-intl';
67

78
function MainLayout({ children }: { children: React.ReactNode }) {
9+
const t = useTranslations('home');
10+
811
return (
912
<div className="flex min-h-screen w-full flex-col">
1013
<header className="sticky top-0 flex h-16 items-center gap-4 border-b bg-background px-4 md:px-6 z-50">
@@ -21,24 +24,28 @@ function MainLayout({ children }: { children: React.ReactNode }) {
2124
</main>
2225
<footer className="mt-auto border-t py-4">
2326
<div className="container mx-auto text-center text-sm text-muted-foreground">
24-
Powered by{" "}
25-
<a
26-
href="https://github.com/ZJUSCT/CSOJ"
27-
target="_blank"
28-
rel="noopener noreferrer"
29-
className="font-medium text-primary underline-offset-4 hover:underline"
30-
>
31-
ZJUSCT/CSOJ
32-
</a>{" "}
33-
&{" "}
34-
<a
35-
href="https://github.com/ZJUSCT/CSOJ-WebUI"
36-
target="_blank"
37-
rel="noopener noreferrer"
38-
className="font-medium text-primary underline-offset-4 hover:underline"
39-
>
40-
ZJUSCT/CSOJ-WebUI
41-
</a>
27+
{t.rich('power_by', {
28+
github1: (chunks) => (
29+
<a
30+
href="https://github.com/ZJUSCT/CSOJ"
31+
target="_blank"
32+
rel="noopener noreferrer"
33+
className="font-medium text-primary underline-offset-4 hover:underline"
34+
>
35+
{chunks}
36+
</a>
37+
),
38+
github2: (chunks) => (
39+
<a
40+
href="https://github.com/ZJUSCT/CSOJ-WebUI"
41+
target="_blank"
42+
rel="noopener noreferrer"
43+
className="font-medium text-primary underline-offset-4 hover:underline"
44+
>
45+
{chunks}
46+
</a>
47+
),
48+
})}
4249
</div>
4350
</footer>
4451
</div>

app/(main)/problems/page.tsx

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22
import { useSearchParams } from 'next/navigation';
33
import useSWR from 'swr';
4+
import { useTranslations } from 'next-intl'; // Import useTranslations
45
import api from '@/lib/api';
56
import { Problem, Submission } from '@/lib/types';
67
import MarkdownViewer from '@/components/shared/markdown-viewer';
@@ -16,24 +17,25 @@ import { Suspense } from 'react';
1617
const fetcher = (url: string) => api.get(url).then(res => res.data.data);
1718

1819
function UserSubmissionsForProblem({ problemId }: { problemId: string }) {
20+
const t = useTranslations('ProblemDetails');
1921
const { data: allSubmissions, isLoading } = useSWR<Submission[]>('/submissions', fetcher);
2022

2123
if (isLoading) return <Skeleton className="h-40 w-full" />;
2224

2325
const problemSubmissions = allSubmissions?.filter(sub => sub.problem_id === problemId) || [];
2426

2527
if (problemSubmissions.length === 0) {
26-
return <p className="text-sm text-muted-foreground">You have not made any submissions for this problem yet.</p>;
28+
return <p className="text-sm text-muted-foreground">{t('submissions.none')}</p>;
2729
}
2830

2931
return (
3032
<Table>
3133
<TableHeader>
3234
<TableRow>
33-
<TableHead>Submission ID</TableHead>
34-
<TableHead>Status</TableHead>
35-
<TableHead>Score</TableHead>
36-
<TableHead>Date</TableHead>
35+
<TableHead>{t('submissions.id')}</TableHead>
36+
<TableHead>{t('submissions.status')}</TableHead>
37+
<TableHead>{t('submissions.score')}</TableHead>
38+
<TableHead>{t('submissions.date')}</TableHead>
3739
</TableRow>
3840
</TableHeader>
3941
<TableBody>
@@ -55,25 +57,33 @@ function UserSubmissionsForProblem({ problemId }: { problemId: string }) {
5557
}
5658

5759
function ProblemDetails() {
60+
const t = useTranslations('ProblemDetails');
5861
const searchParams = useSearchParams();
5962
const problemId = searchParams.get('id');
6063
const { data: problem, error, isLoading } = useSWR<Problem>(problemId ? `/problems/${problemId}` : null, fetcher);
6164

6265
if (!problemId) {
63-
return <Card><CardHeader><CardTitle>No Problem Selected</CardTitle><CardDescription>Please select a problem to view its details.</CardDescription></CardHeader></Card>;
66+
return (
67+
<Card>
68+
<CardHeader>
69+
<CardTitle>{t('noProblem.title')}</CardTitle>
70+
<CardDescription>{t('noProblem.description')}</CardDescription>
71+
</CardHeader>
72+
</Card>
73+
);
6474
}
6575

6676
if (isLoading) return <div><Skeleton className="h-screen w-full" /></div>;
67-
if (error) return <div>Failed to load problem. You may not have access to it yet.</div>;
68-
if (!problem) return <div>Problem not found.</div>;
77+
if (error) return <div>{t('details.loadFail')}</div>;
78+
if (!problem) return <div>{t('details.notFound')}</div>;
6979

7080
return (
7181
<div className="grid gap-8 lg:grid-cols-2">
7282
<div className="space-y-6">
7383
<Card>
7484
<CardHeader>
7585
<CardTitle className="text-2xl">{problem.name}</CardTitle>
76-
<CardDescription>Problem ID: {problem.id}</CardDescription>
86+
<CardDescription>{t('details.id')}: {problem.id}</CardDescription>
7787
</CardHeader>
7888
<CardContent>
7989
<MarkdownViewer
@@ -87,15 +97,15 @@ function ProblemDetails() {
8797
<div className="space-y-6">
8898
<Card>
8999
<CardHeader>
90-
<CardTitle>Submit Solution</CardTitle>
100+
<CardTitle>{t('submitForm.title')}</CardTitle>
91101
</CardHeader>
92102
<CardContent>
93103
<SubmissionUploadForm problemId={problem.id} uploadLimits={problem.upload} />
94104
</CardContent>
95105
</Card>
96106
<Card>
97107
<CardHeader>
98-
<CardTitle>Your Submissions</CardTitle>
108+
<CardTitle>{t('submissions.title')}</CardTitle>
99109
</CardHeader>
100110
<CardContent>
101111
<UserSubmissionsForProblem problemId={problem.id} />

app/(main)/profile/page.tsx

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,22 @@ import { getInitials } from '@/lib/utils';
1515
import { useState } from 'react';
1616
import { useRouter } from 'next/navigation';
1717
import { TokenInfoCard } from '@/components/profile/token-info-card';
18-
19-
const profileSchema = z.object({
20-
nickname: z.string().min(1, 'Nickname is required').max(50),
21-
signature: z.string().max(100).optional(),
22-
});
18+
import { useTranslations } from 'next-intl'; // Import useTranslations
2319

2420
export default function ProfilePage() {
21+
const t = useTranslations('Profile'); // Initialize translations
2522
const { user, isLoading, logout } = useAuth();
2623
const { toast } = useToast();
2724
const router = useRouter();
2825
const [isUploading, setIsUploading] = useState(false);
2926

27+
// Schema definition must be inside the component or outside, but using t() inside requires it to be inside
28+
// or passed as a function argument, let's redefine it here to use t() for error messages.
29+
const profileSchema = z.object({
30+
nickname: z.string().min(1, t('form.nicknameRequired')).max(50),
31+
signature: z.string().max(100).optional(),
32+
});
33+
3034
const form = useForm<z.infer<typeof profileSchema>>({
3135
resolver: zodResolver(profileSchema),
3236
values: {
@@ -47,14 +51,14 @@ export default function ProfilePage() {
4751
await api.post('/user/avatar', formData, {
4852
headers: { 'Content-Type': 'multipart/form-data' },
4953
});
50-
toast({ title: 'Avatar updated successfully!' });
54+
toast({ title: t('avatar.uploadSuccess') });
5155
// Forcing a reload to get the new user profile with updated avatar URL
5256
window.location.reload();
5357
} catch (error: any) {
5458
toast({
5559
variant: 'destructive',
56-
title: 'Upload failed',
57-
description: error.response?.data?.message || 'Could not upload avatar.',
60+
title: t('avatar.uploadFailTitle'),
61+
description: error.response?.data?.message || t('avatar.uploadFailDescription'),
5862
});
5963
} finally {
6064
setIsUploading(false);
@@ -64,14 +68,14 @@ export default function ProfilePage() {
6468
const onSubmit = async (values: z.infer<typeof profileSchema>) => {
6569
try {
6670
await api.patch('/user/profile', values);
67-
toast({ title: 'Profile updated successfully!' });
71+
toast({ title: t('form.updateSuccess') });
6872
// Forcing a reload to get the new user profile
6973
window.location.reload();
7074
} catch (error: any) {
7175
toast({
7276
variant: 'destructive',
73-
title: 'Update failed',
74-
description: error.response?.data?.message || 'Could not update profile.',
77+
title: t('form.updateFailTitle'),
78+
description: error.response?.data?.message || t('form.updateFailDescription'),
7579
});
7680
}
7781
};
@@ -85,12 +89,14 @@ export default function ProfilePage() {
8589
return <Skeleton className="w-full h-96" />;
8690
}
8791

92+
const isSubmitting = form.formState.isSubmitting;
93+
8894
return (
8995
<div className="grid auto-rows-min gap-6 lg:grid-cols-3">
9096
<Card>
9197
<CardHeader>
92-
<CardTitle>Avatar</CardTitle>
93-
<CardDescription>Update your profile picture.</CardDescription>
98+
<CardTitle>{t('avatar.title')}</CardTitle>
99+
<CardDescription>{t('avatar.description')}</CardDescription>
94100
</CardHeader>
95101
<CardContent className="flex flex-col items-center gap-4">
96102
<Avatar className="h-32 w-32">
@@ -99,31 +105,31 @@ export default function ProfilePage() {
99105
</Avatar>
100106
<Input id="avatar-upload" type="file" accept="image/*" onChange={handleAvatarUpload} className="hidden" />
101107
<Button asChild variant="outline">
102-
<label htmlFor="avatar-upload">{isUploading ? "Uploading..." : "Change Avatar"}</label>
108+
<label htmlFor="avatar-upload">{isUploading ? t('avatar.uploading') : t('avatar.change')}</label>
103109
</Button>
104110
</CardContent>
105111
</Card>
106112

107113
<Card className="lg:col-span-2 lg:row-span-2 flex flex-col">
108114
<CardHeader>
109-
<CardTitle>Profile Information</CardTitle>
110-
<CardDescription>Update your account details. Username cannot be changed.</CardDescription>
115+
<CardTitle>{t('form.title')}</CardTitle>
116+
<CardDescription>{t('form.description')}</CardDescription>
111117
</CardHeader>
112118
<CardContent className="flex flex-1 flex-col justify-between">
113119
<Form {...form}>
114120
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
115121
<FormItem>
116-
<FormLabel>Username</FormLabel>
122+
<FormLabel>{t('form.username')}</FormLabel>
117123
<Input disabled value={user.username} />
118124
</FormItem>
119125
<FormField
120126
control={form.control}
121127
name="nickname"
122128
render={({ field }) => (
123129
<FormItem>
124-
<FormLabel>Nickname</FormLabel>
130+
<FormLabel>{t('form.nickname')}</FormLabel>
125131
<FormControl>
126-
<Input placeholder="Your display name" {...field} />
132+
<Input placeholder={t('form.nicknamePlaceholder')} {...field} />
127133
</FormControl>
128134
<FormMessage />
129135
</FormItem>
@@ -134,21 +140,21 @@ export default function ProfilePage() {
134140
name="signature"
135141
render={({ field }) => (
136142
<FormItem>
137-
<FormLabel>Signature</FormLabel>
143+
<FormLabel>{t('form.signature')}</FormLabel>
138144
<FormControl>
139-
<Input placeholder="A short bio" {...field} />
145+
<Input placeholder={t('form.signaturePlaceholder')} {...field} />
140146
</FormControl>
141147
<FormMessage />
142148
</FormItem>
143149
)}
144150
/>
145-
<Button type="submit" disabled={form.formState.isSubmitting}>
146-
{form.formState.isSubmitting ? 'Saving...' : 'Save Changes'}
151+
<Button type="submit" disabled={isSubmitting}>
152+
{isSubmitting ? t('form.saving') : t('form.saveChanges')}
147153
</Button>
148154
</form>
149155
</Form>
150156
<div className="border-t pt-6 mt-6">
151-
<Button variant="destructive" onClick={handleLogout}>Log Out</Button>
157+
<Button variant="destructive" onClick={handleLogout}>{t('logout')}</Button>
152158
</div>
153159
</CardContent>
154160
</Card>

app/layout.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ 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';
910

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

@@ -27,16 +28,18 @@ export default function RootLayout({
2728
inter.variable
2829
)}
2930
>
30-
<ThemeProvider
31-
attribute="class"
32-
defaultTheme="system"
33-
enableSystem
34-
>
35-
<AuthProvider>
36-
<SWRProvider>{children}</SWRProvider>
37-
</AuthProvider>
38-
<Toaster />
39-
</ThemeProvider>
31+
<NextIntlClientProvider>
32+
<ThemeProvider
33+
attribute="class"
34+
defaultTheme="system"
35+
enableSystem
36+
>
37+
<AuthProvider>
38+
<SWRProvider>{children}</SWRProvider>
39+
</AuthProvider>
40+
<Toaster />
41+
</ThemeProvider>
42+
</NextIntlClientProvider>
4043
</body>
4144
</html>
4245
);

0 commit comments

Comments
 (0)