refactor(i18n): replace hardcoded texts with i18n message keys

This commit is contained in:
cfngc4594 2025-04-15 18:22:21 +08:00
parent 8b276ae91e
commit 4428a29306
32 changed files with 608 additions and 154 deletions

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"i18n-ally.localesPaths": [
"messages",
"src/i18n"
]
}

130
messages/en.json Normal file
View File

@ -0,0 +1,130 @@
{
"AppearanceSettings": {
"title": "Choose a theme",
"items": {
"System": "System",
"Light": "Light",
"Dark": "Dark"
}
},
"AvatarButton": {
"Settings": "Settings",
"LogIn": "LogIn",
"LogOut": "LogOut"
},
"Banner": {
"Text": "Star this project if you like it."
},
"BackButton": "Back",
"Bot": {
"title": "Ask Bot",
"description": "Powered by Vercel Ai SDK",
"placeholder": "Bot will automatically get your current code"
},
"BotVisibilityToggle": {
"open": "Open Bot",
"close": "Close Bot"
},
"DetailsPage": {
"BackButton": "All Submissions",
"Time": "Submitted on",
"Input": "Input",
"ExpectedOutput": "Expected Output",
"ActualOutput": "Acutal Output",
"Code": "Code"
},
"Difficulty": {
"EASY": "EASY",
"MEDIUM": "MEDIUM",
"HARD": "HARD"
},
"LanguageSettings": {
"en": {
"flag": "🇺🇸",
"name": "English"
},
"zh": {
"flag": "🇨🇳",
"name": "Chinese"
}
},
"PlaygroundHeader": {
"RunCodeButton": {
"TooltipTrigger": {
"loading": "Running...",
"ready": "Run"
},
"TooltipContent": "Run Code"
}
},
"ProblemPage": {
"Description": "Description",
"Solutions": "Solutions",
"Submissions": "Submissions",
"Details": "Details",
"Code": "Code",
"Testcase": "Testcase",
"Bot": "Bot"
},
"ProblemsetPage": {
"Status": "Status",
"Title": "Title",
"Difficulty": "Difficulty"
},
"SettingsDialog": {
"title": "Settings",
"description": "Customize your settings here.",
"breadcrumb": "Settings",
"nav": {
"Appearance": "Appearance",
"Language": "Language",
"CodeEditor": "CodeEditor",
"Advanced": "Advanced"
}
},
"StatusMessage": {
"PD": "Pending",
"QD": "Queued",
"CP": "Compiling",
"CE": "Compilation Error",
"CS": "Compilation Success",
"RU": "Running",
"TLE": "Time Limit Exceeded",
"MLE": "Memory Limit Exceeded",
"RE": "Runtime Error",
"AC": "Accepted",
"WA": "Wrong Answer",
"SE": "System Error"
},
"SubmissionsTable": {
"Index": "Index",
"Status": "Status",
"Language": "Language",
"Time": "Time",
"Memory": "Memory"
},
"WorkspaceEditorHeader": {
"LspStatusButton": {
"TooltipContent": "Language Server"
},
"ResetButton": {
"TooltipContent": "Reset Code"
},
"UndoButton": {
"TooltipContent": "Undo"
},
"RedoButton": {
"TooltipContent": "Redo"
},
"FormatButton": {
"TooltipContent": "Format"
},
"CopyButton": {
"TooltipContent": "Copy"
}
},
"WorkspaceEditorFooter": {
"Row": "Row",
"Column": "Column"
}
}

130
messages/zh.json Normal file
View File

@ -0,0 +1,130 @@
{
"AppearanceSettings": {
"title": "选择一个主题",
"items": {
"System": "系统",
"Light": "浅色",
"Dark": "深色"
}
},
"AvatarButton": {
"Settings": "设置",
"LogIn": "登录",
"LogOut": "登出"
},
"Banner": {
"Text": "如果喜欢该项目不妨收藏一下"
},
"BackButton": "返回",
"Bot": {
"title": "询问AI助手",
"description": "由Vercel Ai SDK驱动",
"placeholder": "AI助手将自动获取您当前的代码"
},
"BotVisibilityToggle": {
"open": "打开AI助手",
"close": "关闭AI助手"
},
"DetailsPage": {
"BackButton": "所有提交记录",
"Time": "提交于",
"Input": "输入",
"ExpectedOutput": "期望输出",
"ActualOutput": "实际输出",
"Code": "代码"
},
"Difficulty": {
"EASY": "简单",
"MEDIUM": "中等",
"HARD": "困难"
},
"LanguageSettings": {
"en": {
"flag": "🇺🇸",
"name": "英语"
},
"zh": {
"flag": "🇨🇳",
"name": "中文"
}
},
"PlaygroundHeader": {
"RunCodeButton": {
"TooltipTrigger": {
"loading": "运行中...",
"ready": "运行"
},
"TooltipContent": "运行代码"
}
},
"ProblemPage": {
"Description": "题目描述",
"Solutions": "题解",
"Submissions": "提交记录",
"Details": "详情",
"Code": "代码",
"Testcase": "测试用例",
"Bot": "AI助手"
},
"ProblemsetPage": {
"Status": "状态",
"Title": "题目",
"Difficulty": "难度"
},
"SettingsDialog": {
"title": "设置",
"description": "在此处自定义设置。",
"breadcrumb": "设置",
"nav": {
"Appearance": "外观",
"Language": "语言",
"CodeEditor": "代码编辑器",
"Advanced": "高级设置"
}
},
"StatusMessage": {
"PD": "待处理",
"QD": "排队中",
"CP": "编译中",
"CE": "编译错误",
"CS": "编译成功",
"RU": "运行中",
"TLE": "超出时间限制",
"MLE": "超出内存限制",
"RE": "运行时错误",
"AC": "通过",
"WA": "解答错误",
"SE": "系统错误"
},
"SubmissionsTable": {
"Index": "序号",
"Status": "状态",
"Language": "语言",
"Time": "执行用时",
"Memory": "消耗内存"
},
"WorkspaceEditorHeader": {
"LspStatusButton": {
"TooltipContent": "语言服务"
},
"ResetButton": {
"TooltipContent": "重置代码"
},
"UndoButton": {
"TooltipContent": "撤销"
},
"RedoButton": {
"TooltipContent": "恢复"
},
"FormatButton": {
"TooltipContent": "格式化"
},
"CopyButton": {
"TooltipContent": "复制"
}
},
"WorkspaceEditorFooter": {
"Row": "行",
"Column": "列"
}
}

View File

@ -9,6 +9,7 @@ import {
} from "@/components/ui/tooltip";
import { useCallback } from "react";
import { useChat } from "@ai-sdk/react";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { useProblem } from "@/hooks/use-problem";
import MdxPreview from "@/components/mdx-preview";
@ -19,6 +20,7 @@ import { ChatMessageList } from "@/components/ui/chat/chat-message-list";
import { ChatBubble, ChatBubbleMessage } from "@/components/ui/chat/chat-bubble";
export default function Bot() {
const t = useTranslations("Bot");
const { problemId, problem, currentLang, currentValue } = useProblem();
const { messages, input, handleInputChange, setMessages, handleSubmit } = useChat({
@ -58,12 +60,12 @@ export default function Bot() {
{!messages.some(
(message) => message.role === "user" || message.role === "assistant"
) && (
<div className="h-full flex flex-col items-center justify-center gap-2 text-muted-foreground">
<BotIcon />
<span>Ask Bot</span>
<span className="font-thin text-xs">Powered by Vercel Ai SDK</span>
</div>
)}
<div className="h-full flex flex-col items-center justify-center gap-2 text-muted-foreground">
<BotIcon />
<span>{t("title")}</span>
<span className="font-thin text-xs">{t("description")}</span>
</div>
)}
<div className="absolute h-full w-full">
<ScrollArea className="h-full [&>[data-radix-scroll-area-viewport]>div:min-w-0 [&>[data-radix-scroll-area-viewport]>div]:!block">
@ -100,7 +102,7 @@ export default function Bot() {
}
}}
className="h-full bg-muted border-transparent shadow-none rounded-lg"
placeholder="Bot will automatically get your current code"
placeholder={t("placeholder")}
/>
<TooltipProvider delayDuration={0}>

View File

@ -0,0 +1,8 @@
import { getUserLocale } from "@/i18n/locale";
import DetailsPage from "@/app/(app)/problems/[id]/@Details/page";
export default async function DetailsLayout() {
const locale = await getUserLocale();
return <DetailsPage locale={locale} />;
}

View File

@ -7,7 +7,10 @@ import {
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Locale } from "@/config/i18n";
import { getLocale } from "@/lib/i18n";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import { ArrowLeftIcon } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
@ -20,7 +23,14 @@ import type { TestcaseResultWithTestcase } from "@/types/prisma";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { formatDistanceToNow, isBefore, subDays, format } from "date-fns";
export default function DetailsPage() {
interface DetailsPageProps {
locale: Locale;
}
export default function DetailsPage({ locale }: DetailsPageProps) {
const localeInstance = getLocale(locale);
const t = useTranslations("DetailsPage");
const s = useTranslations("StatusMessage");
const { api, submission } = useDockviewStore();
const { editorLanguageConfigs, problemId } = useProblem();
const [lastFailedTestcase, setLastFailedTestcase] =
@ -53,7 +63,7 @@ export default function DetailsPage() {
const createdAt = new Date(submission.createdAt);
const submittedDisplay = isBefore(createdAt, subDays(new Date(), 1))
? format(createdAt, "yyyy-MM-dd")
: formatDistanceToNow(createdAt, { addSuffix: true });
: formatDistanceToNow(createdAt, { addSuffix: true, locale: localeInstance });
const source = `\`\`\`${submission?.language}\n${submission?.code}\n\`\`\``;
@ -76,7 +86,7 @@ export default function DetailsPage() {
className="h-8 w-auto p-2 hover:bg-transparent text-muted-foreground hover:text-foreground"
>
<ArrowLeftIcon size={16} aria-hidden="true" />
<span>All Submissions</span>
<span>{t("BackButton")}</span>
</Button>
</div>
<div className="relative flex-1">
@ -92,10 +102,10 @@ export default function DetailsPage() {
getStatusColorClass(submission.status)
)}
>
<span>{statusMap.get(submission.status)?.message}</span>
<span>{s(`${statusMap.get(submission.status)?.message}`)}</span>
</h3>
<div className="flex max-w-full flex-1 items-center gap-1 overflow-hidden text-xs">
<span className="whitespace-nowrap">Submitted on</span>
<span className="whitespace-nowrap mr-1">{t("Time")}</span>
<span className="max-w-full truncate">
{submittedDisplay}
</span>
@ -118,7 +128,7 @@ export default function DetailsPage() {
className="bg-background has-focus-visible:border-ring has-focus-visible:ring-ring/50 relative border px-4 py-1 outline-none first:rounded-t-md last:rounded-b-md last:border-b has-focus-visible:z-10 has-focus-visible:ring-[3px]"
>
<AccordionTrigger className="py-2 text-[15px] leading-6 hover:no-underline focus-visible:ring-0">
<h4 className="text-sm font-medium">Input</h4>
<h4 className="text-sm font-medium">{t("Input")}</h4>
</AccordionTrigger>
<AccordionContent className="text-muted-foreground pb-2">
<div className="space-y-4">
@ -142,7 +152,7 @@ export default function DetailsPage() {
</div>
<div className="space-y-2">
<h4 className="text-sm font-medium">Expected Output</h4>
<h4 className="text-sm font-medium">{t("ExpectedOutput")}</h4>
<Input
type="text"
value={lastFailedTestcase.testcase.expectedOutput}
@ -153,7 +163,7 @@ export default function DetailsPage() {
{submission.status === "WA" && (
<div className="space-y-2">
<h4 className="text-sm font-medium">Your Output</h4>
<h4 className="text-sm font-medium">{t("ActualOutput")}</h4>
<Input
type="text"
value={lastFailedTestcase.output}
@ -167,14 +177,14 @@ export default function DetailsPage() {
{(submission.status === "CE" ||
submission.status === "SE") && (
<MdxPreview
source={`\`\`\`shell\n${submission.message}\n\`\`\``}
/>
)}
<MdxPreview
source={`\`\`\`shell\n${submission.message}\n\`\`\``}
/>
)}
<div className="flex items-center pb-2">
<div className="flex items-center gap-2 text-sm font-medium">
<span>Code</span>
<span>{t("Code")}</span>
<Separator
orientation="vertical"
className="h-4 bg-muted-foreground"

View File

@ -1,5 +1,6 @@
import prisma from "@/lib/prisma";
import { notFound } from "next/navigation";
import { getUserLocale } from "@/i18n/locale";
import SubmissionsTable from "@/components/submissions-table";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
@ -37,10 +38,12 @@ export default async function SubmissionsPage({ params }: SubmissionsPageProps)
return notFound();
}
const locale = await getUserLocale();
return (
<div className="px-3 flex flex-col h-full border border-t-0 border-muted rounded-b-3xl bg-background">
<ScrollArea className="h-full">
<SubmissionsTable submissions={problem.submissions} />
<SubmissionsTable locale={locale} submissions={problem.submissions} />
<ScrollBar orientation="horizontal" />
</ScrollArea>
</div>

View File

@ -1,5 +1,6 @@
import prisma from "@/lib/prisma";
import { notFound } from "next/navigation";
import { getUserLocale } from "@/i18n/locale";
import ProblemPage from "@/app/(app)/problems/[id]/page";
import { ProblemStoreProvider } from "@/providers/problem-store-provider";
import { PlaygroundHeader } from "@/components/features/playground/header";
@ -59,6 +60,8 @@ export default async function ProblemLayout({
return notFound();
}
const locale = await getUserLocale();
return (
<div className="flex flex-col h-screen">
<ProblemStoreProvider
@ -71,6 +74,7 @@ export default async function ProblemLayout({
<PlaygroundHeader />
<main className="flex flex-grow overflow-y-hidden p-2.5 pt-0">
<ProblemPage
locale={locale}
Description={Description}
Solutions={Solutions}
Submissions={Submissions}

View File

@ -8,10 +8,14 @@ import {
SquareCheckIcon,
SquarePenIcon,
} from "lucide-react";
import { Locale } from "@/config/i18n";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import Dockview from "@/components/dockview";
import { useDockviewStore } from "@/stores/dockview";
interface ProblemPageProps {
locale: Locale;
Description: React.ReactNode;
Solutions: React.ReactNode;
Submissions: React.ReactNode;
@ -22,6 +26,7 @@ interface ProblemPageProps {
}
export default function ProblemPage({
locale,
Description,
Solutions,
Submissions,
@ -30,9 +35,17 @@ export default function ProblemPage({
Testcase,
Bot,
}: ProblemPageProps) {
const [key, setKey] = useState(0);
const { setApi } = useDockviewStore();
const t = useTranslations("ProblemPage");
useEffect(() => {
setKey((prevKey) => prevKey + 1);
}, [locale]);
return (
<Dockview
key={key}
storageKey="dockview:problem"
onApiReady={setApi}
options={[
@ -40,7 +53,7 @@ export default function ProblemPage({
id: "Description",
component: "Description",
tabComponent: "Description",
title: "Description",
title: t("Description"),
params: {
icon: FileTextIcon,
content: Description,
@ -50,7 +63,7 @@ export default function ProblemPage({
id: "Solutions",
component: "Solutions",
tabComponent: "Solutions",
title: "Solutions",
title: t("Solutions"),
params: {
icon: FlaskConicalIcon,
content: Solutions,
@ -65,7 +78,7 @@ export default function ProblemPage({
id: "Submissions",
component: "Submissions",
tabComponent: "Submissions",
title: "Submissions",
title: t("Submissions"),
params: {
icon: CircleCheckBigIcon,
content: Submissions,
@ -80,7 +93,7 @@ export default function ProblemPage({
id: "Details",
component: "Details",
tabComponent: "Details",
title: "Details",
title: t("Details"),
params: {
icon: CircleCheckBigIcon,
content: Details,
@ -91,7 +104,7 @@ export default function ProblemPage({
id: "Code",
component: "Code",
tabComponent: "Code",
title: "Code",
title: t("Code"),
params: {
icon: SquarePenIcon,
content: Code,
@ -105,7 +118,7 @@ export default function ProblemPage({
id: "Testcase",
component: "Testcase",
tabComponent: "Testcase",
title: "Testcase",
title: t("Testcase"),
params: {
icon: SquareCheckIcon,
content: Testcase,
@ -119,7 +132,7 @@ export default function ProblemPage({
id: "Bot",
component: "Bot",
tabComponent: "Bot",
title: "Bot",
title: t("Bot"),
params: {
icon: BotIcon,
content: Bot,

View File

@ -9,6 +9,7 @@ import {
} from "@/components/ui/table";
import prisma from "@/lib/prisma";
import { auth } from "@/lib/auth";
import { getTranslations } from "next-intl/server";
import { getDifficultyColorClass } from "@/lib/utils";
import { CircleCheckBigIcon, CircleDotIcon } from "lucide-react";
@ -32,13 +33,15 @@ export default async function ProblemsetPage() {
const completedProblems = new Set(submissions.filter(s => s.status === "AC").map(s => s.problemId));
const attemptedProblems = new Set(submissions.filter(s => s.status !== "AC").map(s => s.problemId));
const t = await getTranslations();
return (
<Table>
<TableHeader className="bg-transparent">
<TableRow className="hover:bg-transparent">
<TableHead className="w-1/3">Status</TableHead>
<TableHead className="w-1/3">Title</TableHead>
<TableHead className="w-1/3">Difficulty</TableHead>
<TableHead className="w-1/3">{t("ProblemsetPage.Status")}</TableHead>
<TableHead className="w-1/3">{t("ProblemsetPage.Title")}</TableHead>
<TableHead className="w-1/3">{t("ProblemsetPage.Difficulty")}</TableHead>
</TableRow>
</TableHeader>
<TableBody className="[&_td:first-child]:rounded-l-lg [&_td:last-child]:rounded-r-lg">
@ -60,7 +63,7 @@ export default async function ProblemsetPage() {
</Link>
</TableCell>
<TableCell className={`py-2.5 ${getDifficultyColorClass(problem.difficulty)}`}>
{problem.difficulty}
{t(`Difficulty.${problem.difficulty}`)}
</TableCell>
</TableRow>
))}

View File

@ -2,22 +2,25 @@
import Image from "next/image";
import { useTheme } from "next-themes";
import { useTranslations } from "next-intl";
import { CheckIcon, MinusIcon } from "lucide-react";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
const items = [
{ value: "system", label: "System", image: "/ui-system.png" },
{ value: "light", label: "Light", image: "/ui-light.png" },
{ value: "dark", label: "Dark", image: "/ui-dark.png" },
];
export default function AppearanceSettings() {
const t = useTranslations("AppearanceSettings");
const items = [
{ value: "system", label: t("items.System"), image: "/ui-system.png" },
{ value: "light", label: t("items.Light"), image: "/ui-light.png" },
{ value: "dark", label: t("items.Dark"), image: "/ui-dark.png" },
];
const { theme, setTheme } = useTheme();
return (
<fieldset className="space-y-4">
<legend className="text-foreground text-sm leading-none font-medium">
Choose a theme
{t("title")}
</legend>
<RadioGroup className="flex gap-3" defaultValue={theme}>
{items.map((item) => (

View File

@ -9,6 +9,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { auth, signOut } from "@/lib/auth";
import { getTranslations } from "next-intl/server";
import { Skeleton } from "@/components/ui/skeleton";
import LogInButton from "@/components/log-in-button";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
@ -28,6 +29,7 @@ async function handleLogOut() {
export async function AvatarButton() {
const session = await auth();
const t = await getTranslations("AvatarButton");
const isLoggedIn = !!session?.user;
const image = session?.user?.image ?? "https://github.com/shadcn.png";
const name = session?.user?.name ?? "unknown";
@ -64,7 +66,7 @@ export async function AvatarButton() {
<SettingsButton />
<DropdownMenuItem onClick={handleLogOut}>
<LogOut />
Log out
{t("LogOut")}
</DropdownMenuItem>
</DropdownMenuGroup>
</>

View File

@ -1,5 +1,6 @@
import Link from "next/link";
import { cn } from "@/lib/utils";
import { useTranslations } from "next-intl";
import { ArrowLeftIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
@ -14,6 +15,8 @@ export default function BackButton({
className,
...props
}: BackButtonProps) {
const t = useTranslations();
return (
<TooltipProvider delayDuration={0}>
<Tooltip>
@ -29,7 +32,9 @@ export default function BackButton({
</Link>
</Button>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">Back</TooltipContent>
<TooltipContent className="px-2 py-1 text-xs">
{t("BackButton")}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);

View File

@ -1,5 +1,6 @@
import { cn } from "@/lib/utils";
import { siteConfig } from "@/config/site";
import { useTranslations } from 'next-intl';
import { ArrowRightIcon } from "lucide-react";
interface BannerProps {
@ -11,9 +12,11 @@ interface BannerProps {
export function Banner({
className,
link = siteConfig.url.repo.github,
text = "Star this project if you like it.",
text,
...props
}: BannerProps) {
const t = useTranslations();
return (
<header
{...props}
@ -25,7 +28,7 @@ export function Banner({
<p className="flex justify-center text-sm">
<a href={link} className="group">
<span className="me-1 text-base leading-none"></span>
{text}
{text || t("Banner.Text")}
<ArrowRightIcon
className="ms-2 -mt-0.5 inline-flex opacity-60 transition-transform group-hover:translate-x-0.5"
size={16}

View File

@ -8,11 +8,13 @@ import {
} from "@/components/ui/tooltip";
import { BotIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import { Toggle } from "@/components/ui/toggle";
import { useDockviewStore } from "@/stores/dockview";
export default function BotVisibilityToggle() {
const { api } = useDockviewStore();
const t = useTranslations();
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isBotVisible, setBotVisible] = useState<boolean>(false);
@ -37,7 +39,7 @@ export default function BotVisibilityToggle() {
id: "Bot",
component: "Bot",
tabComponent: "Bot",
title: "Bot",
title: t("ProblemPage.Bot"),
position: {
direction: "right",
},
@ -70,7 +72,7 @@ export default function BotVisibilityToggle() {
</div>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">
<p>{isBotVisible ? "Close Bot" : "Open Bot"}</p>
<p>{isBotVisible ? t("BotVisibilityToggle.close") : t("BotVisibilityToggle.open")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@ -1,6 +1,7 @@
import { cn } from "@/lib/utils";
import { RunCode } from "@/components/run-code";
import { auth } from "@/lib/auth";
import BackButton from "@/components/back-button";
import { RunCodeButton } from "@/components/run-code";
import { AvatarButton } from "@/components/avatar-button";
import BotVisibilityToggle from "@/components/bot-visibility-toggle";
@ -8,10 +9,12 @@ interface PlaygroundHeaderProps {
className?: string;
}
export function PlaygroundHeader({
export async function PlaygroundHeader({
className,
...props
}: PlaygroundHeaderProps) {
const session = await auth();
return (
<header
{...props}
@ -33,7 +36,7 @@ export function PlaygroundHeader({
<div className="z-10 absolute left-1/2 top-0 h-full -translate-x-1/2 py-2">
<div className="relative flex">
<div className="relative flex overflow-hidden rounded">
<RunCode className="bg-muted text-muted-foreground hover:bg-muted/50" />
<RunCodeButton session={session} className="bg-muted text-muted-foreground hover:bg-muted/50" />
</div>
</div>
</div>

View File

@ -9,12 +9,14 @@ import {
import { cn } from "@/lib/utils";
import { useState } from "react";
import { Check, Copy } from "lucide-react";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { useProblem } from "@/hooks/use-problem";
export function CopyButton() {
const { editor } = useProblem();
const [copied, setCopied] = useState(false);
const t = useTranslations("WorkspaceEditorHeader.CopyButton");
const handleCopy = async () => {
try {
@ -65,7 +67,7 @@ export function CopyButton() {
</Button>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">
Click to Copy
{t("TooltipContent")}
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@ -2,6 +2,7 @@
import { cn } from "@/lib/utils";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import { useProblem } from "@/hooks/use-problem";
import { CircleXIcon, TriangleAlertIcon } from "lucide-react";
@ -20,6 +21,7 @@ export function WorkspaceEditorFooter({
className,
...props
}: WorkspaceEditorFooterProps) {
const t = useTranslations("WorkspaceEditorFooter");
const { editor, markers } = useProblem();
const [position, setPosition] = useState<{ lineNumber: number; column: number } | null>(null);
@ -62,8 +64,8 @@ export function WorkspaceEditorFooter({
</div>
<span className="truncate">
{position
? `Row ${position.lineNumber}, Column ${position.column}`
: "Row -, Column -"}
? `${t("Row")} ${position.lineNumber}, ${t("Column")} ${position.column}`
: `${t("Row")} -, ${t("Column")} -`}
</span>
</div>
</footer>

View File

@ -7,11 +7,13 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Paintbrush } from "lucide-react";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { useProblem } from "@/hooks/use-problem";
export function FormatButton() {
const { editor } = useProblem();
const t = useTranslations("WorkspaceEditorHeader.FormatButton");
return (
<TooltipProvider delayDuration={0}>
@ -20,7 +22,7 @@ export function FormatButton() {
<Button
variant="outline"
size="icon"
aria-label="Format Code"
aria-label={t("TooltipContent")}
onClick={() => {
editor?.trigger("format", "editor.action.formatDocument", null);
}}
@ -31,7 +33,7 @@ export function FormatButton() {
</Button>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">
Format Code
{t("TooltipContent")}
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@ -6,6 +6,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { useProblem } from "@/hooks/use-problem";
@ -28,6 +29,7 @@ const getLspStatusColor = (webSocket: WebSocket | null) => {
export function LspStatusButton() {
const { webSocket } = useProblem();
const t = useTranslations("WorkspaceEditorHeader.LspStatusButton");
return (
<TooltipProvider delayDuration={0}>
@ -48,7 +50,7 @@ export function LspStatusButton() {
</Button>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">
Language Server
{t("TooltipContent")}
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@ -7,11 +7,13 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Redo2 } from "lucide-react";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { useProblem } from "@/hooks/use-problem";
export function RedoButton() {
const { editor } = useProblem();
const t = useTranslations("WorkspaceEditorHeader.RedoButton");
return (
<TooltipProvider delayDuration={0}>
@ -20,7 +22,7 @@ export function RedoButton() {
<Button
variant="outline"
size="icon"
aria-label="Redo Code"
aria-label={t("TooltipContent")}
onClick={() => {
editor?.trigger("redo", "redo", null);
}}
@ -31,7 +33,7 @@ export function RedoButton() {
</Button>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">
Redo Code
{t("TooltipContent")}
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@ -7,11 +7,13 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { RotateCcw } from "lucide-react";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { useProblem } from "@/hooks/use-problem";
export function ResetButton() {
const { editor, currentTemplate } = useProblem();
const t = useTranslations("WorkspaceEditorHeader.ResetButton");
const handleReset = () => {
if (editor) {
@ -38,7 +40,7 @@ export function ResetButton() {
<Button
variant="outline"
size="icon"
aria-label="Reset Code"
aria-label={t("TooltipContent")}
onClick={handleReset}
disabled={!editor}
className="h-6 w-6 px-1.5 py-0.5 border-none shadow-none hover:bg-muted"
@ -47,7 +49,7 @@ export function ResetButton() {
</Button>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">
Reset Code
{t("TooltipContent")}
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@ -7,11 +7,13 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Undo2 } from "lucide-react";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { useProblem } from "@/hooks/use-problem";
export function UndoButton() {
const { editor } = useProblem();
const t = useTranslations("WorkspaceEditorHeader.UndoButton");
return (
<TooltipProvider delayDuration={0}>
@ -20,7 +22,7 @@ export function UndoButton() {
<Button
variant="outline"
size="icon"
aria-label="Undo Code"
aria-label={t("TooltipContent")}
onClick={() => {
editor?.trigger("undo", "undo", null);
}}
@ -31,7 +33,7 @@ export function UndoButton() {
</Button>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">
Undo Code
{t("TooltipContent")}
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@ -0,0 +1,65 @@
"use client";
import Flag from "react-world-flags";
import { Globe } from "lucide-react";
import { useTranslations } from "next-intl";
import { Locale, locales } from "@/config/i18n";
import { useState, useMemo, useEffect } from "react";
import { getUserLocale, setUserLocale } from "@/i18n/locale";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
export function LanguageSettings() {
const t = useTranslations();
const [selectedOption, setSelectedOption] = useState<Locale>();
useEffect(() => {
const fetchLocale = async () => {
const userLocale = await getUserLocale();
if (!userLocale) return;
setSelectedOption(userLocale);
};
fetchLocale();
}, []);
const localeOptions = useMemo(() => {
const options = locales.map((locale) => ({
value: locale,
label: `${t(`LanguageSettings.${locale}.name`)}`,
}));
return options.sort((a, b) => a.value.localeCompare(b.value));
}, [t]);
const handleValueChange = (value: Locale) => {
setSelectedOption(value);
setUserLocale(value);
};
const getIconForLocale = (locale: Locale) => {
switch (locale) {
case "en":
return <Flag code="US" className="h-4 w-4 mr-2" />;
case "zh":
return <Flag code="CN" className="h-4 w-4 mr-2" />;
default:
return <Globe size={16} className="mr-2" />;
}
};
return (
<Select value={selectedOption} onValueChange={handleValueChange}>
<SelectTrigger className="w-[200px] shadow-none focus:ring-0">
<SelectValue />
</SelectTrigger>
<SelectContent className="w-[200px]">
{localeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<div className="flex items-center">
{getIconForLocale(option.value)}
<span className="truncate">{option.label}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
);
}

View File

@ -1,6 +1,7 @@
"use client";
import { LogIn } from "lucide-react";
import { useTranslations } from "next-intl";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
@ -8,6 +9,7 @@ export default function LogInButton() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const t = useTranslations("AvatarButton");
const handleLogIn = () => {
const params = new URLSearchParams(searchParams.toString());
@ -17,8 +19,8 @@ export default function LogInButton() {
return (
<DropdownMenuItem onClick={handleLogIn}>
<LogIn className="mr-2 h-4 w-4" />
Log In
<LogIn />
{t("LogIn")}
</DropdownMenuItem>
);
}

View File

@ -8,24 +8,32 @@ import {
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { useState } from "react";
import { Session } from "next-auth";
import { judge } from "@/actions/judge";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { useProblem } from "@/hooks/use-problem";
import { useDockviewStore } from "@/stores/dockview";
import { LoaderCircleIcon, PlayIcon } from "lucide-react";
import { showStatusToast } from "@/hooks/show-status-toast";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
interface RunCodeProps {
interface RunCodeButtonProps {
className?: string;
session: Session | null;
}
export function RunCode({
export function RunCodeButton({
className,
...props
}: RunCodeProps) {
session,
}: RunCodeButtonProps) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const { api } = useDockviewStore();
const { currentLang, editor, problemId } = useProblem();
const [isLoading, setIsLoading] = useState<boolean>(false);
const t = useTranslations("PlaygroundHeader.RunCodeButton");
const handleJudge = async () => {
if (!editor) return;
@ -33,18 +41,24 @@ export function RunCode({
const code = editor.getValue() || "";
setIsLoading(true);
try {
const result = await judge(currentLang, code, problemId);
showStatusToast({ status: result.status });
const panel = api?.getPanel("Submissions");
if (panel && !panel.api.isActive) {
panel.api.setActive();
if (!session) {
const params = new URLSearchParams(searchParams.toString());
params.set("redirectTo", pathname);
router.push(`/sign-in?${params.toString()}`);
} else {
try {
const result = await judge(currentLang, code, problemId);
showStatusToast({ status: result.status });
const panel = api?.getPanel("Submissions");
if (panel && !panel.api.isActive) {
panel.api.setActive();
}
} catch (error) {
console.error("Error occurred while judging the code:");
console.error(error);
} finally {
setIsLoading(false);
}
} catch (error) {
console.error("Error occurred while judging the code:");
console.error(error);
} finally {
setIsLoading(false);
}
};
@ -53,7 +67,6 @@ export function RunCode({
<Tooltip>
<TooltipTrigger asChild>
<Button
{...props}
variant="secondary"
className={cn("h-8 px-3 py-1.5", className)}
onClick={handleJudge}
@ -72,10 +85,12 @@ export function RunCode({
aria-hidden="true"
/>
)}
{isLoading ? "Running..." : "Run"}
{isLoading ? t("TooltipTrigger.loading") : t("TooltipTrigger.ready")}
</Button>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">Run Code</TooltipContent>
<TooltipContent className="px-2 py-1 text-xs">
{t("TooltipContent")}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);

View File

@ -1,16 +1,18 @@
"use client";
import { Settings } from "lucide-react";
import { useTranslations } from "next-intl";
import { useSettingsStore } from "@/stores/useSettingsStore";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
export function SettingsButton() {
const t = useTranslations("AvatarButton");
const { setDialogOpen } = useSettingsStore();
return (
<DropdownMenuItem onClick={() => setDialogOpen(true)}>
<Settings />
Settings
{t("Settings")}
</DropdownMenuItem>
);
}

View File

@ -25,29 +25,31 @@ import {
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import AppearanceSettings from "./appearance-settings";
import { useTranslations } from "next-intl";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useSettingsStore } from "@/stores/useSettingsStore";
import AppearanceSettings from "@/components/appearance-settings";
import { LanguageSettings } from "@/components/language-settings";
import { CodeXml, Globe, Paintbrush, Settings } from "lucide-react";
const data = {
nav: [
{ name: "Appearance", icon: Paintbrush },
{ name: "Language & region", icon: Globe },
{ name: "Code Editor", icon: CodeXml },
{ name: "Advanced", icon: Settings },
],
};
export function SettingsDialog() {
const t = useTranslations("SettingsDialog");
const data = {
nav: [
{ id: "Appearance", name: t("nav.Appearance"), icon: Paintbrush },
{ id: "Language", name: t("nav.Language"), icon: Globe },
{ id: "CodeEditor", name: t("nav.CodeEditor"), icon: CodeXml },
{ id: "Advanced", name: t("nav.Advanced"), icon: Settings },
],
};
const { isDialogOpen, activeSetting, setDialogOpen, setActiveSetting } = useSettingsStore();
return (
<Dialog open={isDialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="overflow-hidden p-0 md:max-h-[500px] md:max-w-[700px] lg:max-w-[800px]">
<DialogTitle className="sr-only">Settings</DialogTitle>
<DialogTitle className="sr-only">{t("title")}</DialogTitle>
<DialogDescription className="sr-only">
Customize your settings here.
{t("description")}
</DialogDescription>
<SidebarProvider className="items-start">
<Sidebar collapsible="none" className="hidden md:flex">
@ -59,10 +61,10 @@ export function SettingsDialog() {
<SidebarMenuItem key={item.name}>
<SidebarMenuButton
asChild
isActive={item.name === activeSetting}
onClick={() => setActiveSetting(item.name)}
isActive={item.id === activeSetting}
onClick={() => setActiveSetting(item.id)}
>
<a href="#">
<a>
<item.icon />
<span>{item.name}</span>
</a>
@ -80,11 +82,11 @@ export function SettingsDialog() {
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="#">Settings</BreadcrumbLink>
<BreadcrumbLink>{t("breadcrumb")}</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>{activeSetting}</BreadcrumbPage>
<BreadcrumbPage>{t(`nav.${activeSetting}`)}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
@ -92,16 +94,8 @@ export function SettingsDialog() {
</header>
<ScrollArea className="flex-1 overflow-y-auto p-4 pt-0">
<div className="flex flex-col gap-4">
{activeSetting === "Appearance" ? (
<AppearanceSettings />
) : (
Array.from({ length: 10 }).map((_, i) => (
<div
key={i}
className="aspect-video max-w-3xl rounded-xl bg-muted/50"
/>
))
)}
{activeSetting === "Appearance" && <AppearanceSettings />}
{activeSetting === "Language" && <LanguageSettings />}
</div>
</ScrollArea>
</main>

View File

@ -9,6 +9,9 @@ import {
TableRow,
} from "@/components/ui/table";
import { cn } from "@/lib/utils";
import { Locale } from "@/config/i18n";
import { useTranslations } from "next-intl";
import { enUS, zhCN } from "date-fns/locale";
import { useProblem } from "@/hooks/use-problem";
import { Clock4Icon, CpuIcon } from "lucide-react";
import { useDockviewStore } from "@/stores/dockview";
@ -18,10 +21,23 @@ import { EditorLanguageIcons } from "@/config/editor-language-icons";
import { formatDistanceToNow, isBefore, subDays, format } from "date-fns";
interface SubmissionsTableProps {
locale: Locale;
submissions: SubmissionWithTestcaseResult[];
}
export default function SubmissionsTable({ submissions }: SubmissionsTableProps) {
const getLocale = (locale: Locale) => {
switch (locale) {
case "zh":
return zhCN;
case "en":
default:
return enUS;
}
}
export default function SubmissionsTable({ locale, submissions }: SubmissionsTableProps) {
const s = useTranslations("StatusMessage");
const t = useTranslations("SubmissionsTable");
const { editorLanguageConfigs } = useProblem();
const { api, setSubmission } = useDockviewStore();
@ -41,7 +57,7 @@ export default function SubmissionsTable({ submissions }: SubmissionsTableProps)
id: "Details",
component: "Details",
tabComponent: "Details",
title: submission.status,
title: s(`${submission.status}`),
position: {
referencePanel: "Submissions",
direction: "within",
@ -54,11 +70,11 @@ export default function SubmissionsTable({ submissions }: SubmissionsTableProps)
<Table>
<TableHeader className="bg-transparent">
<TableRow className="hover:bg-transparent">
<TableHead className="w-[100px]">Index</TableHead>
<TableHead className="w-[170px]">Status</TableHead>
<TableHead className="w-[100px]">Language</TableHead>
<TableHead className="w-[100px]">Time</TableHead>
<TableHead className="w-[100px]">Memory</TableHead>
<TableHead className="w-[100px]">{t("Index")}</TableHead>
<TableHead className="w-[170px]">{t("Status")}</TableHead>
<TableHead className="w-[100px]">{t("Language")}</TableHead>
<TableHead className="w-[100px]">{t("Time")}</TableHead>
<TableHead className="w-[100px]">{t("Memory")}</TableHead>
</TableRow>
</TableHeader>
@ -68,11 +84,13 @@ export default function SubmissionsTable({ submissions }: SubmissionsTableProps)
{sortedSubmissions.map((submission, index) => {
const Icon = EditorLanguageIcons[submission.language];
const createdAt = new Date(submission.createdAt);
const localeInstance = getLocale(locale);
const submittedDisplay = isBefore(createdAt, subDays(new Date(), 1))
? format(createdAt, "yyyy-MM-dd")
: formatDistanceToNow(createdAt, { addSuffix: true });
: formatDistanceToNow(createdAt, { addSuffix: true, locale: localeInstance });
const isEven = (submissions.length - index) % 2 === 0;
const message = statusMap.get(submission.status)?.message;
return (
<TableRow
@ -89,7 +107,7 @@ export default function SubmissionsTable({ submissions }: SubmissionsTableProps)
<TableCell>
<div className="flex flex-col truncate">
<span className={getStatusColorClass(submission.status)}>
{statusMap.get(submission.status)?.message}
{s(`${message}`)}
</span>
<span className="text-xs">{submittedDisplay}</span>
</div>

View File

@ -3,6 +3,7 @@ import {
XIcon,
} from "lucide-react";
import { toast } from "sonner";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import type { Status } from "@/generated/client";
import { getStatusColorClass, statusMap } from "@/lib/status";
@ -17,34 +18,38 @@ const StatusToast = ({
Icon: LucideIcon;
message: string;
colorClass: string;
}) => (
<div className="bg-background text-foreground w-full rounded-md border px-4 py-1 shadow-lg h-10 flex items-center">
<div className="flex gap-2">
<div className="flex grow gap-3">
<Icon
className={`mt-0.5 shrink-0 ${colorClass}`}
size={16}
aria-hidden="true"
/>
<div className="flex grow justify-between gap-12">
<p className="text-sm">{message}</p>
}) => {
const s = useTranslations("StatusMessage");
return (
<div className="bg-background text-foreground w-full rounded-md border px-4 py-1 shadow-lg h-10 flex items-center">
<div className="flex gap-2">
<div className="flex grow gap-3">
<Icon
className={`mt-0.5 shrink-0 ${colorClass}`}
size={16}
aria-hidden="true"
/>
<div className="flex grow justify-between gap-12">
<p className="text-sm">{s(`${message}`)}</p>
</div>
</div>
<Button
variant="ghost"
className="group -my-1.5 -me-2 size-8 shrink-0 p-0 hover:bg-transparent"
onClick={() => toast.dismiss(t)}
aria-label="Close sonner"
>
<XIcon
size={16}
className="opacity-60 transition-opacity group-hover:opacity-100"
aria-hidden="true"
/>
</Button>
</div>
<Button
variant="ghost"
className="group -my-1.5 -me-2 size-8 shrink-0 p-0 hover:bg-transparent"
onClick={() => toast.dismiss(t)}
aria-label="Close sonner"
>
<XIcon
size={16}
className="opacity-60 transition-opacity group-hover:opacity-100"
aria-hidden="true"
/>
</Button>
</div>
</div>
);
)
};
interface ShowStatusToastProps {
status: Status;

12
src/lib/i18n.ts Normal file
View File

@ -0,0 +1,12 @@
import { Locale } from "@/config/i18n";
import { enUS, zhCN } from "date-fns/locale";
export const getLocale = (locale: Locale) => {
switch (locale) {
case "zh":
return zhCN;
case "en":
default:
return enUS;
}
}

View File

@ -25,16 +25,16 @@ export const getStatusColorClass = (status: Status) => {
};
export const statusMap = new Map<Status, { icon: LucideIcon; message: string }>([
["PD", { icon: AlertTriangleIcon, message: "Pending" }],
["QD", { icon: AlertTriangleIcon, message: "Queued" }],
["CP", { icon: AlertTriangleIcon, message: "Compiling" }],
["CE", { icon: AlertTriangleIcon, message: "Compilation Error" }],
["CS", { icon: CircleCheckIcon, message: "Compilation Success" }],
["RU", { icon: AlertTriangleIcon, message: "Running" }],
["TLE", { icon: AlertTriangleIcon, message: "Time Limit Exceeded" }],
["MLE", { icon: AlertTriangleIcon, message: "Memory Limit Exceeded" }],
["RE", { icon: AlertTriangleIcon, message: "Runtime Error" }],
["AC", { icon: CircleCheckIcon, message: "Accepted" }],
["WA", { icon: AlertTriangleIcon, message: "Wrong Answer" }],
["SE", { icon: BanIcon, message: "System Error" }],
["PD", { icon: AlertTriangleIcon, message: "PD" }],
["QD", { icon: AlertTriangleIcon, message: "QD" }],
["CP", { icon: AlertTriangleIcon, message: "CP" }],
["CE", { icon: AlertTriangleIcon, message: "CE" }],
["CS", { icon: CircleCheckIcon, message: "CS" }],
["RU", { icon: AlertTriangleIcon, message: "RU" }],
["TLE", { icon: AlertTriangleIcon, message: "TLE" }],
["MLE", { icon: AlertTriangleIcon, message: "MLE" }],
["RE", { icon: AlertTriangleIcon, message: "RE" }],
["AC", { icon: CircleCheckIcon, message: "AC" }],
["WA", { icon: AlertTriangleIcon, message: "WA" }],
["SE", { icon: BanIcon, message: "SE" }],
]);